The Error
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 hit this the moment you ran terraform plan or terraform validate. Two resource blocks share the same type and name โ Terraform doesn't guess which one you meant. It stops at parse time, before touching state or calling any provider.
Why This Happens
Every resource in Terraform has a two-part address: resource_type.resource_name. That combination must be unique within a module. Hit the same pair twice and Terraform won't load at all. Here are the four most common ways it happens:
- Copy-paste with a forgotten rename โ you duplicated a block to tweak a few values and left the label unchanged.
- The same resource in two
.tffiles โ Terraform merges every.tffile in the directory. A resource instorage.tfand the same resource inmain.tfwill collide. - A module called twice with the same label โ if a child module declares a resource internally and you invoke it twice using identical labels, the names collide in the parent namespace.
- Merge conflict leftovers โ a sloppy Git merge leaves both versions of a resource block unresolved in the same file.
Step-by-Step Fix
Step 1 โ Find all occurrences
The error already tells you where both declarations are: main.tf:5 for the first, main.tf:15 for the duplicate. For larger projects with many files, a single grep is faster than hunting manually:
# Search across all .tf files in the module directory
grep -rn 'resource "aws_s3_bucket" "example"' .
Every matching file and line number shows up immediately.
Step 2 โ Decide which block to keep
Open both locations side by side. Usually one is the original, and the other is an accidental leftover โ or one is simply outdated. Keep the correct one and delete the other.
Before (broken):
# main.tf โ lines 5-10
resource "aws_s3_bucket" "example" {
bucket = "my-app-assets"
}
# main.tf โ lines 15-20 (copy-pasted and forgotten)
resource "aws_s3_bucket" "example" {
bucket = "my-app-assets"
tags = { Environment = "prod" }
}
After (fixed) โ remove the first block, keep the one with tags:
resource "aws_s3_bucket" "example" {
bucket = "my-app-assets"
tags = { Environment = "prod" }
}
Step 3 โ If you need two buckets, rename one
Maybe both blocks are intentional โ they just got the same label by accident. Give the second one a distinct name and you're done:
resource "aws_s3_bucket" "example" {
bucket = "my-app-assets"
}
resource "aws_s3_bucket" "example_logs" {
bucket = "my-app-assets-logs"
}
Step 4 โ Fix a module called twice with the same label
Calling a child module twice in one parent is fine โ as long as each call has a unique label. Without distinct labels, both instances try to register the same resource names in the parent namespace:
# BROKEN: same label used twice
module "storage" {
source = "./modules/storage"
bucket_name = "assets"
}
module "storage" {
source = "./modules/storage"
bucket_name = "logs"
}
# FIXED: unique label per call
module "storage_assets" {
source = "./modules/storage"
bucket_name = "assets"
}
module "storage_logs" {
source = "./modules/storage"
bucket_name = "logs"
}
Each call gets its own namespace. Resources inside won't collide.
Step 5 โ For many similar resources, use for_each
Copying resource blocks to spin up similar infrastructure is exactly the habit this error punishes. for_each is the fix โ one block, many instances:
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
}
}
This gives you three separate instances โ aws_s3_bucket.example["assets"], aws_s3_bucket.example["logs"], and aws_s3_bucket.example["backup"] โ from a single block. No duplication, no collision.
Verify the Fix
terraform validate is the fastest first check. It catches HCL errors without credentials or state access:
terraform validate
Clean output:
Success! The configuration is valid.
Then run terraform plan to confirm everything looks right.
One edge case: if you renamed a resource rather than deleting a duplicate, Terraform will plan to destroy the old one and recreate it under the new name. Skip that destroy/recreate cycle by moving the state entry first:
# Move state to the new address before applying
terraform state mv aws_s3_bucket.example aws_s3_bucket.example_logs
After the move, terraform plan should show no changes for that resource.
Quick Checklist
- Run
grep -rn 'resource "TYPE" "NAME"'across all.tffiles to find every declaration site. - Check for unresolved Git merge conflict markers (
<<<<<<<,=======,>>>>>>>) inside.tffiles. - Two module calls with the same label? Rename at least one.
- Creating multiple similar resources? Use
for_eachinstead of copying blocks. - Renamed a resource? Run
terraform state mvbefore applying to avoid a destroy/recreate.

