0x55aa
Back to Blog

🗝️ The Terraform State File: Your Infrastructure's Diary (And Its Biggest Liability)

|
5 min read

🗝️ The Terraform State File: Your Infrastructure's Diary (And Its Biggest Liability)

Here's a sentence that should make every platform engineer slightly nervous: Terraform has no idea what your infrastructure actually looks like.

It doesn't ask AWS "hey, what exists right now?" and reconcile from there — not by default, not on every run. It reads a file. A single, unglamorous JSON blob called terraform.tfstate, decides that's the truth, and plans your next apply against it. If that file is stale, wrong, or missing, Terraform will confidently propose changes based on a version of reality that no longer exists.

I've watched a terraform plan calmly announce it was going to recreate a production RDS instance because the state file thought it had a different engine_version than what someone had hotfixed by hand in the console three weeks earlier. The database was fine. The state file was lying. Terraform believed the liar.

What state actually is

At its core, the state file is a mapping: "this resource block in my .tf files corresponds to this real-world object with this ID and these attributes." Strip away the tooling and it's genuinely just JSON.

{
  "resources": [
    {
      "type": "aws_instance",
      "name": "web",
      "instances": [
        {
          "attributes": {
            "id": "i-0abcd1234efgh5678",
            "instance_type": "t3.medium",
            "ami": "ami-0c94855ba95c71c99"
          }
        }
      ]
    }
  ]
}

Every plan is really a three-way diff: your .tf config, this state file, and (optionally, via a refresh) the real provider API. When all three agree, Terraform is boring and reliable, which is the whole point. When they diverge — someone clicked around in the console, another pipeline ran an apply you didn't know about, or the state file itself got corrupted — Terraform doesn't know which version to trust. It trusts state, because state is the only thing it has a contract with.

The locking problem nobody explains until it bites you

State is also, by design, a single mutable file that many humans and pipelines want to write to at once. Run terraform apply twice concurrently against the same unlocked state, and you get a race condition applied to your cloud account — two processes both reading the same "current" state, both computing a plan from it, both writing back different results. Best case, one overwrites the other's changes. Worst case, your state file is now internally inconsistent with reality on both sides.

This is why remote backends with locking exist, and why "just use a local .tfstate file" stops being cute the moment more than one person touches the repo:

terraform {
  backend "s3" {
    bucket         = "acme-terraform-state"
    key            = "prod/network/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

S3 holds the file, DynamoDB holds the lock. Two engineers apply at once, and the second one gets a polite "someone else is holding this lock" instead of a silent, undetected collision. It's not exciting infrastructure — nobody puts "DynamoDB lock table" on their architecture diagram with pride — but it's the difference between a queue and a demolition derby.

At Cubet Techno Labs, splitting one enormous state file into per-environment, per-component state (network, data, compute, each with its own backend key) did more for our sanity than any linting rule ever has. A blast radius that used to mean "any apply could touch 400 resources" became "an apply to the network state touches network resources." Smaller state files also mean faster plans, since Terraform has less to refresh and less to reason about per run.

Drift is not a bug report, it's a lifestyle

The real enemy isn't the state file itself — it's drift, the slow accumulation of differences between what state believes and what actually exists. Someone bumps a security group rule in the console during an incident at 2 a.m. because it was faster than waiting for CI. Nobody reverts it in code afterward. Now your state and your reality quietly disagree, and the next unrelated apply might try to "fix" that security group back to the old, wrong rule.

terraform plan with a refresh will surface this, but only if someone actually runs it and reads the diff instead of reflexively hitting apply:

terraform plan -refresh-only -out=refresh.plan
terraform show refresh.plan

-refresh-only updates state to match reality without proposing any resource changes — it's the "let's just see what's actually true" command, and running it on a schedule (a nightly CI job that posts drift to Slack) turns drift from a surprise into a Tuesday-morning ticket.

The lesson

Terraform's power comes entirely from treating that state file as authoritative, and its danger comes from exactly the same fact. Lock it, split it into blast-radius-sized chunks, back it up (S3 versioning is free, use it), and check for drift on purpose instead of discovering it during an incident. The file is small. What it's responsible for is not.

If you're still running local state on a shared repo, or one giant state file for your whole account — that's this week's fix, not next quarter's. Your future self, mid-incident, will thank you.

Share:LinkedInXHacker News

Thanks for reading!

Back to all posts