Terraform Dynamic Blocks
Learn how Terraform dynamic blocks generate nested configuration, where they fit well, how to use them safely, and when they make code too complex.
What Are Dynamic Blocks?
Terraform dynamic blocks let you generate repeated nested blocks inside a resource, data source, provider, or provisioner configuration. They are useful when the block you want to repeat is not a top-level resource but a nested configuration block such as ingress, egress, statement, or another provider-defined structure.
A normal resource block with count or for_each repeats the entire resource. A dynamic block repeats just one nested portion inside the resource.
That is the key idea.
For example, an AWS security group can contain multiple ingress blocks. If the number of ingress rules varies by environment or is driven by input data, hardcoding every ingress block can become repetitive. A dynamic block lets Terraform generate those nested blocks from a collection.
Why Dynamic Blocks Exist
Many provider schemas contain nested blocks rather than flat arguments. Examples include:
- security group
ingressandegressrules - IAM policy statements or conditions in some patterns
- load balancer listener actions
- route definitions
- container definitions in some structured providers
If the number of nested blocks is fixed and small, writing them directly is often best. But when the structure is variable, duplicated, or based on input data, dynamic blocks reduce repetition.
The goal is not to make Terraform feel like a general-purpose programming language. The goal is to keep configuration declarative while still allowing repeated nested structures.
Dynamic Block Syntax
Here is the general form:
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
Break it down:
dynamic "ingress"says Terraform will generate one or moreingressblocksfor_eachdecides how many blocks to createcontent { ... }defines what each generated block contains- the iteration object is named after the block label, so
ingress.valuerefers to the current item
This syntax can feel unusual at first because it is not the same each.key/each.value pattern you use on resources. Inside a dynamic block, the iterator defaults to the block label unless you explicitly change it.
A Basic Security Group Example
Suppose you want to create a security group with several ingress rules based on a variable.
variable "ingress_rules" {
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
default = [
{
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = aws_vpc.main.id
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
Terraform will generate two ingress blocks, one for port 80 and one for port 443.
This is the most common beginner use case for dynamic blocks.
Using the Current Item Inside content
The current item is usually accessed through <block_label>.value.
In the previous example:
ingress.value.from_portingress.value.to_portingress.value.protocolingress.value.cidr_blocks
If the collection is a map, you can also use the key.
Terraform also supports an explicit iterator argument if you want a different name:
dynamic "ingress" {
for_each = var.ingress_rules
iterator = rule
content {
from_port = rule.value.from_port
to_port = rule.value.to_port
}
}
This can improve readability, especially when nested dynamic blocks are involved.
Dynamic Blocks in Security Groups
Security groups are a great teaching example because AWS security groups often need several nearly identical nested blocks.
Hardcoded approach
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
This is perfectly fine when the rule set is small and fixed.
Dynamic approach
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = aws_vpc.main.id
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
description = ingress.value.description
}
}
}
This is better when:
- rules differ across environments
- a module caller should define the rules
- the number of rules is not fixed
- you want to avoid repeating the same nested structure many times
Dynamic Blocks in IAM Policies
Dynamic blocks can also help when nested policy fragments vary by input.
For example, imagine a module builds multiple statements from a collection.
variable "policy_statements" {
type = list(object({
sid = string
actions = list(string)
resources = list(string)
}))
}
data "aws_iam_policy_document" "app" {
dynamic "statement" {
for_each = var.policy_statements
content {
sid = statement.value.sid
actions = statement.value.actions
resources = statement.value.resources
}
}
}
This pattern is helpful when different callers need different policy statements but the overall document structure remains the same.
Why this matters
Instead of hardcoding five statement blocks and editing them manually for each project, you let input data describe the policy shape.
That makes modules more reusable while still keeping policy logic explicit.
Nesting Dynamic Blocks
Terraform also supports nested dynamic blocks when the provider schema contains repeated nested blocks inside repeated nested blocks.
Example pattern:
dynamic "statement" {
for_each = var.policy_statements
content {
sid = statement.value.sid
dynamic "condition" {
for_each = statement.value.conditions
content {
test = condition.value.test
variable = condition.value.variable
values = condition.value.values
}
}
}
}
This is powerful, but it can become difficult to read quickly.
When nested dynamic blocks are worth it
Use them when:
- the provider schema is genuinely nested
- the data model is structured and meaningful
- the repetition would otherwise be large and error-prone
When nested dynamic blocks are risky
Avoid them when:
- the configuration becomes hard to understand
- a few explicit blocks would be clearer
- the abstraction hides important security or networking intent
Dynamic Block vs Multiple Hardcoded Blocks
This is one of the most important design decisions.
| Approach | Best when | Tradeoff |
|---|---|---|
| Hardcoded nested blocks | Small, fixed configuration | More repetition |
| Dynamic blocks | Variable or input-driven nested configuration | More abstraction |
Hardcoded blocks are often clearer
If you only have two ingress rules and they will never change, writing them directly is often the best answer. Anyone reading the resource sees exactly what is allowed.
Dynamic blocks are often better for reusable modules
If a module should accept a caller-defined set of rules, a dynamic block usually gives the right balance of flexibility and structure.
When NOT to Use Dynamic Blocks
Dynamic blocks are useful, but they are easy to overuse.
Do not use them just because you can. They are not automatically better than explicit blocks.
Avoid dynamic blocks when the config is tiny and fixed
This is too abstract for little gain:
dynamic "ingress" {
for_each = [80, 443]
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
It works, but two explicit ingress blocks may be easier for a beginner to read.
Avoid dynamic blocks when they hide important intent
Security policies and firewall rules are high-impact infrastructure. If abstraction makes them harder to audit, it may not be worth it.
Avoid dynamic blocks when input complexity becomes the real problem
Sometimes a large, deeply nested variable structure is harder to maintain than a few repeated blocks. In those cases, simplify the interface instead of adding more dynamic logic.
A Complete Practical Example
Here is a realistic security group module pattern.
variable "name" {
type = string
}
variable "vpc_id" {
type = string
}
variable "ingress_rules" {
type = list(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
}
resource "aws_security_group" "this" {
name = var.name
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.ingress_rules
iterator = rule
content {
description = rule.value.description
from_port = rule.value.from_port
to_port = rule.value.to_port
protocol = rule.value.protocol
cidr_blocks = rule.value.cidr_blocks
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
This keeps the security group reusable while leaving the default egress rule explicit and easy to audit.
Best Practices for Dynamic Blocks
1. Use them for repeated nested blocks, not for everything
If the entire resource is repeated, count or for_each is usually the right tool. Dynamic blocks solve a different problem.
2. Keep input data well-typed
List or map objects with clear fields make dynamic blocks easier to read and validate.
3. Consider an explicit iterator
If the default label-based iterator is confusing, rename it to something clearer.
4. Prefer explicit blocks when the structure is small
Clarity is often more valuable than removing a few lines.
5. Be extra careful with security-related abstractions
Anything involving ports, CIDRs, principals, or policy statements should remain easy to inspect.
Common Beginner Mistakes
Mistake 1: Using dynamic blocks to repeat whole resources
Dynamic blocks repeat nested blocks, not top-level resources.
Mistake 2: Making simple configuration harder to read
If the dynamic version feels more complicated than the hardcoded version, it probably is.
Mistake 3: Confusing iterators
Inside dynamic "ingress", the current item is ingress.value unless you define another iterator name.
Mistake 4: Over-nesting everything
Nested dynamic blocks can be valid, but they should serve a clear purpose.
Final Thoughts
Terraform dynamic blocks are a focused tool for a specific job: generating repeated nested configuration. They are especially useful in reusable modules where nested blocks such as ingress rules or IAM statements vary based on input data.
The most important ideas are:
- dynamic blocks repeat nested blocks, not whole resources
for_eachdecides how many nested blocks are generatedcontentdefines the body of each generated block- readability matters just as much as reducing repetition
If explicit blocks are clearer, use explicit blocks. If nested configuration truly needs to be driven by data, dynamic blocks can make Terraform modules much more flexible without losing structure.
Knowledge Check
Question 1: Purpose
What problem do Terraform dynamic blocks primarily solve?
Question 2: Syntax
Inside dynamic "ingress" { ... }, which expression normally accesses the current item's value?
Question 3: Overuse
When should you usually avoid dynamic blocks?