When a Terraform apply goes badly wrong, teams almost always start by reviewing the resource blocks that changed. That is usually the wrong place to look first. The state file is what tells Terraform what already exists and what it is responsible for managing, and problems with how that file is stored, locked and shared cause more real damage than almost any misconfigured resource.

Local state is a single point of failure disguised as convenience

Terraform's default behavior is to write state to a local file. That is fine for a five-minute experiment and a genuine risk the moment more than one person touches the same infrastructure. Local state means no locking, no shared source of truth, and a real chance that two people apply changes based on two different views of what already exists. The fix is well known and still frequently skipped early on: move to remote state with locking before a second person joins the repository, not after the first conflict happens.

  • Remote state backends (S3 with DynamoDB locking, Terraform Cloud, Azure Storage with state locking, and similar) prevent concurrent applies from corrupting state
  • Local state files are easy to accidentally commit to version control, which can expose sensitive values
  • A lost local state file means Terraform no longer knows what it manages, even though the real infrastructure still exists

One state file for everything is a scaling mistake, not a simplification

A single, large state file feels simpler at first. It becomes a liability as the infrastructure grows, because every plan and apply operation has to evaluate the entire file, every lock blocks every other change regardless of which resource is actually being touched, and one bad apply can put the whole environment at risk instead of a contained part of it.

Splitting state by environment and by logical boundary, such as networking, data layer and application infrastructure, reduces blast radius and makes plans faster to review because they only show what is actually relevant to that boundary.

Manual changes outside Terraform cause silent drift

The console is fast for a genuine emergency fix, and that speed is exactly what causes drift. Once a resource is changed outside Terraform, the state file no longer reflects reality, and the next apply either tries to revert the manual fix or produces a plan nobody trusts because it looks unexpectedly large. Drift compounds quietly. A resource changed manually once is easy to remember; a resource changed manually six times over a year, by different people, during different incidents, is not.

Hardcoded secrets and credentials in state, not just in code

It is common practice by now to avoid committing secrets into version control. It is less commonly understood that Terraform state files can contain sensitive values in plain text, including database passwords set through a resource attribute, because state stores the full resource attributes it manages. A state backend without encryption at rest and tightly scoped access can leak exactly the secrets a team already worked hard to keep out of their source code.

No plan review process before apply

Terraform's biggest safety feature is that it shows you the plan before it changes anything. That feature is worthless if applies happen straight from a local machine without anyone reviewing the plan output first. A pull request based workflow, where the plan output is posted for review before an apply is allowed, catches a meaningful share of mistakes before they touch real infrastructure, including changes that look correct in the code but produce a surprising, unintended diff.

  • Require the plan output to be visible in the pull request, not just the code diff
  • Block apply until a plan has been reviewed and approved by someone other than the author
  • Treat any plan showing an unexpected destroy as a stop-and-investigate signal, not a rubber stamp

Modules with no versioning strategy

Referencing a module directly from a branch, rather than a pinned version or tag, means every consumer of that module can be affected by a change the moment it merges, whether or not that was the intent. This is a common way a small, well-intentioned module fix turns into an incident across several unrelated environments at the same time.

A short list worth checking this week

  • Confirm state is stored remotely with locking enabled, for every environment
  • Split state by environment and logical boundary instead of one file for everything
  • Run a scheduled plan to catch drift before it compounds
  • Verify state backend encryption and access controls, not just source code secrets
  • Require plan review before apply, through a pull request workflow
  • Pin module versions instead of referencing branches directly

Most of this is inexpensive to fix compared to the cost of the incident it prevents. The pattern we see most often is a team that outgrew their original Terraform setup gradually, without a single moment that made the risk obvious, until an apply went wrong in a way that was much harder to unwind than it needed to be.

Does this match your situation?

Talk to BashClouds about the specifics of your setup, no obligation.

Discuss a projectMore guides