Fix Terraform 'Reference to undeclared resource' When Using Wrong Iterator Name in Dynamic Block

intermediate๐Ÿ—๏ธ Terraform2026-07-07| Terraform 0.13+, HashiCorp HCL, any OS (Linux, macOS, Windows)

Error Message

Error: Reference to undeclared resource. A dynamic block's "content" must refer to the block label or the "iterator" argument.
#terraform#dynamic-block#iterator#hcl

The Error

Error: Reference to undeclared resource. A dynamic block's "content" must refer to the block label or the "iterator" argument.

You hit this during terraform plan or terraform apply. Terraform flags a specific line inside your dynamic block's content section and refuses to resolve a name โ€” even though that name looks completely valid to you.

Root Cause

Every dynamic block automatically exposes an iterator object named after the block label. Reference anything else inside content, and Terraform treats it as an undeclared resource โ€” it won't search locals, variables, or anywhere else.

Here's where it typically goes wrong:

dynamic "ingress" {
  for_each = var.ingress_rules

  content {
    from_port   = rule.value.from_port   # ERROR: "rule" is not defined
    to_port     = rule.value.to_port
    protocol    = rule.value.protocol
    cidr_blocks = rule.value.cidr_blocks
  }
}

The block label is ingress, so the iterator is also ingress. Not rule. Using rule.value.* triggers the error because Terraform scans for a resource named rule and finds nothing.

Two other common triggers: copying a dynamic block between resources without updating iterator references, or renaming a block label while leaving the old iterator name scattered through content.

Fix 1 โ€” Use the Correct Auto-Generated Iterator Name

The simplest fix: rename your iterator references inside content to match the block label.

# Block label is "ingress" โ†’ iterator is automatically "ingress"
dynamic "ingress" {
  for_each = var.ingress_rules

  content {
    from_port   = ingress.value.from_port   # Correct
    to_port     = ingress.value.to_port
    protocol    = ingress.value.protocol
    cidr_blocks = ingress.value.cidr_blocks
  }
}

ingress.value gives you the current element; ingress.key gives you the map key (or list index).

Fix 2 โ€” Declare an Explicit Iterator

Prefer a shorter name like rule? Add an iterator argument directly inside the dynamic block. This overrides the default name.

dynamic "ingress" {
  for_each = var.ingress_rules
  iterator = rule          # Explicitly name the iterator "rule"

  content {
    from_port   = rule.value.from_port   # Now valid
    to_port     = rule.value.to_port
    protocol    = rule.value.protocol
    cidr_blocks = rule.value.cidr_blocks
  }
}

The iterator argument is a local alias only โ€” it changes how you reference the current item inside content, but has zero effect on the generated Terraform configuration.

Full Working Example

A complete AWS security group with two dynamic ingress rules, ports 80 and 443:

variable "ingress_rules" {
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = [
    { from_port = 80,  to_port = 80,  protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
    { from_port = 443, to_port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
  ]
}

resource "aws_security_group" "web" {
  name = "web-sg"

  # Approach 1: use block label as iterator
  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Nested Dynamic Blocks

Each nested dynamic block gets its own iterator scope, defaulting to the block label. That works fine โ€” until two labels share the same name, or you accidentally reference the outer iterator from inside the inner content. Explicit iterator names eliminate the ambiguity entirely:

resource "aws_wafv2_rule_group" "example" {
  name     = "example"
  scope    = "REGIONAL"
  capacity = 100

  dynamic "rule" {
    for_each = var.rules
    iterator = waf_rule     # outer iterator

    content {
      name     = waf_rule.value.name
      priority = waf_rule.value.priority

      dynamic "statement" {
        for_each = waf_rule.value.statements
        iterator = stmt     # inner iterator

        content {
          # Use stmt.value here, not rule.value
          byte_match_statement {
            search_string = stmt.value.search_string
          }
        }
      }
    }
  }
}

Without iterator = stmt, the inner block defaults to statement as its name. Write rule.value inside it and you silently grab the outer iterator, producing wrong data. Use a third name entirely and you're back to the original undeclared reference error.

Verify the Fix

  • Run terraform validate first โ€” no backend connection required:
terraform validate
# Expected: Success! The configuration is valid.
  • Run terraform plan and check that the expanded blocks appear in the output:
terraform plan
# Look for the expanded blocks in the plan output, e.g.:
# + ingress {
#     + from_port   = 80
#     + to_port     = 80
#     ...

Both passing means the iterator is resolved correctly. Keep in mind that validate only checks syntax and references โ€” you need plan to confirm the actual expanded values look right.

Prevention

  • Start with the block label. When writing a new dynamic block, use the auto-generated iterator name first. Only add an explicit iterator when the label is ambiguous or conflicts with a nested block.
  • Rename label and iterator together. Changing a block label? Do a scoped find-replace on the old iterator name inside that block's content before committing.
  • Add terraform validate to CI. It runs in under a second for most modules, needs no backend, and catches reference errors like this before a pull request merges.
  • Always name inner iterators explicitly. Nested dynamic blocks with default iterator names are a readability trap. One iterator argument makes the scope unambiguous at a glance.

Related Error Notes