skip to content

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