エラーの内容
Error: Duplicate resource "aws_s3_bucket" configuration
on main.tf line 15:
15: resource "aws_s3_bucket" "example" {
A managed resource "aws_s3_bucket" "example" has already been declared at main.tf:5,1-33
このエラーは、terraform plan または terraform validate を実行した瞬間に発生します。2つのリソースブロックが同じタイプと名前を持っているため、Terraformはどちらを意図したのか判断できません。状態ファイルへのアクセスやプロバイダーの呼び出しを行う前に、パース段階で処理が停止します。
発生する原因
Terraformの各リソースは resource_type.resource_name という2つの部分からなるアドレスを持ちます。この組み合わせはモジュール内で一意でなければなりません。同じペアが2回登場すると、Terraformは一切ロードを行いません。よくある原因は以下の4つです:
- コピー&ペースト時のラベル変更忘れ — ブロックを複製して値を調整する際に、ラベルをそのまま残してしまった場合。
- 2つの
.tfファイルに同じリソースが存在する — Terraformはディレクトリ内のすべての.tfファイルをマージします。storage.tfとmain.tfの両方に同じリソースがあると競合します。 - 同じラベルでモジュールを2回呼び出している — 子モジュールがリソースを内部で宣言しており、同一ラベルで2回呼び出すと、親の名前空間でリソース名が衝突します。
- Gitマージ時の競合が残っている — 不完全なGitマージにより、同一ファイル内にリソースブロックの両バージョンが未解決のまま残っている場合。
修正手順
手順1 — すべての該当箇所を見つける
エラーメッセージにはすでに両方の宣言場所が示されています:最初の宣言が main.tf:5、重複が main.tf:15 です。ファイル数が多い大規模プロジェクトでは、手動で探すより grep を使うほうが速いです:
# モジュールディレクトリ内のすべての .tf ファイルを検索する
grep -rn 'resource "aws_s3_bucket" "example"' .
一致するファイル名と行番号がすぐに表示されます。
手順2 — 残すブロックを決める
両方の場所を並べて確認します。通常、一方が元のブロックで、もう一方は誤って残ったものか、単純に古くなったものです。正しい方を残し、もう一方を削除します。
修正前(エラーあり):
# main.tf — 5〜10行目
resource "aws_s3_bucket" "example" {
bucket = "my-app-assets"
}
# main.tf — 15〜20行目(コピー&ペーストされたまま放置)
resource "aws_s3_bucket" "example" {
bucket = "my-app-assets"
tags = { Environment = "prod" }
}
修正後 — 最初のブロックを削除し、タグ付きのブロックを残す:
resource "aws_s3_bucket" "example" {
bucket = "my-app-assets"
tags = { Environment = "prod" }
}
手順3 — 2つのバケットが必要な場合は一方の名前を変える
両方のブロックが意図的なものであり、たまたま同じラベルになってしまった場合は、2つ目に別の名前を付ければ解決します:
resource "aws_s3_bucket" "example" {
bucket = "my-app-assets"
}
resource "aws_s3_bucket" "example_logs" {
bucket = "my-app-assets-logs"
}
手順4 — 同じラベルで2回呼び出しているモジュールを修正する
1つの親から子モジュールを2回呼び出すこと自体は問題ありません。ただし、各呼び出しに一意のラベルが必要です。同じラベルを使うと、両方のインスタンスが親の名前空間に同じリソース名を登録しようとして衝突します:
# 問題あり:同じラベルを2回使用している
module "storage" {
source = "./modules/storage"
bucket_name = "assets"
}
module "storage" {
source = "./modules/storage"
bucket_name = "logs"
}
# 修正済み:呼び出しごとに一意のラベルを使用する
module "storage_assets" {
source = "./modules/storage"
bucket_name = "assets"
}
module "storage_logs" {
source = "./modules/storage"
bucket_name = "logs"
}
各呼び出しが独自の名前空間を持つため、内部のリソースが衝突しません。
手順5 — 類似リソースが多い場合は for_each を使う
似たようなインフラをセットアップするためにリソースブロックをコピーする習慣こそ、このエラーの温床です。for_each を使えば、1つのブロックで複数のインスタンスを管理できます:
locals {
buckets = {
assets = "my-app-assets"
logs = "my-app-assets-logs"
backup = "my-app-backup"
}
}
resource "aws_s3_bucket" "example" {
for_each = local.buckets
bucket = each.value
tags = {
Name = each.key
}
}
これにより、1つのブロックから aws_s3_bucket.example["assets"]、aws_s3_bucket.example["logs"]、aws_s3_bucket.example["backup"] という3つの独立したインスタンスが作成されます。重複も衝突も発生しません。
修正の確認
terraform validate が最も手軽な最初の確認手段です。認証情報や状態ファイルへのアクセスなしにHCLエラーを検出できます:
terraform validate
正常時の出力:
Success! The configuration is valid.
続けて terraform plan を実行し、内容が正しいことを確認します。
注意点:重複を削除するのではなくリソースをリネームした場合、Terraformは古い名前のリソースを削除し、新しい名前で再作成する計画を立てます。その削除と再作成を避けるには、適用前に状態エントリを移動してください:
# 適用前に新しいアドレスへ状態を移動する
terraform state mv aws_s3_bucket.example aws_s3_bucket.example_logs
移動後、terraform plan を実行するとそのリソースに変更がないことが確認できます。
確認チェックリスト
- すべての
.tfファイルに対してgrep -rn 'resource "TYPE" "NAME"'を実行し、すべての宣言箇所を特定する。 .tfファイル内に未解決のGitマージ競合マーカー(<<<<<<<、=======、>>>>>>>)がないか確認する。- 同じラベルでモジュールを2回呼び出している場合は、少なくとも一方をリネームする。
- 複数の類似リソースを作成する場合は、ブロックをコピーする代わりに
for_eachを使用する。 - リソースをリネームした場合は、削除と再作成を防ぐために適用前に
terraform state mvを実行する。

