エラーの内容
Error: Attempt to get attribute from null value
on main.tf line 15, in resource "aws_route53_record" "api":
15: zone_id = module.dns.zone_id
This value is null, so it does not have any attributes.
Terraformがnullと評価されたものから属性を読み取ろうとすると、このエラーが発生します。9割のケースはcount = 0またはfor_each = {}で制御された条件付きリソースが原因です。もう一つのよくある原因は、変数の条件によってリソースが作成されなかった場合に、モジュールの出力がnullを返すことです。
発生する理由
1. countを使った条件付きリソース
resource "aws_eip" "nat" {
count = var.enable_nat ? 1 : 0
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id # count = 0 のときに失敗する
}
count = 0の場合、aws_eip.natは単一のオブジェクトではなく空のリストになります。リストに対して直接.idにアクセスするとnullが返され、そのnullが次の属性アクセスでエラーを引き起こします。
2. nullを返すモジュール出力
module "ssl" {
source = "./modules/ssl"
enabled = var.use_ssl
}
resource "aws_lb_listener" "https" {
certificate_arn = module.ssl.certificate_arn # ssl が無効のときはnull
}
モジュールが内部でcountを使って条件付きで証明書を作成する場合、無効化されているとき出力はnullになります。呼び出し側はその事情を知らず、ただnullを受け取るだけです。
3. 一致する結果がないデータソース
data "aws_vpc" "existing" {
filter {
name = "tag:Name"
values = ["my-vpc"]
}
}
resource "aws_subnet" "main" {
vpc_id = data.aws_vpc.existing.id # VPCタグが存在しない場合はnull
}
一部のデータソースは、一致するリソースがない場合に特定の属性にnullを返します。別のケースでは完全にエラーをスローします。いずれの場合も、nullオブジェクトから.idを読み取ると、まったく同じエラーが発生します。
手順ごとの修正方法
修正1: countベースのリソースにはone()を使う
one()は0または1要素のリストを値またはnullに変換します。インデックスのパニックも予期せぬ動作もありません。
# 変更前:
allocation_id = aws_eip.nat.id
# 変更後:
allocation_id = one(aws_eip.nat[*].id)
# フォールバック付き:
allocation_id = one(aws_eip.nat[*].id) != null ? one(aws_eip.nat[*].id) : ""
修正2: try()でラップする
try()は式を評価し、チェーン内で何かがスローされた場合にフォールバックを返します。オプションのモジュール出力に最適です。
certificate_arn = try(module.ssl.certificate_arn, null)
# 深くネストされたチェーンにも対応:
subnet_id = try(module.network.subnets["private-a"].id, null)
属性がオプションの場合はフォールバックにnullを指定します。具体的な値が必要な場合は、空文字列やプレースホルダーIDなど、そのリソースが受け入れる実際のデフォルト値を使用してください。
修正3: 条件式による明示的なnullガード
# パターン: 条件 ? 値 : フォールバック
vpc_id = var.existing_vpc_id != null ? var.existing_vpc_id : aws_vpc.new.id
# モジュール出力の場合:
certificate_arn = module.ssl.certificate_arn != null ? module.ssl.certificate_arn : ""
修正4: モジュールの出力定義そのものを修正する
nullがモジュール内部から生じている場合は、出力定義を修正してください。呼び出し側でその場しのぎの対処をすべきではありません。
# modules/ssl/outputs.tf
# 変更前 — count = 0 のときにクラッシュする:
output "certificate_arn" {
value = aws_acm_certificate.cert[0].arn
}
# 変更後 — どのcountでも安全:
output "certificate_arn" {
value = length(aws_acm_certificate.cert) > 0 ? aws_acm_certificate.cert[0].arn : null
}
これで呼び出し側はtry(module.ssl.certificate_arn, null)を使用でき、双方が扱う内容を明示的に示せます。
修正5: クロスモジュールのタイミング問題にはdepends_onを追加する
複雑なモジュールチェーンでは、Terraformが上流リソースの完全な解決前に参照を評価することがあります。これはcount = 0による構造的なnullは修正しませんが、マルチモジュールグラフでの順序付けに役立ちます。
resource "aws_nat_gateway" "main" {
allocation_id = one(aws_eip.nat[*].id)
subnet_id = aws_subnet.public.id
depends_on = [aws_eip.nat]
}
修正の確認
エラーを引き起こした正確な変数の組み合わせでterraform planを再実行してください。
# 原因となったフラグで実行:
terraform plan -var="enable_nat=false"
# またはtfvarsファイルで:
terraform plan -var-file="prod.tfvars"
出力にError: Attempt to get attributeが表示されなければ完了です。式そのものを確認したい場合はterraform consoleを使ってください。
terraform console
> try(module.ssl.certificate_arn, "fallback")
# "fallback" または実際のARNが返される — エラーは発生しない
ヒント
try()と条件式の使い分け: 式全体がスローする可能性がある場合(ネストされた属性チェーン、不明な型へのスプラット式など)はtry()を使います。特定の変数がnullかどうかチェックする場合は条件 ? a : bを使います。[0]よりone()を使う:resource.example[0].idは空のリストでパニックを起こします。one(resource.example[*].id)は安全にnullを返します。countベースのリソースでは常にこちらを使ってください。- 本当のバグを隠さない: そのリソースが存在すべきであれば、nullはコンフィグのバグです。すべてを
try()でラップするのではなく、根本原因を追ってください。 - nullable出力をドキュメント化する: どの出力がnullになり得るかを
outputs.tfにコメントで記載しておきましょう。次の担当者の20分間のデバッグを節約できます。 terraform.tfvarsを確認する: このエラーは環境固有です。開発環境では動作するプランが、単一のフラグが異なるだけで本番環境では失敗します。必ず失敗した環境の正確な変数の組み合わせで再現確認を行ってください。

