Skip to main content
RunBook Academy

TerraformVII · Resources, Data Sources, and count/for_eachProduction Terraform

Dynamic Blocks for Conditional Configuration

Intermediate⏱ ~10 minbash

What you'll learn

  • Use dynamic blocks to generate nested configuration from a collection
  • Choose the right iterator label and avoid shadowing outer variables
  • Recognise when a dynamic block adds more confusion than clarity
  • Convert a dynamic block back to literal HCL when the structure is fixed

Prerequisites

None — start here.

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

A security group needs twenty ingress rules — one per port, one per CIDR. The team could write them as twenty literal blocks. They could also write a dynamic block that generates them from a list. The trade-off is readability versus brevity, and the trade-off is production-relevant. This lesson is the discipline of when to use dynamic, when to write it out by hand, and how to make the dynamic version reviewable.

What a dynamic block does

A dynamic block generates nested configuration blocks from a collection. The block label (e.g., ingress) becomes the type of block generated; the for_each collection drives the iteration; the content block is the template for each generated block.

resource "aws_security_group" "web" {
  name        = "web"
  description = "Web tier"

  dynamic "ingress" {
    for_each = var.web_ingress_ports
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

With var.web_ingress_ports = [443, 80], Terraform generates two ingress blocks:

ingress {
  from_port   = 443
  to_port     = 443
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

ingress {
  from_port   = 80
  to_port     = 80
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

The generated blocks are the same as if they had been written literally. terraform plan shows them as if they were written literally. The provider sees them as literal blocks.

Iterator labels

The default iterator variable is the block label. For an ingress block, the iterator is ingress. If the block label collides with an outer variable (for example, the resource block has an ingress attribute), the iterator must be renamed:

locals {
  web_ingress = [
    { port = 443, cidr = "10.0.0.0/16" },
    { port = 80,  cidr = "0.0.0.0/0" },
  ]
}

resource "aws_security_group" "web" {
  name        = "web"
  description = "Web tier"

  dynamic "ingress" {
    for_each = { for p in local.web_ingress : p.port => p }
    iterator = port

    content {
      from_port   = port.value.port
      to_port     = port.value.port
      protocol    = "tcp"
      cidr_blocks = [port.value.cidr]
    }
  }
}

Inside content, the iterator variable is port (not ingress). Use iterator = <name> to choose a name that does not collide.

When dynamic is the right tool

Use dynamic when:

  • The nested blocks are driven by a variable that changes per environment (e.g., a list of ports in a Terraform variable).
  • The number of blocks is not known at write time (e.g., one rule per route table association).
  • The structure wraps another dynamic block that itself varies (e.g., dynamic ingress blocks inside dynamic egress blocks).

A canonical use case: per-environment ingress rules:

variable "ingress_rules" {
  type = map(object({
    port        = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = {}
}
resource "aws_security_group" "app" {
  name        = "app-${var.environment}"
  description = "Application security group"

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

Per environment, the rules differ; the configuration does not need to change.

When dynamic is the wrong tool

Do not use dynamic when:

  • The blocks are fixed (the team has decided the structure is stable). Write them literally. Reviewers can read them.
  • The number of blocks is small (one or two). The abstraction cost exceeds the brevity savings.
  • The blocks control security-sensitive configuration and the generation logic is hard to audit. Generate them, then inspect the generated output with terraform show -json.
  • The collection is derived from another resource that is itself dynamic. Two layers of dynamic blocks obscure the generated structure.

A counter-example. The team writes:

dynamic "ingress" {
  for_each = [
    { port = 80, cidr = "10.0.0.0/16" },
    { port = 443, cidr = "10.0.0.0/16" },
  ]
  content {
    from_port   = ingress.value.port
    to_port     = ingress.value.port
    protocol    = "tcp"
    cidr_blocks = [ingress.value.cidr]
  }
}

The collection is hard-coded. The dynamic block adds complexity without adding value. The literal version is shorter and easier to read:

ingress {
  from_port   = 80
  to_port     = 80
  protocol    = "tcp"
  cidr_blocks = ["10.0.0.0/16"]
}

ingress {
  from_port   = 443
  to_port     = 443
  protocol    = "tcp"
  cidr_blocks = ["10.0.0.0/16"]
}

A simple rule: if the collection driving the dynamic block is a literal in the same file, write the blocks literally.

Converting dynamic back to literal HCL

When a dynamic block has grown unweildy, convert it back to literal blocks. The process is mechanical:

  1. Run terraform show -json on the current plan to see the generated structure.
  2. Replace the dynamic block with the literal blocks from the generated output.
  3. Remove the variable or collection driving the dynamic.
  4. Verify terraform plan is empty.
terraform plan -out=tfplan
terraform show -json tfplan | jq '.resource_changes[] | select(.address == "aws_security_group.app") | .change.after.ingress'

The output is the literal JSON representation of the ingress blocks. Use it as the source of truth when converting.

Production failure modes

  1. Iterator name collision. The iterator variable (ingress) collides with an outer variable or attribute. Symptom: the generated block references the wrong value. Recovery: rename the iterator with iterator = port.

  2. Hard-coded collection. The for_each is a literal list in the same file. The dynamic block is an abstraction with no callers. Symptom: reviewers cannot tell why the abstraction exists. Recovery: write the blocks literally.

  3. Missing key in the driving map. A key is referenced in the content block but is missing from the per-iteration object. Symptom: Error: Unsupported attribute on the iterator. Recovery: add the missing key to the input variable default, or guard with try().

  4. Nested dynamic blocks. A dynamic block contains another dynamic block. Symptom: the generated structure is very hard to read; plan output is large. Recovery: flatten the structure; generate the inner blocks from a precomputed map.

  5. dynamic "egress" opens the wrong CIDR. A bug in the driving map accidentally includes 0.0.0.0/0 in a production egress rule. Symptom: the security group allows egress to the entire internet. Recovery: add a policy check (terraform validate plus a Sentinel/Conftest policy); audit the generated security group with terraform show -json.

  6. Dynamic blocks driving IAM policies. A dynamic "statement" inside an IAM policy generates wildcard actions. Symptom: the policy grants more than intended; the audit trail shows only the dynamic block, not the generated JSON. Recovery: prefer literal policies for IAM; if dynamic is necessary, render the policy to JSON and commit it for review.

Security and performance

Security. Dynamic blocks that drive security-sensitive configuration (security groups, IAM, network ACLs) must be auditable. Render the generated structure with terraform show -json and review it as part of code review. For high-sensitivity configurations, prefer literal blocks.

Performance. A dynamic block is no slower than the equivalent literal blocks at runtime. The cost is at parse time and review time. A configuration with thousands of generated blocks can slow terraform plan; flatten the structure or move to modules.

What to do in production

  • Use dynamic when the nested blocks are driven by a variable or by another dynamic structure.
  • Use literal blocks when the structure is fixed or the count is small.
  • Pick clear iterator names. Avoid the default (ingress, egress) when those names collide with outer variables.
  • Render the generated structure with terraform show -json and review it as part of code review.
  • For security-sensitive configuration, prefer literal blocks. Audit the generated output before approving the apply.
  • Add a comment explaining the iteration when the dynamic block is non-obvious. The next reader will not have your context.

Verification

# 1. List dynamic blocks in the configuration
grep -rE '^\s*dynamic\s+"' .

# 2. Render the generated structure for one resource
terraform plan -out=tfplan
terraform show -json tfplan | jq '.resource_changes[] | select(.address == "aws_security_group.web") | .change.after.ingress'

# 3. Confirm the count of generated blocks matches the input
terraform show -json tfplan | jq '.resource_changes[] | select(.address == "aws_security_group.web") | .change.after.ingress | length'

# 4. Verify the security group on the provider
aws ec2 describe-security-groups --group-ids sg-0123456789abcdef0

# 5. Validate the configuration syntactically
terraform validate

A clean verification:

$ terraform show -json tfplan | jq '.resource_changes[] | select(.address == "aws_security_group.web") | .change.after.ingress | length'
2

The number of generated ingress blocks matches the input.

Knowledge check · 7 questions

  1. Q1. What does a `dynamic` block do?

  2. Q2. What is the default name of the iterator variable inside a `dynamic` block?

  3. Q3. Dynamic blocks drive the same API calls as equivalent literal blocks.

  4. Q4. Which of the following are appropriate uses of a `dynamic` block? (Select all that apply.)

  5. Q5. What command renders the generated structure of a resource for audit?

  6. Q6. A team uses a `dynamic "statement"` block inside an `aws_iam_policy_document` to generate IAM statements from a map. The plan is approved, but the resulting IAM policy grants `s3:*` on `*`. The team expected read-only access. What is the most likely cause?

  7. Q7. How do you rename the iterator variable inside a `dynamic` block?

Passing score: 75%. Answers are checked in this browser.