
If your Terraform configuration is starting to repeat the same nested blocks again and again, a terraform dynamic block may be the right tool to clean it up.
Dynamic blocks help infrastructure teams write more reusable, flexible, and maintainable Terraform code. Instead of manually repeating similar nested configuration blocks, you can generate them from input variables, maps, lists, or objects.
That sounds simple, but dynamic blocks are also easy to overuse. Used well, they make Terraform modules cleaner. Used poorly, they make configurations harder to read, review, and govern.
This guide explains what Terraform dynamic blocks are, when to use them, how the syntax works, practical AWS and Azure examples, common mistakes, and how platform teams can manage dynamic Terraform patterns at scale.
What Are Dynamic Blocks in Terraform?
A dynamic block in Terraform is a way to generate repeated nested blocks inside a resource, data source, provider, or provisioner block. Terraform’s documentation describes dynamic blocks as a way to construct repeatable nested block structures programmatically when a resource type requires nested configuration.
In simple terms, a dynamic block is useful when a Terraform resource expects repeated child blocks, but the number or content of those child blocks changes depending on input.
For example, an AWS security group may need multiple ingress rules. An Azure resource may need multiple subnet, delegation, or rule blocks. A Kubernetes resource may need repeated environment variables or volume mounts.
Without dynamic blocks, you might write each nested block manually. That works for one or two blocks, but it becomes difficult to maintain when configurations vary by team, environment, region, customer, or application.
A dynamic block lets you define the pattern once and generate the repeated blocks from structured input.
Why Terraform Dynamic Blocks Matter
Terraform is built around infrastructure as code. The goal is not just to provision infrastructure, but to make infrastructure reviewable, repeatable, and easier to manage.
Repeated nested blocks create a few problems for infrastructure teams.
First, they increase code duplication. If five environments need similar rules with slightly different values, engineers often copy and paste the same blocks across multiple files.
Second, repeated blocks increase the chance of inconsistency. One environment may receive an updated configuration while another keeps the old version.
Third, duplicated code is harder to review. Pull requests become longer, and reviewers must check whether every repeated block is correct.
Dynamic blocks help reduce that duplication by allowing teams to define reusable patterns. Instead of writing ten nearly identical nested blocks, you provide a list or map of inputs and allow Terraform to generate the correct structure.
That makes dynamic blocks especially useful for module authors, platform teams, and infrastructure teams that support many environments.
When to Use Dynamic Blocks
A terraform dynamic block is most useful when you need to generate repeated nested blocks inside a resource.
Good use cases include repeated security group rules, repeated subnet configuration, repeated listener rules, repeated storage lifecycle rules, repeated access rules, repeated environment variable blocks, and repeated policy-related blocks.
Dynamic blocks are also helpful when you are building Terraform modules. A module should be flexible enough to support different teams without forcing every team to edit the module source. Dynamic blocks allow module users to pass structured input while the module generates the required nested configuration.
For example, a platform team may create one approved networking module. Different application teams can pass different subnet rules, route rules, or access settings into that module. The module stays consistent, but the configuration remains flexible.
You should consider using dynamic blocks when the repeated configuration is predictable, structurally similar, and controlled by input variables.
When Not to Use Dynamic Blocks
Dynamic blocks are not always the best answer.
If you only have one or two nested blocks and they are unlikely to change, writing them directly is often clearer. Terraform code should be easy to read, especially during review. A dynamic block that saves five lines but makes the configuration harder to understand is usually not worth it.
You should also avoid dynamic blocks when each nested block has very different logic. If every generated block needs its own special case, the dynamic block may become more confusing than the repeated configuration it replaces.
Dynamic blocks should not be used just to make Terraform look more advanced. They should solve a real maintainability problem.
A good rule is simple: use dynamic blocks when they reduce repetition without hiding important infrastructure behavior.
Terraform Dynamic Block Syntax
A dynamic block has four main parts: the block label, the collection to loop over, the iterator, and the content block.
Here is the basic structure:
dynamic "block_name" {
for_each = var.items
content {
name = block_name.value.name
value = block_name.value.value
}
}
The label after dynamic must match the nested block type expected by the resource. For example, if the resource expects an ingress block, your dynamic block should be labeled ingress.
The for_each argument tells Terraform what collection to loop through. This can be a list, map, set, or object depending on how your input variable is structured. Terraform’s for_each is commonly used to manage repeated objects from maps or sets, while dynamic blocks use the same looping idea to generate nested blocks.
The content block defines what each generated nested block should contain.
Inside the content block, Terraform gives you access to the current item. By default, the iterator uses the same name as the dynamic block label. For example, inside dynamic "ingress", you can reference ingress.value.
You can also define a custom iterator name if that improves readability.
dynamic "ingress" {
for_each = var.ingress_rules
iterator = rule
content {
from_port = rule.value.from_port
to_port = rule.value.to_port
protocol = rule.value.protocol
cidr_blocks = rule.value.cidr_blocks
}
}
This is often easier to read because rule.value explains what the loop represents.
Terraform Dynamic Block Example for AWS Security Groups
One of the most common examples of a terraform dynamic block is an AWS security group with multiple ingress rules.
Without a dynamic block, the configuration may look like this:
resource "aws_security_group" "app" {
name = "app-security-group"
description = "Security group for application traffic"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
}
This works, but it does not scale well when rules vary by environment.
With a dynamic block, the rules can come from a variable:
variable "ingress_rules" {
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
}
resource "aws_security_group" "app" {
name = "app-security-group"
description = "Security group for application traffic"
dynamic "ingress" {
for_each = var.ingress_rules
iterator = rule
content {
from_port = rule.value.from_port
to_port = rule.value.to_port
protocol = rule.value.protocol
cidr_blocks = rule.value.cidr_blocks
}
}
}
Then each environment can pass its own rules:
ingress_rules = [
{
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
},
{
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
]
This keeps the resource definition clean while allowing the inputs to change by environment.
Terraform Dynamic Block Example for Azure
Dynamic blocks are also useful in Azure configurations, especially when a resource supports repeated nested settings.
For example, an Azure Network Security Group may need multiple security rules. Instead of writing each rule manually, you can pass the rules as input.
variable "security_rules" {
type = list(object({
name = string
priority = number
direction = string
access = string
protocol = string
source_port_range = string
destination_port_range = string
source_address_prefix = string
destination_address_prefix = string
}))
}
resource "azurerm_network_security_group" "app" {
name = "app-nsg"
location = var.location
resource_group_name = var.resource_group_name
dynamic "security_rule" {
for_each = var.security_rules
iterator = rule
content {
name = rule.value.name
priority = rule.value.priority
direction = rule.value.direction
access = rule.value.access
protocol = rule.value.protocol
source_port_range = rule.value.source_port_range
destination_port_range = rule.value.destination_port_range
source_address_prefix = rule.value.source_address_prefix
destination_address_prefix = rule.value.destination_address_prefix
}
}
}
This pattern is useful when teams manage different rule sets across development, staging, production, and shared environments.
The important point is not that dynamic blocks are AWS-specific or Azure-specific. They are a Terraform language feature that works wherever the provider schema expects repeatable nested blocks.
Nested Dynamic Blocks
Some Terraform resources include nested blocks inside other nested blocks. Terraform supports nested dynamic blocks for these cases. HashiCorp’s documentation notes that dynamic blocks can be nested when provider resources define multiple levels of block structures.
Nested dynamic blocks are powerful, but they should be used carefully because readability can decline quickly.
Here is a simplified example:
dynamic "origin_group" {
for_each = var.origin_groups
iterator = group
content {
name = group.value.name
dynamic "origin" {
for_each = group.value.origins
iterator = origin
content {
name = origin.value.name
host = origin.value.host
}
}
}
}
This type of structure can be useful when each parent object contains its own list of child objects.
However, nested dynamic blocks should be reviewed closely. If the structure becomes too deep, it may be better to simplify the input model or break the logic into smaller modules.
Dynamic Blocks vs for_each
Terraform teams often confuse dynamic blocks with for_each.
The difference is important.
Use for_each on a resource or module when you want to create multiple instances of that resource or module. For example, you might use for_each to create multiple S3 buckets, multiple IAM users, or multiple virtual networks.
Use a dynamic block when you want to create multiple nested blocks inside one resource.
Terraform’s documentation explains that for expressions are used to transform collection values, but they cannot generate nested blocks by themselves. Dynamic blocks are the feature designed for generating repeated nested blocks.
So the decision is usually straightforward.
If you need multiple resources, use for_each.
If you need multiple nested blocks inside a resource, use a dynamic block.
Common Mistakes With Terraform Dynamic Blocks
The first common mistake is using dynamic blocks too early. If a direct block is clearer, use the direct block. Readability matters more than cleverness.
The second mistake is using weak variable types. Dynamic blocks work best when input variables are clearly typed. A list of objects with defined attributes is easier to validate, review, and maintain than a loosely structured variable.
The third mistake is hiding important security or networking behavior inside overly abstract inputs. For example, if a security group module accepts a very broad object and generates many rules, reviewers may miss risky changes. Infrastructure code should make important behavior visible.
The fourth mistake is not using meaningful iterator names. Default iterator names work, but custom names such as rule, subnet, origin, or policy often make the code easier to understand.
The fifth mistake is creating deeply nested dynamic logic. Terraform supports nested dynamic blocks, but heavy nesting can make the configuration hard to reason about. When logic becomes too complex, consider splitting the module or simplifying the input.
The sixth mistake is treating dynamic blocks as a replacement for governance. Dynamic Terraform code still needs review, approval, policy checks, cost visibility, and drift detection.
Best Practices for Terraform Dynamic Blocks
Start with clarity. Before adding a dynamic block, ask whether the code will become easier to understand. If the answer is no, write the nested block directly.
Use strong input types. Define variables as lists or maps of objects with clear attributes. This makes the module easier to use and reduces accidental misconfiguration.
Use custom iterators when they improve readability. A name like rule.value is usually clearer than ingress.value when the block represents a security rule.
Keep dynamic blocks close to the resource they affect. Avoid spreading the logic across too many locals, variables, and module layers unless there is a clear reason.
Document the input structure. Module users should understand what values they need to provide, what each value controls, and how changes affect the generated infrastructure.
Review the Terraform plan carefully. A dynamic block can generate many nested changes from a small input change. The plan is where those generated changes become visible before they are applied.
Use dynamic blocks for patterns, not exceptions. If every item in the collection needs special handling, the abstraction may not be the right one.
Where Dynamic Blocks Fit in Terraform Modules
Dynamic blocks are especially valuable in Terraform modules because modules are designed for reuse.
A platform team may create a standard module for application networking, storage configuration, or environment setup. Different teams can pass different inputs, but the module enforces the approved structure.
For example, one team may need two ingress rules, while another needs five. One production environment may need stricter rules than a development environment. A dynamic block lets the module support those differences without duplicating the resource logic.
This is where dynamic blocks support IaC onboarding and codification. Instead of asking every team to learn every detail of the provider resource, the platform team can expose a controlled input model. Teams provide the values they need, and the module generates the approved Terraform configuration.
That approach helps teams move faster while keeping infrastructure patterns consistent.
The Governance Angle for env zero
Dynamic blocks make Terraform more flexible, but flexibility creates governance questions.
Who is allowed to change the inputs that generate security rules? How are generated changes reviewed before they reach production? Can the team see the plan clearly before apply? Are risky changes blocked by policy? Is drift detected if someone changes the resource outside Terraform?
These questions become more important as Terraform usage grows across teams.
env zero helps teams manage that operational layer around Terraform and OpenTofu. Teams can keep using their existing HCL and modules while adding plan review workflows, approval gates, RBAC, policy controls, drift detection, cost visibility, and standardized execution across environments.
For dynamic blocks specifically, that matters because one small input change can generate multiple infrastructure changes. A clear plan-and-approval workflow helps teams understand what Terraform will create, change, or remove before the apply happens.
This is where Terraform and env zero work together. Terraform defines the infrastructure. env zero helps teams govern how that infrastructure is reviewed, approved, applied, and monitored across the organization.
Summary
Terraform dynamic blocks help infrastructure teams write cleaner, more reusable configurations when resources require repeated nested blocks.
They are useful for security rules, networking settings, lifecycle rules, access controls, and module-driven infrastructure patterns. They reduce duplication, support flexible inputs, and help platform teams create reusable Terraform modules for different environments and teams.
But dynamic blocks should be used with care. They are most effective when the repeated structure is predictable and easy to understand. Overusing them can make Terraform harder to read and harder to review.
The best approach is to use dynamic blocks where they improve clarity, define strong input types, keep reviewability in mind, and pair reusable Terraform patterns with governance workflows.
For infrastructure teams scaling Terraform across environments, dynamic blocks are not just a syntax feature. They are part of a larger move toward cleaner IaC, better module design, and more controlled infrastructure delivery.
.webp)