Terraform — State
State is the thing that makes Terraform Terraform — and the thing that causes most of its operational pain. Understand state and you understand 80% of real-world Terraform problems.
What state is
Terraform builds a graph of your desired infrastructure (the .tf files) and compares it to reality. State (terraform.tfstate, a JSON file) is Terraform’s record of what it created — the mapping from your resource blocks to real cloud resource IDs.
.tf files (desired) ──┐
├──► terraform plan ──► diff ──► apply
state (last known) ───┘
Without state, Terraform couldn’t know that aws_instance.web in your config is that specific EC2 instance i-0abc123. It would have no way to update or destroy what it made.
State also caches resource attributes (so a plan doesn’t have to re-query every attribute of every resource) and tracks resource dependencies and metadata.
Why local state doesn’t work for a team
The default is a local terraform.tfstate file. For anything beyond a solo experiment, that’s broken:
- Not shared — your teammate’s state doesn’t have the resources you created.
- No locking — two people running
applysimultaneously corrupt the state. - Not durable — laptop dies, state is gone, Terraform can no longer manage the infrastructure it created.
- Secrets in plaintext — state contains resource attributes, including some sensitive values (DB passwords, generated keys). A local file is unencrypted.
Remote state
Store state in a shared, durable, lockable backend. The standard AWS setup: S3 + DynamoDB.
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks" # the lock table
encrypt = true # encrypt state at rest
}
}
- S3 holds the state file — durable, versioned (enable bucket versioning so you can recover a corrupted state), encrypted.
- DynamoDB provides the lock. Before
apply, Terraform writes a lock item; if someone else holds it, you get “Error acquiring the state lock” instead of a corrupted concurrent write. On finish, the lock is released. - The
keypath namespaces state per environment/component (prod/network/...,staging/app/...) — this is how you keep environments and components in separate state files (see 03_workspaces_and_environments.md).
Other backends: Terraform Cloud / HCP Terraform (managed state + locking + runs), GCS, Azure Blob. S3+DynamoDB is the classic AWS answer; modern AWS provider versions also support S3-native locking, reducing the need for the separate DynamoDB table.
State locking
The lock prevents concurrent writes. Mechanics:
terraform apply(andplanwith-lock) acquires the lock.- If held, Terraform waits / errors with the lock holder’s info.
terraform force-unlock <LOCK_ID>— manual override only when you’re certain the holder is dead (a crashed CI job that never released it). Misusing it during a real concurrent run corrupts state.
State is sensitive — treat it like secrets
State files contain resource attributes, and some are secret: RDS passwords, generated private keys, etc. So:
- Encrypt at rest (
encrypt = trueon S3, plus bucket SSE/KMS). - Lock down access — the state bucket’s IAM policy should be tight; reading state = reading secrets.
- Don’t commit state to git. Ever. (
.gitignore*.tfstate*.) - Mark variables/outputs
sensitive = trueso they’re redacted in CLI output — though they’re still in the state file;sensitiveis about display, not storage.
State commands you’ll actually use
| Command | What it does |
|---|---|
terraform state list |
list resources Terraform is tracking |
terraform state show <addr> |
show one resource’s tracked attributes |
terraform state rm <addr> |
stop tracking a resource (doesn’t destroy it) — Terraform “forgets” it |
terraform state mv <src> <dst> |
rename/move a resource in state without destroy+recreate (e.g. after refactoring into a module) |
terraform import <addr> <id> |
adopt an existing real resource into state — see 04_plan_apply_drift_import.md |
terraform refresh (or plan -refresh-only) |
re-sync state with real-world attributes |
state mv is the one that saves you when you refactor: moving aws_instance.web into a module changes its address from aws_instance.web to module.web.aws_instance.this — without state mv, Terraform would destroy the old and create a new one.
Common state problems
- “Error acquiring the state lock” — someone (or a dead CI job) holds the lock. Check who;
force-unlockonly if certifiably stale. - State drift — someone changed a resource in the AWS console; state no longer matches reality.
planwill show the diff; decide whether to revert (re-apply) or absorb (update.tf). See the drift file. - State and config diverged after a refactor — you renamed/moved resources in
.tfbut not in state → Terraform wants to destroy+recreate. Fix withstate mv. - Corrupted state — recover from S3 bucket versioning (this is why you enable it).
- Resource deleted out-of-band — exists in state, gone in reality;
planshows it’ll be recreated. Orstate rmit if it’s intentionally gone. - One giant state file — everything in one state means every
planis slow, everyapplyhas a huge blast radius, and the lock serializes the whole org. Split state by component/environment (separatekeypaths).
Interview angle
- “What is Terraform state and why does it exist?” — Terraform’s record of which real cloud resources correspond to which config blocks (the mapping from
aws_instance.webtoi-0abc123). Without it Terraform can’t update or destroy what it created, andplanwould have nothing to diff against. - “Why is local state a problem for a team?” — not shared (teammates don’t see your resources), no locking (concurrent applies corrupt it), not durable (laptop loss = lost state), and it holds secrets in plaintext. Use remote state.
- “How do you set up remote state on AWS?” — S3 backend for the file (versioned, encrypted) + DynamoDB table for locking. The
keypath namespaces state per environment/component. Modern AWS provider versions also support S3-native locking. - “What does state locking prevent and what’s
force-unlock?” — locking stops two concurrentapplys from corrupting state.force-unlockmanually releases a stuck lock — only safe when the holder is certifiably dead (a crashed CI run); using it during a live apply corrupts state. - “You refactored a resource into a module and now Terraform wants to destroy and recreate it. Why, and the fix?” — moving it changed its state address; Terraform sees the old address gone and a new one appear.
terraform state mv <old> <new>updates the address in state without touching the real resource. - “Is the state file sensitive?” — yes — it stores resource attributes including secrets (DB passwords, keys). Encrypt at rest, lock down bucket IAM, never commit it to git.
sensitive = trueonly redacts CLI output; the value is still in state.