
TL;DR — Terraform’s state file is the bridge between your code and reality: a JSON snapshot of every resource it manages, its IDs, and — by default, in plaintext — every attribute, including secrets. Everything that makes Terraform work (dependency graphs, drift detection, fast plans) depends on that file, and everything that can go wrong operationally (concurrent applies leaking secrets into CI logs, a stale plan destroying the wrong resources) traces back to how state is stored, locked and accessed. Remote state with locking, encryption, least-privilege access and no secrets-in-state where avoidable isn’t a best-practice nicety — it’s the load-bearing design decision of infrastructure-as-code at any team scale.
Infrastructure as code has a quiet paradox: the code describes what should exist, but Terraform also needs a machine-readable record of what does exist — every resource ID, every attribute, every mapping from your resource blocks to real cloud objects. That record is the state file, and if you run Terraform without thinking about it, you will eventually have an incident with it. This is an engineering-explainer slot piece in the same spirit as our time synchronization deep dive: the infrastructure component everyone depends on and nobody reads about until it breaks.
What State Actually Does
Four jobs, all load-bearing:
- Identity mapping. Your
aws_instance.webcorresponds toi-0abc123…. Without state, Terraform cannot know which real object a code block refers to — the mapping lives nowhere else. - Dependency and graph caching. State stores the resource graph that makes plans fast and ordered (databases before apps, certificates before load balancers).
- Drift baseline.
terraform plancompares three things: code, state, and reality. If reality changed outside Terraform — a console “quick fix,” an incident hotpatch — plan surfaces it only relative to state. No state, no drift detection. - Performance. Providers query state rather than re-describing every cloud resource on each run — the difference between seconds and API-rate-limit timeouts at a few hundred resources.
| Operation | Reads state? | Writes state? | Needs lock? |
|---|---|---|---|
plan |
Yes | Planned-values only (with -out) | Recommended |
apply |
Yes | Yes — this is the real write | Yes |
refresh |
Yes | Yes (reality → state) | Yes |
destroy |
Yes | Yes (empties it) | Yes |
The Plaintext Problem
Here is the fact that should reorganize how you treat state: Terraform stores resource attributes in state as plaintext JSON — including values you marked sensitive = true. The sensitive flag masks output in your terminal; it does not encrypt state. Database passwords generated by Terraform, IAM secret keys, private key material: if Terraform created or read them, they’re in the file. A local terraform.tfstate on a laptop is a dossier of the infrastructure; a state file committed “temporarily” to a repo is a dossier with version history and broad read access. The same class of “trusted internal artifact that quietly aggregates secrets” we analyzed in SSRF to cloud metadata — the fix is structural, not vigilance.
The structural fixes, in order of impact: keep secrets out of Terraform’s management where possible (secret-manager resources that Terraform provisions but never reads contents of), remote state encrypted at rest with strict IAM, and CI pipelines that never echo state to logs. Then assume the file is still sensitive and size access accordingly.
Remote State and Locking
Local state fails the moment two engineers or two pipeline jobs exist. The standard answer is a remote backend:
- S3 as the canonical example (with equivalents on every cloud): state in a versioned, encrypted bucket; access via IAM. Bucket versioning is your accidental-deletion and corruption recovery — treat it as mandatory, not optional.
- State locking prevents the classic team-scale failure: two applies racing, both reading state v42, one writing v43, the other writing v44 — and reality diverging from both. On AWS the classic pattern paired S3 with a DynamoDB lock table; note that HashiCorp’s current guidance deprecates DynamoDB locking in favor of S3-native lockfiles — new setups should follow the current backend documentation rather than older tutorials.
- Workspaces or separate states per environment — production and non-production never share a state file, so an experimental apply cannot plan against prod resources.
Drift: The Operational Payoff
State’s unglamorous superpower is that it makes unmanaged change visible. Scheduled plan -detailed-exitcode runs answer “has reality diverged from code?” with a machine-readable exit code — the input for alerts when someone hand-edited a security group at 2 a.m. The workflow pairs naturally with the detection-as-code discipline from our detection engineering piece: drift events are telemetry, and an unexplained drift report deserves the same triage as an unexplained certificate (the CT monitoring habit, applied to your own infrastructure).
The reverse discipline matters too: when drift is legitimate (incident response), back-port the change into code promptly, or the next apply will “fix” your hotpatch away at the worst possible moment — a failure mode every SRE learns exactly once.
The Licensing Detour Worth Knowing
In August 2023, HashiCorp relicensed Terraform from MPL 2.0 to the Business Source License (BUSL 1.1), restricting competing commercial use. HashiCorp maintains a licensing FAQ for the transition, and the community response was a fork under the Linux Foundation — OpenTofu, MPL-licensed, registry- and workflow-compatible, and now widely adopted as the open-source path (including features like provider-level state encryption that arrived in the fork first). For engineering teams the practical read: your state and configuration are portable across the two tools, so the choice is an ecosystem and policy decision, not a lock-in — but whichever you run, the state-management practices in this piece apply identically, because both manage the same state format.
Running State Like a Production System
- Remote, versioned, encrypted, locked — the four non-negotiables, from day one; migrating later is possible but never fun.
- IAM scoped to the pipeline identity — state read/write is infrastructure-administration power; humans get read for debugging, automation gets the write path, break-glass is logged.
- No state in git, no state in logs — pre-commit hooks for local files, CI log scrubbing for plan output that echoes sensitive values.
- One state per environment per blast-radius — split by environment and by lifecycle (networking vs workloads), so slow-changing foundations don’t couple to fast-changing apps.
- Scheduled drift plans with alerts — and a written rule for how fast legitimate out-of-band changes must be codified.
- Backups beyond versioning — periodic state snapshots into your normal backup system; versioning protects against overwrites, not bucket-level mistakes, and a restore drill (to a scratch workspace) is the only proof a backup is real.
Key Takeaways
- State is the identity map, dependency graph, drift baseline and performance cache — Terraform is unusable without it, so it deserves production-grade operations.
- State stores secrets in plaintext JSON by default;
sensitivemasks display, not storage — design secrets out where possible and treat the file as crown-jewel data. - Remote backends with locking, versioning and encryption are the baseline; on AWS, follow current guidance (S3-native lockfiles) rather than legacy DynamoDB tutorials.
- Drift detection via scheduled plans is state’s biggest operational payoff — treat unexplained drift as a security signal and legitimate drift as tech debt with a deadline.
- The 2023 BUSL relicense spawned the OpenTofu fork (Linux Foundation, MPL); state and configs remain portable, so the choice is ecosystem policy, not data lock-in.
FAQ
Can I run Terraform without state?
Not with core Terraform — even “stateless” styles just use ephemeral state, losing drift detection and identity mapping. Remote state done well is the answer, not no state.
Why is my database password in the state file if I marked it sensitive?
sensitive = true controls CLI output display. State stores the real value because Terraform must track it for diffing — assume state contains every secret Terraform manages.
What happens if two applies run at once?
Without locking: read-read-write-write races where the second apply plans against stale reality and can destroy or duplicate resources. Locking serializes applies; it’s the single most important backend feature.
Is committing state to git ever okay?
No. Beyond secrets, git history makes rollbacks and access control far worse than a versioned remote backend. Add *.tfstate to .gitignore today if it isn’t.
What’s the difference between Terraform and OpenTofu here?
Practically none for state: same format, same backends, same practices. OpenTofu is the MPL-licensed fork created after the 2023 BUSL relicense, hosted by the Linux Foundation.
How often should I check for drift?
Continuously for high-change environments (scheduled plans with alerting), at least daily elsewhere. The interval is your mean-time-to-detect for unmanaged change — pick it deliberately, and write it down next to the alert owner, because an unowned drift alert is drift you have agreed not to notice.
References
- HashiCorp — Terraform state
- HashiCorp — S3 backend (locking, use_lockfile)
- HashiCorp — State locking
- HashiCorp tutorials — Manage state
- AWS DevOps Blog — Best practices for managing Terraform state in AWS CI
- OpenTofu FAQ — the fork, licensing and compatibility
- OpenTofu — the open-source fork (Linux Foundation)
- Wikipedia — Terraform (software)
- Spacelift — Terraform remote state: setup, locking, best practices
Current as of September 2026. Educational engineering reference — follow your backend’s current documentation; state features evolve quickly.
