Dynamic Blocks
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.
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.
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.
allow rules in one firewall, many env blocks in one container.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 — 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.
- Reaching for a dynamic block to create multiple whole resources — one bucket per region — when
for_eachon 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
dynamicover 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
dynamicblock for a fixed, known set of twoallowrules 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.valueinside adynamic "env"instead ofenv.valueand getting an undefined-reference error.
- Use a dynamic block only for repeated nested blocks within one resource — firewall
allowrules, Cloud Runenvblocks — andfor_eachon 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.
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_eachon the resource for multiple whole resources each addressable in state - A dynamic block for multiple whole resources each tracked in state;
for_eachon 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_eachis 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_eachinside 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_eachover 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 = 0conditional so the unwanted allow rule disappears with it - Set the dynamic block's
contentbody tonullso 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