The Tuesday Incident: Two Engineers, Two State Files, One AWS Account
It starts innocently. You and a teammate both clone the same repository, both run terraform plan, both see an identical, reassuring no-surprises diff — because you are each diffing against your own copy of reality, and both copies were accurate twenty minutes ago.
Recall what state is for: it is the baseline every diff is computed against. When your teammate applied their change an hour ago, their AWS account changed; your local terraform.tfstate still describes the world as it was when you last applied. Your plan is therefore confidently wrong: it does not see their new instance, so it proposes nothing about it, and if your configuration no longer contains the resource they added, your apply may even destroy what it cannot see — the object exists in the cloud but not in your baseline, and Terraform manages what it knows.
The mechanism is worth stating precisely, because it explains why "just be careful" never fixes it. Terraform does not reconcile against the cloud wholesale at apply time; it reconciles against its plan, which was built from state. Local state makes "the plan" a per-machine artifact. There is no shared write channel, so nothing stops these sequences:
- You plan at 10:00 against your copy (serial 40).
- Your teammate applies at 10:15; their state becomes serial 41 with their resources.
- You apply at 10:30; your plan was computed from serial 40, and your state write overwrites theirs — their resources are now untracked in the merged reality, orphaned in the cloud and invisible to every future plan you both run.
That overwrite is not hypothetical sabotage; it is the default behavior of two independent files. Serial numbers exist precisely to detect this — but serials only compare when both writers read and write the same storage. Two laptops hold two serial counters, and neither can see the other.
Even read-only collaboration fails. A teammate reviewing your PR cannot run a trustworthy plan to check your work, because their state is their last apply, not yours. Reviewers end up trusting your pasted plan output, which is exactly the workflow infrastructure-as-code was supposed to replace.
The conclusion is structural: a configuration that describes shared infrastructure needs shared, current state — one baseline with a write discipline. That is what a remote backend provides, and the difference is not convenience but correctness.
The tempting shortcut deserves a direct answer, because every team considers it: why not just commit the state file to git and call it shared? It fails on three counts. Writes still race — two simultaneous applies produce a merge conflict in a JSON blob with no mechanism to tell you which side reflects reality, and hand-resolving it is exactly the surgery the file's invariants punish. Every commit republishes the secrets the state contains to everyone with repository access, permanently, as established earlier. And git offers none of the primitives the problem actually needs: no conditional writes, no locking, no versioning you can restore by policy. Teams that try it usually do so mid-incident and revert the decision within a month. Interviewers ask "what breaks with local state in a team?" for exactly this reason — the answer must include the stale-baseline mechanism, not just "it is not shared." The next chapter adds the second failure mode, where even a single engineer on multiple machines loses the race against themselves.
Locking That Only Reaches One Machine, and a Laptop as a Single Point of Failure
A common misconception: "Terraform has no locking with local state." The truth is more interesting. The local backend does lock — while an operation runs, Terraform creates .terraform.tfstate.lock.info beside the state file and refuses a second operation on the same state with an Error acquiring the state lock message. The limitation is scope: the lock is a file on your filesystem. It coordinates two terminals on one laptop. It has no opinion about your colleague's machine, a teammate's CI runner, or the scheduled pipeline that applies nightly.
So the race that matters is cross-machine, and it has two shapes:
- Lost update. Both writers read serial 40, both compute changes, both write; the second write silently discards the first. Terraform's serial comparison would catch this if they shared storage — with separate files, nobody compares anything.
- Split brain. After weeks of independent applies, each machine's state describes a different subset of the estate. Neither is a complete baseline; the next person to consolidate discovers the divergence as a pile of unmanaged resources and duplicate creations.
CI makes it worse rather than better. A pipeline that runs terraform apply inside an ephemeral container starts each run with a fresh working directory — and therefore a state file that does not exist. To that pipeline, the entire estate looks unmanaged: the next apply creates a second VPC, a second database, billing alarms twice, until a human notices. "But CI checked the plan!" — the plan was computed from an empty baseline, so it planned to create everything. Persistence of state across runs is not a nice-to-have for pipelines; it is the precondition for them doing anything meaningful.
The durability problem is the quiet one. With local state, the file on your laptop may be the only record tying configuration addresses to live objects. Laptop stolen, disk dies, someone runs rm in the wrong directory — and now every plan in the world reports that everything should be created, because nothing knows what exists. Recovering without that record means importing every resource by hand, one terraform import per object, guided by console screenshots. A state file you cannot afford to lose is, by definition, a state file that belongs in durable storage with a recovery story — not in ~/projects/infra/ next to a TODO.md.
Detection is harder than prevention, and the cheap audit is embarrassingly manual: run terraform state list on two machines and compare the outputs. When the sets differ, you are already in split brain, and reconciliation is a careful session of state mv, state rm, and import guided by the cloud's actual inventory — hours of work that shared state would have made unnecessary. Nobody schedules that session; it schedules you.
Neither problem has a configuration-level fix. You cannot lock across machines that do not share storage, and you cannot make a local file durable by wanting it to be. Both are solved by moving state to a store designed for it: shared, conditionally written, versioned, encrypted. Before configuring one, one more local-state failure completes the picture — the version you commit to git by accident, which is exactly what the next chapter unpacks.
Secrets, Git History, and Ephemeral CI: The Two Remaining Ways Local State Fails
Local state fails for teams, for locking, and for durability — and then it fails twice more in ways that produce incidents even for careful individuals.
Failure one: it gets committed. The repository is new, terraform.tfstate is sitting in the working directory, and a git add . sweeps it in. From the outside this looks harmless — a small JSON file. But you know from State File Internals what that JSON contains: RDS master passwords, generated key material, the contents of every data source that returned a secret — all in clear text, because sensitive masks display, not storage.
The damage outlives the fix. Deleting the file in a follow-up commit changes nothing: the blobs live in history, in every clone that fetched them, in CI caches, and in fork copies. Rotation is the only real remediation — every credential ever written to that state must be assumed leaked and replaced. The prevention is one .gitignore line applied before the first commit, and a git log --all -- terraform.tfstate check on any repository you inherit.
Failure two: CI runners are ephemeral. Covered from the plan side in the previous chapter, the secret-bearing angle deserves its own emphasis: teams that solve CI persistence by copying state into the runner from an artifact store end up distributing the secret-laden file across every build machine, log-adjacent scratch space, and cache. The file follows the credentials problem wherever it goes, and each copy expands the blast radius of a single compromised pipeline.
There is a third, gentler failure that still costs real money: local state invites local-only automation. A cron job on someone's workstation applies a stack nightly, and the state lives on that workstation. The person goes on leave; the disk dies; the job silently stops; drift accumulates for two weeks. Infrastructure whose baseline exists on exactly one machine has exactly one machine's worth of availability.
Before backends existed, teams improvised the same properties by hand: a state file on a shared network drive, a cron job copying it to a backup share, and a Slack channel where people announced their applies. Every improvisation failed on the same missing primitive — nothing enforced exclusive access during a run — and all three are precisely what purpose-built state storage now provides natively. Recognizing that pattern pays off beyond this tool: when a tool's correctness depends on a storage property, outsourcing storage to something that guarantees the property beats any discipline you can write into a runbook.
One sentence of policy is still worth putting in that runbook: terraform.tfstate is a secret-bearing artifact — it lives only in the state backend, laptops hold no copies, and CI receives it only through the backend's API. Every incident in this topic — the committed file, the artifact-copied runner, the lost laptop — is prevented by that sentence plus one .gitignore line.
Put the four chapters of this phase together and the shape of the solution writes itself:
| Failure | Requirement |
|---|---|
| Stale per-machine baselines | One shared state store |
| Cross-machine write races | Conditional writes / locking in that store |
| Laptop loss | Durable, versioned storage with backups |
| Secrets in git and runners | Encrypted store with restricted access, never in the working copy |
That requirements list is the definition of a remote backend. Configuring a Remote Backend takes the first entry — the backend block and the state migration that moves an existing local file into shared storage without touching a single real resource.
Quiz
1. You apply at 10:30 from a plan computed at 10:00, while a teammate applied at 10:15 from their own local state. What happened to their state changes?
2. What does the local backend's locking actually protect against?
3. An ephemeral CI container runs terraform apply with no state carried between runs. What is the most likely outcome?
4. terraform.tfstate was committed and later deleted in a follow-up commit. Why is rotation still required?
Flashcards
Question
Why is a plan computed against local state untrustworthy for a teammate?
Click to reveal answer
Answer
Because state is the diff baseline: your local copy reflects the world as of your last apply, so anything a teammate applied since then is invisible to your plan — it will neither modify their resources nor notice if your configuration would destroy them. The plan is confidently wrong, not merely incomplete.
Question
How does local-state locking work, and what is its critical limitation?
Click to reveal answer
Answer
The local backend creates .terraform.tfstate.lock.info during an operation and refuses concurrent operations on the same state (Error acquiring the state lock). The limitation: it is a file on one filesystem — it cannot coordinate applies from other machines, other users, or CI runners.
Question
What is the lost-update failure with local state, and why do serial numbers not prevent it?
Click to reveal answer
Answer
Two writers each read serial 40, compute changes, and write; the second write silently discards the first. Serial comparison catches conflicting writes only within one shared store — separate files carry separate counters, so nothing compares anything and the overwrite proceeds.
Question
What is the CI failure mode of ephemeral runners with local state, and the dangerous workaround?
Click to reveal answer
Answer
Each run starts with an empty baseline, so apply plans to create the entire estate again. The dangerous workaround is copying state into the runner from an artifact store: it spreads the secret-bearing file across build machines and caches, multiplying the blast radius of one compromised pipeline.
Question
Why does committing terraform.tfstate to git require credential rotation even after deletion?
Click to reveal answer
Answer
State stores secrets in clear text, and git preserves the blobs in history — plus every clone, fork, and CI cache may retain them. Deleting the file only fixes the current tree; every credential ever written to that state must be treated as leaked and rotated. Prevention: .gitignore before the first commit.
Revision Notes
Key Takeaways
- 1. State is the diff baseline, so local state makes every plan a per-machine snapshot that silently disagrees with teammates and CI
- 2. Local locking is real but filesystem-scoped; cross-machine applies race freely, and the loser's writes are overwritten without complaint
- 3. A laptop-held state file is a single point of failure whose loss turns recovery into one manual import per resource
- 4. State contains plaintext secrets; committing it exposes them permanently, and git deletion does not un-leak git history
- 5. Ephemeral CI without persistent state plans to duplicate the entire estate — which is why backends are a correctness requirement, not a convenience
Interview Tips
- • "What breaks when two engineers use local state?" — explain the stale-baseline mechanism and the overwrite: separate files mean separate serial counters, so the second apply clobbers the first and orphans resources; naming serial-scoping shows depth
- • "Does Terraform lock with local state?" — yes, via .terraform.tfstate.lock.info, but only within one filesystem; then extend to CI and teams to argue for backend locking
- • "Our CI creates duplicate infrastructure every run. Why?" — the runner starts with no state, so the baseline is empty; the fix is persistent shared state (a backend), and warn against copying state files into runners as an artifact
- • "State was committed to git. What now?" — rotate every credential it contained, purge history only as hygiene (clones remain), add .gitignore before the first real commit, and check inherited repos with git log --all -- terraform.tfstate
Cheat Sheet
Limitations of Local State
Failure modes:
| # | Failure | Consequence |
|---|---|---|
| 1 | Per-machine baselines | Teammates plan against stale reality; last write wins and orphans resources |
| 2 | Lock only reaches one filesystem | Cross-machine / CI races: lost update, split brain |
| 3 | Laptop is the only copy | Lose the file → everything looks unmanaged; manual import per resource |
| 4 | Secrets in the file | Committed state = leaked credentials; git history retains blobs → rotate |
| 5 | Ephemeral CI runners | Empty baseline each run → duplicate estate; artifact-copy workaround spreads secrets |
Key mechanism: plan diffs against state, not the cloud — an untracked-but-existing resource is invisible until something tracks it.
Local lock: .terraform.tfstate.lock.info — real lock, wrong scope. Error acquiring the state lock on one machine; silence between machines.
Requirements derived: shared store · conditional writes/locking · durability+versioning · encryption+restricted access = the definition of a remote backend.