Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCIII · Infrastructure Repository Anti-PatternsAntiPatterns

Terraform state in Git — the file that does not belong in the repository

Intermediate⏱ ~26 mingit

What you'll learn

  • Describe what Terraform state contains and why every byte of it is sensitive
  • Identify the four failure modes produced by state in version control
  • Compare remote backends (S3, GCS, Azure Storage, Terraform Cloud) by their locking and encryption properties
  • Configure .gitignore and pre-commit guards that block state from ever reaching a commit

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A terraform.tfstate file in a repository is a JSON document mapping every resource the configuration has created to the Terraform expression that created it. The file is the source of drift detection, the unit of locking, and the record the next terraform plan diffs against. None of those properties survive being put in Git.

What state actually contains

A state file is not configuration. It is current reality serialised as JSON. It contains:

  • Resource identifiers. The cloud-provider-assigned IDs of every resource, plus the attribute values Terraform last observed.
  • Outputs. Values marked sensitive = false and values marked sensitive = true - the sensitive flag only affects CLI output, not what is written to disk.
  • Provisioner and dependency metadata. The internal graph Terraform uses to plan the next apply.
  • Anything passed through a resource attribute. Database passwords set via password = var.db_password are visible in state once applied, regardless of the variable’s sensitive flag.
flowchart LR
    A["terraform apply"] --> B["terraform.tfstate"]
    B --> C["resource IDs"]
    B --> D["attribute values"]
    B --> E["outputs - sensitive or not"]
    B --> F["resource graph"]
    B --> G["passed-in secrets"]

A state file is therefore a credential dump that grows with every apply. It is the wrong artifact to track in version control.

The four failure modes

A state file in Git produces four distinct failures:

  • Merge conflicts. State is rewritten on every apply. Two engineers applying concurrently produce divergent state files; a merge commit picks one, and the rejected apply’s changes are gone.
  • Locking failure. Local-state locking is a local lock on a file the second engineer is also writing. Race conditions corrupt state and surface only on the next plan as a resource that “needs to be created” because the state no longer knows it exists.
  • Secret exposure. A state file exposes every resource attribute, including the database root password that was set in Terraform a year ago and never rotated.
  • Audit-trail loss. State in Git is rewritten by the next apply. The version that produced the current world is gone.

What the alternative looks like

The correct home for Terraform state is a remote backend that stores the file outside the repository, locks it during applies, and serves it to plans:

  • AWS S3 with DynamoDB lock. State versioning is automatic; locking uses a DynamoDB item; encryption uses S3 SSE.
  • GCS with object-level locking. State versioning is via object versioning on the bucket.
  • Azure Blob Storage with lease-based locking. Lease locks map onto Terraform’s lock protocol.
  • Terraform Cloud / HCP Terraform. A managed backend with built-in locking, versioning, run history, and policy controls.

In every case the repository references the backend through a partial configuration block. The state file never appears on disk in the working tree; it lives behind an identity the runner is granted through short-lived credentials.

Production discipline

  1. State never lives in the working tree. .gitignore must block *.tfstate, *.tfstate.backup, .terraform/, *.tfplan, and crash.log at the first commit.
  2. State lives behind a remote backend with locking and versioning. Local state is acceptable only for terraform import experiments that never apply to real infrastructure.
  3. State access is the most sensitive IAM grant in the Terraform repository. A principal with write access to state can replace or destroy every resource.
  4. pre-commit blocks state files. A check-added-large-files plus a custom pattern for *.tfstate stops accidental commits before they reach the remote.

Cross-course references

  • Terraform for Production Sysadmins Parts IX-XII cover remote state, locking, and migration patterns in depth.
  • This course, Part L (TerraformCI) covers the plan and apply jobs and the credentials each requires.
  • This course, Part CII (LargeRepoPerf) covers the LFS and .gitignore disciplines that prevent state files from ever bloating a working tree.
  • This course, Part XXXV (SecretsInGit) covers the secret-leak dimension of state exposure.

Quiz

Knowledge check · 4 questions

  1. Q1. Why is terraform.tfstate the wrong file to commit to a Git repository?

  2. Q2. Marking a Terraform variable as sensitive prevents its value from being stored in the state file.

  3. Q3. Name the four properties a Terraform state backend must provide, and identify which one is missing from a local-state-in-Git setup.

  4. Q4. Diagnose a state corruption incident and recommend the remote-backend fix.

    Two engineers apply Terraform concurrently against the same monorepo using local state. Both write to terraform.tfstate in the working tree; each pushes; the second push is rejected; the first push's state becomes HEAD. A week later, terraform plan reports that 14 resources need to be created from scratch - the state no longer knows they exist. The state file also contained a database root password passed in via a variable.

Passing score: 75%. Answers are checked in this browser.