Chapter 4: Variables, Outputs, and Expressions
Topic 28

Dynamic Blocks

Expressions

A dynamic block generates repeated nested blocks inside a resource from a collection — multiple ingress rules in a security group, multiple setting blocks in a resource — when you can't simply set an argument to a list. They earn their place when the count of nested blocks is genuinely data-driven, and they are easy to overuse: a config wallpapered in dynamic blocks is often harder to read than the explicit version it replaced.

The distinction that makes them necessary is between an argument and a nested block. An argument takes a value, including a list, directly. A nested block is structural — ingress { ... } is not a value you can assign a list to — so when you need a variable number of them, a dynamic block is the mechanism that produces them.

The Problem They Solve

A security group's ingress is a nested block, not a list argument. You can write three ingress { ... } blocks by hand, but you cannot bind ingress to a variable-length list of rules. When the number of rules comes from a variable that changes per environment, that is exactly the gap a dynamic block fills.

Anatomy of a Dynamic Block

A dynamic "ingress" block takes a for_each over a collection and a content block that defines one generated block per element. Inside content, an iterator object — named after the block by default — exposes .key and .value for the current element. The result is one ingress block per element of the collection.

A dynamic block expands a collection into nested blocks
list(object) source
dynamic block
repeated ingress blocks
main.tf — generating ingress rules
resource "aws_security_group" "app" {
  name = "app-sg"

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

A Worked Source Variable

The source for that loop is a clearly-typed variable: a list of objects, each with a port and a cidr. Typing it as list(object({...})) makes the shape obvious to anyone reading the variable, and the validation that comes with a precise type catches a malformed rule at plan time rather than mid-apply.

variables.tf — a typed source for the rules
variable "ingress_rules" {
  type = list(object({
    port = number
    cidr = string
  }))
  default = [
    { port = 443, cidr = "0.0.0.0/0" },
    { port = 22,  cidr = "10.0.0.0/8" },
  ]
}

The Iterator and Nesting

The iterator defaults to the block's name but can be renamed with iterator = name, which matters when you nest one dynamic block inside another and need to tell the two levels apart. Nesting is where readability falls off a cliff: a dynamic block inside a dynamic block inside a resource quickly becomes something nobody can read at a glance, and that cost is real even when it works.

When Not to Use Them

A fixed, small set of nested blocks is clearer written out. Generating two or three static ingress blocks with a dynamic block trades explicit, greppable config for indirection that buys nothing — the count never varies. Reserve dynamic blocks for the case where the number of blocks genuinely comes from data, and write the rest out by hand.

Common Mistakes
  • Generating two or three static nested blocks with a dynamic block, trading clear, greppable config for indirection that buys nothing.
  • Confusing the iterator's .value scope in nested dynamics and referencing the wrong level, producing a block with the wrong values.
  • Driving security-group rules from a list whose reordering churns the plan, instead of a set or a stably-ordered source.
  • Nesting dynamic blocks several levels deep until no reader can tell what the resource actually declares.
  • Feeding the dynamic block an untyped variable, so a malformed rule fails deep in the apply instead of at plan time.
Best Practices
  • Use a dynamic block only when the number of nested blocks is genuinely data-driven and varies.
  • Write out a small fixed set of nested blocks explicitly rather than generating them.
  • Keep the source in a clearly-typed variable such as list(object({...})) so the shape is obvious.
  • Use a set or a stably-ordered source for the for_each to avoid plan churn from reordering.
  • Rename the iterator with iterator = when nesting, so each level's .value is unambiguous.
Comparable tools CloudFormation has no direct equivalent — you template or duplicate the blocks Pulumi uses an ordinary loop to build the nested structures Ansible uses with_items loops over tasks rather than nested config blocks

Knowledge Check

Why do you need a dynamic block for a variable number of ingress rules instead of just assigning a list?

  • ingress is a nested block, not an argument, so it can't be bound to a list directly
  • Lists of any kind are simply not permitted inside a security group resource at all
  • A dynamic block applies the generated rules faster than passing them as a plain list argument
  • Terraform forbids declaring more than one static ingress block per security group resource

Inside a dynamic "ingress" block, what does the iterator expose for each element?

  • .key and .value for the current element of the for_each collection
  • The full set of the resource's computed attributes as they exist after apply
  • A positional counter named count.index tracking the current iteration only
  • The previously generated block so the current one can reference it

When does a dynamic block hurt rather than help?

  • When it generates a small fixed set of blocks that never varies — explicit blocks would be clearer
  • Whenever the source collection grows beyond ten elements, since iteration overhead dominates past that point
  • When the resource also sets ordinary top-level arguments alongside the generated blocks
  • Any time the source is a typed variable rather than an inline literal collection

How does the ordering of the for_each source affect a dynamic block?

  • Reordering a list source can churn the plan; a set or stable order avoids that noise
  • Ordering has no effect at all — dynamic blocks always sort their generated output alphabetically
  • A reordered source causes Terraform to skip the resource entirely on the next apply
  • Ordering only matters for map sources and never has any effect on lists

You got correct