A Terraform dynamic block replaces repeated nested blocks
What is a Terraform dynamic block?
A dynamic block is a Terraform language construct that generates multiple nested blocks inside a resource block, data block, provider block, or provisioner block, based on a complex value such as a list or map. It isn't a resource type of its own, but rather a way to iterate inside a resource's configuration, producing one generated block for each element in the collection you give it.
The basic Terraform dynamic block syntax
A dynamic block has a consistent shape wherever it appears.
The dynamic keyword is followed by a label matching the type of nested block being generated, ingress, for example, or tag. Inside that, a for_each argument points to the list or map to iterate over, and a content block defines what each generated block should contain.
Here is how that looks when generating one firewall rule for each entry in a list of allowed protocols:
Within the content block, Terraform exposes the current element through a temporary variable named after the block label by default, so a dynamic "allow" block gives you allow.value and, for maps, allow.key as well.
With a list, for_each exposes only the current element value inside the content block; with a map, it exposes both a key and a value for the current element.
Maps are the natural fit for anything key-value shaped, like tags, and lists suit ordered collections like ingress rules.
If that default naming clashes with something already in scope, or you're nesting one dynamic block inside another and need to keep the two clearly separated, the iterator argument lets you override the temporary variable name.
What a Terraform dynamic block actually does
A dynamic block's output only becomes visible once you run terraform plan. The raw HCL shows the loop, the for_each argument, and the content block, not the generated blocks themselves. As a result, anyone reading the configuration has to trace through the referenced list or map to see what will actually get created.
A dynamic block only expands nested blocks inside a single resource, data, provider, or provisioner instance, meaning the generated blocks don't get an address of their own in state or in terraform plan output the way for_each-created resource instances do.
They simply become part of that one instance's ingress, tag, or other nested-block attributes, however many the loop produced.
Dynamic blocks are supported inside those four block types, but only for arguments that are themselves nested blocks; they can't generate top-level resource arguments, and Terraform's own meta-arguments, like lifecycle, can't be generated with a dynamic block either.
An example of a Terraform dynamic block in action
Get the most out of dynamic blocks with lifecycle rules on storage buckets, as the number of rules a bucket needs rarely stays fixed for long.
Here's a full aws_s3_bucket_lifecycle_configuration resource built around a dynamic rule block, driven by a list of rule objects.
The lifecycle_rules variable is a list, and each element is an object holding a rule ID, the object prefix the rule applies to, and the number of days after which matching objects expire.
Inside the aws_s3_bucket_lifecycle_configuration resource, the dynamic rule block loops over that list, and for each element value, it builds one rule block using the same code, referencing rule.value to pull out the relevant fields.
Add or remove an entry from the list, and the number of generated rule blocks changes with it, while no other part of the resource needs to be touched.
Notice that the abort-incomplete-uploads rule never varies in this example, so it sits outside the dynamic block as an ordinary static block. A single resource can mix dynamic nested blocks with static ones wherever you need it to, rather than forcing everything through one dynamic block.
Dynamic block using a map
A list suits lifecycle rules, but tags are a common case where you should turn to a map, as tags are inherently key-value pairs rather than an ordered set.
Add an entry to the resource_tags map, and the outer dynamic block produces an additional tag block automatically, without any change to the resource itself. It's the same pattern for any repeatable nested block where a map of settings drives the configuration.
Common mistakes users make with Terraform dynamic blocks
Dynamic blocks solve a real problem, but you'll encounter a few practical issues when they're in production Terraform code.
It becomes more challenging to trace nested dynamic blocks (one dynamic block inside another) through terraform plan output.
As each level of nesting introduces its own iterator object, working out which generated block came from which list element index could force you to read through several levels of for_each before finding the source.
Name the iterator argument explicitly on at least one level to avoid confusion once there's more than one dynamic block in play. Do this carefully, and keep nesting shallow even though multiple levels are supported.
Debugging is another issue. When one generated block among many fails to apply, working out which entry in the underlying list or map caused it isn't always easy from the error message alone – particularly with large blocks generated from a long list.
It's also important not to overuse dynamic blocks, as a dynamic block that's built around a static block that rarely, if ever, changes adds a layer of complexity without buying much back.
If the local variable or input variable driving the loop only ever has one practical value, the dynamic block is mostly working against readability rather than for it.
The nearest alternative to a Terraform dynamic block
Terraform resources are normally written with a fixed, static block for each nested argument.
An aws_security_group resource, for example, might have two or three ingress blocks written out directly. A dynamic block replaces that repetition with one block that says, in effect, produce one of these for every item in this list.
If the number of nested blocks a resource needs depends on an input variable, a map of settings, or something that changes across multiple environments, writing each block out manually isn't an effective use of time. In each case, the alternative is either duplicating near-identical blocks across multiple resources or generating configuration outside of Terraform entirely. A dynamic block keeps that logic inside the Terraform configuration itself, which is easier to reason about across the full infrastructure lifecycle than logic split across tools.
The most common examples are:
- Security group rules, where the number of ingress rules or egress rules genuinely differs between environments rather than staying fixed.
- Tags, where a map of key-value pairs drives how many tag blocks a resource ends up with, and the map itself might vary by team or project.
- Lifecycle rules on a storage bucket, where the number of rules depends on a list defined elsewhere in the Terraform configuration.
If the number of nested blocks is fixed and small, writing them out directly is usually the cleaner option. Three static ingress rules that never change are easier to read as three separate ingress blocks than as one dynamic block wrapping a local variable nobody can see the contents of without checking elsewhere.
Dynamic blocks shouldn't be used purely to reduce line count; a shorter resource block isn't automatically a better one. If the count never changes, you should write the blocks out manually.
Stategraph's perspective on Terraform dynamic blocks
Nested dynamic blocks can become difficult to trace through terraform plan output on a single resource, but it becomes even harder when dynamic blocks are running across a fleet of resources and multiple environments, where the gap between what the Terraform code says and what has actually been created can have a greater impact the longer infrastructure remains live.
If your team manages shared or long-lived infrastructure across multiple environments, check the actual deployed state directly. Don't just rely on reading the loop logic back out of the configuration.
Rather than replacing Terraform or dynamic blocks, or changing how you write Terraform configuration, Stategraph acts as the operational database underneath your existing state.
A dynamic block's output only becomes concrete after terraform plan or terraform apply runs. Stategraph gives visibility into what actually got deployed, resource by resource, which is a useful way to confirm that a dynamic block generated the lifecycle rules, tags, or firewall rules you expected, rather than assuming the loop behaved as intended.
It's also useful if you're trying to spot drift between what a dynamic block should have produced and what's actually running, and for auditing infrastructure without manually reconstructing the for_each and content logic that built it in the first place.
Used well, a dynamic block keeps repeated nested blocks out of a Terraform configuration and lets one block do the work of many static ones.
That convenience comes at a cost, though. The more nested dynamic blocks pile up, the harder it becomes to trace their output back to its source.
When you use them for genuinely repetitive, variable-driven blocks, dynamic blocks make infrastructure scalable; if it becomes a habit to use them, they'll just make your Terraform configuration harder to read.
More on how Stategraph handles this is in the Stategraph docs, or try Stategraph free to see how it fits into the broader Terraform workflow around state storage and governance.
Related Terraform terms
- Terraform map variables – The data structure a dynamic block most often iterates over when tags or key-value settings are involved.
- Terraform security – Covers the security patterns that shape rule-driven resources, like the lifecycle example above.
- Terraform state locking, explained – The state-locking behavior that becomes important once dynamic blocks are running across a large, shared fleet of resources.
- Terraform blast radius – What's actually at stake when a dynamic block's generated output doesn't match what a team expected.