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"] のような特定のアドレスが表示されるはずです。エラーが解消されない場合は、id、arn、または primary_access_key などを誤ってマップのキーとして使用していないか再確認してください。

