Remote state & locking
State for teams, done safely.
The local state file works for one person experimenting, but the moment a team shares infrastructure it breaks down: everyone has a different copy, nobody has the truth, and two people applying at once corrupt it. A remote backend solves this by storing state in a shared, central location — an S3 bucket, Terraform Cloud, a GCS bucket — so the whole team (and CI) reads and writes the same state. This is the first thing to set up for any real Terraform project.
terraform {backend "s3" {bucket = "acme-tfstate"key = "prod/network/terraform.tfstate"region = "us-east-1"dynamodb_table = "tf-locks" # the lock table (see below)encrypt = true # encrypt state at rest}}
State locking: the critical piece
Shared state introduces a new danger — two people (or two CI runs) applying simultaneously can interleave writes and corrupt the state file. State locking prevents this: before an apply, Terraform acquires a lock (via a DynamoDB table for the S3 backend, or built in for Terraform Cloud), does its work, and releases it; anyone else who tries to apply meanwhile is blocked until the lock frees. Without locking, remote state is a shared file two people can clobber; with it, applies are serialized and safe. Always configure locking with your backend.
# with locking, a concurrent apply is blocked rather than corrupting state:$ terraform applyError: Error acquiring the state lockLock Info:ID: 4f2a...Who: deploy@ci-runner-3Created: 2026-07-03 10:14:02 # someone else is applying — wait, do not force# (terraform force-unlock exists for a truly stuck lock — use it very carefully)
Security and separation
Because state holds secrets in plaintext, the remote backend must protect it: enable encryption at rest (encrypt = true, plus the bucket’s own encryption), lock down access with IAM so only the right people and CI can read it, and enable versioning so a corrupted or bad state can be rolled back. It is also good practice to split state by environment and component (separate state files for prod-network, prod-db) so a mistake in one has a small blast radius and applies are faster — which is exactly what the key = "prod/network/..." path above is doing.