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 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).
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"]
}
}
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).
Dynamic block evaluation flow
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)
}
}
}
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.
| Mechanism | Primary use | Strengths | Cautions |
|---|---|---|---|
| dynamic block | Variable nested-block generation (ingress, lifecycle_rule, etc.) | Schema validation applies; promotes DRY | for_each must be known at plan time; over-nesting hurts readability |
| resource for_each | Vary the number of resources (multiple instances of the same kind) | Easy individual addressing via each.key | Key stability is essential (prefer map over set) |
| count | Simple N-way iteration (indexed access) | Simple and lightweight | Adding/removing elements easily shifts indices |
| Comprehension/merge | Producing attribute values (map/list) | Easy to keep readable and type-consistent | Cannot 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
}
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.
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
}
}
}
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.
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
}
}
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.
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)
}
}
}
}
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?
正解: 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.
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.
Practice with certification-focused question sets
無料で問題を解いてみる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.
HCL Syntax: Terraform's Configuration Language (2026)
HCL2 fundamentals for Terraform — blocks, attributes, expres...
Terraform Authoring & Operations Pro: Complete Guide (2026)
Tactics for the Terraform Pro exam — module authoring, works...
Terraform Providers: Plugin Management Fundamentals (2026)
Provider mechanics — required_providers, versions, mirrors, ...
Terraform Resource Blocks: Declarative Infra Units (2026)
Resource block fundamentals — addresses, references, common ...
Terraform Data Sources: Read-Only External Data (2026)
Data source basics — declaration, refresh behavior, dependen...