Skip to main content
RunBook Academy

TerraformX · State Operations: Read, Move, Remove, ImportProduction Terraform

Import: Adopting Existing Infrastructure

Intermediate⏱ ~14 minbash

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

Not yet marked complete on this device.

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 typeID formatWhere to find it
aws_instancei-0a1b2c3d4e5f6a7b8EC2 console or aws ec2 describe-instances
aws_vpcvpc-0123456789abcdef0VPC console or aws ec2 describe-vpcs
aws_s3_bucketmy-bucket-nameS3 console or aws s3api list-buckets
google_compute_instanceprojects/<proj>/zones/<zone>/instances/<name>gcloud compute instances list
azurerm_resource_group/subscriptions/.../resourceGroups/<name>Azure portal or az group list
kubernetes_poddefault/web-pod-abc123kubectl 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 .tf from existing infrastructure for many providers.
  • tfimport — generates the import block list from existing infrastructure.
  • Provider-specific importers — aws terraform import generators, 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_on references in configuration must be imported first.
  • Handle resources with count or for_each keys 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

  1. terraform state rm to detach the wrongly-imported resource.
  2. Investigate why the import failed (wrong ID, wrong configuration).
  3. Retry the import with the corrected inputs.
  4. 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

  1. Q1. What does `terraform import` do?

  2. Q2. Which Terraform version introduced the declarative import block?

  3. 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.

  4. Q4. For a `count = 3` resource, how many import commands are needed to adopt all three instances?

  5. Q5. Which of these are valid forms of the import command? (Select all that apply.)

  6. Q6. After a successful import, the plan proposes to overwrite many attributes. What does this mean?

  7. 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.