Skip to content
Holits
Terraform4 min readBy Holits

Your Terraform problem is a state layout problem

Teams blame Terraform for slow plans, scary applies and mysterious drift. Almost always the tool is fine and the state boundaries are wrong.

A recurring conversation: a team is unhappy with Terraform. Plans take eight minutes. Nobody applies without a second person watching. There is drift that appears out of nowhere. Somebody suggests migrating to Pulumi, or CDK, or going back to clicking.

Then you look, and there is one state file containing the VPC, the Kubernetes cluster, the databases, the DNS records and an S3 bucket someone added in 2023.

The tool is not the problem. The blast radius is.

What a single state actually costs

Every plan reads everything. Refresh is proportional to resource count, so changing a DNS record costs the same eight minutes as rebuilding the cluster.

Every apply risks everything. The plan output is 400 lines and the two that matter are somewhere in the middle. This is how a targeted change becomes an outage: not because someone was careless, but because the diff exceeded what a human can meaningfully review.

Locking serialises the whole organisation. One engineer applying a change blocks everyone else, so people batch changes, which makes each apply larger and scarier, which makes people batch more.

Blast radius is unbounded by construction. There is no structural reason a mistake in a DNS module cannot destroy a database. Only review stands between them, and review gets tired.

Cut along lifecycle, not along org chart

The instinct is to split state per team. That produces boundaries that cut straight through dependency chains, and then you are wiring remote_state data sources everywhere and have coupled your teams and your states.

Split by rate of change and blast radius instead:

  • Foundation — VPCs, subnets, IAM baseline, DNS zones. Changes rarely. Everything depends on it. Small, boring, heavily reviewed.
  • Platform — clusters, shared databases, ingress, observability stack. Changes monthly.
  • Applications — per-service infrastructure. Changes daily, blast radius bounded to one service.

Dependencies flow one way: applications depend on platform depends on foundation. Never the reverse. If you find yourself wanting a foundation resource to reference an application resource, the boundary is wrong.

Publish contracts, not state files

The mechanism between layers matters more than people expect.

Reaching into another layer's state with terraform_remote_state couples you to its internal resource structure. Rename a resource in foundation and you break platform, for no reason a reader of either would predict.

Publish an explicit contract instead — SSM parameters, or outputs consumed through a data source that looks things up by tag or name. The upstream layer can then restructure freely as long as the contract holds. This is the same information-hiding argument you would make about a library API, and it applies for the same reasons.

Modules should be composable, not universal

The other common failure: one module per resource type, each with forty variables, trying to cover every case anyone has ever needed.

Nobody can read a forty-variable module and predict what it does. The count = var.enabled ? 1 : 0 pattern spreads through it. Eventually people copy it rather than call it, because copying is comprehensible.

Prefer small modules that do one thing with few inputs and clear outputs, and compose them. A module with three variables that you call four times is better than one with twelve that you call once.

Drift is a signal, not an annoyance

Persistent drift means something outside Terraform is writing to your infrastructure. That is worth finding, because it is the same class of problem as a change made by clicking at 3am by someone guessing.

Run plan on a schedule and alert on non-empty output. Either the drift is legitimate — in which case it belongs in code — or something is writing to production out of band, which you want to know about before an incident rather than during one.

The migration is less scary than it looks

terraform state mv and import do the work. Do it incrementally: carve out the least-coupled layer first, usually applications, and let foundation stay monolithic for a while. You do not need a big-bang restructure, and attempting one is how these projects get abandoned halfway.

The measure of success is simple: how large is the diff when you change one thing? If a DNS record change produces a plan you can read in ten seconds, the boundaries are right.