Terraform Import: Fixing State Conflicts with Existing Resources
Getting state conflicts or duplicate resource errors with terraform import? Learn how to import existing AWS resources, fix config mismatches, and use moved blocks to rename state entries.
Running terraform import is one of those operations that sounds simple until it blows up. You created a resource manually in the AWS console, lost a state file, or onboarded an existing environment — now Terraform doesn't know about it, and you need to bring it under management without destroying and recreating it.
Here's how to do it without breaking your infrastructure.
When You Actually Need terraform import
Three common scenarios:
- Resource created manually — someone clicked through the AWS console and now the pipeline fails with a resource already existing
- Lost state file — the
.tfstatewas deleted, corrupted, or was never stored in a remote backend - Onboarding an existing environment — you're writing Terraform for infrastructure that's been running for years
Write the Config First — This is Mandatory
Before you run terraform import, you need a resource block in your .tf files that matches the resource. Import only adds the resource to state; it does NOT write your config for you.
Example: you have an S3 bucket my-app-prod-logs that was created manually.
resource "aws_s3_bucket" "prod_logs" {
bucket = "my-app-prod-logs"
}Now import it:
terraform import aws_s3_bucket.prod_logs my-app-prod-logsOutput:
aws_s3_bucket.prod_logs: Importing from ID "my-app-prod-logs"...
aws_s3_bucket.prod_logs: Import prepared!
Prepared aws_s3_bucket for import
aws_s3_bucket.prod_logs: Refreshing state... [id=my-app-prod-logs]
Import successful!
The Most Common Problem: Config Doesn't Match
After import, run terraform plan. If the plan shows changes, your config doesn't match the real resource. This is the most frequent issue — and it will cause drift.
terraform plan
# Plan shows:
# ~ resource "aws_s3_bucket" "prod_logs" {
# + tags = {}
# ~ versioning {
# + enabled = false -> true
# }
# }Fix: inspect the actual resource with terraform state show, then update your config to match.
terraform state show aws_s3_bucket.prod_logsThis dumps every attribute Terraform read from AWS. Copy the values you care about into your resource block. For attributes you don't want to manage, use lifecycle { ignore_changes = [...] }:
resource "aws_s3_bucket" "prod_logs" {
bucket = "my-app-prod-logs"
lifecycle {
ignore_changes = [tags, cors_rule]
}
}After updating config, terraform plan should show no changes.
Error: Resource Already Managed
Error: Resource already managed by Terraform
on main.tf line 5, in resource "aws_s3_bucket" "prod_logs":
5: resource "aws_s3_bucket" "prod_logs" {
To import to this address you must first remove the existing object
from the state.
This means the resource address is already in your state file (pointing to a different real resource, or a stale entry). Fix:
# Remove the stale entry from state
terraform state rm aws_s3_bucket.prod_logs
# Then re-import
terraform import aws_s3_bucket.prod_logs my-app-prod-logsImporting Complex Resources: IAM Roles
IAM roles are tricky because Terraform splits the role and its policy attachments into separate resources.
# Import the role itself
terraform import aws_iam_role.app_role my-app-role
# Import the policy attachment (format: role-name/policy-arn)
terraform import aws_iam_role_policy_attachment.app_role_policy \
my-app-role/arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
# Import an inline policy (format: role-name:policy-name)
terraform import aws_iam_role_policy.inline_policy \
my-app-role:my-inline-policy-nameEach of these needs a corresponding resource block before you import. After importing all three, run terraform plan and fix any mismatches.
Using the moved Block for State Renames
If you refactor your Terraform code — moving a resource into a module or renaming it — instead of removing from state and re-importing, use the moved block:
moved {
from = aws_s3_bucket.prod_logs
to = module.storage.aws_s3_bucket.logs
}This tells Terraform "the resource at this old address is now at this new address." No destroy, no import needed. After applying, remove the moved block from your config.
Automating Imports with import Block (Terraform 1.5+)
Terraform 1.5 added declarative imports — no CLI command needed:
import {
to = aws_s3_bucket.prod_logs
id = "my-app-prod-logs"
}
resource "aws_s3_bucket" "prod_logs" {
bucket = "my-app-prod-logs"
}Run terraform plan — it shows the import as part of the plan. Run terraform apply — it imports and applies. This is now the preferred approach for new workflows.
Checklist Before You Import
- Write the resource block first
- Use
terraform state listto check if address already exists - Run
terraform planafter import and fix all diffs - Store state in a remote backend (S3 + DynamoDB) before doing any of this on a team
- Never import into a workspace someone else is actively using — lock the state first
The terraform import workflow takes patience, but it's far safer than deleting and recreating resources that have live traffic or data attached to them.
Today I Fixed
Short real fixes from production — posted daily
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.
Related Articles
Terraform Backend S3 Init Failed — Every Cause and Fix (2026)
terraform init fails with S3 backend errors — access denied, bucket does not exist, state lock issues, wrong region. Here's every cause and the exact fix for each one.
Terraform S3 Access Denied — How to Fix It (2026)
Getting AccessDenied when Terraform tries to read or write your S3 backend? Here's every cause and the exact fix.
Terraform Error Acquiring the State Lock: Causes and Fix
Terraform state lock errors can block your entire team. Learn why they happen, how to safely unlock state, and how to prevent lock conflicts for good.