TerraformTerraform

Error: resource already exists

Terraform tried to create something that is already there. How to decide between importing it and renaming, and why this usually means state and reality have diverged.

medium fix6 min read

the terraform error
Error: creating S3 Bucket (acme-assets): BucketAlreadyOwnedByYou: Your previous request to create the named bucket succeeded and you already own it

Error: creating IAM Role (lambda-exec): EntityAlreadyExists: Role with name lambda-exec already exists.

Error: InvalidGroup.Duplicate: The security group 'app' already exists for VPC 'vpc-0a1b2c3d'

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Check whether Terraform already knows about it

    terraform state list | grep -i assets

    Present in state means the resource is tracked and something else is wrong, usually a name collision with a second configuration. Absent means state and reality have diverged and the object exists outside Terraform's knowledge.

  2. 2

    Import it instead of recreating it

    terraform plan -generate-config-out=generated.tf

    Used with an import block, this writes matching configuration for a resource that already exists, which is far more reliable than hand-writing it and discovering differences during the next apply.

  3. 3

    Verify the import produced no changes

    terraform plan

    A clean plan after import means your configuration matches reality. Any diff is a real difference between what you wrote and what exists, and applying it will change live infrastructure.

All 8 sections

Terraform's plan said "create", the provider said "it is already there". State and reality disagree.

First, establish which:

terraform state list | grep -i assets

Not in state. The object exists in the cloud and Terraform does not know about it. Import it, or rename yours.

In state. Terraform thinks it manages it and still tried to create it. Usually a second configuration managing the same object, or a resource that was replaced and the create ran before the destroy.

How state and reality diverge

  • Someone created it by hand in the console.
  • An earlier apply created it and then failed before writing state. Local state on a laptop that was never committed is a classic.
  • Two configurations, or two workspaces, generating the same name.
  • State was deleted or rolled back.
  • terraform state rm was run on something that still exists.

The last two are worth checking first if this appeared suddenly.

Fix 1: Import

The right answer when the existing object should be managed by this configuration.

Since Terraform 1.5 use an import block, which is reviewable and runs as part of the plan:

import {
  to = aws_s3_bucket.assets
  id = "acme-assets"
}

resource "aws_s3_bucket" "assets" {
  bucket = "acme-assets"
}
terraform plan
terraform apply

For a resource you have not written configuration for yet, let Terraform generate it:

terraform plan -generate-config-out=generated.tf

That writes a matching resource block, which is far more reliable than hand-writing one and discovering the differences during the next apply. Review it before committing; generated configuration includes every attribute, many of which are defaults you would rather omit.

The older CLI form still works:

terraform import aws_s3_bucket.assets acme-assets

The import block is preferable because it lives in code, goes through review, and is visible in the plan rather than being an out-of-band command someone ran once.

The import id is resource-specific and is the most common thing to get wrong. Some take a name, some an ARN, some a compound string:

ResourceImport id
aws_s3_bucketBucket name
aws_iam_roleRole name
aws_security_groupGroup id, sg-...
aws_instanceInstance id, i-...
aws_iam_role_policy_attachmentrole-name/policy-arn
aws_route53_recordzoneid_name_type

Each resource's provider documentation has an import section. Check it rather than guessing.

After importing, always plan

terraform plan

A clean plan means your configuration matches reality. Anything else is a real difference, and applying it changes live infrastructure. A frequent surprise is tags: the existing object has tags your configuration does not, so the plan proposes removing them.

Import brings the object into state with its current attributes. It does not modify the object.

Fix 2: Rename yours

When the existing object belongs to something else and you want a separate one, change the name:

resource "aws_s3_bucket" "assets" {
  bucket = "acme-assets-${var.environment}"
}

Globally unique namespaces make this common. S3 bucket names are unique across all of AWS, so acme-assets may be owned by a stranger, which surfaces as BucketAlreadyExists rather than BucketAlreadyOwnedByYou. Those two messages are worth distinguishing: one means you own it, the other means somebody else does.

IAM roles and policies are unique per account; security groups are unique per VPC.

For generated names, random_id avoids collisions:

resource "random_id" "suffix" {
  byte_length = 4
}

resource "aws_s3_bucket" "assets" {
  bucket = "acme-assets-${random_id.suffix.hex}"
}

Fix 3: Two configurations own the same object

If a resource is in state and the create still failed, something else may have created it between plan and apply.

terraform state list
terraform state show aws_s3_bucket.assets

Two pipelines applying the same configuration concurrently produce this, and it is the reason state locking exists. Confirm the backend actually locks:

terraform {
  backend "s3" {
    bucket       = "acme-tfstate"
    key          = "prod/terraform.tfstate"
    region       = "eu-west-1"
    use_lockfile = true
  }
}

S3 native locking via use_lockfile replaced the DynamoDB table approach in Terraform 1.10. Older configurations use dynamodb_table, which still works.

Without locking, two concurrent applies interleave and produce exactly this class of error.

Workspaces and naming

resource "aws_iam_role" "lambda" {
  name = "lambda-exec"          # same in every workspace
}

Workspaces isolate state, not names. Applying in dev and then prod creates the second one against an existing IAM role.

  name = "lambda-exec-${terraform.workspace}"

A checklist

  1. terraform state list | grep <name>.
  2. Not in state → import it, or rename yours.
  3. Use an import block rather than the CLI command, so it is reviewable.
  4. Unknown configuration → terraform plan -generate-config-out=generated.tf.
  5. Check the import id format in the provider docs; it varies per resource.
  6. Always terraform plan after importing, and read the diff before applying.
  7. In state and still failing → check for a concurrent apply and confirm locking.
  8. Same name across workspaces → include terraform.workspace in the name.

Frequently Asked Questions

Should I import the existing resource or delete it?

Import, unless you are certain the object is disposable. Deleting production infrastructure to let Terraform recreate it causes an outage and usually loses data, and for stateful resources such as S3 buckets and RDS instances it may not be recoverable at all. Import brings the object under management with its current attributes and changes nothing about it. The only case for deleting is a genuinely empty leftover from a failed apply that you have verified nothing depends on.

What is the difference between an import block and terraform import?

The CLI command is imperative and runs immediately, so the import happens outside your code and outside review; anyone reading the repository later has no record of it. An import block, available since Terraform 1.5, lives in the configuration, appears in the plan output, and is applied as part of a normal apply. It can also be paired with -generate-config-out to write the matching resource configuration for you. The block is the better default, and you can remove it once the import has been applied.

Why does terraform plan show changes right after an import?

Because import records the object's actual current attributes in state, and your configuration differs from them. The diff is therefore genuine: applying it will modify live infrastructure. Tags are the usual culprit, since the existing object typically has some your configuration omits and the plan proposes removing them. Read the diff carefully and adjust your configuration to match reality, rather than applying and discovering what changed afterwards.

Why do two workspaces conflict on the same resource name?

Workspaces isolate state, not the names of the objects you create. If a resource has a hardcoded name, applying the configuration in a second workspace tries to create a second object with the identical name in the same account, which most providers reject. Include the workspace in generated names, for example "lambda-exec-${terraform.workspace}", or use separate accounts per environment, which is generally the stronger isolation and avoids the whole class of problem.

What does BucketAlreadyOwnedByYou mean, as opposed to BucketAlreadyExists?

BucketAlreadyOwnedByYou means the bucket exists in your own account, so import is available and is almost certainly what you want. BucketAlreadyExists means the name is taken by a different AWS account entirely, because S3 bucket names are globally unique across all of AWS, and no import is possible. In that case you must choose a different name. Distinguishing the two messages saves a lot of time, because they look similar and imply completely different actions.

Reference and practice

Learn the underlying concept

Other Terraform errors