Terraform Dynamic Blocks and Conditional Logic

Some resources require you to repeat the same nested block multiple times with different values — security group rules, ingress/egress configurations, or tag sets. Writing these by hand does not scale. Dynamic blocks generate repeated nested blocks automatically from a list or map. Combined with conditional expressions, they make configurations powerfully flexible without becoming unreadable.

The Problem: Repetitive Nested Blocks

Consider an AWS security group. Every allowed port requires its own ingress block. Without dynamic blocks:

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

  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"]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]
  }

  # Adding a fourth port means adding another whole block manually
}

Three ports, three blocks. Ten ports would mean ten blocks. Any change to the allowed ports means editing the file.

Dynamic Blocks: The Solution

A dynamic block generates nested blocks from a variable or local value. You define the pattern once and Terraform repeats it for every item in a collection.

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"

  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
    }
  }
}

Dynamic Block Syntax Breakdown

dynamic "NESTED_BLOCK_NAME" {
  for_each = COLLECTION         # list or map to iterate over
  content {
    # The nested block's arguments
    # Use NESTED_BLOCK_NAME.value to access each item's value
    # Use NESTED_BLOCK_NAME.key for map keys
  }
}

Add a new port to var.ingress_rules and Terraform generates the additional ingress block automatically — no code changes in the resource block itself.

Dynamic Blocks with Maps

locals {
  tags_to_apply = {
    Environment = "production"
    Owner       = "platform-team"
    CostCenter  = "engineering"
  }
}

resource "some_resource" "example" {
  dynamic "tag" {
    for_each = local.tags_to_apply
    content {
      key   = tag.key
      value = tag.value
    }
  }
}

When iterating a map, tag.key is the map key and tag.value is the map value. When iterating a list, use tag.value for the item and tag.key for the index.

Conditional Resource Creation

Sometimes a resource should only be created in certain environments. Use the count meta-argument with a conditional expression to toggle resource creation on or off.

variable "enable_bastion" {
  type    = bool
  default = false
}

resource "aws_instance" "bastion" {
  count         = var.enable_bastion ? 1 : 0
  ami           = data.aws_ami.linux.id
  instance_type = "t3.micro"
}

When enable_bastion = true, count is 1 and Terraform creates the bastion host. When false, count is 0 and the resource does not exist. This is the standard pattern for optional resources in Terraform.

Referencing a Conditionally Created Resource

output "bastion_ip" {
  value = var.enable_bastion ? aws_instance.bastion[0].public_ip : "No bastion deployed"
}

Because the instance may not exist, always use a conditional expression before accessing its attributes. Access index [0] since count creates a list.

Conditional Nested Block with Dynamic

Combine dynamic with a conditional expression to include or exclude a nested block based on a variable:

variable "enable_https_redirect" {
  type    = bool
  default = true
}

resource "aws_alb_listener" "http" {
  # ...

  dynamic "default_action" {
    for_each = var.enable_https_redirect ? [1] : []
    content {
      type = "redirect"
      redirect {
        port        = "443"
        protocol    = "HTTPS"
        status_code = "HTTP_301"
      }
    }
  }
}

The trick is for_each = condition ? [1] : []. An empty list produces zero iterations (block absent). A single-item list produces one iteration (block present).

Key Points

  • Dynamic blocks generate repeated nested blocks from a list or map — eliminating copy-paste repetition inside resource definitions.
  • Use BLOCK_NAME.value to access item values and BLOCK_NAME.key for map keys or list indexes inside a dynamic block.
  • Use count = var.enable ? 1 : 0 to conditionally create or skip a resource entirely.
  • Always guard attribute references to conditionally created resources with a ternary expression to avoid errors when count is 0.
  • Use for_each = condition ? [1] : [] to conditionally include or exclude a nested block using a dynamic block.

Leave a Comment