Terraform State Operations — Import, Move, and Recover Without Losing Anything
The state commands that are genuinely dangerous, what each one actually does, and how to run them with a way back.
State operations are the part of Terraform where a mistake is not caught by a plan, because you are editing the thing the plan is derived from. They deserve more caution than an apply.
What state actually is
State maps configuration addresses to real infrastructure identifiers, and caches attribute values so plans can be computed without querying everything.
The critical consequence: state describes reality; it does not control it. Removing a resource from state does not delete the resource. It makes Terraform forget the resource exists — which means the next apply may well create a duplicate.
That asymmetry is the source of most state incidents.
Import — adopt existing infrastructure
Use the import block rather than the CLI command. It is planned, reviewable, and reversible in
the same way any other configuration change is:
import {
to = aws_s3_bucket.artifacts
id = "example-artifacts-bucket"
}
resource "aws_s3_bucket" "artifacts" {
bucket = "example-artifacts-bucket"
}
terraform plan -generate-config-out=generated.tf # if you have no config yet
terraform plan -out=tfplan # review before importing
terraform apply tfplan
The plan shows what will be imported and — importantly — what changes Terraform intends to make to the imported resource afterwards. That second part is where imports go wrong: the configuration does not exactly match reality, so the import succeeds and the same apply immediately modifies or replaces the resource you just adopted.
Read the plan after the import lines. If it proposes anything beyond the import itself, fix the configuration to match reality before applying.
Move — rename or restructure
# Preferred: a moved block, which is reviewable and lives in the repository
moved {
from = aws_instance.web
to = aws_instance.api
}
A moved block records the rename in code, shows up in the plan as a move rather than as a
destroy-and-create, and works for anyone who applies afterwards. The CLI equivalent
(terraform state mv) does the same thing invisibly and only on the machine that ran it.
Without a move recorded, renaming a resource in configuration reads to Terraform as: destroy the old one, create a new one. For a database, that is the whole incident in one refactor.
Remove — stop managing without destroying
removed {
from = aws_s3_bucket.legacy
lifecycle {
destroy = false
}
}
This is the safe way to stop managing a resource. destroy = false says explicitly: forget it,
do not delete it.
The CLI form is terraform state rm, which does the same thing with no record and no plan.
After removing, the resource is unmanaged. Nothing is tracking its configuration, nobody is alerted when it drifts, and the next person to write similar configuration may create a duplicate. Removal should be paired with a note somewhere durable about who now owns it.
Recovering from a broken state
State was corrupted or a bad operation applied
Restore from the versioned backend. This is why versioning is non-negotiable.
# S3 backend: list versions and restore
aws s3api list-object-versions --bucket example-tfstate --prefix platform/terraform.tfstate
aws s3api get-object --bucket example-tfstate --key platform/terraform.tfstate \
--version-id <previous-version-id> restored.json
terraform state push restored.json
The lock is stuck
A crashed or cancelled apply can leave the lock held.
terraform force-unlock <LOCK_ID>
Before running it, confirm the process holding the lock is genuinely gone. Force-unlocking a lock held by a running apply lets a second apply start against the same state, and concurrent applies are how state gets corrupted in the first place.
Check the CI job, check the machine, ask the person named in the lock message. Then unlock.
Resource exists in reality but not in state
Import it. Do not let Terraform create a duplicate — for uniquely-named resources it will fail, and for others it will succeed and you will have two.
Resource is in state but not in reality
Someone deleted it outside Terraform.
terraform plan -refresh-only # confirm what Terraform now believes
Applying a refresh-only plan updates state to match reality, after which a normal plan will propose recreating the resource. That is usually what you want — but check whether the deletion was intentional first.
Decision path
State and reality disagree.
├─ Resource exists in reality, absent from state
│ └─ import block → review the post-import plan carefully
│
├─ Resource in state, absent from reality
│ └─ plan -refresh-only → then decide whether to recreate
│
├─ Resource needs a new address (rename, module move)
│ └─ moved block → verify the plan shows a move, not a destroy
│
├─ Resource should no longer be managed
│ └─ removed block with destroy = false → record the new owner
│
└─ State is damaged
└─ restore the previous version from the versioned backend
Rules that prevent most of this
- Pull a state backup before every state operation, without exception
- Prefer
import,moved, andremovedblocks over CLI state commands — they are planned, reviewed, and shared - Never edit a state file by hand. If you believe you must, you are recovering, and you should restore a version instead
- Version the state backend, and confirm you can restore from it before you need to
- Treat
force-unlockas an operation requiring confirmation that the holder is dead - Read the plan after an import, not just the import itself
Verification status
This resource has not been executed end to end in a lab environment. Commands and configuration are reviewed by an engineer, but treat them as reference rather than as a tested procedure.
Author
James Joyner
Builds and operates the infrastructure layers underneath production AI systems.
James founded Inside The AI Stack to publish the kind of infrastructure and operations material he wanted while running production systems: specific, tested where it claims to be tested, and written by someone who has had to fix the thing at 3am. He works across AI infrastructure, private cloud, and platform engineering, and reviews every technical resource published here before it is marked as verified.
- AI infrastructure
- OpenStack operations
- Kubernetes
- Terraform
- Linux systems engineering
- Observability
Primary sources
- Terraform CLI — state command — HashiCorp
- Import block — HashiCorp
Continue from here
Related resources chosen because they are the next thing you would actually need — not because they share a keyword.
Terraform Production Practices
Running Terraform against production infrastructure: reviewing the plan mechanically, containing blast radius, structuring state, and avoiding the destroys nobody noticed.
Tool
Terraform Plan Analyzer
Find destructive and high-risk changes in a Terraform plan before you apply it.
Available now
Newsletter
Inside The AI Stack Brief
A practical weekly briefing on AI engineering, infrastructure, production operations, and the technologies powering the AI stack.
One email a week. No sponsorship placements inside the technical sections. Unsubscribe in one click.