Initializing Enclave...

How to Fix Terraform 'Error: Resource with ID Already Exists' Duplicate State Conflict

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10–20 mins


TL;DR

  • What broke: Terraform's state file references a resource ID that already exists in the provider (AWS/GCP/Azure), causing a hard conflict on terraform plan or terraform apply.
  • How to fix it: Either import the existing resource into state with terraform import, remove the orphaned state entry with terraform state rm, or deduplicate the HCL resource block that was accidentally copied.
  • Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your failing .tf block or state snippet and get the corrected import command or deduplicated HCL instantly.

The Incident (What Does the Error Mean?)

Raw error output from terraform plan:

Error: error creating <RESOURCE_TYPE> (<RESOURCE_ID>): EntityAlreadyExists: resource with ID "arn:aws:iam::123456789012:role/my-app-role" already exists.

  on main.tf line 14, in resource "aws_iam_role" "my_app_role":
  14: resource "aws_iam_role" "my_app_role" {

Or the state-side variant:

Error: Resource already managed by Terraform

Terraform is already managing a remote object for
aws_s3_bucket.data_lake. To import it, first remove the existing object
from the state using `terraform state rm`.

Immediate consequence: terraform apply is fully blocked. No changes in the plan will be applied — not just the conflicting resource, but the entire run. In a pipeline, this means a dead deployment. If this is a terraform apply -auto-approve in CI/CD, the job fails and any downstream modules depending on this resource's outputs will also fail, cascading across your stack.


The Attack Vector / Blast Radius

This error surfaces from three distinct root causes, each with a different blast radius:

1. Out-of-band resource creation (most common in production outages) Someone created the resource manually via the AWS Console, aws cli, or a one-off script. Terraform has no state entry for it. When you write the HCL to codify it, Terraform tries to CREATE it — but the provider rejects the call because the ID already exists at the API level. The risk: If you force-apply, you either get a hard error or, worse, Terraform creates a second resource with a slightly different name, leaving a zombie resource you'll pay for and never track.

2. Duplicate HCL resource blocks (copy-paste incident) A developer copies a resource block and changes the logical Terraform name but forgets to change the provider-side name/ID argument (e.g., name = "my-app-role" is identical in both blocks). Terraform generates two API calls for the same provider-side ID. The risk: Non-deterministic apply behavior — whichever resource block wins the race gets created; the second call fails. Your state is now partially applied.

3. State file desync after terraform state rm or workspace switch A resource was removed from state (intentionally or by accident) but the real infrastructure still exists. Re-running apply re-triggers a create call. The risk: In multi-workspace or Terraform Cloud setups, this can cause the same physical resource to be "owned" by two workspaces simultaneously, making future destroys unpredictable and dangerous.


How to Fix It (The Solution)

Root Cause 1: Resource Exists in Provider But Not in State → Use terraform import

- # DO NOT: Run terraform apply hoping it will reconcile
- # This will attempt a CREATE call and hard-fail at the provider API

+ # CORRECT: Import the existing resource into Terraform state first
+ terraform import aws_iam_role.my_app_role my-app-role
+ # Syntax: terraform import <RESOURCE_TYPE>.<LOGICAL_NAME> <PROVIDER_SIDE_ID>
+ # Then run: terraform plan
+ # Verify the plan shows no changes (or only acceptable drift)

Root Cause 2: Duplicate HCL Resource Blocks → Deduplicate and Use for_each

- # BAD: Two resource blocks with identical provider-side `name` argument
- resource "aws_iam_role" "app_role_v1" {
-   name = "my-app-role"
-   assume_role_policy = data.aws_iam_policy_document.assume.json
- }
- 
- resource "aws_iam_role" "app_role_v2" {
-   name = "my-app-role"  # DUPLICATE — will collide at the API
-   assume_role_policy = data.aws_iam_policy_document.assume.json
- }

+ # GOOD: Single resource block, or use for_each with unique names
+ locals {
+   app_roles = {
+     "app-role-primary"   = "arn:aws:iam::aws:policy/ReadOnlyAccess"
+     "app-role-secondary" = "arn:aws:iam::aws:policy/PowerUserAccess"
+   }
+ }
+ 
+ resource "aws_iam_role" "app_roles" {
+   for_each = local.app_roles
+   name     = each.key  # Unique provider-side name per iteration
+   assume_role_policy = data.aws_iam_policy_document.assume.json
+ }

Root Cause 3: State Desync After state rm → Re-import or Use moved Block

- # BAD: Manually removing state and re-applying without importing
- # terraform state rm aws_s3_bucket.data_lake  ← someone ran this
- # terraform apply                              ← now tries to CREATE, fails

+ # OPTION A: Re-import the orphaned resource
+ terraform import aws_s3_bucket.data_lake my-data-lake-bucket-prod
+
+ # OPTION B (Terraform >= 1.1): Use a moved block to handle logical renames
+ # without dropping and re-importing state
+ moved {
+   from = aws_s3_bucket.old_name
+   to   = aws_s3_bucket.data_lake
+ }

Enterprise Best Practice: Enforce Import-Before-Apply in Pipelines

- # BAD: Raw terraform apply in CI with no drift detection
- steps:
-   - run: terraform apply -auto-approve

+ # GOOD: Plan first, check for known-after-apply conflicts, gate on approval
+ steps:
+   - run: terraform plan -detailed-exitcode -out=tfplan
+   - run: |
+       # Exit code 2 = changes present, exit code 1 = error (including duplicate ID)
+       # Fail fast on exit code 1 before apply is ever attempted
+       if [ $? -eq 1 ]; then
+         echo "Terraform plan failed — check for duplicate resource IDs or state drift"
+         exit 1
+       fi
+   - run: terraform apply tfplan  # Only runs if plan succeeded

💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

1. Checkov — Detect duplicate resource names pre-plan:

checkov -d . --check CKV_TF_1
# Also run: checkov -d . --framework terraform
# Checkov will flag resource blocks with identical name arguments

2. terraform validate + tflint as a pre-commit gate:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    hooks:
      - id: terraform_validate
      - id: terraform_tflint
        args:
          - --args=--enable-rule=terraform_required_providers

3. OPA/Conftest policy — Block plans with create actions on already-tracked IDs:

# policy/no_duplicate_ids.rego
package terraform

deny[msg] {
  rc := input.resource_changes[_]
  rc.change.actions[_] == "create"
  rc.change.before != null  # Resource already has prior state — should be update, not create
  msg := sprintf("Resource '%v' has prior state but action is CREATE — possible duplicate ID conflict", [rc.address])
}

4. Terraform Cloud / Atlantis — Always require plan approval before apply: Never run apply -auto-approve on resources that manage unique provider-side identifiers (IAM role names, S3 bucket names, RDS identifiers). Gate every apply on a human-reviewed plan artifact. Use remote state locking (DynamoDB for S3 backend) to prevent concurrent applies that can generate race-condition duplicate ID errors.

5. Tag and track manually created resources: Enforce an IaC tag policy (e.g., ManagedBy = terraform) on all resources. Any resource without this tag in your account is a candidate for the duplicate ID error the next time someone tries to codify it. Use AWS Config rules or GCP Asset Inventory to audit untagged resources before they collide with your HCL.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →