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 rmwas 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:
| Resource | Import id |
|---|---|
aws_s3_bucket | Bucket name |
aws_iam_role | Role name |
aws_security_group | Group id, sg-... |
aws_instance | Instance id, i-... |
aws_iam_role_policy_attachment | role-name/policy-arn |
aws_route53_record | zoneid_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
terraform state list | grep <name>.- Not in state → import it, or rename yours.
- Use an
importblock rather than the CLI command, so it is reviewable. - Unknown configuration →
terraform plan -generate-config-out=generated.tf. - Check the import id format in the provider docs; it varies per resource.
- Always
terraform planafter importing, and read the diff before applying. - In state and still failing → check for a concurrent apply and confirm locking.
- Same name across workspaces → include
terraform.workspacein 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.