Remote Backends and Locking
The idea in one sentence
Section titled “The idea in one sentence”A remote backend stores state in a shared location like S3 instead of on one person’s laptop, and locking stops two people from writing to that shared state at the same time.
Why local state does not work for a team
Section titled “Why local state does not work for a team”By default, terraform.tfstate lives on the machine where you ran apply. That is fine for a solo experiment, but it breaks down the moment a second person joins:
- Teammate A runs
applyand creates a VPC. Their laptop’s local state file now knows about it. - Teammate B, on a different laptop, has never seen that state file. They run
planagainst their own stale or empty local state and either try to recreate the VPC or get a plan that makes no sense next to what actually exists. - CI/CD pipelines have the same problem in a sharper form — a pipeline runner is a fresh, disposable environment with no local state file at all. It needs a canonical copy of state to read from and write to on every run, not a copy that happens to be sitting on someone’s laptop.
The fix is to move state off any individual machine and into a shared backend that every team member and every CI run points at.
Configuring the S3 backend with native locking
Section titled “Configuring the S3 backend with native locking”The current recommended way to store Terraform state on AWS is an S3 bucket configured as the backend, with native S3 state locking turned on:
terraform { backend "s3" { bucket = "my-company-terraform-state" key = "networking/terraform.tfstate" region = "us-east-1" use_lockfile = true }}bucket and key together identify exactly which object in S3 holds this configuration’s state — key is the path within the bucket, so different configurations (or different Terragrunt units) typically use different keys in the same bucket. use_lockfile = true is the important line: Terraform uses a conditional-write lock file stored alongside the state object directly in S3 to coordinate locking. No separate locking infrastructure is required — the S3 bucket alone is enough.
You will still encounter the older pattern in existing codebases, using a separate DynamoDB table for locking:
# Legacy locking pattern — dynamodb_table is deprecated and scheduled for removal.# Prefer use_lockfile = true (shown above) in new configurations.terraform { backend "s3" { bucket = "my-company-terraform-state" key = "networking/terraform.tfstate" region = "us-east-1" dynamodb_table = "terraform-state-lock" }}That pattern works, and you should not panic if you inherit a codebase that uses it, but dynamodb_table is now a deprecated argument on the S3 backend and is planned for removal in a future Terraform version. New configurations should use use_lockfile = true instead — it does the same job without a second AWS resource to create, pay for, and keep in sync with the state bucket.
What locking actually prevents
Section titled “What locking actually prevents”Imagine locking did not exist. Two people, or a person and a CI run, both execute terraform apply against the same S3-backed state at nearly the same moment. Both read the current state, both compute a plan, and both start writing their results back to the same state object. Depending on timing, the second write can overwrite the first — silently dropping the first run’s recorded changes even though the first run’s infrastructure changes actually happened — or the state file can end up corrupted mid-write.
A lock turns that race into a queue. The first apply to start acquires the lock; the second apply either waits for the lock to be released, or fails immediately with a clear “state is locked” error, depending on the flags used. Either way, the second run does not get to write over the first run’s state while it is mid-flight.
# A run that finds the state already locked fails fast with a clear message,# rather than corrupting the state fileterraform apply# Error: Error acquiring the state lockflowchart LR eng1["Engineer A: terraform apply"] -->|acquires lock| s3["S3 state + lock file"] eng2["CI run: terraform apply"] -->|blocked, waits or fails| s3 s3 -->|lock released after apply| eng2