terraform · devops · iac · best-practices

Four Terraform Anti-Patterns That Will Bite You

Infrastructure as Code done badly is worse than no IaC at all. Here are the four anti-patterns I keep seeing in inherited Terraform codebases.

Every few months I inherit a Terraform codebase from a team that genuinely believed they were doing IaC properly. The files are there. The .tf extensions are there. The confidence is very much there.

The infrastructure is not.

Here are the four anti-patterns I see most often — and why each one quietly negates the point of using Terraform in the first place.

1. Copy-paste environments instead of modules

The setup looks like this: a dev/ folder, a staging/ folder, a prod/ folder. Each one contains almost identical .tf files, with minor differences buried somewhere on line 47.

When someone updates the dev VPC config, staging doesn't get updated. Neither does prod. Three months later, nobody knows why prod behaves differently to dev, and the answer turns out to be a security group rule that exists in two out of three environments.

Modules exist precisely to prevent this. One definition, multiple instantiations, consistent behaviour. If you're copy-pasting Terraform, you're not doing IaC — you're doing manual configuration management with extra steps.

2. State stored locally (or worse, on a laptop)

terraform.tfstate on someone's MacBook is not a state management strategy. It's a single point of failure with legs.

When that person is on holiday, nobody can safely run terraform plan. When they leave the company, you have one of two outcomes: you find the file, or you don't. Neither is great.

Remote state in S3 (with DynamoDB locking) or Terraform Cloud takes about 20 minutes to set up. It means any authorised person can run Terraform safely, state doesn't get corrupted by two simultaneous applies, and you have a history of what changed and when. There is no good reason not to do this from day one.

3. Secrets and hardcoded values in .tf files

I've seen AWS secret keys committed to git. Database passwords. Stripe API keys. Always with some version of "we'll fix it later."

Later doesn't come.

git log is permanent. Even if you remove the secret in the next commit, it lives in the history. Anyone with read access to the repo has it. If the repo is public — even briefly — it's already been scraped.

Secrets belong in a secrets manager (AWS Secrets Manager, HashiCorp Vault, even SSM Parameter Store as a minimum). Environment IDs and account numbers that aren't sensitive still belong in variables.tf with proper defaults, not hardcoded inline where they're invisible to reviewers.

4. No plan review before apply

terraform apply should never be the first command you run against production.

terraform plan shows you exactly what will be created, modified, or destroyed. It takes 30 seconds. Yet I regularly find pipelines — and engineers — who skip it entirely because "it should be fine."

Destroying a production RDS instance because someone changed a resource name is not fine. It happens. It is preventable with a plan review step.

The minimum: run plan in CI, require a human to review the output before apply is permitted. If your pipeline doesn't have this, it's not a deployment pipeline — it's a surprise generator.


The point of Terraform isn't to have .tf files. It's predictability. The ability to look at your infrastructure like code — version it, review it, test it, recreate it. Each of these anti-patterns chips away at that guarantee until what you have is a false sense of control.

If any of these look familiar, the fix is usually simpler than it seems. The hard part is stopping long enough to do it.

work together

Need this for your team?

This is the kind of infrastructure work I do for clients — cloud platforms, CI/CD automation, and GitOps workflows that teams can actually operate.