Chapter 5: The Language — Variables, Outputs, Expressions
Topic 34

Dynamic Blocks

Language

A dynamic block generates repeated nested blocks inside a resource from a collection — the tool for when a single resource needs N child blocks you do not want to write by hand. On GCP that is multiple allow rules in a google_compute_firewall, several env blocks in a Cloud Run container, or repeated IAM binding blocks within one resource.

The catch is that a dynamic block trades readability for brevity. Past a point — nested dynamics, dynamics over complex objects — a static block or a for_each on the whole resource is the clearer tool. The distinction the rest of this topic draws, between repeating a nested block and creating a whole resource, is the one that keeps configurations readable.

The Mechanism

A dynamic block has three parts: a label naming the nested block type it generates, a for_each supplying the collection, and a content block that is the template for each generated block. A dynamic "env" over a map of environment variables inside a Cloud Run container generates one env block per map entry — turning a variable-length set of blocks into data.

A dynamic env block in Cloud Run
template {
  containers {
    image = "gcr.io/hatch-pipeline-dev/event-processor"
    dynamic "env" {
      for_each = var.env_vars
      content {
        name  = env.key
        value = env.value
      }
    }
  }
}

One input map drives every env block, so the event-processor service's environment is a single variable instead of a column of hand-written blocks.

The Iterator

Each generated block exposes an iterator named after the block label: inside the content of a dynamic "env", you reference env.key and env.value to fill the block from the current element. When the label collides with something else in scope, an iterator = e renames it so you reference e.key instead. The iterator is how each generated block reads its own element.

Firewall Allow Rules

A google_compute_firewall can carry several allow blocks, one per protocol-and-port set. A dynamic "allow" over a list of { protocol, ports } objects generates them from data, turning a variable-length rule set into an input variable instead of copy-pasted blocks. When the firewall needs three protocols in prod and one in dev, the difference is the input, not the resource.

Generating firewall allow rules from data
resource "google_compute_firewall" "ingest" {
  name    = "hatch-ingest"
  network = google_compute_network.vpc.id
  dynamic "allow" {
    for_each = var.allow_rules
    content {
      protocol = allow.value.protocol
      ports    = allow.value.ports
    }
  }
}

The rule set lives in var.allow_rules as data; the resource generates one allow block per entry. Adding a rule is editing the variable, not the resource.

Cloud Run Env and Volumes

The same pattern fits a Cloud Run service's repeated env blocks and volume mounts. Driving them with a dynamic "env" over a map lets the service's environment be a single input the caller supplies, rather than a fixed set of hand-written blocks that has to be edited every time a variable is added or removed. The resource stays the same shape regardless of how many variables the environment carries.

When a Dynamic Block Hurts

Nested dynamics, and dynamics over complex objects, become unreadable fast — at two or three levels deep, no reviewer can tell what the resource generates. And there is a sharper boundary: if you are iterating to create whole resources — N buckets, N service accounts — that is a for_each on the resource, not a dynamic block. A dynamic block only generates nested blocks within one resource; it cannot create new addressable resources.

Repeating a block vs repeating a resource
dynamic block
Repeats a nested block inside one resource — many allow rules in one firewall, many env blocks in one container.
for_each on a resource
Creates multiple whole resources, each its own addressable entry in state you can reference and target.

The Empty-Collection Case

A for_each over an empty map generates zero blocks cleanly. That is the idiomatic way to make a nested block optional — zero or more allow rules — without a separate conditional wrapping the block. Pass an empty collection and the block simply does not appear; pass entries and it generates one per entry. The optionality falls out of the collection's size.

dynamic block vs for_each on a resource

dynamic block — repeats a nested block inside one resource: multiple allow rules in one firewall, multiple env blocks in one Cloud Run container. Choose it when the repeated thing is a child block of a single resource.

for_each on a resource — creates multiple whole resources: three buckets, five service accounts, each its own addressable entry in state. Choose it when each iteration should be a separate resource you can reference and target individually.

Common Mistakes
  • Reaching for a dynamic block to create multiple whole resources — one bucket per region — when for_each on the resource is the right tool; a dynamic block only generates nested blocks within a single resource and cannot.
  • Nesting dynamic blocks two or three deep until the resource is unreadable and no reviewer can tell what it generates — at that depth, restructure the input shape or split the resource.
  • Driving a dynamic over a list and then losing track of which generated block is which when the order shifts — prefer a map so each block keys off a stable identifier.
  • Writing a dynamic block for a fixed, known set of two allow rules that never varies — the static blocks are clearer, and the dynamic only earns its place when the count is genuinely variable.
  • Forgetting that the iterator is named after the block label, then referencing each.value inside a dynamic "env" instead of env.value and getting an undefined-reference error.
Best Practices
  • Use a dynamic block only for repeated nested blocks within one resource — firewall allow rules, Cloud Run env blocks — and for_each on the resource for multiple whole resources.
  • Drive dynamic blocks from a map rather than a list so each generated block keys off a stable identifier, not a position that churns when order shifts.
  • Keep them shallow — avoid nested dynamics; if you need two levels, restructure the input shape or split the resource instead.
  • Prefer static nested blocks when the set is small and fixed, reserving dynamic blocks for genuinely variable-length collections.
  • Use an empty collection to make a nested block optional, letting zero entries produce zero blocks instead of wrapping the block in a separate conditional.
Comparable tools Pulumi native loops over child config, no special construct needed Jsonnet comprehensions Helm range over nested template fragments Kustomize patches, the overlay-based alternative

Knowledge Check

When should you use a dynamic block, and when a for_each on the resource?

  • A dynamic block for repeated nested blocks inside one resource; for_each on the resource for multiple whole resources each addressable in state
  • A dynamic block for multiple whole resources each tracked in state; for_each on the resource for the repeated nested blocks that live inside one
  • They are interchangeable and produce an identical resource address and state layout
  • A dynamic block only works at the module level, while for_each is restricted to the resource level

Why is a map-driven dynamic block more stable than a list-driven one?

  • Each generated block keys off a stable identifier rather than a position, so reordering the input does not churn the blocks
  • A list can never be passed to for_each inside a dynamic block, even after converting it to a set
  • Maps let the dynamic block generate its nested blocks in parallel during apply while list-driven ones are processed strictly serially
  • A map-driven block is cached between plans so it is not regenerated every run

How do you make a nested block like a firewall allow rule optional with a dynamic block?

  • for_each over an empty collection generates zero blocks cleanly, so the optionality falls out of the collection's size
  • Wrap the entire firewall resource in a count = 0 conditional so the unwanted allow rule disappears with it
  • Set the dynamic block's content body to null so it emits nothing
  • Dynamic blocks cannot be made optional, so you must duplicate the firewall resource once with the rule and once without it

Why are dynamic blocks nested two or three levels deep an anti-pattern?

  • At that depth no reviewer can tell what the resource generates; restructure the input shape or split the resource instead
  • Terraform rejects any dynamic block nested more than one level deep during validation
  • Nested dynamics force the whole resource to be destroyed and recreated on every apply, even when none of its arguments changed
  • They double the state file size for each additional level of nesting you add to the generated block

You got correct