Terraform Import Workflow: Adopt Existing Infrastructure Without Downtime
Import Is a Binding, Not a Full Migration
Import tells Terraform that an existing provider object belongs to a resource address. It does not magically recover your architecture, module boundaries, naming policy, or operational intent. The hard part is writing configuration that converges to the live object without changing it.
The success criterion is simple: after import and reconciliation, a refreshed normal plan says there are no changes.
---
Inventory Before You Touch State
Suppose the payments team created a production database manually during an outage. Its provider ID is db-prod-83a1. Before importing, record its region, engine, size, network, deletion policy, tags, and dependent services. Confirm no other Terraform state already owns it.
Choose one durable address. For a direct root resource:
resource "fakecloud_database" "payments" {
name = "payments-prod"
engine = "postgres"
size_gb = 200
}
For a module instance, use the final address from the beginning:
module "payments" {
source = "../../modules/database"
name = "payments-prod"
}
The address might be module.payments.fakecloud_database.this. Importing temporarily at the root and moving later creates avoidable risk.
---
Prefer a Declarative import Block
An import block makes adoption reviewable and repeatable:
import {
to = module.payments.fakecloud_database.this
id = "db-prod-83a1"
}
Run:
terraform init
terraform plan -out=adopt.tfplan
terraform show adopt.tfplan
terraform apply adopt.tfplan
The plan should show one import and no destructive action. If it shows replacement, stop. Import itself does not recreate the database, but applying configuration differences after import can.
For bulk adoption, for_each keeps the mapping explicit:
locals {
legacy_buckets = {
audit = "bucket-17c2"
exports = "bucket-91ef"
}
}
import {
for_each = local.legacy_buckets
to = fakecloud_bucket.archive[each.key]
id = each.value
}
resource "fakecloud_bucket" "archive" {
for_each = local.legacy_buckets
name = "payments-${each.key}"
}
Keep import blocks in history as an adoption record unless team policy removes them after all target states have applied them.
---
When the CLI Command Is Appropriate
The classic command remains useful for a one-state operational repair:
terraform import 'module.payments.fakecloud_database.this' 'db-prod-83a1'
For a for_each instance, quote the key:
terraform import 'fakecloud_bucket.archive["audit"]' 'bucket-17c2'
CLI import updates state immediately and does not generate the resource configuration. It is less convenient for pull-request review and bulk imports, but excellent for repairing a missing binding while automation is paused.
---
Generated Configuration Is a Draft
Terraform can generate configuration for configuration-driven imports when planning with -generate-config-out:
terraform plan -generate-config-out=generated_resources.tf
Treat the result as scaffolding. Providers often expose computed attributes, deprecated arguments, defaults, and implementation details that should not become policy. Move the useful arguments into your intended module structure, add descriptions and validation, then plan again.
---
Converge Without Hiding Real Drift
After the binding exists, inspect it:
terraform state show 'module.payments.fakecloud_database.this'
terraform plan -out=post-import.tfplan
Classify every difference:
- A configurable live value should usually be represented in HCL.
- A provider-computed value should remain computed.
- An intentional policy change should be a separate pull request after adoption.
- A genuinely external controller may justify narrowly scoped
ignore_changes.
Do not add ignore_changes = all merely to obtain a green plan. That converts managed infrastructure into an object Terraform can no longer meaningfully reconcile.
The Zero-Downtime Runbook
- Freeze manual edits and concurrent applies for the target state.
- Back up state and inventory the live object.
- Write the resource at its final address.
- Add a reviewed import block or run one exact CLI import.
- Reject any unexpected replace or destroy action.
- Apply the import, then iterate configuration until the normal plan is empty.
- Add lifecycle protection for irreplaceable data where appropriate.
- Resume CI and document the new owner.
Try the complete workflow in Import Existing Resource. It uses real Terraform import behavior against offline FakeCloud, so you can practice reaching a zero-diff plan without a cloud bill or production risk.