skip to content

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%

answer

  1. variable is named after the label
  2. iterator only renames it
  3. list gives an index, map gives the key
  4. set: key equals value, don't use it
  5. outer iterator stays in scope when nesting

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.

solid answer

~50 s

By default the temporary variable inside `content` is named after the dynamic block's label, so `dynamic "ingress"` gives you `ingress.key` and `ingress.value`. `iterator` renames it — `iterator = rule` makes it `rule.value`. You need it in two situations: when dynamic blocks nest and the inner label would shadow or collide with the outer one, and when the label reads badly or ambiguously in a long content body. The contents differ by collection type: with a list, `.key` is the element's position index and `.value` is the element; with a map, `.key` is the map key and `.value` the map value; with a set, `.key` is identical to `.value`, and the documentation tells you not to use `.key` there. In a nested dynamic block the outer iterator stays in scope, which is how the inner `for_each` reads a field of the outer element.

code

hcl · 20 lines
hcl
variable "ports" {
  type    = set(number)
  default = [80, 443]
}

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

  dynamic "ingress" {
    for_each = var.ports
    iterator = port
    content {
      from_port   = port.value
      to_port     = port.value
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/8"]
    }
  }
}

go deeper

for a junior

Remember that the variable inside content is named after the dynamic block's label, not each, and that iterator simply gives it a different name.

for a middle

Be ready to state what key and value contain for a list, a map and a set, and to explain why key is useless with a set.

for a senior

Show that you know when nesting has gone too far: two generated levels is already hard to review, and reshaping the input data usually beats a third.

for a principal

Frame it as a config-legibility standard for the estate — how much generation a repository tolerates before a plan becomes the only way to know what is declared.

## The default is the label A dynamic block introduces one temporary variable, visible only inside its `content` block, that represents the current element. Its name defaults to the dynamic block's label. This is a small but frequently-missed detail: people reach for `each.value` out of habit, because that is what a resource-level `for_each` provides, and get an error saying there is no such variable. ```hcl dynamic "ingress" { for_each = var.ports content { from_port = ingress.value # named after the label to_port = ingress.value } } ``` ## What iterator changes `iterator` sets that variable's name explicitly: ```hcl dynamic "ingress" { for_each = var.ports iterator = port content { from_port = port.value to_port = port.value } } ``` Nothing else changes — it is purely a naming argument. Two reasons to use it: 1. **Nesting.** A dynamic block can appear inside another dynamic block's `content`, generating a nested block inside a generated block. Both iterators are in scope inside the inner content, and if the same block type nests inside itself the default names would collide. Renaming one of them keeps the two levels distinguishable. 2. **Readability.** Provider block labels are often generic (`setting`, `rule`, `statement`). `iterator = target_group` in the middle of a forty-line content body says more than `statement.value` does. ## Key and value by collection type The temporary variable always exposes exactly two attributes, `.key` and `.value`, but what they mean follows the collection: | for_each | .key | .value | | --- | --- | --- | | list | element index (0, 1, 2…) | the element | | map | the map key | the map value | | set | identical to .value | the element | The set row is the trap. Terraform's documentation states that with a set, `key` is identical to `value` and should not be used. It exists only so the shape of the variable is uniform. Anyone who writes `ingress.key` expecting a stable name while feeding a `set(string)` is really just re-reading the value. A further consequence: with a **map**, elements come out sorted by key, since Terraform maps are ordered lexically. With a **list**, the configuration order is preserved. This matters when the block type being generated is order-sensitive. Note also that a dynamic block's `for_each` accepts a list, which a resource's `for_each` does not — a resource needs a map or a set of strings because it must derive stable instance keys for state. Generated blocks have no state addresses of their own, so an index-keyed list is acceptable here. ## Nesting in practice A nested dynamic block is how you generate a block that itself contains repeated blocks — for example a set of lifecycle rules where each rule carries its own list of storage transitions: ```hcl dynamic "rule" { for_each = var.lifecycle_rules iterator = lc content { id = lc.key status = "Enabled" dynamic "transition" { for_each = lc.value.transitions content { days = transition.value.days storage_class = transition.value.storage_class } } } } ``` The inner `for_each` reads `lc.value.transitions` — the outer iterator remains in scope, which is the whole point of nesting rather than flattening. Note that this only works because the outer element is an object carrying its own inner collection; if your data is flat, a `for` expression that groups it first is usually clearer than a second level of nesting. ## Where nesting stops being worth it Two levels of generation is roughly where a reviewer loses the thread: they can no longer tell from the source what the applied resource looks like, and must read a plan to find out. HashiCorp's own guidance warns that multi-level block structures produced by nested dynamic blocks are hard to read and maintain. If you find yourself at three levels, the honest options are to restructure the input data so one level suffices, or to write the blocks out literally.

  • If you nest a dynamic block inside another, which iterators are in scope in the inner content block?
    Both. The inner content sees its own iterator and the outer one, which is exactly how the inner `for_each` reads a collection out of the outer element, as in `for_each = lc.value.transitions`. That scoping is why `iterator` matters: if the two levels would otherwise share a name, renaming one keeps them unambiguous.
  • Why does a dynamic block accept a list for for_each when a resource's for_each does not?
    A resource's `for_each` must derive a stable key per instance, because each instance gets its own address in state — so it demands a map or a set of strings. Blocks generated by a dynamic block have no independent state address; they are attributes of one resource. With nothing to key, an index-ordered list is perfectly usable.

saying these in an interview costs you the question

  • Uses each.value inside a dynamic block's content
  • Thinks iterator changes iteration order or filtering
  • Reads .key from a set expecting a stable name
  • Assumes the inner content cannot see the outer iterator
  • Believes iterator is mandatory whenever blocks nest

context