Terraformの「Reference to undeclared resource」エラーをDynamicブロックの誤ったイテレータ名で修正する

intermediate🏗️ Terraform2026-07-07| Terraform 0.13+、HashiCorp HCL、任意の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ブロック#イテレータ#hcl

エラー内容

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

このエラーは terraform plan または terraform apply の実行中に発生します。Terraform が dynamic ブロックの content セクション内の特定の行を指摘し、あなたには完全に正しく見える名前であっても、その名前を解決できずに処理を拒否します。

根本原因

すべての dynamic ブロックは、ブロックラベルと同じ名前のイテレータオブジェクトを自動的に公開します。content 内でそれ以外の名前を参照すると、Terraform はそれを未宣言のリソースとして扱い、ローカル変数や変数など、どこも検索しません。

典型的なミスの例を示します:

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
  }
}

ブロックラベルは ingress なので、イテレータも ingress になります。rule ではありません。rule.value.* を使用すると、Terraform が rule という名前のリソースを検索して見つけられないため、エラーが発生します。

他によくある原因として、イテレータの参照を更新せずに dynamic ブロックをリソース間でコピーすること、またはブロックラベルを変更した後、content 内に古いイテレータ名が残ってしまうことが挙げられます。

修正方法1 — 自動生成される正しいイテレータ名を使用する

最もシンプルな修正方法:content 内のイテレータ参照をブロックラベルと一致する名前に変更します。

# ブロックラベルが "ingress" → イテレータは自動的に "ingress" になる
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
  }
}

ingress.value で現在の要素を取得でき、ingress.key でマップキー(またはリストインデックス)を取得できます。

修正方法2 — 明示的なイテレータを宣言する

rule のような短い名前を使いたい場合は、dynamic ブロック内に iterator 引数を直接追加します。これによりデフォルト名が上書きされます。

dynamic "ingress" {
  for_each = var.ingress_rules
  iterator = rule          # イテレータに明示的に "rule" という名前を付ける

  content {
    from_port   = rule.value.from_port   # これで有効になる
    to_port     = rule.value.to_port
    protocol    = rule.value.protocol
    cidr_blocks = rule.value.cidr_blocks
  }
}

iterator 引数はローカルエイリアスにすぎません。content 内で現在のアイテムを参照する方法が変わるだけで、生成される Terraform 設定には一切影響しません。

完全な動作例

ポート80と443の2つの動的インgressルールを持つ完全な AWS セキュリティグループの例:

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"

  # アプローチ1: ブロックラベルをイテレータとして使用する
  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"]
  }
}

ネストされた dynamic ブロック

ネストされた dynamic ブロックはそれぞれ独自のイテレータスコープを持ち、デフォルトでブロックラベルが使われます。これは通常問題ありませんが、2つのラベルが同じ名前を持つ場合や、内側の content から誤って外側のイテレータを参照してしまう場合に問題が発生します。明示的な iterator 名を使うことで、この曖昧さを完全に解消できます:

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

  dynamic "rule" {
    for_each = var.rules
    iterator = waf_rule     # 外側のイテレータ

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

      dynamic "statement" {
        for_each = waf_rule.value.statements
        iterator = stmt     # 内側のイテレータ

        content {
          # ここでは stmt.value を使う(rule.value ではない)
          byte_match_statement {
            search_string = stmt.value.search_string
          }
        }
      }
    }
  }
}

iterator = stmt がない場合、内側のブロックはデフォルトで statement という名前になります。その中で rule.value と書くと、外側のイテレータを暗黙的に参照してしまい、誤ったデータが生成されます。まったく別の名前を使うと、最初の未宣言参照エラーに戻ってしまいます。

修正の確認

  • まず terraform validate を実行します — バックエンド接続は不要です:
terraform validate
# Expected: Success! The configuration is valid.
  • terraform plan を実行し、展開されたブロックが出力に表示されることを確認します:
terraform plan
# Look for the expanded blocks in the plan output, e.g.:
# + ingress {
#     + from_port   = 80
#     + to_port     = 80
#     ...

両方が通過すれば、イテレータが正しく解決されています。validate は構文と参照のみをチェックすることに注意してください。実際に展開された値が正しいかどうかを確認するには plan が必要です。

予防策

  • **ブロックラベルから始める。**新しい dynamic ブロックを書く際は、まず自動生成されるイテレータ名を使用します。ラベルが曖昧な場合や、ネストされたブロックと衝突する場合にのみ、明示的な iterator を追加します。
  • **ラベルとイテレータは一緒に変更する。**ブロックラベルを変更する場合は、コミット前にそのブロックの content 内の古いイテレータ名をスコープを絞って一括置換します。
  • **CI に terraform validate を追加する。**ほとんどのモジュールで1秒以内に実行でき、バックエンドも不要で、プルリクエストがマージされる前にこのような参照エラーを検出できます。
  • **内側のイテレータには常に明示的な名前を付ける。**デフォルトのイテレータ名を持つネストされた dynamic ブロックは可読性の落とし穴です。iterator 引数を1つ追加するだけで、スコープが一目瞭然になります。

Related Error Notes