Terraform

Mastering Terraform Dynamic Blocks: Safely Generate Variable Configuration Blocks

2026-04-19
NicheeLab Editorial Team

The dynamic block is a language feature that lets you grow or shrink nested blocks inside a resource based on variable content.

Its purpose is different from count/for_each at the resource level: dynamic is mainly for generating nested blocks (e.g., ingress, lifecycle_rule).

Dynamic Block Basics and Syntax

dynamic is the syntax for generating a variable number of nested blocks inside a resource or data source. It takes the shape dynamic "<block-name>" { for_each = ... iterator = ... content { ... } }: for_each supplies the iteration target, and content expands each element.

iterator is optional; when omitted, the default name each is used (refer to elements with each.value). Pass a map to for_each and you get each.key/each.value; with list/set you mostly use each.value.

Important caveat: dynamic exists to generate the nested blocks defined by a provider; it cannot generate meta-arguments (count, for_each, depends_on, provider, lifecycle, and the like).

  • When to use: nested blocks that scale from zero or more (e.g., multiple ingress/egress, multi-rule lifecycle_rule)
  • Default reference: each.value when iterator is omitted; <iterator>.value when set explicitly
  • The for_each value must be determinable at plan time (unknown values raise an error)
  • Attributes (e.g., tags = { ... }) are built directly with expressions, not dynamic

Generate security group ingress with dynamic

variable "ingress_rules" {
  description = "List of allowed ingress rules"
  type = list(object({
    from_port         = number
    to_port           = number
    protocol          = string
    cidr_blocks       = list(string)
    description       = optional(string)
    ipv6_cidr_blocks  = optional(list(string), [])
  }))
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Web tier security group"

  # Generate a variable number of ingress blocks
  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
      ipv6_cidr_blocks = try(rule.value.ipv6_cidr_blocks, null)
      description      = try(rule.value.description, null)
    }
  }

  # Default egress (single fixed block, so no dynamic needed)
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Pattern Catalog: Conditional, Filter, and Nested

dynamic is not just for plain iteration: it also handles conditional inclusion, filtering via an enabled flag, and nested-block expansion. To toggle a 0/1 block conditionally, the practical pattern is to pass [] or [singleElement] to for_each.

If you filter the input list down to enabled = true items before handing it to for_each, the conditional logic stays out of content and the code reads more clearly. For nested structures, either write a fixed sub-block inside dynamic, or nest dynamic again when truly necessary (excess nesting hurts readability).

  • 0/1 toggle: for_each = condition ? [obj] : []
  • Filter: for v in var.items : v if v.enabled
  • Name the iterator for clarity (e.g., iterator = log, rule)
  • Keep nesting to roughly two levels at most

Dynamic block evaluation flow

Variables/LocalsFilter/Map opsfor_each (known?)Empty => 0 blocksdynamic iterationExpand content {...}Provider schema evalReflected in plan (diff surfaced)Variables/Locals → for_each (known?) → dynamic iteration → Provider schema eval

Dynamically generate a 0/1 optional block

variable "access_logging" {
  type = object({
    bucket = string
    prefix = optional(string)
  })
  default = null
}

resource "aws_s3_bucket" "b" {
  bucket = "example-bucket-1234"

  dynamic "logging" {
    for_each = var.access_logging == null ? [] : [var.access_logging]
    iterator = log
    content {
      target_bucket = log.value.bucket
      target_prefix = try(log.value.prefix, null)
    }
  }
}

Dynamic vs. Other Mechanisms: How to Choose

There are several ways to express variability. dynamic is the tool when you want to add or remove nested blocks. To change the number of resources themselves, use for_each/count at the resource level. To compute attribute values, comprehensions and merge are enough; dynamic is unnecessary.

Both on the exam and in real work, start from the question "what exactly is variable here — resource count, nested-block count, or just an attribute value?" and you will pick the right tool.

  • Iterating nested blocks → dynamic
  • Iterating resource instances → resource-level for_each/count
  • Building a single attribute (e.g., tags, labels) → expressions (map comprehension/merge)
  • Generating HCL via string templates is discouraged (type checking does not apply)
MechanismPrimary useStrengthsCautions
dynamic blockVariable nested-block generation (ingress, lifecycle_rule, etc.)Schema validation applies; promotes DRYfor_each must be known at plan time; over-nesting hurts readability
resource for_eachVary the number of resources (multiple instances of the same kind)Easy individual addressing via each.keyKey stability is essential (prefer map over set)
countSimple N-way iteration (indexed access)Simple and lightweightAdding/removing elements easily shifts indices
Comprehension/mergeProducing attribute values (map/list)Easy to keep readable and type-consistentCannot create nested blocks (a different role from dynamic)

Anti-patterns and alternatives

# NG: trying to build an attribute (map) with dynamic is wrong
# dynamic "tags" { ... } is not allowed. tags is an attribute, not a block.

# OK: build attributes with expressions
locals {
  base_tags = {
    app = "web"
  }
}

resource "aws_instance" "example" {
  ami           = "ami-12345678"
  instance_type = "t3.micro"

  tags = merge(local.base_tags, {
    env = var.env
  })
}

# To vary the number of resources, use resource for_each
resource "aws_iam_user" "u" {
  for_each = var.user_names  # prefer map (stable keys)
  name     = each.key
}

Common Pitfalls and Best Practices

When the value passed to for_each is unknown at plan time, dynamic cannot expand and Terraform errors out. If the count depends on data-source results, revisit the inputs or rework the design to resolve the value beforehand.

Iterating list/set with for_each tends to produce unstable keys, so converting to a map with stable keys is safer. For optional attributes, wrap them with try(..., null) to align with the provider's treatment of null as "unset" and keep diffs quiet.

  • Known-at-plan-time rule: for_each must be a value that can be determined at plan time
  • Stable keys: convert from list/set to a map (key = logical name) before iterating
  • Optional attributes: use try(..., null) to lean toward "unset" semantics
  • Meta-arguments cannot be generated with dynamic (count/for_each/lifecycle, etc.)
  • For excessive nesting, pre-process in locals and expand in a single level

Idiomatic type guards and filters

variable "rules" {
  type = list(object({
    name     = string
    enabled  = optional(bool, true)
    priority = number
  }))
}

locals {
  # Keep only enabled items and rebuild as a map keyed by logical name (stable keys)
  active_rules = { for r in var.rules : r.name => r if try(r.enabled, true) }
}

resource "example_resource" "main" {
  # Generate a nested rule block (illustrative example)
  dynamic "rule" {
    for_each = local.active_rules
    iterator = r
    content {
      name     = r.value.name
      priority = r.value.priority
    }
  }
}

Exam Prep Points (Associate / Pro)

A frequently tested point is to not confuse the role of dynamic (generating nested blocks) with resource-level for_each/count (varying the number of resources). Be comfortable with iterator references, each.value, and the conditional 0/1 generation pattern.

Classic gotchas: dynamic cannot create meta-arguments, attributes are built with expressions, and the for_each value must be known at plan time.

  • dynamic is for nested blocks only — attributes like tags are out of scope
  • When iterator is omitted use each, when set explicitly use <iterator>.value
  • 0/1 generation: the [] / [obj] toggle is the idiomatic pattern
  • for_each must be known at plan time (unknown data-source results are not allowed)
  • Meta-arguments (lifecycle, count, for_each, etc.) cannot use dynamic

Check iterator naming and references

dynamic "ingress" {
  for_each = var.ingress_rules
  iterator = blk
  content {
    from_port = blk.value.from_port
    to_port   = blk.value.to_port
    protocol  = blk.value.protocol
  }
}

Real-World Case: Variable Lifecycle Rules on a Bucket

Storage-bucket lifecycle rules are frequently defined as multiple combinations of conditions and actions, which makes them a natural fit for dynamic. Toggle them with an enabled flag and wrap optional attributes with try(..., null) for stable diffs.

The example below uses Google Cloud Storage, but the same approach applies to lifecycle_rule equivalents in other providers.

  • Use an enabled flag to safely add and remove rules
  • Condition sets such as matches_prefix are left null (unset) when empty
  • When a logical key like a rule name exists, convert to a map for stable iteration

Generate GCS bucket lifecycle_rule with dynamic

variable "lifecycle_rules" {
  type = list(object({
    enabled  = optional(bool, true)
    action   = object({
      type          = string              # e.g. "Delete" | "SetStorageClass"
      storage_class = optional(string)
    })
    condition = object({
      age                   = optional(number)
      matches_prefix        = optional(list(string))
      matches_suffix        = optional(list(string))
      with_state            = optional(string)
      matches_storage_class = optional(list(string))
    })
  }))
}

locals {
  active_lc = [for r in var.lifecycle_rules : r if try(r.enabled, true)]
}

resource "google_storage_bucket" "b" {
  name     = "example-bucket-xyz"
  location = "US"

  dynamic "lifecycle_rule" {
    for_each = local.active_lc
    iterator = lc
    content {
      action {
        type          = lc.value.action.type
        storage_class = try(lc.value.action.storage_class, null)
      }
      condition {
        age                   = try(lc.value.condition.age, null)
        matches_prefix        = try(lc.value.condition.matches_prefix, null)
        matches_suffix        = try(lc.value.condition.matches_suffix, null)
        with_state            = try(lc.value.condition.with_state, null)
        matches_storage_class = try(lc.value.condition.matches_storage_class, null)
      }
    }
  }
}

Check Your Understanding

Associate / Pro

問題 1

In Terraform, you want to generate zero or more nested blocks (e.g., ingress) based on variable content. Which implementation is most appropriate?

  1. Apply for_each to dynamic "ingress" and expand each element inside content
  2. Set count on the resource and use count.index to grow/shrink ingress blocks
  3. Generate HCL strings with templatefile and embed them in locals
  4. Use dynamic to generate the lifecycle meta-argument block for control

正解: A

dynamic is the right tool for variable nested-block generation. count iterates resource instances and cannot grow/shrink nested blocks. Generating HCL with templatefile bypasses type validation and is discouraged. Meta-arguments such as lifecycle cannot be produced via dynamic.

Frequently Asked Questions

Which should I pass to dynamic for_each: list, set, or map?

Prefer a map. Stable keys give you stable plans and diffs. list/set also work, but ordering and implicit keys tend to drift, producing spurious diffs on later edits. When needed, convert with { for v in var.list : v.name => v } before iterating.

What happens if I pass a data source result (unknown at plan time) to for_each?

If the value is still unknown at plan time, Terraform cannot expand dynamic and returns an error. for_each values must be known at plan time. Safer designs are to drive the value from input variables or resolve it in a pre-process step before passing it to Terraform.

Can dynamic generate meta-arguments such as lifecycle, count, or for_each?

No. dynamic targets ordinary nested blocks defined by the provider. Meta-arguments are part of the Terraform language itself, so write them as regular static blocks or arguments.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.