Terraform Dynamic Blocks in Practice

Khimananda Oli 9 min read Virtualization
Terraform Dynamic Blocks in Practice

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.

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.

Standard Resource for_eachResource AResource BResource CCreates N separate state entriesDynamic Block IterationSingle Resource InstanceNested Block 1 (generated)Nested Block 2 (generated)Nested Block 3 (generated)One state entry, N internal blocks
Resource-level for_each creates multiple state objects, while Terraform dynamic blocks in practice generate nested arguments within a single resource.

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.

Nested Dynamic Block Evaluation FlowOuter Dynamic Blockiterator = statementfor_each = var.rulesContent Block ExpansionGenerates N parent blocksEach contains inner dynamicInner Dynamic Blockiterator = conditionfor_each = statement.value.conditionsExpanded Output Structurestatement[0]condition[0]condition[1]statement[1]condition[0]statement[2]condition[0]condition[1]
Nested Terraform dynamic blocks in practice require unique iterator names at each level to prevent variable shadowing and scope errors.

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.

PitfallSymptomMitigation Strategy
Iterator shadowingInner block references outer scope values incorrectlyAlways declare explicit, unique iterator names at every nesting level
List vs Map orderingPlan shows destructive recreation due to index shiftsPrefer maps with stable keys over lists for any block affecting resource identity
Empty collection handlingValidation errors when no blocks should existUse for_each = length(var.items) > 0 ? var.items : {} or provider-native defaults
Complex object validationRuntime failures deep inside nested content blocksAdd validation blocks on input variables with descriptive error messages
State drift detectionPerpetual diffs after manual console changesImplement 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.

Static vs Dynamic Block Decision MatrixStart: Need Repeated Blocks?Count known & fixed (<=3)?YESNOUse Static BlocksBetter readability & audit trailVaries by environment/input?YESNOUse Dynamic BlocksMap keys + explicit iteratorsReconsider DesignMay not need repetitionRule: Prefer clarity over cleverness. Dynamic blocks earn their complexity through genuine variability.
Decision framework for applying Terraform dynamic blocks in practice versus maintaining explicit static configuration for auditability.

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.

Frequently Asked Questions

Dynamic blocks generate repeated nested configuration blocks programmatically using for_each, eliminating manual duplication in resources like security groups or IAM policies.

Avoid them when block count is static or readability suffers. Prefer explicit blocks for simple configurations to maintain clear infrastructure code and easier state debugging.

Use the dynamic keyword with a label matching the nested block name, then define iterator and content arguments inside to map variables to attributes properly.

Yes, Terraform supports nested dynamic blocks since version 0.12. Ensure each level has unique iterator names to prevent variable shadowing and scope conflicts during plan operations.

No, dynamic blocks only influence configuration generation at plan time. The resulting expanded blocks are stored normally in state without any special metadata or structural differences.

for_each iterates over maps or sets allowing keyed access, while count uses integer indices. Use for_each in dynamic blocks to preserve meaningful identifiers during updates.

Check that the for_each expression evaluates to a non-empty collection. Null values, empty lists, or conditional expressions returning empty maps cause silent zero-iteration behavior without errors.

Yes, locals are fully accessible within dynamic block content sections. Reference them directly by name to compute attribute values or filter iteration collections before expansion.

Dynamic blocks work with any provider supporting nested configuration blocks. Compatibility depends on schema structure, not provider version, as expansion occurs during core configuration processing.

Run terraform console to test for_each expressions independently. Use terraform plan with detailed output to inspect generated blocks and verify iterator variable assignments match expectations.

No, dynamic blocks have no direct cost impact. They only change how configuration is authored; actual resource provisioning and billing depend solely on the expanded infrastructure definitions.

Wrap the dynamic block itself in a conditional for_each expression. Return an empty collection when conditions fail to skip block generation completely rather than creating empty blocks.

Excessive dynamic blocks slow plan phases due to HCL evaluation overhead. Keep iterations reasonable and prefer data source lookups over complex inline transformations for large collections.

Add validation blocks to input variables feeding dynamic blocks. Check list lengths, required keys, or value formats before iteration to catch configuration errors early in planning.

Terraform imposes no hard nesting limit, but practical readability degrades beyond three levels. Refactor deeply nested dynamics into modules or separate resources for maintainability in 2026.