Terraformの「Invalid for_each argument」エラーを修正する:不明な値や機密情報の扱い

intermediate🏗️ Terraform2026-03-31| Terraform CLI (バージョン0.12.x以降のすべて), Linux/macOS/Windows

Error Message

Error: Invalid for_each argument The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.
#terraform#for_each#devops#iac#トラブルシューティング

Terraformがプランを停止する理由

Terraformは厳格な規約に基づいて動作します。インフラストラクチャを操作する前に、すべてのリソースの正確なアドレスを予測しなければなりません。リソースのアドレスが aws_instance.web["web-01"] である場合、キーとなる "web-01" はプラン(plan)フェーズで既知である必要があります。Terraformは、リソース名が何になるかを決定するためにapplyフェーズまで待つことはできません。

このエラーは通常、生成されたAWSインスタンスIDや機密性の高いパスワードのような動的な値を、for_each ループのキーとして使用したときに発生します。これらの値はプロバイダーが実際にリソースを作成するまで存在しないため、Terraformは論理的な行き止まりに突き当たります。そして、ステートファイルの不整合を防ぐためにプロセスを停止します。

シナリオ:動的なDNSレコード

3つのEC2インスタンスのクラスターをデプロイすると仮定します。インスタンスIDをドメイン名の一部として使用し、各インスタンスに対してRoute53 DNSレコードを自動的に作成したいと考えています。次のようなコードを記述するかもしれません。

resource "aws_instance" "server" {
  count = 3
  ami   = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
}

resource "aws_route53_record" "dns" {
  # apply中にIDが生成されるため、これは失敗します
  for_each = { for s in aws_instance.server : s.id => s.private_ip }
  
  zone_id = "Z0123456789ABCDEF"
  name    = "${each.key}.example.com"
  type    = "A"
  ttl     = 300
  records = [each.value]
}

AWSは、インスタンスが実行された後にのみID(i-04f123456789ab など)を割り当てます。Terraformは terraform plan の段階ではこれらのIDを知ることができないため、DNSレコードのキーを決定できません。その結果、Invalid for_each argument エラーが発生します。

戦術的な修正:-targetによる回避策

すぐに修正が必要な場合は、-target フラグを使用して、Terraformに依存関係を先に作成させるように強制できます。

terraform apply -target=aws_instance.server

このコマンドはインスタンスを作成し、そのIDをステートファイルに保存します。その後の terraform apply では、IDが「既知(known)」の値となっているため、正常に動作します。ただし、これは控えめに使用してください。これは手動の応急処置であり、自動化されたCI/CDパイプラインを壊す原因になることがよくあります。

恒久的な修正1:静的なキーの使用

最も信頼性の高い解決策は、リソースキーを動的なクラウド属性から切り離すことです。生成されたIDをキーとして使用する代わりに、設定ファイル内で定義した一意の文字列を使用します。

マップまたは文字列のリストを使用することで、プランフェーズですぐにキーを利用できるようになります。

variable "node_names" {
  type    = list(string)
  default = ["api-prod-01", "api-prod-02", "api-prod-03"]
}

resource "aws_instance" "server" {
  for_each      = toset(var.node_names)
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  tags = { Name = each.key }
}

resource "aws_route53_record" "dns" {
  # 修正:each.keyはvar.node_namesからの静的な文字列になりました
  for_each = aws_instance.server
  
  zone_id = "Z0123456789ABCDEF"
  name    = "${each.key}.example.com"
  type    = "A"
  ttl     = 300
  records = [each.value.private_ip]
}

これでTerraformは、AWSと通信する前であっても aws_route53_record.dns["api-prod-01"] を作成する必要があることを認識できます。private_ip の値は「apply後に判明(known after apply)」のままでも問題ありません。なぜなら、静的である必要があるのはキーだけだからです。

恒久的な修正2:機密情報の取り扱い

技術的にはキーが判明していても、Terraformがそれらを sensitive(機密)としてマークしているために非表示にすることがあります。Terraformは、ターミナルやステートファイル内にプレーンテキストで表示されるリソースキーに、機密データを使用することを拒否します。

その値がリソースアドレスとして公開されても安全であると確信できる場合は、nonsensitive() 関数でラップします。

resource "vault_generic_secret" "example" {
  # これらのキーをリソースアドレスで使用できるようにするため、nonsensitiveを使用します
  for_each = { for k, v in var.secret_map : nonsensitive(k) => v }
  path     = "secret/${each.key}"
  data_json = jsonencode(each.value)
}

恒久的な修正3:データソースの活用

既存のインフラストラクチャを参照する必要がある場合、データソースがそのギャップを埋めることができます。管理リソースとは異なり、データソースの引数が静的であれば、プランの「refresh」フェーズで解決できることがよくあります。

data "aws_subnets" "public" {
  filter {
    name   = "tag:Network"
    values = ["public"]
  }
}

resource "aws_instance" "nodes" {
  # サブネットIDはプランのrefresh中に取得されるため、これは機能します
  for_each      = toset(data.aws_subnets.public.ids)
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
}

結果の確認

terraform plan を実行して作業を確認してください。修正が成功していれば、リソースのアクションが明確なリストとして表示されます。たとえば、エラーメッセージの代わりに aws_route53_record.dns["api-prod-01"] のような特定のアドレスが表示されるはずです。エラーが解消されない場合は、idarn、または primary_access_key などを誤ってマップのキーとして使用していないか再確認してください。

Related Error Notes