Terraform State Management

Why the state file is load-bearing, not just a cache

The state file is the only record connecting an HCL resource block to the real-world resource it corresponds to — for resource "aws_instance" "web", the state file is what actually stores "this maps to EC2 instance i-0abc123def456." Without it, Terraform has no way to know that the resource declared in HCL already exists (leading it to try to create a duplicate) or how to find it to check for drift or destroy it later. Losing the state file doesn't just lose "bookkeeping" — it loses the entire link between configuration and reality.

Local state is unsafe for anything beyond solo use

By default, state lives in a local terraform.tfstate file. Two concrete problems the moment more than one person runs Terraform against the same infrastructure:

graph LR e1["Engineer A: terraform apply"] --> race["Both read the SAME state<br/>at nearly the same time"] e2["Engineer B: terraform apply"] --> race race --> corrupt["Whichever writes state LAST wins —\nthe other's changes are silently lost\nor state becomes inconsistent with reality"]

No locking (two concurrent applies can corrupt the file or create duplicate resources) and no shared visibility (each engineer's local file can silently diverge from what's actually been applied by someone else).

Remote state with locking

graph TD e1["Engineer A"] -->|"acquire lock"| backend["Remote backend<br/>(S3 + DynamoDB, or Terraform Cloud)"] e2["Engineer B"] -.->|"blocked until lock released"| backend backend --> state["Shared state file"]
terraform {
  backend "s3" {
    bucket         = "my-tfstate-bucket"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
  }
}

The classic AWS pattern: state stored in S3 (durable, versioned — S3 versioning gives a rollback path if state is ever corrupted), with a DynamoDB table used purely for locking — before apply (or plan, depending on configuration) starts, Terraform writes a lock record to DynamoDB; a second concurrent apply sees the existing lock and blocks or errors instead of proceeding, closing the exact race condition local state has no defense against. Terraform Cloud/Enterprise provides the same locking + shared-state guarantee as a managed service instead of self-hosted S3+DynamoDB.

Drift and terraform refresh / import

When real infrastructure changes outside Terraform (a manual console edit, another automation tool, a resource deleted directly), the state file becomes stale. terraform plan (which implicitly refreshes state by re-reading actual resource attributes from the provider before diffing) surfaces this as an unexpected diff — a change plan shows that nobody actually made through Terraform.

terraform import handles the related-but-distinct case of a resource that exists in the real world but was never created through Terraform at all (created manually, or by a different tool) — importing links an existing resource's ID to a resource block in HCL, so Terraform starts tracking and managing it going forward, without destroying and recreating it.

Common pitfall

Treating an unexpected plan diff as something to blindly apply away is dangerous specifically because the diff doesn't distinguish "someone made an unauthorized/accidental change that should be reverted" from "someone made a deliberate emergency fix that the Terraform config hasn't caught up to yet" — both look identical as a diff. Investigating why a drift diff exists before applying it away is the only way to avoid accidentally undoing a legitimate emergency change (like a manual scale-up during an incident) the moment the next routine apply runs.