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:
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:
environments/
prod/
network/ # own state file
database/ # own state file
app-cluster/ # own state fileCross-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:
terraform plan -refresh-only -out=refresh.tfplan
terraform show refresh.tfplanThat 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.
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.