Dynamic Blocks
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.
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.
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.
- Generating two or three static nested blocks with a dynamic block, trading clear, greppable config for indirection that buys nothing.
- Confusing the iterator's
.valuescope 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.
- 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_eachto avoid plan churn from reordering. - Rename the iterator with
iterator =when nesting, so each level's.valueis unambiguous.
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?
ingressis 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
ingressblock per security group resource
Inside a dynamic "ingress" block, what does the iterator expose for each element?
.keyand.valuefor the current element of thefor_eachcollection- The full set of the resource's computed attributes as they exist after apply
- A positional counter named
count.indextracking 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