Terraform
Terraform Import Blocks: Bring Existing AWS Resources Under Terraform
How the Terraform import block works, how to generate configuration for resources someone built by hand, and how to import many AWS resources without breaking production.
2026-11-15 · 6 min read
Most teams don't start with Terraform. They start with a few resources clicked together in the AWS console, then a few more, and one day someone asks: "can we get all of this into code?"
That's what the import block is for. It tells Terraform that a resource already exists in your account and should be managed from now on, without destroying or recreating it. This guide covers how it works, how to generate the configuration instead of writing it by hand, and the mistakes that cause trouble when you import real production resources.
If the resources already exist in Terraform but someone has changed them in the console, that's drift, not import. See how to detect Terraform drift for that case.
The import block in one example
Since Terraform 1.5 (and OpenTofu 1.6), you declare an import in configuration, next to your other code:
import {
to = aws_s3_bucket.assets
id = "acme-prod-assets"
}
resource "aws_s3_bucket" "assets" {
bucket = "acme-prod-assets"
}
tois the address the resource will have in Terraform.idis the identifier AWS uses for it. The format depends on the resource type: a bucket name for S3, an instance ID such asi-0abc123for EC2, an ARN for some others. Each resource's page in the AWS provider docs lists the import ID format at the bottom.
Run terraform plan and Terraform shows the import as part of the plan:
aws_s3_bucket.assets: Preparing import... [id=acme-prod-assets]
aws_s3_bucket.assets: Refreshing state... [id=acme-prod-assets]
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
Nothing happens until you run terraform apply. After the apply, the bucket is in your state, and you can delete the import block. Leaving it is harmless, but removing it keeps the code tidy.
Why the block is better than terraform import
The older terraform import command still works, but the block is safer for real teams:
terraform import command | import block | |
|---|---|---|
| Shows up in a plan first | No, it writes state immediately | Yes, you review it before apply |
| Can be code-reviewed in a pull request | No | Yes |
| Can import many resources in one run | One per command | As many blocks as you like |
| Can generate configuration | No | Yes, with -generate-config-out |
The first row matters most. With the command, a typo in the ID or address changes your state the moment you press Enter. With the block, it's just another plan your team reviews.
Generate the configuration instead of writing it
Writing a matching resource block by hand is the slow, error-prone part. If your config doesn't match what's in AWS, the plan shows changes, and applying them would modify the live resource.
Terraform can write the first draft for you. Add only the import blocks, with no resource blocks, then run:
terraform plan -generate-config-out=generated.tf
Terraform reads each resource from AWS and writes matching HCL into generated.tf. Then:
- Review the generated file. It includes every attribute, including defaults you'd normally leave out. Delete what you don't need.
- Replace hard-coded values with references. A generated security group rule will contain a literal VPC ID; change it to
aws_vpc.main.idif the VPC is also in Terraform. - Run
terraform planagain. The goal is0 to add, 0 to change, 0 to destroyapart from the imports. If anything else shows up, adjust the config until it doesn't. - Move the code out of
generated.tfinto the files and modules where it belongs.
The generated code is a starting point, not finished Terraform. Expect to spend most of your time on step 2.
Importing many resources at once
From Terraform 1.7 and OpenTofu 1.7, an import block can use for_each, which helps with groups of similar resources:
locals {
buckets = {
assets = "acme-prod-assets"
logs = "acme-prod-logs"
backup = "acme-prod-backup"
}
}
import {
for_each = local.buckets
to = aws_s3_bucket.this[each.key]
id = each.value
}
resource "aws_s3_bucket" "this" {
for_each = local.buckets
bucket = each.value
}
The harder part of a large import is usually finding everything in the first place: which resources exist, which are already managed, and which depend on each other. Tag-based inventories, AWS Config, or the Resource Explorer service can give you the list.
Mistakes that cause trouble
- Importing into a resource that will be replaced. If your config sets an attribute that forces replacement (for example, a different
availability_zoneon a subnet), the plan showsmust be replacedafter the import. Read every plan line before applying, and never apply a replace on an imported production resource without understanding why. - Importing the same resource twice. Two addresses pointing at one real resource means two configurations fighting over it. Check the existing state first with
terraform state list. - Forgetting the related resources. An S3 bucket's policy, versioning and encryption settings are separate Terraform resources in recent AWS provider versions. Import the bucket alone and the next plan may try to create or change those settings.
- Importing into the wrong workspace or state. Run
terraform workspace showand check the backend before importing production resources. - Importing something you want to delete later. If a resource should go away, deleting it in the console is simpler than importing it and then destroying it through Terraform.
Stopping Terraform managing a resource
The reverse also comes up: you want Terraform to forget a resource without deleting it. Since Terraform 1.7 there's a removed block for this:
removed {
from = aws_s3_bucket.legacy
lifecycle {
destroy = false
}
}
After an apply, the bucket is still in AWS, but no longer in your state.
Doing this across a whole AWS account
For a handful of resources, the workflow above works fine. For an account that grew by hand over a year or two, the work is mostly bookkeeping: listing what exists, matching it to code, generating config, and getting each import reviewed.
That's the part OrchesTerra automates. Connect an AWS account and it lists resources that exist but aren't managed by Terraform. You pick what to adopt, and it generates the import and the configuration. The change goes through a reviewed plan and an approval step, like any other change. You can see this in the read-only demo without signing up, or read more about drift detection and reconciliation.
Quick checklist
- Use
importblocks, not theterraform importcommand, so imports get reviewed. - Generate config with
terraform plan -generate-config-out=generated.tf, then clean it up. - Aim for a plan with only imports and no other changes before you apply.
- Import related resources (policies, settings) together with the main resource.
- Delete the import blocks after a successful apply.
