backend / iac / terraform / 01_terraform_state.md

Terraform — State

6 interview angles 5 min read source

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 apply simultaneously 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 key path 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 (and plan with -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 = true on 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 = true so they’re redacted in CLI output — though they’re still in the state file; sensitive is 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-unlock only if certifiably stale.
  • State drift — someone changed a resource in the AWS console; state no longer matches reality. plan will 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 .tf but not in state → Terraform wants to destroy+recreate. Fix with state 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; plan shows it’ll be recreated. Or state rm it if it’s intentionally gone.
  • One giant state file — everything in one state means every plan is slow, every apply has a huge blast radius, and the lock serializes the whole org. Split state by component/environment (separate key paths).

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.web to i-0abc123). Without it Terraform can’t update or destroy what it created, and plan would 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 key path 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 concurrent applys from corrupting state. force-unlock manually 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 = true only redacts CLI output; the value is still in state.