TerraformX · State Operations: Read, Move, Remove, ImportProduction Terraform
Import: Adopting Existing Infrastructure
What you'll learn
- Use terraform import and the import block to bring existing resources under Terraform management
- Find the real-world resource ID for any provider resource
- Choose between the CLI form (terraform import) and the configuration form (import block)
- Verify the import with a clean plan and handle import-only attribute drift
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
A team has been running production for six months. The databases,
load balancers, and VPCs were provisioned manually. The team wants
to bring them under Terraform. terraform import is the bridge:
it writes a state entry pointing at the existing real-world
resource. The next plan sees the configuration, sees the state,
and (after a careful write-back of attributes) proposes no
changes.
Two forms: CLI and configuration
Terraform 1.5 introduced a declarative import block. The CLI form remains for one-off operations.
CLI form (any version):
terraform import 'aws_instance.web' 'i-0a1b2c3d4e5f6a7b8'
The first argument is the resource address in configuration. The second is the real-world identifier. Terraform reads the real-world attributes and writes them into state.
Configuration form (Terraform 1.5+, OpenTofu 1.6+):
import {
to = aws_instance.web
id = "i-0a1b2c3d4e5f6a7b8"
}
The block lives in the .tf file alongside the resource block. On
the next terraform plan, Terraform reads the ID, fetches the
real-world attributes, and writes them into state. The block can
be removed after a successful import; it is a one-shot
declaration, not a permanent fixture.
Finding the real-world ID
The import command needs the resource ID in the format the provider expects. Common patterns:
| Resource type | ID format | Where to find it |
|---|---|---|
aws_instance | i-0a1b2c3d4e5f6a7b8 | EC2 console or aws ec2 describe-instances |
aws_vpc | vpc-0123456789abcdef0 | VPC console or aws ec2 describe-vpcs |
aws_s3_bucket | my-bucket-name | S3 console or aws s3api list-buckets |
google_compute_instance | projects/<proj>/zones/<zone>/instances/<name> | gcloud compute instances list |
azurerm_resource_group | /subscriptions/.../resourceGroups/<name> | Azure portal or az group list |
kubernetes_pod | default/web-pod-abc123 | kubectl get pod -n default -o name |
The provider documentation for each resource type lists the expected ID format. There is no canonical way to derive it from the resource address; the operator must look it up.
The procedure
# 1. Add the resource block to configuration (the address)
# resource "aws_instance" "web" { ... }
# Use placeholder values for attributes; you will overwrite them.
# 2. Import using the CLI form
terraform import -backup-file=/tmp/import-backup.tfstate \
'aws_instance.web' 'i-0a1b2c3d4e5f6a7b8'
# Output:
# aws_instance.web: Importing from ID "i-0a1b2c3d4e5f6a7b8"...
# aws_instance.web: Import complete!
# id = i-0a1b2c3d4e5f6a7b8
# aws_instance.web: Refreshing state...
# Import success! The state was populated. The next plan will not
# propose any changes for this resource unless its attributes differ
# from the configuration.
# 3. Inspect the imported state
terraform state show 'aws_instance.web'
# Read every attribute. The values are what the provider API returned.
# 4. Copy the imported attributes back into the configuration
# (or use terraform plan -refresh-only and let the plan show the diff)
# 5. Plan; expect no changes (or only the attribute write-back)
terraform plan
# If attributes differ from configuration: copy them into the .tf file.
# If they match: no changes.
# 6. Repeat for every resource being adopted
The step “copy attributes back into configuration” is the part
most teams skip. Without it, the next plan shows a massive diff
proposing to overwrite every imported attribute with the values in
the .tf file.
The import block workflow
import {
to = aws_instance.web
id = "i-0a1b2c3d4e5f6a7b8"
}
resource "aws_instance" "web" {
# Placeholder values — overwritten by the import
ami = "ami-placeholder"
instance_type = "t3.medium"
}
terraform plan
# aws_instance.web: Refreshing state... [id=i-0a1b2c3d4e5f6a7b8]
# aws_instance.web: Importing... [id=i-0a1b2c3d4e5f6a7b8]
# Plan: 0 to add, 0 to change, 0 to destroy.
# (After the import, plan shows the diff between imported state and config.)
# Apply to write the imported state
terraform apply
# aws_instance.web: Import complete!
# Plan: 0 to add, 0 to change, 0 to destroy.
# Remove the import block — it is one-shot
# Edit the .tf file to remove the import { } block
The advantage: the import is reviewable in the same pull request as the configuration block. The reviewer sees what is being adopted.
Generated configuration for large imports
For projects adopting dozens of resources, the manual procedure is slow. Tools exist to generate the configuration:
terraformer— generates.tffrom existing infrastructure for many providers.tfimport— generates the import block list from existing infrastructure.- Provider-specific importers —
aws terraform importgenerators, etc.
The discipline: review every generated resource block before applying. Generated configuration is a starting point, not a finished artefact.
What import does NOT do
import does not:
- Modify the real-world resource. It only writes state.
- Update the configuration. The operator must do that.
- Resolve dependencies automatically. Resources with
depends_onreferences in configuration must be imported first. - Handle resources with
countorfor_eachkeys automatically. Each instance is a separate import.
Validation
READ-ONLY
# Confirm the import succeeded
terraform state list | grep 'aws_instance.web'
# aws_instance.web
# Confirm the ID matches
terraform state show 'aws_instance.web' | grep '^ id '
# id = i-0a1b2c3d4e5f6a7b8
# Confirm the plan reflects the imported state
terraform plan
# Expect: 0 to add, 0 to change, 0 to destroy (after the attribute write-back)
If the plan still proposes changes, the configuration block has
attributes that differ from the real-world values. Update the
configuration to match (or use lifecycle.ignore_changes for
attributes the team does not want to manage).
Production failure modes
Symptom: “Error: Cannot import non-existent remote object”.
Cause: the real-world ID is wrong or the resource has been
deleted. Verify the ID with the provider’s API:
aws ec2 describe-instances --instance-ids i-0abc....
Symptom: import succeeds but plan proposes to overwrite every attribute. Cause: the configuration block has placeholder values that differ from the real-world attributes. Copy the imported attributes back into the configuration.
Symptom: import succeeds but the resource block is missing attributes that the provider requires. Cause: the configuration block is incomplete. Add the missing required attributes; some can be sourced from the imported state.
Symptom: import for a count resource imports the first instance
only. Cause: each count instance has a separate ID. Import
each one explicitly:
terraform import 'aws_instance.web[0]' 'i-0aaa'
terraform import 'aws_instance.web[1]' 'i-0bbb'
Symptom: import block errors on terraform plan with
“id is required”. Cause: the import block has no id
attribute. Add the real-world ID.
Recovery
terraform state rmto detach the wrongly-imported resource.- Investigate why the import failed (wrong ID, wrong configuration).
- Retry the import with the corrected inputs.
- Verify with a clean plan.
What comes next
The next lesson covers moved blocks — the declarative
counterpart to state mv for refactoring resource addresses in
code.
Verification
- You can choose between the CLI import and the import block for each adoption scenario.
- You can find the real-world resource ID for a provider resource.
- You can copy imported attributes back into configuration to produce a clean plan.
- You can handle a
count-based import with one import per instance.
Knowledge check · 7 questions
Q1. What does `terraform import` do?
Q2. Which Terraform version introduced the declarative import block?
Q3. After a successful import, the next plan is empty only if the configuration block's attributes already match the attributes that were imported into state.
Q4. For a `count = 3` resource, how many import commands are needed to adopt all three instances?
Q5. Which of these are valid forms of the import command? (Select all that apply.)
Q6. After a successful import, the plan proposes to overwrite many attributes. What does this mean?
Q7. A team wants to bring 30 existing AWS resources under Terraform management. They want the changes to be reviewable in a pull request. Which import form is preferred?
Passing score: 75%. Answers are checked in this browser.