Terraform manages a staggering share of the world’s cloud infrastructure. According to the 2022 State of Infrastructure as Code report, a large majority of organizations that use infrastructure as code rely on Terraform as their primary tool. That popularity is well earned: it lets teams describe servers, networks, and databases in plain text and rebuild them on demand. But the same power that makes Terraform useful also makes its mistakes expensive. A single careless command can delete a production database, and a badly organized state file can slow a team to a crawl. This guide walks through the most common Terraform mistakes, why each one hurts, and what actually works in practice.
Why Terraform Mistakes Are So Costly
Terraform is declarative. You describe the end state you want, and the tool figures out how to get there. That model is elegant, but it means the tool trusts you completely. There is no “are you sure?” prompt that understands your business context. If your configuration says a resource should not exist, Terraform will happily destroy it.
The stakes rise because infrastructure changes are often irreversible. Deleting a database or a storage bucket can mean permanent data loss. The good news is that nearly every common mistake is well documented and preventable. The bad news is that most teams learn these lessons the hard way, usually at 2 a.m., usually during a deploy.
Mistake 1: Committing State Files to Version Control

This is the classic mistake, and it still shows up in tutorials and side projects. The `terraform.tfstate` file records the mapping between your configuration and real-world resources. It also often contains sensitive values in plaintext, including passwords and connection strings.
Committing state to Git creates two problems at once. First, secrets leak into repository history where anyone with read access can find them. Second, teammates end up with conflicting copies of state, which leads to drift and duplicate resources.
The fix is remote state. Store state in a backend designed for it, such as an object storage bucket with locking enabled, or a managed service like Terraform Cloud. Locking matters as much as storage: it prevents two engineers from running `apply` at the same moment and corrupting the state.
Mistake 2: Running Apply Without Reading the Plan
`terraform plan` exists for one reason: to show you exactly what will change before it changes. Skipping it, or running `apply -auto-approve` in a pipeline without any review step, is how accidental deletions happen.
In practice, the danger hides in the diff. A small edit to a resource name can cause Terraform to destroy and recreate the resource rather than rename it. On a database or a load balancer, that “small edit” becomes an outage.
What works: make plan output visible in every pull request. Tools that post the plan as a comment turn review into a genuine safety net. Engineers should look specifically for lines marked for destruction, and any plan that destroys a stateful resource should require a second pair of eyes.
Mistake 3: Hardcoding Secrets in Configuration
Putting an access key directly into a `.tf` file or a `terraform.tfvars` file is a mistake that keeps resurfacing. Even if you delete it later, the value lives on in Git history and, critically, in state files.
A subtler version of this mistake is passing secrets as Terraform variables. Those values get stored in state, which means anyone who can read state can read the secret. The practical approach is to have Terraform reference a secret manager rather than hold the secret itself. Let the provider fetch credentials at runtime, and keep sensitive values out of both code and state wherever the provider allows it. (Introduction to Terraform)
Mistake 4: Using Count Where for_each Belongs
This one is subtle and bites experienced engineers too. The `count` argument indexes resources by position. If you use it to create a list of users or subnets and later remove an item from the middle, every item after it shifts index. Terraform then destroys and recreates resources that did not actually change.
`for_each` with stable keys solves this. Each resource is tracked by a meaningful key rather than a number, so removing one item leaves the others untouched. The rule of thumb: use `count` for a simple on/off toggle, and use `for_each` for any collection of real resources. The Terraform documentation on for_each explains the mechanics clearly.
Mistake 5: Leaving Versions Unpinned
Terraform itself and its providers both release frequently. Without version constraints, a routine `terraform init` can quietly pull a new major provider version with breaking changes. The result is a plan full of unexpected diffs, or worse, a failed apply during a release window.
Pin both layers. Set `required_version` for Terraform and use the `required_providers` block to lock provider versions to a sensible range. Then upgrade deliberately, testing the new version in a non-production workspace first. Treat version bumps as changes worth reviewing, not background noise.
Mistake 6: Skipping Formatting and Validation in CI
`terraform fmt` and `terraform validate` are cheap, fast, and routinely ignored. Formatting drift makes diffs noisy, which makes real changes harder to spot during review. Validation catches syntax errors and invalid references before they reach an environment.
Wiring both into a pre-commit hook or a CI pipeline takes minutes and pays for itself immediately. Add a linter on top, and you catch deprecated arguments and risky patterns before they merge. These checks are the seatbelts of a Terraform workflow: boring, inexpensive, and genuinely effective.
Mistake 7: Making Manual Changes in the Console
Clicking around in a cloud console feels faster than editing code, and sometimes it is. But every manual change creates drift between your configuration and reality. The next `plan` will show a diff that nobody expects, and someone will spend an afternoon figuring out why.
The discipline that works is simple: the code is the source of truth. If a change is needed urgently, make it in the console if you must, but immediately follow up with a code change that matches. Better yet, use `terraform import` to bring existing resources under management rather than leaving them as undocumented orphans. (Infrastructure as code)
Mistake 8: Building One Giant Root Module
A single root module with hundreds of resources in one state file seems convenient at first. Plans get slow, blast radius gets huge, and a mistake in one area can affect everything. State locking also becomes a bottleneck when the whole team shares one state.
Splitting infrastructure into smaller, purpose-built states (networking, data, application) limits the damage any single change can do. The right size is debatable, and teams often overcorrect into dozens of tiny states that are hard to coordinate. A reasonable middle ground is to separate by lifecycle and ownership: things that change together should live together.
Mistake 9: Misusing depends_on
Terraform infers dependencies automatically from references. When you write `depends_on`, you override that inference with an explicit instruction, and it applies to the entire resource or module. Overusing it creates unnecessary delays and can mask real ordering problems.
The common error is adding `depends_on` to a whole module when only one resource inside it needs the dependency. A cleaner approach is to pass outputs between modules so the dependency is implicit and precise. Reserve explicit `depends_on` for cases where a dependency genuinely cannot be expressed through references.
Mistake 10: Treating -target as a Daily Workflow
The `-target` flag lets you apply a subset of resources. It is invaluable during an incident or when bootstrapping. It is also dangerous as a habit, because targeted applies can leave state partially updated and skip resources that depended on the ones you changed.
Use `-target` as a debugging tool, then run a full plan afterward to confirm everything is consistent. If you find yourself targeting the same resources repeatedly, that is a signal your module structure or dependency graph needs attention.
A Quick Reference: Mistake, Symptom, Fix
| Mistake | Symptom | Fix |
|---|---|---|
| State in Git | Leaked secrets, drift | Remote backend with locking |
| Skipping plan review | Surprise destruction | Plan posted in every PR |
| Hardcoded secrets | Credentials in history and state | Reference a secret manager |
| `count` on collections | Resources recreated on list edits | Use `for_each` with stable keys |
| Unpinned versions | Unexpected diffs after init | Pin Terraform and providers |
| No CI checks | Noisy diffs, syntax errors | Run fmt, validate, and a linter |
| Console changes | Unexplained plan diffs | Code as source of truth |
| Monolithic state | Slow plans, large blast radius | Split by lifecycle and ownership |
| Overusing `depends_on` | Slow applies, hidden ordering | Rely on implicit references |
| Routine `-target` | Partial state updates | Debug only, then full plan |
Practical Habits That Prevent Most of These Mistakes
Prevention comes down to a handful of habits. Review plans the way you review code. Keep state remote and locked. Pin versions and upgrade on purpose. Run the cheap checks automatically. And when something goes wrong, resist the urge to fix it by hand in the console.
One underrated habit is documenting why a resource exists. A short comment explaining that a bucket is retained for compliance reasons prevents a future engineer from “cleaning it up.” Infrastructure code is read far more often than it is written, and context is the thing most often lost.
Another is practicing recovery. Teams that have rehearsed restoring state from a backup, or rebuilding an environment from scratch, handle real incidents with far less panic. If you are also documenting operational procedures for your team, clear step-by-step guides matter everywhere, whether you are writing runbooks or explaining how to take a screenshot on Windows to a new colleague. Clarity under pressure is a skill you build in advance. (How to take a screenshot on a mac)
Conclusion
Terraform rewards discipline and punishes shortcuts, but the mistakes in this guide are all avoidable. The pattern behind them is consistent: they come from treating infrastructure code as casual text rather than as a system with real-world consequences. Remote state, reviewed plans, pinned versions, and a strict “code is the source of truth” rule eliminate the majority of incidents before they start. None of these practices is glamorous. All of them are the difference between a calm deploy and a long night. Start with the two that matter most, state management and plan review, and build from there.
Frequently Asked Questions
Should terraform.tfstate ever be committed to Git?
No. State files often contain sensitive values in plaintext, and shared copies cause drift and lock conflicts. Use a remote backend with locking instead, and add state files to your ignore list.
What is the difference between count and for_each, and which should I use?
`count` indexes resources by position, so removing an item from a list shifts every index after it and can trigger unnecessary destruction. `for_each` tracks resources by stable keys. Use `count` for simple toggles and `for_each` for collections of real resources.
Is it safe to use terraform apply -auto-approve in CI?
It can be, but only when a plan has been reviewed first and the pipeline is scoped to the right environment. Blind auto-approval without plan review is how unintended deletions reach production.
How do I handle secrets in Terraform?
Reference a secret manager from your provider configuration rather than storing values in variables or files. Remember that variable values are written to state, so anything sensitive should be fetched at runtime wherever possible.
Why does my plan show resources being destroyed when I only renamed something?
Terraform identifies resources by their address. Renaming a resource or changing a key changes its address, so Terraform sees a different resource. Use `moved` blocks to tell Terraform about the rename so it updates state instead of recreating the resource.
How often should I upgrade Terraform and provider versions?
Regularly, but deliberately. Pin versions, review changelogs, and test upgrades in a non-production workspace before rolling them out. Upgrading in small, frequent steps is far safer than jumping several major versions at once.


