
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Repetitive configuration is the silent killer of maintainable Infrastructure as Code, turning simple security group updates into error-prone copy-paste marathons. Terraform dynamic blocks in practice solve this by letting you iterate over nested arguments like ingress, tag, or setting using standard HCL loops instead of duplicating code. This guide moves beyond basic syntax to show you how to implement dynamic blocks safely, handle complex nesting, and avoid the state drift issues that plague poorly designed iterations.
for_each argument inside a dynamic label. Instead of writing multiple identical ingress or tag blocks, you define a single dynamic block that iterates over a map or list variable, producing clean, DRY HCL that scales with your infrastructure requirements.How do Terraform dynamic blocks work compared to standard for_each?
A common point of confusion for engineers adopting infrastructure as code with Terraform is distinguishing between resource-level iteration and block-level iteration. Standard for_each creates multiple distinct resources (e.g., five separate aws_instance resources). Dynamic blocks, conversely, create multiple nested arguments within a single resource instance. Understanding this distinction prevents architectural mistakes that lead to state file bloat or unintended resource replacement.
The mechanism relies on two key components: the for_each argument providing the collection to iterate over, and the content block defining the structure of each generated element. When Terraform evaluates the configuration, it expands the dynamic block into discrete HCL blocks before applying changes. This expansion happens during the plan phase, meaning you can inspect exactly what will be created before execution. For teams managing Amazon EKS clusters or complex VPCs, this visibility is critical for safe deployments.
When to use dynamic blocks versus static configuration
- Use dynamic blocks when the number of nested items varies based on input variables, environment-specific data, or computed values from other resources.
- Use static blocks when the structure is fixed, compliance-mandated, or unlikely to change across environments. Static blocks are easier to read and audit for SOC 2 evidence collection.
- Avoid dynamic blocks for simple cases with only 2–3 known items where explicit blocks improve readability without significant duplication.
How do you write a dynamic block for AWS security group ingress rules?
Security groups represent the most frequent use case for Terraform dynamic blocks in practice. Hardcoding ingress rules leads to massive duplication across staging, production, and disaster recovery environments. The pattern below uses a map variable to drive rule generation, allowing environment-specific overrides without modifying the resource definition itself.
variable "ingress_rules" {
type = map(object({
port = number
protocol = string
cidr_blocks = list(string)
description = string
}))
default = {
https = {
port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTPS from internet"
}
ssh_admin = {
port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
description = "SSH from corporate VPN"
}
}
}
resource "aws_security_group" "app" {
name = "${var.environment}-app-sg"
description = "Application security group"
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.ingress_rules
iterator = rule
content {
from_port = rule.value.port
to_port = rule.value.port
protocol = rule.value.protocol
cidr_blocks = rule.value.cidr_blocks
description = rule.value.description
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
} Note the explicit iterator = rule declaration. While optional when the dynamic block label matches the iterator name, specifying it prevents shadowing conflicts in nested scenarios and improves code clarity during peer reviews. In my experience auditing infrastructure for Nepali fintech companies handling sensitive transactions, explicit iterators reduce misconfiguration risks during compliance assessments.
Handling conditional rules and filtering
Sometimes you need to exclude certain rules based on environment flags or feature toggles. Use the for expression with an if clause directly in the for_each argument rather than wrapping the entire dynamic block in a conditional:
dynamic "ingress" {
for_each = {
for k, v in var.ingress_rules : k => v
if var.enable_ssh || k != "ssh_admin"
}
iterator = rule
content {
from_port = rule.value.port
to_port = rule.value.port
protocol = rule.value.protocol
cidr_blocks = rule.value.cidr_blocks
description = rule.value.description
}
} This approach keeps the filtering logic co-located with the iteration, making it obvious which rules are conditional. It also avoids creating empty dynamic blocks that could confuse plan output interpretation.
How do you nest dynamic blocks inside other dynamic blocks?
Certain cloud resources require deeply nested structures. AWS WAFv2 web ACLs, CloudFront distributions, and Kubernetes ingress controller configurations often demand dynamic blocks within dynamic blocks. Nesting introduces complexity around iterator naming and scope resolution that catches many practitioners off guard.
The golden rule for nesting is simple: every dynamic block at a different depth must have a unique iterator name. If your outer block uses iterator = statement, your inner block cannot reuse that identifier. Failing to rename causes silent failures where the inner block references the wrong scope, producing valid but incorrect plans.
resource "aws_wafv2_web_acl" "main" {
name = "${var.environment}-web-acl"
scope = "REGIONAL"
default_action {
allow {}
}
dynamic "rule" {
for_each = var.waf_rules
iterator = statement
content {
name = statement.key
priority = statement.value.priority
action {
dynamic "block" {
for_each = statement.value.action == "block" ? [1] : []
iterator = _
content {}
}
dynamic "allow" {
for_each = statement.value.action == "allow" ? [1] : []
iterator = _
content {}
}
}
statement {
dynamic "ip_set_reference_statement" {
for_each = lookup(statement.value, "ip_set_arn", null) != null ? [1] : []
iterator = ip_ref
content {
arn = ip_ref.value
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = statement.key
sampled_requests_enabled = true
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${var.environment}-web-acl-default"
sampled_requests_enabled = true
}
} This WAFv2 example demonstrates three patterns essential for production use: conditional single-block generation using ternary operators with singleton lists, lookup-based optionality for nested statements, and consistent iterator naming conventions. When building reusable Terraform modules, encapsulate these patterns behind validated variables to prevent consumers from constructing invalid nested structures.
What are the common pitfalls when using Terraform dynamic blocks?
Despite their utility, dynamic blocks introduce failure modes absent in static configuration. Recognizing these patterns early saves hours of debugging during incident response or compliance audits.
| Pitfall | Symptom | Mitigation Strategy |
|---|---|---|
| Iterator shadowing | Inner block references outer scope values incorrectly | Always declare explicit, unique iterator names at every nesting level |
| List vs Map ordering | Plan shows destructive recreation due to index shifts | Prefer maps with stable keys over lists for any block affecting resource identity |
| Empty collection handling | Validation errors when no blocks should exist | Use for_each = length(var.items) > 0 ? var.items : {} or provider-native defaults |
| Complex object validation | Runtime failures deep inside nested content blocks | Add validation blocks on input variables with descriptive error messages |
| State drift detection | Perpetual diffs after manual console changes | Implement lifecycle policies and automated drift detection in CI pipelines |
The list-versus-map problem deserves special emphasis. When you iterate over a list, Terraform tracks items by numeric index. Inserting or removing an item mid-list shifts all subsequent indices, triggering unnecessary destroy/create cycles for stateful resources. Maps with semantic keys (https, ssh_admin) remain stable regardless of insertion order. This matters profoundly for resources like database users, IAM policies, or Kubernetes RBAC bindings where recreation causes downtime or permission gaps.
Debugging dynamic block expansion
When plans produce unexpected results, use terraform console to test your iteration expressions independently before applying:
> [for k, v in var.ingress_rules : "${k}: ${v.port}"]
[
"https: 443",
"ssh_admin: 22",
]
> {for k, v in var.ingress_rules : k => v if v.port != 22}
{
"https" = {
"cidr_blocks" = ["0.0.0.0/0"]
"description" = "Allow HTTPS from internet"
"port" = 443
"protocol" = "tcp"
}
} This interactive validation catches filtering logic errors and type mismatches before they reach production. For teams operating under ISO 27001 or SOC 2 frameworks, documenting these validation steps provides auditable evidence of change control rigor.
When should you avoid Terraform dynamic blocks entirely?
Not every repetition warrants abstraction. Overusing dynamic blocks creates configurations that are technically DRY but cognitively opaque. During my years helping Nepal-based startups scale to global standards, I've seen teams spend more time deciphering clever abstractions than shipping features. Know when simplicity wins.
Avoid dynamic blocks when compliance requirements demand explicit, human-readable configuration. Auditors reviewing SOC 2 Type II evidence prefer seeing exact ingress rules spelled out rather than tracing through variable indirection. Similarly, skip dynamics for foundational networking primitives (VPC CIDRs, subnet definitions) where accidental modification carries catastrophic risk. The cognitive overhead of understanding the abstraction rarely justifies the line savings for infrastructure that changes quarterly at most.
Also reconsider dynamics when your iteration source comes from remote state or API calls with unpredictable schemas. Brittle data contracts break plans unexpectedly. In these cases, normalize the data upstream using locals or preprocessing scripts before feeding it into dynamic blocks. Defensive coding practices matter more than elegant HCL when operating production systems serving real users.
Applying Terraform Dynamic Blocks in Practice Safely
Mastering Terraform dynamic blocks in practice means balancing abstraction with operational safety. Start by converting your most duplicated nested blocks—security group rules, IAM policy statements, Kubernetes manifest labels—into map-driven dynamics with explicit iterators. Validate inputs rigorously, prefer maps over lists for stateful resources, and resist the urge to abstract everything. The goal isn't minimal code; it's infrastructure your team can understand, modify, and defend during incidents or audits six months from now.
If your team struggles with dynamic block adoption or needs help designing compliant, scalable Terraform architectures, reach out to discuss your infrastructure challenges. Whether you're preparing for SOC 2 certification, optimizing multi-cloud costs, or building platform engineering foundations, experienced guidance accelerates safe adoption far faster than trial-and-error in production.