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"
}
  • to is the address the resource will have in Terraform.
  • id is the identifier AWS uses for it. The format depends on the resource type: a bucket name for S3, an instance ID such as i-0abc123 for 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 commandimport block
Shows up in a plan firstNo, it writes state immediatelyYes, you review it before apply
Can be code-reviewed in a pull requestNoYes
Can import many resources in one runOne per commandAs many blocks as you like
Can generate configurationNoYes, 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:

  1. Review the generated file. It includes every attribute, including defaults you'd normally leave out. Delete what you don't need.
  2. Replace hard-coded values with references. A generated security group rule will contain a literal VPC ID; change it to aws_vpc.main.id if the VPC is also in Terraform.
  3. Run terraform plan again. The goal is 0 to add, 0 to change, 0 to destroy apart from the imports. If anything else shows up, adjust the config until it doesn't.
  4. Move the code out of generated.tf into 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

  1. Importing into a resource that will be replaced. If your config sets an attribute that forces replacement (for example, a different availability_zone on a subnet), the plan shows must be replaced after the import. Read every plan line before applying, and never apply a replace on an imported production resource without understanding why.
  2. 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.
  3. 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.
  4. Importing into the wrong workspace or state. Run terraform workspace show and check the backend before importing production resources.
  5. 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 import blocks, not the terraform import command, 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.

Platform evaluation

See OrchesTerra on your infrastructure

Request access to generate architecture from a repository, review the plan, and run governed reconciliation across AWS, Azure, GCP, and OCI.