terraform-state-the-team-contract
← Back to Blog

CLOUD & DEVOPS / TERRAFORM

Terraform State: The Team Contract

Why state is an operational boundary, not just a file in a repo.

Key Insight: Terraform state isn't an implementation detail of the tool. It's the single source of truth your entire team is implicitly agreeing to trust every time they run apply. Treat it with the seriousness that deserves.

Terraform state gets treated casually more often than almost any other piece of infrastructure — a file that "just works" until the day two people run apply at the same time, or state drifts from reality, or a corrupted state file turns a routine change into an incident. The teams that avoid this treat state management as a first-class part of their infrastructure design, not an afterthought.

Remote state and locking aren't optional

Local state files — the default if you don't configure otherwise — mean the state of your infrastructure lives on one person's laptop, unshared, unlocked, one accidental deletion away from becoming a very bad day. Remote state backends (Azure Storage with state locking via blob leases, S3 with DynamoDB locking, Terraform Cloud) solve two problems at once: the team shares a single source of truth, and concurrent apply operations are prevented from racing each other and corrupting state.

This should be configured on day one of any Terraform project meant to last past a prototype, not retrofitted after the first incident makes the gap obvious.

State drift is a signal, not noise

Drift — when real infrastructure no longer matches what's recorded in state, usually because someone made a change through the portal or CLI directly, bypassing Terraform — is treated by too many teams as a nuisance to terraform apply away. It's actually valuable information: it means your infrastructure has an undocumented change that Terraform doesn't know about, and that change will get silently reverted the next time someone runs apply, possibly breaking something a person fixed manually for a good reason.

Run terraform plan regularly, even outside of deliberate changes, specifically to catch drift early. When drift is found, the right response is to understand why it happened and either codify the change into Terraform or explicitly revert the manual change — not to just let the next apply paper over it.

Splitting state: blast radius is a deliberate design choice

A single, monolithic state file covering your entire infrastructure means every apply touches the state for everything, and a mistake in one part of the plan can affect the state consistency of parts that had nothing to do with the change. Splitting state by boundary — per environment at minimum, often per logical system or team — limits blast radius: a bad apply to the payments module's state can't accidentally corrupt the state for networking.

This isn't free — it means managing dependencies between state files (via remote state data sources or a tool like Terragrunt), and it's a real design decision worth making deliberately based on how your teams and systems are actually organized, not applied uniformly without thought.

Treat destructive plan output as a stop sign

terraform plan showing an unexpected destroy or replace on a resource that shouldn't need it is the single most valuable safety signal Terraform gives you, and it's the one most often clicked past under time pressure. A resource marked for replacement, especially a stateful one like a database, deserves a pause to understand exactly why before approving — a name change, a tag update, or a provider quirk can trigger a replacement that looks routine in the diff and is catastrophic in practice.

-/+ resource "azurerm_postgresql_flexible_server" "main" { # forces replacement }

Key takeaways

Configure remote state with locking from day one — local state is a liability, not a starting point to graduate from later. Treat drift as a signal to investigate, not noise to override. Split state deliberately along real organizational and system boundaries to limit blast radius. Never approve a plan showing an unexpected destroy or replace without understanding exactly why it's happening first.