CoursesTerraformIntermediate

Remote state & locking

State for teams, done safely.

Intermediate14 min · lesson 7 of 15

A local state file works for one person, but the moment a team shares infrastructure it breaks: everyone has a different copy, and two people applying at once corrupt it. Remote state fixes both problems. A backend stores the single state file in shared storage — an S3 bucket, an Azure blob, Terraform Cloud, a GCS bucket — so everyone reads and writes the same state, and it can be encrypted and versioned centrally.

backend.tf
terraform {
backend "s3" {
bucket = "acme-tf-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
encrypt = true # encrypt state at rest
dynamodb_table = "acme-tf-locks" # state locking (see below)
}
}

Locking: the other half

Shared storage alone is not enough — without coordination, two simultaneous applies still race and corrupt state. Locking prevents that: before an operation, the backend takes a lock (in DynamoDB for S3, natively in Terraform Cloud/GCS), and a second apply waits or fails fast rather than colliding. So the two properties you need from a real backend are encryption at rest and locking; S3-plus-DynamoDB is the classic AWS combination that provides both.

terminal
$ terraform apply
Acquiring state lock. This may take a few moments...
# meanwhile, a teammate’s apply:
Error: Error acquiring the state lock
Lock Info: ID: 3f0c..., Who: alice@acme, Created: 2m ago
# → it refuses to run rather than corrupt shared state

Splitting state to limit blast radius

Beyond sharing, remote state lets you split one giant state into several — network, data, and app in separate state files (the key in the backend). Smaller states mean a smaller blast radius (a mistake in the app state cannot touch the network), faster plans, and clearer ownership. One state reads another’s outputs via a terraform_remote_state data source, which is how separated stacks still pass values to each other.

Bootstrapping and never force-unlocking blindly
Two operational notes. The backend bucket and lock table are a chicken-and-egg problem — create them in a small separate config (or by hand) before pointing everything at them. And terraform force-unlock exists for when a crashed run leaves a stale lock, but running it while someone is genuinely applying corrupts state — confirm the lock really is stale (check who holds it and when) before you ever force it.