Dynamic Blocks in Terraform
Learn how to use dynamic blocks in Terraform to generate repeated nested blocks from collections, reducing duplication and keeping your code clean. This lesson explains the problem dynamic blocks solve, the core concept and mental model, step-by-step syntax, and a hands-on walkthrough. It compares dynamic blocks with a
Focus: use dynamic blocks for repeated resources
You know the drill: you’re writing Terraform for a set of security group rules, a list of ingress ports, or a handful of tags that need to become nested blocks. You start copy–pasting the same block five times, changing only one value. It works, but the moment the spec changes, you have to edit every copy — and heaven forbid you forget one. That’s the pain dynamic blocks solve: they let you generate repeated nested blocks from a collection, so your code stays DRY, readable, and easy to update in one place.
The problem this lesson solves
Terraform’s resource syntax is rigid: most resources accept nested blocks like ingress, rule, tag, or setting. When you need several of those, the instinct is to write them out manually. That approach breaks down fast:
- Duplication – You repeat nearly identical blocks, often dozens of times across a configuration.
- Maintenance hell – A change to port numbers, names, or values means editing every copy.
- Resource bloat – More code to read, review, and debug means slower, riskier deployments.
- Inconsistency – It’s easy to miss one block or introduce a typo in a single rule.
The core problem: you need repeated nested blocks, but Terraform doesn’t allow variables directly inside those blocks. Dynamic blocks are the escape hatch.
Core concept / mental model
Think of a dynamic block as a template with a for loop baked in. Instead of writing five identical ingress blocks, you declare one dynamic "ingress" block and give it a list of objects. Terraform expands it into the final resource definition before it talks to the provider.
A mental model that helps: as if you wrote a for_each loop inside the resource itself. The dynamic block has three parts:
for_each– the collection (list or map) that drives the loop.iterator– (optional) the name of the loop variable (defaults to the block label).content– the block body, where you useiterator.valueto inject data.
Here’s the big insight: dynamic blocks only work inside a resource or data source, not at the top level. They generate nested blocks — they can’t create new resources. That distinction keeps your mental model honest: dynamic blocks are for block repetition, not resource repetition (that’s for_each and count).
How it works step by step
Let’s break the syntax down piece by piece.
1. The dynamic block placeholder
Inside a resource, you write:
resource "aws_security_group" "web" {
name = "web-sg"
dynamic "ingress" {
for_each = var.allowed_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
2. The for_each collection
for_each can be a list or a map. If it’s a list of strings (like port numbers), you use ingress.value to get the current element. If it’s a map, you also get ingress.key.
3. The content block
Inside content, you reference the iterator just like you would in a for_each expression. The iterator’s name defaults to the block label (ingress in the example), but you can override it with iterator = "rule" to avoid confusion when nesting loops.
4. Nested dynamic blocks
You can nest dynamic blocks inside dynamic blocks. Each level gets its own iterator, so name them clearly to keep the code readable.
resource "aws_autoscaling_group" "example" {
dynamic "tag" {
for_each = var.tags
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
}
5. When Terraform expands the block
Terraform evaluates the for_each collection, iterates over it, and builds a list of nested blocks. It does this during the plan phase, so terraform plan will show you exactly how many blocks will be created.
The full lifecycle: write dynamic block → terraform validate checks syntax → terraform plan expands the loop → provider receives the final resource definition.
Hands-on walkthrough
Let’s build a real example: an AWS security group with a dynamic set of ingress rules.
Step 1 – Define your variables
variable "ingress_ports" {
type = list(number)
default = [80, 443, 8080]
}
Step 2 – Write the dynamic block
resource "aws_security_group" "web" {
name = "web-sg"
description = "Allow HTTP, HTTPS, and app port"
dynamic "ingress" {
for_each = var.ingress_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Step 3 – Add a map for richer rules
If you need different protocols per port, switch to a map:
variable "ingress_rules" {
type = map(object({
port = number
protocol = string
}))
default = {
http = { port = 80, protocol = "tcp" }
https = { port = 443, protocol = "tcp" }
}
}
resource "aws_security_group" "web" {
name = "web-sg"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
cidr_blocks = ["0.0.0.0/0"]
}
}
}
Step 4 – Run it
Save the files, run terraform init, then terraform plan. You’ll see the security group with three (or more) ingress blocks expanded. The plan output shows each rule explicitly — confirm the list matches your intent.
Pro tip: Use
terraform consoleto preview yourfor_eachexpression:[for p in var.ingress_ports : p]will show you the exact list the loop will use.
Compare options / when to choose what
Dynamic blocks aren’t the only way to handle repetition. Here’s how they stack up against alternatives:
| Approach | Best for | Pros | Cons |
|---|---|---|---|
| Dynamic blocks | Repeated nested blocks within a single resource | Clean, keeps logic inside the resource | Slightly harder to read when deeply nested |
for_each on resources |
Creating multiple entire resources | Scales across many resource instances | Can’t touch nested blocks inside a resource |
| Static blocks | Fixed, one-off rules | Simplest to read | Duplicates code when lists grow |
| Modules | Reusable chunks of configuration | Encapsulates logic, reduces duplication | More overhead for small cases |
When to prefer dynamic blocks:
- The repetition is inside a resource (e.g., security group rules, IAM policies).
- The list of blocks changes based on variables or data sources.
- You want to avoid maintaining hundreds of copied blocks.
When NOT to use them:
- If you only need 2–3 static blocks, plain blocks are simpler.
- If you need to create several resources, use for_each or count on the resource itself.
- If the nested blocks have wildly different structures, dynamic blocks with if may become hard to read — consider splitting the resource.
Troubleshooting & edge cases
1. “The given for_each argument value is unsuitable”
This happens when you pass a null or a non-collection value. Make sure your variable is a list or map, and if you use optional, provide a default or handle null:
variable "ports" {
type = list(number)
default = []
}
2. Empty collection results in zero blocks
If for_each = [] or {}, the dynamic block simply disappears. That’s usually what you want, but it can silently remove all rules. Check your inputs.
3. Iterator name clashes
If you nest dynamic blocks and both use the same label, the inner loop shadows the outer. Use iterator to disambiguate:
dynamic "rule" {
iterator = "outer_rule"
for_each = var.outer
content {
dynamic "sub_rule" {
iterator = "inner_rule"
for_each = outer_rule.value.sub_rules
content {
# use inner_rule.value
}
}
}
}
4. Using if inside dynamic blocks
You can conditionally include a block with if in the for_each expression:
dynamic "ingress" {
for_each = var.enable_https ? [443] : []
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
}
}
5. Plan looks wrong
Common cause: the variable’s default value changes between runs. Always verify the for_each expression in the plan output. Use terraform state list to see actual created resources.
What you learned & what's next
You now understand how to use dynamic blocks for repeated resources — the core idea of generating nested blocks from collections, the step-by-step syntax, and when to choose it over for_each or static blocks. You practiced with an AWS security group example and saw how to handle edge cases like empty collections and iterator name clashes.
Next in the Terraform foundations track, you’ll tackle variable validation and preconditions, ensuring your inputs are safe before Terraform even plans. That lesson builds directly on the collections you used here — the same values you loop over can be validated upfront, preventing bad configurations before they become infrastructure changes.
Practice recap
Try this mini exercise: define a variable ingress_rules as a map of objects with port and protocol, then write a resource aws_security_group that uses a dynamic block to create an ingress rule for each. Run terraform plan and verify the output expands to the correct number of rules. Then change one value in the map and see how quickly the plan updates — that’s the maintenance win.
Common mistakes
- Using
for_eachon a resource when you actually need to repeat nested blocks — they serve different purposes. - Forgetting that
for_eachrequires a collection — passingnullor a scalar value causes a plan-time error. - Overriding the iterator name but still referencing the old label — the default matches the block label, but after you change
iterator, you must use the new name. - Assuming dynamic blocks create new resources — they only generate nested blocks inside a single resource; use
countorfor_eachon the resource itself for that.
Variations
- Use a
mapinstead of a list infor_eachto accesskeyandvaluefor richer nested block attributes. - Nest dynamic blocks to handle multi-level structures, using explicit
iteratornames to keep your code readable. - Combine dynamic blocks with conditional expressions (
if) to include or exclude blocks based on variables or data sources.
Real-world use cases
- Generate firewall rules in an AWS security group from a variable list of ports and protocols.
- Create IAM policy statements with multiple actions per resource using a dynamic block over a map of permissions.
- Define autoscaling group tags from a map of key–value pairs, ensuring each tag propagates to launched instances.
Key takeaways
- Dynamic blocks let you generate repeated nested blocks from a collection, reducing duplication and maintenance burden.
- The
for_eachargument must be a list or map; an empty collection produces zero blocks. - Use the
iteratorargument to disambiguate nested loops and avoid shadowing. - Dynamic blocks are not a replacement for
for_eachon resources — they operate only inside a resource or data source. - Always inspect
terraform planoutput to confirm how many blocks were expanded from your dynamic block.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.