Fix Terraform 'Duplicate resource' Error: A managed resource has already been declared

beginner๐Ÿ—๏ธ Terraform2026-06-01| Terraform 1.x, any OS (Linux/macOS/Windows), HCL configuration files

Error Message

A managed resource "aws_s3_bucket" "example" has already been declared at main.tf:5,1-33
#terraform#hcl#duplicate-resource#module

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 .tf files โ€” Terraform merges every .tf file in the directory. A resource in storage.tf and the same resource in main.tf will 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 .tf files to find every declaration site.
  • Check for unresolved Git merge conflict markers (<<<<<<<, =======, >>>>>>>) inside .tf files.
  • Two module calls with the same label? Rename at least one.
  • Creating multiple similar resources? Use for_each instead of copying blocks.
  • Renamed a resource? Run terraform state mv before applying to avoid a destroy/recreate.

Related Error Notes