DevOps and Infrastructure

Terraform Dynamic Blocks and For_Each: Advanced Patterns for Managing Variable-Length Collections

Infrastructure as Code (IaC) often involves managing resources with configurations that are not fixed at compile time. Whether you are provisioning AWS security groups, configuring Kubernetes deployments, or setting up Azure network interfaces, the number of nested blocks or list items frequently varies. Hardcoding these configurations leads to repetitive code and maintenance nightmares. This is where Terraform dynamic blocks and the for_each meta-argument shine.

For intermediate to advanced developers, mastering these two features is essential for writing clean, scalable, and maintainable HCL (HashiCorp Configuration Language) code. This post explores advanced patterns for combining these tools to handle complex, variable-length collections.

Understanding the Building Blocks

Before diving into advanced patterns, let’s briefly clarify the distinction. for_each is a meta-argument that can be used on resources or modules to create multiple instances of a resource based on a map or set of strings. It is ideal for top-level resource creation.

On the other hand, dynamic blocks allow you to generate nested blocks (like ingress in a security group) inside a resource dynamically. They iterate over a list or map and inject the corresponding block configuration for each element.

Pattern 1: The Nested Dynamic Block

The most common use case for dynamic blocks is generating nested configurations. Consider an AWS aws_security_group. You want to define ingress rules based on a variable, but the number of rules is unknown. Using a dynamic block, you can iterate over a list of maps.

variable "ingress_rules" {
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = []
}

resource "aws_security_group" "example" {
  name = "example-sg"

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

# Usage in main.tf
resource "aws_security_group" "web_sg" {
  ingress_rules = [
    {
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/8"]
    }
  ]
}

This pattern keeps your variable definitions clean and allows your infrastructure to adapt instantly when new rules are added to the input list.

Pattern 2: Combining for_each and Dynamic Blocks

Advanced scenarios often require creating multiple resources, each with its own set of dynamic nested blocks. For example, you might be deploying multiple application services, each with specific environment variables or container ports.

By combining for_each on the resource and dynamic blocks inside the resource, you achieve a high degree of flexibility.

variable "services" {
  type = map(object({
    name = string
    ports = list(object({
      containerPort = number
      protocol      = string
    }))
  }))
}

resource "aws_lb_target_group" "this" {
  for_each = var.services

  name     = each.key
  port     = 80
  protocol = "HTTP"

  # Note: Dynamic blocks inside target groups are limited, 
  # but this pattern applies to resources like aws_instance or aws_lb_listener
  
  tags = {
    Name = each.key
  }
}

While the above example is simplified, a more complex implementation involves using for_each to spin up distinct modules or resources, where each instance receives a specific configuration map that drives its internal dynamic blocks.

Best Practices and Performance Considerations

When managing variable-length collections, keep the following in mind:

  • Immutability of Keys: When using for_each, ensure your keys are stable. Changing a key in a map will cause Terraform to destroy and recreate the resource rather than updating it.
  • Simplify Variables: Use structured variable types (maps of objects or lists of objects) to validate inputs early. This prevents runtime errors when dynamic blocks try to access undefined attributes.
  • Readability: While powerful, excessive nesting of dynamic blocks can make code hard to read. Extract complex configurations into local values or separate modules if the logic becomes too convoluted.

Conclusion

Dynamic blocks and for_each are not just convenience features; they are fundamental tools for building robust, cloud-native infrastructure with Terraform. By leveraging these advanced patterns, you can eliminate code duplication, reduce the risk of configuration drift, and create infrastructure that scales as gracefully as your applications do. Start refactoring your hard-coded blocks today to unlock the full potential of your IaC strategy.

Share: