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.
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.
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.
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.
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.
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?
"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.
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.