When you need import
A Terraform project rarely starts from scratch. Common situations:
- Legacy infrastructure. Buckets, IAM roles, and VPCs were created by hand or with another tool. You want to bring them under Terraform. You need to "adopt" them.
- Duplicate-resource conflict. One project ran
apply; another project lost its state. Now both try to create the same resource, and the cloud rejects it. Either remove the old resource withstate rmand import it into the new project, or decide which project is the owner. - State recovery. The state file is gone. The resources are still in the cloud. Import each one. There is no other way.
Import does not write HCL for you. It only links "this resource block in HCL" to "this ID in the cloud." You must write the resource block yourself.
The old way: terraform import
A CLI command that works with all Terraform versions.
# 1. Describe the resource in HCL, without cloud attributes for now
cat > main.tf <<'EOF'
resource "aws_s3_bucket" "legacy" {bucket = "my-legacy-bucket-2018"
}
EOF
# 2. Run the import
terraform import aws_s3_bucket.legacy my-legacy-bucket-2018
After this:
- State now contains
aws_s3_bucket.legacywith the real attributes from the cloud. terraform plancompares HCL against state. Any difference appears as a diff.
The ID format depends on the resource type
Each AWS resource has its own import ID format:
| Resource | ID format |
|---|---|
aws_s3_bucket | bucket name: my-bucket |
aws_iam_role | role name: my-role |
aws_security_group | sg-id: sg-1234abcd |
aws_route_table_association | composite: subnet_id/rtb_id |
aws_vpc | vpc-id: vpc-1234abcd |
aws_instance | instance-id: i-1234abcd |
The Registry page for each resource has an "Import" section at the bottom.
If you do not know the ID, use aws s3 ls, aws iam list-roles,
or aws ec2 describe-*.
The new way: import block (TF 1.5+)
Declarative, leaves a trace in HCL, and is visible in plan.
# main.tf
import {to = aws_s3_bucket.legacy
id = "my-legacy-bucket-2018"
}
resource "aws_s3_bucket" "legacy" {bucket = "my-legacy-bucket-2018"
}
Run:
terraform plan
Output:
Terraform will perform the following actions:
# aws_s3_bucket.legacy will be imported
id = "my-legacy-bucket-2018"
...
terraform apply
After apply:
- State contains the resource.
- HCL contains the
importblock (you can delete it; it is no longer needed) and theresourceblock.
Advantages over the CLI command:
- Visible in a diff. A PR with an
importblock makes it clear: "we are adopting this resource." - Plan before apply. The CLI
terraform importacts immediately. Theimportblock does not:planshows what will happen first, thenapplycommits it. - Catches HCL mistakes. If you mistyped the bucket name in HCL, plan shows the conflict before anything changes.
- Supports for_each. An
importblock withfor_each(TF 1.7+) adopts N resources with one block.
import block with for_each
variable "legacy_buckets" {type = set(string)
default = ["bucket-a", "bucket-b", "bucket-c"]
}
import {for_each = var.legacy_buckets
to = aws_s3_bucket.legacy[each.key]
id = each.value
}
resource "aws_s3_bucket" "legacy" {for_each = var.legacy_buckets
bucket = each.value
}
One block, N imports. Especially useful when migrating from another IaC system (Pulumi, CloudFormation) where the list of resources is already known.
HCL generation: -generate-config-out
Writing HCL by hand for a large resource (say, aws_iam_policy with dozens of
statements) is tedious. With TF 1.5+ you can ask Terraform to generate a draft:
# main.tf - no resource block, only the import block
import {to = aws_iam_policy.legacy
id = "arn:aws:iam::123456789012:policy/legacy"
}
terraform plan -generate-config-out=generated.tf
generated.tf will contain a generated resource "aws_iam_policy" "legacy" { ... }
with all attributes. Read it carefully. It may include:
- Computed-only fields (the provider sets these automatically; you can remove them from HCL).
- Lifecycle blocks you did not intend.
- Arguments that no longer exist in newer versions of the AWS provider.
The generated HCL is a draft, not a final version. You must review and clean it up.
What to do after import
- Run
terraform plan. It must be clean (No changes). - If it shows a diff, your HCL attributes differ from the real resource. Adjust HCL to match the cloud. If you intentionally want to change something, apply will reconcile the cloud to your HCL, but that is risky in production.
- You can delete
importblocks after a successful apply. Many teams keep them in HCL as documentation of what was adopted and when.
Pitfalls
-
Import does not write HCL attributes. It only links state to the cloud. Writing HCL is your job. Without a matching HCL block,
applywill try to destroy the resource (it is in state but absent from HCL). -
Module resources use their full address.
terraform import module.app.aws_s3_bucket.this <ID>. With animportblock inside a module, you can declare the block within the module and writeto = aws_s3_bucket.this; Terraform resolves the address from context. -
count/for_eachresources require an index. Useaws_iam_user.user["alice"], notaws_iam_user.user. A wrong address imports into the wrong slot, and plan will show a destroy+create pair. -
Not every resource supports import. The AWS provider covers most resources, but exceptions exist. Some
aws_lambda_aliasandaws_ssm_parameter_*resources may lack an import handler. Check the "Import" section at the bottom of each resource's documentation page, or look for "This resource cannot be imported." -
-generate-config-outplaces the file in the current directory. Terraform writesgenerated.tfnext to your other files and does not integrate it further. Reviewing, splitting across files, and removing unneeded parts is your responsibility. -
Secret data ends up in state after import. If you import an
aws_db_instance, the database password appears in state in plain text. This is how import works, not a bug. See tf-state for guidance on protecting state. -
Import does not carry over tags, lifecycle rules, or similar metadata. If your HCL has
lifecycle { prevent_destroy = true }, that setting is a Terraform-side metadata instruction and is not pushed to the cloud resource.