TerraformX · State Operations: Read, Move, Remove, ImportImports
Importing Existing Infrastructure
What you'll learn
- Explain what `terraform import` does and what it does not do
- Import a single resource into state
- Plan an incremental import of many existing resources
- Recognise the production traps of import
Prerequisites
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-12
terraform import adopts a real-world resource into Terraforms
state. It does not write a configuration. It does not replace
the resource. It only updates the state to record that the
resource exists.
This lesson teaches what the import does, how to use it, and the limits of what it does for a production team adopting an existing estate.
What import does
An import workflow:
# 1. Write the resource block in the configuration (no values yet)
resource "aws_instance" "web" {
# ... arguments to be filled in
}
# 2. Run the import command
terraform import aws_instance.web i-0abc123def456789
# 3. Inspect the state to learn the attribute values
terraform state show aws_instance.web
# 4. Copy the attribute values into the configuration
# (this is the manual step)
# 5. Run a plan to verify the configuration matches the state
terraform plan
The import command:
- Reads the configuration to find the resource declaration.
- Calls the provider to look up the resource by the given ID.
- Updates the state to include the resources attributes.
- Does not modify the configuration.
After the import, the configuration is empty; the state is populated. The next plan will propose to create the resource and to update it to match the configuration. The configuration must be filled in before the plan is empty.
The import block
The import block is a configuration-level declaration that
records the intent to import a resource:
resource "aws_instance" "web" {
# ... arguments to be filled in
import {
id = "i-0abc123def456789"
}
}
The terraform plan runs the import as part of the plan. The
resource is added to state. The next plan verifies the
configuration.
The import block is preferred over the CLI command for
configuration-managed imports. It is auditable in source control.
What import does not do
The import does not:
- Write the configuration. The configuration is the operators job. The import populates the state; the operator must populate the configuration.
- Replace the resource. The resource is unchanged in the real world.
- Generate dependencies. The imported resource has no documented dependencies. The operator must add references to other resources.
- Validate the resource. The import trusts the providers data. The providers data is what Terraform will manage going forward.
- Generate the configuration. The operator must write the configuration to match the real-world state.
A common misconception is that import “imports the resource into Terraform”. This is misleading. The correct phrasing is “imports the resource into Terraforms state”. The configuration is a separate artefact.
The incremental import workflow
A 300-resource estate cannot be imported in one session. The recommended workflow:
- Inventory. List every resource in the real world. Use the providers API to enumerate. Save the inventory as a spreadsheet or a file.
- Group. Bucket the resources by type, environment, and owner. The grouping will become the state boundaries.
- Prioritise. Start with the most-changed resources (databases, IAM roles, network) and the least-critical (test environments, ephemeral resources).
- Import per group. Each group becomes a state. The import is per-state, not per-resource.
- Write the configuration per state. The configuration must match the imported state exactly.
- Verify. For each state, run
terraform planand verify the plan is empty.
The incremental approach limits the blast radius of an import mistake. A wrong import in one state does not affect the other states.
The production trap: wrong import ID
The most common import failure is the wrong ID:
terraform import aws_instance.web i-0wrongid123456789
The provider accepts the command and updates state with the wrong resources attributes. The next plan shows the configuration as mismatched. The operator notices the mismatch but the state is now bound to the wrong resource.
The fix:
# Remove the wrong imported resource from state
terraform state rm aws_instance.web
# Re-import with the correct ID
terraform import aws_instance.web i-0correctid123456789
terraform state rm removes the resource from state only. The
real-world resource is unaffected. The next plan will propose
to recreate the resource from the configuration.
The configuration-write workflow
After import, the configuration must be written to match the state. The workflow:
- Run
terraform state show <address>. The output has every attribute value. - Copy the values into the configuration. Map each attribute to the corresponding argument. Defaults that match the state are omitted.
- Handle computed attributes. Attributes that are
(known after apply)in the state are not in the configuration — they are produced by the provider. - Handle complex blocks. Some blocks (e.g.
tags,lifecycle) have nested structures. The states representation is not the same as the configuration syntax. - Run a plan. The plan must be empty.
The state-to-configuration mapping is non-trivial. The state has every attribute returned by the provider; the configuration has only the arguments the operator chose to declare. The mapping is the inverse of the providers schema.
The for_each import trap
A common pattern is to import a set of resources into a
for_each:
resource "aws_subnet" "public" {
for_each = toset(["a", "b", "c"])
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${each.key}.0/24"
}
The early Terraform versions required one import per for_each
member. The import block (Terraform 1.5+) supports for_each:
resource "aws_subnet" "public" {
for_each = toset(["a", "b", "c"])
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${each.key}.0/24"
for_each = ... # invalid duplicate
}
Wait, thats not right. The correct pattern is to use an
import block with a for_each:
import {
for_each = toset(["a", "b", "c"])
to = aws_subnet.public[each.key]
id = "subnet-0abc123def456789" # dynamic based on each.key
}
The for_each import is more complex. The IDs are usually
discovered via the providers API before the import.
Importing across state boundaries
A common pattern is to import a resource whose state will live in a different state file:
# In the network configuration
import {
to = aws_subnet.public["a"]
id = "subnet-0abc123def456789"
}
The resources state lives in the network state. The consumer
reads the network states outputs via terraform_remote_state.
The import is per-configuration. The producers state is populated by the producers import; the consumer reads the outputs.
The migration workflow
A full migration of an existing estate to Terraform:
- Inventory. Enumerate every resource via the providers API.
- Group. Bucket the resources by state boundary.
- Bootstrap. Create the state backends (S3 buckets, DynamoDB tables, etc.).
- Import per state. Each state imports its resources.
- Configure per state. The configuration is written to match the state.
- Verify per state. The plan is empty.
- Operate. The state is live. The team now manages changes via Terraform.
The migration may take weeks or months. The team operates the existing estate in parallel with the migration. The boundary between the migrated and unmigrated portions is a high-risk transition.
What comes next
The next lesson is import strategy — the way to approach an import of 300+ existing resources incrementally.
Knowledge check · 7 questions
Q1. What is the role of state list?
Q2. What is the role of state mv?
Q3. state rm destroys the real world.
Q4. What is the role of state replace-provider?
Q5. Which state operations mutate the state? (Select all that apply.)
Q6. What is the role of the moved block?
Q7. A team renames a resource in the configuration. The plan proposes to destroy the old and create the new. What is the fix?
Passing score: 75%. Answers are checked in this browser.