skip to content

Dynamic Blocks

dynamic blocks generate repeated nested blocks — security-group rules, tags, listeners — from a variable. I should know how to write one and, just as importantly, when the readability cost is not worth it.

part ofTerraformoverview, primer and where to startread it →
on this pageshow

questions

5

In Terraform, what does a dynamic block do, and how would you use one to generate a security group's ingress blocks from a variable?

level: juniorimportance: must knowfreq 68%

answer

  1. blocks are syntax, not values
  2. one element in, one block out
  3. label names the block type
  4. content is the body template
  5. empty collection means no block

basics

~20 s

A dynamic block produces repeated nested blocks from a collection: for_each supplies one element per block and the content block holds the block body. For a security group you iterate a list of rule objects to emit one ingress block each.

solid answer

~40 s

Nested blocks in HCL are syntax, not values, so no expression can return "three `ingress` blocks". A `dynamic` block bridges that gap. Its label names the nested block type to generate, `for_each` takes a list, set or map with one element per desired block, and `content` is the body Terraform stamps out per element. Inside `content`, a temporary variable named after the label exposes `.key` and `.value` — so `dynamic "ingress"` gives you `ingress.value.port`. For a security group I declare a variable holding rule objects and write one `dynamic "ingress"` block whose content maps each object onto `from_port`, `to_port`, `protocol` and `cidr_blocks`. An empty collection generates zero blocks, which is the usual way to make a nested block optional.

code

hcl · 25 lines
hcl
variable "ingress_rules" {
  type = list(object({
    port        = number
    cidr_blocks = list(string)
  }))
  default = [
    { port = 80, cidr_blocks = ["0.0.0.0/0"] },
    { port = 443, cidr_blocks = ["0.0.0.0/0"] },
  ]
}

resource "aws_security_group" "web" {
  name        = "web"
  description = "web tier"

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

go deeper

for a junior

Be able to write one from memory: label, for_each, content, and the label-named variable with .value inside. Say plainly that it generates nested blocks, not resources.

for a middle

Explain what .key and .value hold for a list, a map and a set, and why an empty collection is the idiomatic way to make a nested block optional.

for a senior

Show judgment about when generated blocks are worth their review cost, and note that everything a dynamic block emits shares one state address, so no single generated block can be replaced on its own.

for a principal

Own the module-interface question: whether callers should hand you a collection you expand internally or declare the blocks themselves, and what that choice does to review, diff readability and blast radius across teams.

## Arguments take values; blocks are syntax Terraform configuration mixes two things. **Arguments** take values: `name = "web"`, and any expression that produces a value can fill one. **Nested blocks** are part of the resource type's schema: `ingress { ... }` inside `aws_security_group` is written out, one block per rule. No expression evaluates to "a list of blocks", because blocks are not values. That is fine while the rules are fixed. It breaks when the rules come from an input variable, differ per environment, or are assembled by a module for its callers. The `dynamic` block is the escape hatch: a meta-block that tells Terraform to *generate* nested blocks from a collection. ## Anatomy ```hcl dynamic "ingress" { # label = the nested block type to generate for_each = var.rules # one element per generated block iterator = rule # optional: rename the temp variable content { # the body of each generated block from_port = rule.value.port } } ``` Four parts matter: - **The label** is the name of the nested block type being produced. `dynamic "ingress"` produces `ingress` blocks; the label must be a block type the resource, data source, provider or provisioner actually accepts. - **`for_each`** accepts a list, a set, or a map. Unlike a resource's `for_each`, a list is allowed here, because generated blocks have no state addresses to key. - **`content`** is mandatory. It is the template body, evaluated once per element. - **The temporary variable** is named after the label unless `iterator` renames it. It exposes `.key` and `.value`. ## What key and value hold - **List** — `.key` is the element's index, `.value` is the element. - **Map** — `.key` is the map key, `.value` is the map value; elements come out in lexical key order. - **Set** — `.key` is identical to `.value`; the documentation says not to use `.key` with a set. This is why security-group examples usually take `list(object(...))` or `map(object(...))`: each element carries every field the block needs. ## The security-group shape ```hcl variable "ingress_rules" { type = list(object({ port = number, cidr_blocks = list(string) })) } resource "aws_security_group" "web" { name = "web" dynamic "ingress" { for_each = var.ingress_rules content { from_port = ingress.value.port to_port = ingress.value.port protocol = "tcp" cidr_blocks = ingress.value.cidr_blocks } } } ``` All the generated blocks belong to the single `aws_security_group.web` resource. They are attributes of one object in state, not separate addressable things. ## The zero-or-one idiom Because an empty collection generates no blocks at all, a conditional list is the standard way to make a whole nested block optional: ```hcl dynamic "logging" { for_each = var.enable_logging ? [1] : [] content { target_bucket = var.log_bucket } } ``` One element means one block; the empty list means the block is simply absent, which is exactly what a provider needs when "absent" and "present but empty" mean different things. ## Three things it is not 1. **Not a way to create resources.** A resource's own `for_each` or `count` creates multiple resource instances with their own state addresses. A `dynamic` block creates repeated *blocks inside one* resource instance. 2. **Not for arguments.** `tags` on most AWS resources is a map argument, not a block, so you build it with an expression such as `merge(var.common_tags, { Name = "web" })` — a dynamic block cannot produce it. 3. **Not free.** Generated configuration is harder to read and to review than blocks written out literally, and each generated block loses the individual identity that a separate resource would have had. HashiCorp's own guidance is to reach for `dynamic` mainly when a reusable module needs to hide repetition behind a clean interface.

  • What happens when the for_each collection of a dynamic block is empty?
    It generates zero blocks, and the resource is configured exactly as if the block had never been written. That is the standard idiom for an optional nested block: `for_each = var.enabled ? [1] : []` yields one block when enabled and none otherwise, which matters when a provider treats an absent block differently from an empty one.
  • Could you use a dynamic block to build the tags on an aws_instance?
    No. On most AWS provider resources `tags` is a map argument, not a repeatable nested block, and `dynamic` can only generate blocks. You build the map with an expression instead — `merge(var.common_tags, { Name = "api" })`, or a `for` expression over a collection. If a resource genuinely uses repeated `tag` blocks, then a dynamic block does apply.
  • How is a dynamic block different from putting for_each on the resource itself?
    Resource-level `for_each` creates multiple resource instances, each with its own address in state, its own plan diff and its own lifecycle. A dynamic block creates repeated nested blocks inside a single resource instance, so all of them live and change together under one address. The choice determines how granular your plans and targeted operations can be.

saying these in an interview costs you the question

  • Says a dynamic block creates multiple resources rather than nested blocks
  • Writes each.value inside content instead of the block label
  • Claims dynamic blocks can generate the tags map argument
  • Thinks the content block is optional when for_each is set
  • Believes for_each on a dynamic block must be a map or set

context

open as a page

In Terraform, what is the iterator argument of a dynamic block for, and what do key and value hold when for_each is a list versus a set?

level: middleimportance: should knowfreq 42%

basics

~20 s

The iterator argument renames the temporary variable inside a dynamic block, which otherwise takes the block's label. For a list, key is the element index and value the element; for a set, key is identical to value and should not be used.

open as a page

In Terraform, which parts of a resource can a dynamic block NOT generate, and why?

level: middleimportance: should knowfreq 36%

basics

~20 s

A dynamic block can only generate repeatable nested blocks defined by the resource, data source, provider or provisioner schema. It cannot produce plain arguments such as tags, nor meta-argument blocks like lifecycle, which Terraform must process before evaluating expressions.

open as a page

When should you avoid a Terraform dynamic block and instead write the nested blocks out literally or declare separate resources?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Write blocks literally when the set is small and known; reach for a dynamic block mainly to hide repetition behind a reusable module's interface. When each item needs its own diff, lifecycle or targeted replacement, prefer separate resources instead.

open as a page

A Terraform config generates CloudFront ordered_cache_behavior blocks with a dynamic block whose for_each is a map. Why is that risky, and what would you use instead?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

A map iterates in lexical key order, so the precedence of order-sensitive blocks follows key names rather than intent, and adding or renaming a key silently reshuffles them. Drive order-sensitive nested blocks from a list, where configuration order is preserved.

open as a page