Terraform's configuration files get all the attention in code review, but the state file is where most real incidents happen. State is the only record Terraform has of what it believes exists in the real world, and when that record diverges from reality — or two people write to it at once — the fallout ranges from a confusing plan to an accidental resource deletion. Treating state as an afterthought is the single most common mistake in teams adopting Terraform.

Remote backends and locking are not optional

Local state files (terraform.tfstate sitting in a repo or a laptop) fail the moment more than one person touches the infrastructure. Two applies running concurrently against local state will corrupt it or silently overwrite each other's changes. A remote backend with locking is table stakes for anything beyond a solo experiment:

hcl
terraform {
  backend "s3" {
    bucket         = "example-org-tfstate"
    key            = "prod/network/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

The dynamodb_table entry provides state locking: Terraform writes a lock record before any plan or apply and refuses to proceed if another operation already holds it. Without this, a second terraform apply kicked off mid-run by CI or a teammate can interleave writes and leave state inconsistent with reality.

Splitting state by blast radius

A single monolithic state file covering an entire environment means every apply touches everything Terraform manages, and a mistake in one resource's plan can threaten unrelated resources through Terraform's dependency graph. Splitting state along natural boundaries — networking, data stores, application infrastructure — limits blast radius:

text
environments/
  prod/
    network/       # own state file
    database/      # own state file
    app-cluster/    # own state file

Cross-state references use terraform_remote_state or, more commonly now, values passed through a shared data source like SSM Parameter Store, avoiding a hard coupling between state files that would force them to always apply in lockstep.

Handling drift without guessing

Drift happens constantly in real accounts — someone changes a security group rule in the console during an incident, or an auto-scaling event modifies a tag. terraform plan will show that drift on the next run, but blindly applying to "fix" it can undo a legitimate emergency change. Run refresh-only plans to see drift without proposing changes:

bash
terraform plan -refresh-only -out=refresh.tfplan
terraform show refresh.tfplan

That separates "here's what changed outside Terraform" from "here's what I'm about to change," which matters a lot when the drift was intentional and needs to be reflected back into the .tf files rather than reverted.

Avoiding state file secrets exposure

State files store resource attributes in plain text, including ones you'd consider sensitive — database passwords set via a resource argument, generated TLS keys, connection strings. Anyone with read access to the state file has read access to those values regardless of how carefully the .tf files themselves handle secrets.

hcl
resource "aws_db_instance" "main" {
  # ...
  password = var.db_password  # this ends up in state, in plain text
}

Mitigate this by enabling backend encryption at rest (as in the S3 example above), restricting state bucket access as tightly as production database access itself, and preferring resources that reference secrets managers over ones that embed generated secrets directly. terraform state show should be treated as a command that can leak credentials, not a harmless inspection tool.