Terraform Advanced — Dynamic Blocks

1 minute read

Dynamic blocks let you generate repeated nested blocks from a variable — eliminating copy-paste for security group rules, tags, and more.


The Problem

Without dynamic blocks, adding 10 ingress rules means 10 copy-pasted blocks:

# ❌ Anti-pattern: copy-paste hell
resource "aws_security_group" "web" {
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  # ... 8 more blocks
}

Dynamic Block Syntax

# ✅ Best practice: dynamic block
variable "ingress_rules" {
  type = list(object({
    port        = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = [
    { port = 80,  protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
    { port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
    { port = 22,  protocol = "tcp", cidr_blocks = ["10.0.0.0/8"] },
  ]
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Web security group"
  vpc_id      = var.vpc_id

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

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

How It Works

dynamic "<block_label>" {
  for_each = <collection>     # list or map to iterate over
  content {
    # <block_label>.value  → current item
    # <block_label>.key    → index (list) or key (map)
  }
}

Nested Dynamic Blocks

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = var.cert_arn

  dynamic "default_action" {
    for_each = var.default_actions
    content {
      type             = default_action.value.type
      target_group_arn = default_action.value.target_group_arn

      dynamic "fixed_response" {
        for_each = default_action.value.type == "fixed-response" ? [1] : []
        content {
          content_type = "text/plain"
          message_body = "OK"
          status_code  = "200"
        }
      }
    }
  }
}

💡 Production Tip: Don’t nest more than 2 levels of dynamic blocks. If you need deeper nesting, refactor into a module.


What’s Next?

Master advanced for_each patterns.

← Advanced HCL Advanced for_each →

Updated: