How to Import Existing Resources into Terraform
Bring cloud resources that already exist under Terraform management without recreating them, one at a time or hundreds at once, using import blocks or the classic import command.
Key takeaways
- Import an existing cloud resource by adding an import block with the resource address and cloud ID, then running terraform plan -generate-config-out=generated.tf to draft the HCL.
- Clean up the generated config, apply, and keep tuning until plan reports no changes.
- Import blocks need Terraform 1.5 or later; for_each on them needs 1.7.
On this page
Import an existing resource by adding an import {} block that maps a resource address to the cloud ID, then running terraform plan -generate-config-out=generated.tf. Terraform drafts the HCL, the plan shows 1 to import, and nothing is created or destroyed. Clean the generated file, apply, and keep tuning until plan reports no changes.
Verified against
Terraform 1.17.1 (current release), with import blocks from the 1.5 changelog and import for_each and removed blocks from the 1.7 changelog, against the developer.hashicorp.com import docs. Checked October 2026.
You end up here when a resource exists in the cloud but not in state. Someone clicked it together during an incident, or it predates your Terraform adoption. Write matching config and run apply, and you get:
Error: creating S3 Bucket: BucketAlreadyOwnedByYou
State has no record of the bucket, so Terraform tries to create it. Don't delete and recreate. Import tells Terraform "this real resource maps to this address in my config," and from then on Terraform manages it like anything else.
Two ways to import
Classic: the terraform import command. It writes state but nothing else, so you hand-write the matching resource block yourself.
Modern: the import {} block (added in Terraform 1.5) plus terraform plan -generate-config-out=. Terraform reads the live resource and generates the HCL for you. This is the recommended path, so we start there.
The modern path: import blocks
Say an S3 bucket named acme-prod-assets exists in AWS but not in your config. First, find its import ID. For S3 it's the bucket name (current AWS provider versions also accept an identity object instead of id); for an EC2 instance it's the instance ID like i-0abc123; for a security group, sg-.... Confirm it with the CLI:
$ aws s3api list-buckets --query "Buckets[?Name=='acme-prod-assets']"
Add an import block to any .tf file, pointing a resource address at that ID:
# imports.tf
import {
to = aws_s3_bucket.assets
id = "acme-prod-assets"
}
You haven't written the resource "aws_s3_bucket" "assets" block yet. Let Terraform generate it:
$ terraform plan -generate-config-out=generated.tf
Terraform reads the live bucket, writes a generated.tf with a full resource block, and prints a plan showing 1 to import. Read the file carefully, because generated config is working HCL, not good HCL:
- It includes read-only and default attributes you usually want to delete; they clutter the file and sometimes conflict on apply.
- It hard-codes everything. A generated security group references
vpc-0abc123as a string instead ofaws_vpc.main.id, an account ID or KMS key ARN appears as a literal. Until you wire those into references, Terraform doesn't know the resources depend on each other and can't order operations correctly. - It captures drift as if it were intent. If someone manually set an odd
apply_immediatelyor left a default tag off, the generated config bakes that in. Normalize tags, naming and lifecycle rules against your conventions as you go. - It may leave gaps Terraform couldn't infer, marked with comments.
Move the cleaned block into your real files (for example s3.tf), delete generated.tf, and run terraform apply. You want 1 imported, 0 added, 0 changed, 0 destroyed.
The gotcha: config must match, or plan wants to change it
Import brings the resource into state exactly as it exists. If your HCL says one thing and the cloud says another, the next plan reconciles them, usually by changing the live resource to match your config. If the real bucket has versioning enabled and your config omits it, Terraform may plan to alter it.
The rule: keep tuning config and re-running plan until it reports no changes.
$ terraform plan
# No changes. Your infrastructure matches the configuration.
That line means state, config and reality agree. Any remaining diff is a change Terraform would make on the next apply.
Importing into modules, for_each and count
If the bucket lives inside a module, the to address includes the module path:
import {
to = module.storage.aws_s3_bucket.assets
id = "acme-prod-assets"
}
For resources created with for_each or count, address the specific instance by key, like aws_s3_bucket.assets["prod"]. Since Terraform 1.7 the import block itself also accepts for_each, which saves writing one block per resource:
import {
for_each = local.buckets
to = aws_s3_bucket.this[each.key]
id = each.value
}
The to address must exactly match what Terraform would print in a plan. When in doubt, run terraform plan and copy the address.
Importing at scale: 200 resources, no state file
Picture the common inheritance: an AWS account with a one-line handover doc and roughly 200 resources built by hand over several years, covering VPCs, security groups, RDS instances and a tangle of IAM roles. No Terraform, no CloudFormation. Rebuilding is off the table because these are live production resources with data and traffic. Hand-writing HCL that matches years of console tweaks for the classic command is days of work and a real chance of a plan that proposes deleting the production database. Generated config turns it into review and clean-up.
What keeps it manageable:
Discover from the API, not the console or a runbook. Generate import blocks programmatically so the list is complete:
$ aws ec2 describe-security-groups \
--query 'SecurityGroups[].[GroupId,GroupName]' \
--output text | while read id name; do
printf 'import {\n to = aws_security_group.%s\n id = "%s"\n}\n' \
"$(echo "$name" | tr -c 'a-zA-Z0-9' '_')" "$id"
done > sg_imports.tf
For a first pass across everything, terraformer can bulk-generate state and config, but its output is messy; we treat it as a discovery tool, not the final artifact.
Batch by dependency order and blast radius. Not 200 import blocks in one giant plan. Networking first (VPCs, subnets, route tables), because everything depends on it. Then security groups. Stateful resources (RDS, ElastiCache) last, because that's where a mistaken -replace is catastrophic.
Read every plan line by line. Import populates state but not the depends_on or data-source relationships you'd normally write. The classic trap: an RDS instance imports cleanly with a green plan, but its subnet group was imported separately and the dependency was never expressed. A later refactor reorders things, and Terraform proposes a subnet group change that forces the database to be recreated. You catch that in plan review or you catch it in an outage. During an import project, the plan output is the safety net, not a formality.
The classic command, and the way back out
The old approach still shows up in scripts. It works when the resource block is already written:
$ terraform import aws_s3_bucket.assets acme-prod-assets
It mutates state immediately: no plan step, no generated config, no undo beyond editing state. Because you write the HCL by hand, the config-must-match gotcha bites harder.
Import's opposite is terraform state rm aws_s3_bucket.assets. Terraform forgets the resource; the resource keeps running. That's what you want when handing a resource to another state file, splitting a monolith, or retrying an import that went wrong. Pair it with a fresh import block to move a resource between configurations without downtime. The declarative removed {} block (Terraform 1.7+) does the same through plan review, which is safer in team workflows. For renames and splits that stay inside Terraform, moving Terraform state safely covers moved blocks and state mv.
Questions people ask
How do I import an existing resource into Terraform?
Add an import block with the resource address in to and the cloud ID in id, run terraform plan -generate-config-out=generated.tf, then move the cleaned HCL into your real files and apply. Keep adjusting the config until terraform plan reports no changes, which proves state, config and the live resource agree.
Can I import many resources at once in Terraform?
Yes. Generate import blocks from your cloud provider's API with a script, or put for_each on a single import block, which Terraform supports from 1.7. Batch the work by dependency order: networking first, security groups next, stateful resources such as RDS last. Review each plan before applying, because a missing dependency can turn into a proposed replacement.
What's the difference between terraform import and an import block?
The terraform import command writes state immediately, generates no config, and has no plan step, so you must hand-write a matching resource block first. An import block, available since Terraform 1.5, goes through plan, can draft HCL with -generate-config-out, and sits in a pull request for review before anything touches state.
The decision, concretely
- One resource and the HCL already written?
terraform importis fine for a quick one-off. - Anything else? Use
importblocks with-generate-config-outso the change is planned and reviewable. - Dozens or hundreds of resources? Generate the blocks from the API, batch network first and stateful last, and wire hard-coded IDs into references.
- Import went sideways?
terraform state rmor aremoved {}block, then re-import cleanly.
The call we'd make
Reach for import blocks with -generate-config-out every time. The generated HCL saves transcription, and the whole flow fits in a pull request. Treat generated config as a draft: strip the defaults, wire in references and variables, and don't apply until plan reports no changes. At scale, read every plan during the migration like the database depends on it, because sometimes it does. For the wider set of state and apply failures that send people here, see our Terraform error troubleshooting guide.
Sources
Cite this article
Kiril Urbonas (2026). How to Import Existing Resources into Terraform. DevOpsNess. Updated 2026-10-31. https://www.devopsness.com/blog/terraform-import-existing-resources
[How to Import Existing Resources into Terraform](https://www.devopsness.com/blog/terraform-import-existing-resources) (Kiril Urbonas, DevOpsNess, updated 2026-10-31)
More from DevOps
Explore more articles in this category
15 SRE Interview Questions With Answers, From SLOs to On-Call
SRE interviews test numbers and judgment. Fifteen questions on SLIs, error budgets, incident command, toil, capacity, on-call and postmortems, plus a systems-design scenario, each with the answer that scores and the one that loses the room.
Sign Container Images With Cosign: Keyless GitHub Actions Workflow, SBOM and In-Cluster Checks
One workflow that builds, signs keylessly with GitHub OIDC, attaches an SBOM and SLSA provenance, plus the Kyverno policy that refuses anything unsigned. Copy it, change three names, ship it.
Fix Prometheus High Cardinality: 6 Commands to Find and Drop Bad Series
When Prometheus runs out of memory, one label is almost always to blame. Six commands to find it, the relabel rules that drop it, and the scrape limits that stop it coming back.
You might have missed
Evergreen posts worth revisiting.