← Back to Learning

Terraform Best Practices

Most Terraform advice is really "here's the mistake that caused a real problem, and the habit that would have prevented it." These are ranked roughly by how much damage the mistake causes, worst first.

Never hand-edit state, ever

Not even to fix something quickly. terraform state has real subcommands for the operations people are tempted to do by hand (state mv, state rm, import) that update state consistently instead of leaving it slightly wrong in a way that only shows up on the next plan, often as "destroy and recreate" on something that was actually fine.

Remote state, with locking, from day one

Local state works right up until it doesn't: two people apply within a minute of each other, or a laptop dies with the only copy of state on it. A remote backend (S3+DynamoDB, GCS, Terraform Cloud, etc.) with locking turns "corrupted state, reconstruct it by hand" into "the second apply just waits for the first one to finish." This is cheap to set up correctly at the start and expensive to migrate to later once real infrastructure already exists without it.

Pin provider and module versions

An unpinned provider means the exact same configuration can behave differently on two different days, on two different machines, for no reason visible in a diff. Pin with a constraint (~> 4.52), commit the generated .terraform.lock.hcl, and a plan run six months from now uses the same provider build as the one run today.

Secrets never belong in the config or in state

A hardcoded secret in a .tf file is the obvious mistake; the less obvious one is that plenty of resource attributes containing secrets end up written into the state file in plain text regardless of where the value came from, since state has to record the real value to know if it's drifted. Treat state as sensitive: encrypt it at rest (most remote backends do this by default), restrict who can read it, and prefer resources/providers that support write-only or ephemeral values where the underlying API allows it, so the secret is used without ever being persisted to state at all.

Small, composable modules over one giant configuration

A single 3,000-line main.tf is technically valid and practically unmaintainable. A module that does one coherent thing, with a small number of well-named inputs and outputs, can be reasoned about, tested, and reused. The test that actually matters: could someone else read this module's variables and outputs and understand what it does without reading the resource blocks inside it?

Plan output is not optional reading

"It applied without an error" and "it did what I expected" are different claims. A plan that says -/+ (destroy and recreate) where you expected ~ (update in place) is Terraform telling you a change you thought was minor actually forces replacement — worth knowing before it deletes something with real data on it, not after.

Consistent naming, everywhere

Resource names, variable names, and tags that follow one obvious pattern make a codebase searchable and a diff readable at a glance. Whatever the pattern is matters less than that there is one and it's followed — the cost of inconsistency compounds every time someone has to guess which of three naming styles a new resource should follow.