Quick answer: To import an existing resource into Terraform state, write a resource block for it in your configuration and then tell Terraform which real object it corresponds to, either with an import block (Terraform 1.5+, reviewable in a pull request and able to generate the configuration for you) or with the older terraform import ADDRESS ID command. Terraform records the mapping in state without changing the resource; you then run terraform plan until it shows no changes.
Almost every team adopts Terraform after some infrastructure already exists: a VPC built by hand, an S3 bucket created by a script, a database nobody remembers provisioning. Import is how you bring those resources under management without recreating them. This guide covers both import methods, how to find resource IDs, how to generate configuration automatically, how to import many resources at once, and the mistakes that lead to accidental deletion.
Why import exists and what it does not do
Terraform only manages what is in its state. A resource created outside Terraform is invisible to it, so plan would try to create a duplicate (and usually fail with “already exists”). Import adds a state entry that maps a resource address in your code to the real object’s ID. After that, the resource is managed exactly as if Terraform had created it.
Two things import does not do:
- It never modifies, recreates or deletes the real resource. Import is a state-only operation.
- Before Terraform 1.5 it did not write configuration for you. With the CLI command, you must author the
resourceblock by hand and make it match reality.
Understanding what state records, and why it must agree with your configuration, is the foundation here; if you need a refresher, read Understanding Terraform State: What It Is and Why It Matters.
Method 1: the import block (Terraform 1.5 and later)
The modern, recommended approach is declarative. You write an import block alongside your resources, and Terraform performs the import as part of plan and apply. Suppose an EC2 instance i-0a1b2c3d4e5f67890 was created manually in Mumbai.
# imports.tf
import {
to = aws_instance.legacy_app
id = "i-0a1b2c3d4e5f67890"
}
Now let Terraform write the resource block for you:
terraform init
terraform plan -generate-config-out=generated.tf
Terraform reads the real instance and writes a complete resource "aws_instance" "legacy_app" block into generated.tf, with every attribute filled in. Clean it up: delete computed values such as arn and id that cannot be set, replace hard-coded values with variables where appropriate, and move the block into your normal .tf files. Then:
terraform plan # should show: 1 to import, 0 to add, 0 to change, 0 to destroy
terraform apply # performs the import and writes state
Once applied, delete the import block; it has done its job. Because the block lives in code, a reviewer can see exactly what will be imported before anything happens, which the CLI command never allowed.
Method 2: the terraform import command
The CLI approach still works in every 1.x version and is handy for one-off fixes. Write a minimal resource block first; Terraform requires the address to exist in configuration.
resource "aws_instance" "legacy_app" {
ami = "ami-0f58b397bc5c1f2e8" # fill in real values after import
instance_type = "t3.medium"
}
terraform import aws_instance.legacy_app i-0a1b2c3d4e5f67890
terraform state show aws_instance.legacy_app # copy real attributes into your code
terraform plan # iterate until "No changes."
The loop of state show, edit, plan is the tedious part. Any attribute you leave out that has no default, or that differs from reality, appears in the plan as an update or, worse, a replacement. Keep adjusting the configuration until the plan is empty.
Finding the right resource ID
Each resource type defines its own import ID format, and getting it wrong is the most common failure. The provider documentation page for every resource has an “Import” section at the bottom. Some frequently needed formats:
| Resource type | Import ID format | Example |
|---|---|---|
aws_instance |
Instance ID | i-0a1b2c3d4e5f67890 |
aws_s3_bucket |
Bucket name | tkh-prod-assets |
aws_iam_role_policy_attachment |
ROLE/POLICY_ARN |
app-role/arn:aws:iam::aws:policy/ReadOnlyAccess |
aws_security_group_rule |
Composite: sg id, type, protocol, ports, CIDR | sg-0abc_ingress_tcp_443_443_0.0.0.0/0 |
azurerm_resource_group |
Full Azure resource ID | /subscriptions/<sub>/resourceGroups/rg-prod |
google_compute_instance |
project/zone/name |
tkh-prod/asia-south1-a/web-1 |
kubernetes_namespace |
Namespace name | payments |
Use the cloud CLI to look IDs up: aws ec2 describe-instances --filters Name=tag:Name,Values=legacy-app --query 'Reservations[].Instances[].InstanceId', az resource list, or gcloud compute instances list.
Importing many resources and into modules
Import blocks accept expressions, so a whole fleet can be imported with for_each:
locals {
legacy_buckets = {
assets = "tkh-prod-assets"
logs = "tkh-prod-logs"
backup = "tkh-prod-backup"
}
}
import {
for_each = local.legacy_buckets
to = aws_s3_bucket.legacy[each.key]
id = each.value
}
resource "aws_s3_bucket" "legacy" {
for_each = local.legacy_buckets
bucket = each.value
}
# Importing into a module works the same way
import {
to = module.network.aws_vpc.this
id = "vpc-0f1e2d3c4b5a69788"
}
Note that the to address must exactly match how the resource is addressed inside the module, including any for_each keys. Run terraform plan and verify the summary line reads “to import” with zero destroys before applying. For how resource addresses and references fit together, see Creating and Managing Infrastructure Resources in Terraform.
Six import mistakes that cause damage
- Applying before the plan is clean. If the plan shows a replacement after import, your configuration does not match reality. Fix the code; do not apply and hope.
- Importing into the wrong address.
aws_instance.webalready exists in state and you import another ID into it: Terraform refuses, but if you usedstate rmfirst, the original is now orphaned. Double-check addresses. - Forgetting dependent sub-resources. An S3 bucket’s versioning, policy and lifecycle rules are separate resources in the AWS provider (
aws_s3_bucket_versioningand friends) and must be imported individually or Terraform will “add” them. - Running import without a lock or against the wrong workspace. Import writes state. Confirm the backend and workspace with
terraform workspace showfirst. - Leaving generated configuration unreviewed.
-generate-config-outproduces verbose code with hard-coded IDs and sensitive values. Refactor it before merging. - Assuming every resource is importable. A few resource types (and some
_attachmentor association types in older providers) do not support import. Check the docs; if import is unsupported, you may need to recreate under Terraform during a maintenance window.
Frequently asked questions
Does importing change the resource in the cloud?
No. Import only writes to state. The first apply after import changes the resource only if your configuration differs from what was imported, which is why you must get the plan to zero changes first.
Can I import a resource and keep it from ever being destroyed?
Add lifecycle { prevent_destroy = true } to critical imported resources such as databases. Terraform will refuse any plan that would delete them.
What is the difference between import and terraform state mv?
Import brings an object that Terraform does not know about into state. state mv (or a moved block) renames an object that is already in state. If you refactored code and Terraform wants to destroy and recreate, you need moved, not import.
Is there a tool to import an entire account?
Yes. Terraformer (by Google) and various cloud-specific exporters such as Azure’s aztfexport scan an account and emit both state and configuration. They are a good starting point for large migrations, but always review and refactor their output.
Key takeaways
- Import maps an existing cloud object to a resource address in state; it never touches the object itself.
- Prefer
importblocks with-generate-config-outon Terraform 1.5+; fall back toterraform import ADDRESS IDfor quick one-offs. - Look up the exact import ID format in the provider docs, and import sub-resources separately.
- Do not apply until
terraform planreports zero adds, changes and destroys.
Migrating real infrastructure into Terraform is a core skill for DevOps roles, and our DevOps course covers import, state management and CI/CD with hands-on cloud labs, mentor support and placement assistance. Prefer to learn by watching? Follow our YouTube channel.



