TerraformXVIII · Troubleshooting and RecoveryProduction Terraform
Troubleshooting State Issues
What you'll learn
- Separate a state lock, state data problem, and resource-address problem
- Use read-only state inspection commands before considering a state mutation
- Recover a wrong resource address with a reviewed moved block or state mv
- Restore a corrupt state from a verified backend version without hand-editing JSON
- Protect state integrity, confidentiality, and recovery evidence in production
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
Terraform state is a structured record of the objects Terraform manages and the relationships between them. State is not a substitute for the configuration, and the lock that protects state is not the same thing as the state data itself. Troubleshooting state issues means identifying which of those things is wrong before deciding whether to wait, move an address, import an object, or restore a version.
A lock prevents simultaneous state writes. A corrupted state cannot be read or interpreted reliably. A wrong resource address makes Terraform look for a managed object under a different identity. These failures have different symptoms and different safe recoveries.
State, lock, and address are separate boundaries
Backend
|-- state data: resource instances, attributes, outputs, serial
|-- lock metadata: lock ID, owner, operation, timestamp
`-- configuration: resource addresses and dependency graph
Terraform reads the state for the selected backend and workspace, then builds a plan from configuration, state, and the real infrastructure reported by providers. A lock failure happens before the intended graph can be applied. A wrong address is a consistency problem between configuration and state. Corruption is a data-integrity problem in the state representation.
The state diagnostic order
Use this order consistently:
- Preserve the lock evidence. Record the lock ID, owner, operation, timestamp, backend, and workspace. Do not force-unlock yet.
- Confirm the address. Record the exact resource address and
instance key, including module names and
for_eachkeys. - Inspect before mutating. Use
state list,state show, and a refresh-only plan. Save a read-only copy if the investigation needs a local comparison. - Classify the failure. Lock, malformed state, address mismatch, missing import, or backend access problem.
- Protect the recovery. Create a backend or state backup, confirm ownership of the real object, and agree on the rollback.
- Apply the narrow fix. Move an address, import an untracked object, restore a verified state version, or correct the backend.
- Verify. Run a plan, check the state serial, and confirm the object in the real platform.
1. Lock contention
A remote backend reports contention with details similar to:
Error: Error acquiring the state lock
Error message: lock held by another process
Lock Info:
ID: 7c2e1c91-4cc0-2b1d-a64b-4db28d71b82d
Operation: OperationTypeApply
Who: ci-job-1842
Version: 1.9.8
Created: 2026-08-13 02:14:03 +0000 UTC
Path: infra-production
The lock is a coordination mechanism, not an error in the state JSON. First check the owner and process. A short wait is safer than force-unlock when the operation is active. The following command is read-only, but it does not tell you which process owns a remote lock; use the job or process information named by the error and the backend’s own lock diagnostics.
READ-ONLY
ps -ef | grep '[t]erraform'
terraform plan -input=false -no-color -refresh-only
If the lock is confirmed stale, the operator who owns the change can use the exact lock ID with the approved command. The command may ask for confirmation.
SERVICE-IMPACT — can permit concurrent state writes if used against a live lock. Use only after verifying the owner is gone and recording the decision.
terraform force-unlock '7c2e1c91-4cc0-2b1d-a64b-4db28d71b82d'
force-unlock is not a routine error recovery. Disabling the lock with
-lock=false is not a substitute for investigating the owner.
2. State data corruption
A corrupt local state or backend version can produce a parse error, an
unexpected serialised value, or a failure while loading a resource
address. terraform validate checks configuration and providers; it
does not prove that the current state is readable or correct.
READ-ONLY
terraform state list
terraform state show 'aws_instance.web'
terraform state pull
If state pull fails or the state is visibly truncated, stop all state
writes. Preserve the current version and the backend metadata. For a
remote backend, use its native versioning or object-recovery mechanism
to select a known good state version. There is no universal Terraform
command that safely repairs arbitrary state corruption, and hand-editing
JSON does not verify that the changed entry matches the real object.
After a restore, run a refresh-only plan. The plan should be explainable against the configuration and the provider. If the restored version is old enough to include a resource that no longer exists, do not force the plan through; resolve the ownership question first.
3. Wrong resource address
A rename from aws_instance.web to aws_instance.api normally makes
Terraform plan a destroy and create. That is a visible clue that the
configuration address changed, not proof that the instance must be
replaced.
READ-ONLY
terraform state list | grep -E -e 'aws_instance\.web' -e 'aws_instance\.api'
terraform state show 'aws_instance.web'
terraform plan -input=false -no-color -refresh-only
Prefer a moved block in the configuration so the refactor is
reviewable and repeatable:
resource "aws_instance" "api" {
ami = var.api_ami
instance_type = "t3.small"
}
moved {
from = aws_instance.web
to = aws_instance.api
}
The block changes the address in Terraform’s model. It does not create or destroy the real instance. Run a plan and expect Terraform to report the move rather than a replacement. For an operational recovery where the configuration cannot be changed immediately, use the imperative form with an explicit backup.
CONFIGURATION — changes state address only. It does not change the real object. A backup is mandatory.
terraform state mv -backup-file=/tmp/state-before-move.tfstate \
'aws_instance.web' 'aws_instance.api'
terraform state list | grep -E -e 'aws_instance\.web' -e 'aws_instance\.api'
terraform plan -input=false -no-color
If the source and destination belong to different resource types or
different real objects, state mv is the wrong tool. Use a reviewed
moved block only for a compatible address change, and use import
when Terraform does not already own the object.
4. Missing ownership or untracked object
A resource may exist in the platform but not in state after a manual creation, an incomplete adoption, or a lost mapping. The diagnostic symptom is a plan to create an object that already exists.
CONFIGURATION — records an existing real object in state. The cloud object is not recreated, but the provider may need permission to read or modify it.
terraform import 'aws_instance.web' 'i-0123456789abcdef0'
terraform plan -input=false -no-color
Confirm the provider resource type, region, and real identifier before importing. Importing an identifier into the wrong address can make the next plan attempt to modify or replace the wrong object.
5. State command damage
state rm removes a resource from state. It does not destroy the
object, and it can leave an unmanaged resource behind. It is sometimes
appropriate during a deliberate adoption, but it is never a diagnostic
shortcut. If it is approved, use a backup and verify the plan before any
later apply.
DATA-LOSS-RISK — removes the state entry. Use only after confirming that the real object is intentionally unmanaged and that the backup is recoverable.
terraform state rm -backup-file=/tmp/state-before-rm.tfstate \
'aws_instance.web'
terraform plan -input=false -no-color
Never use a text editor to repair a provider ID, dependency, or resource schema. State is an internal data structure, not a hand-maintained configuration format.
Verification commands and expected evidence
Use these checks after a state recovery. The grep output is
illustrative and the serial number will differ between workspaces.
READ-ONLY
terraform state list
terraform state show 'aws_instance.api'
terraform state pull | jq '.serial'
terraform plan -input=false -no-color -refresh-only
For the moved example, the expected shape is:
aws_instance.api
Plan: 0 to add, 0 to change, 0 to destroy
If the plan shows a destroy or create after a pure address move, stop. The source address, destination address, module path, or resource type is wrong. Do not repeat the state move until the mismatch is explained.
Security and performance
State is a sensitive production artefact. Use a remote backend with encryption, versioning, least-privilege read and write roles, and an audit trail. Keep the lock and state access roles separate where the platform supports it. Back up state independently of the local working directory and test restoration before an outage forces you to use it.
Large state files increase lock duration, refresh time, memory use, and
the cost of every plan. Keep resource count bounded, use stable
identities, and avoid importing a whole estate into one state file. A
refresh-only plan helps separate drift from an intentional action. Do
not use -refresh=false for routine production plans just to make them
faster, because it can hide the mismatch you are trying to diagnose.
Production guidance
- Enable backend versioning and test a state restore in staging.
- Keep lock timeouts explicit, but never use them to justify force-unlocking a live operation.
- Prefer
movedblocks for permanent configuration refactors; usestate mvonly for a controlled operational recovery with a backup. - Protect state files and saved plans as production data; a local
terraform.tfstateis not a recovery strategy. - After a state incident, verify the real platform object, its state address, the backend serial, and the next plan before closing the incident.
Verification
- You can distinguish a lock failure from malformed state and a wrong resource address.
- You can inspect state and generate a refresh-only plan before changing state.
- You can use a
movedblock or backed-upstate mvfor a compatible address change. - You know when to use
importand whystate rmcan create an unmanaged object. - You can restore a verified state version without editing JSON by hand.
- You can verify the state serial, plan, and real object after recovery.
Knowledge check · 7 questions
Q1. What should you record first when Terraform reports a state lock?
Q2. What is the safe first response to a confirmed stale state lock?
Q3. `terraform validate` checks the configuration and its provider requirements, so it proves nothing about whether state is readable or matches the real platform.
Q4. Which symptoms should be classified as state incidents rather than ordinary configuration errors? (Select all that apply.)
Q5. A resource was renamed in configuration and the next plan proposes destroying the live object. The source and destination have the same type and provider. What is the preferred permanent fix?
Q6. What is the correct recovery for a malformed remote state version?
Q7. What should happen after a resource address is moved with `terraform state mv`?
Passing score: 75%. Answers are checked in this browser.