エラー内容
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つ追加するだけで、スコープが一目瞭然になります。

