terraform
iac
cloud

The 5 Terraform Mistakes Everyone Makes

And How to Avoid Them

Terraform is powerful—but unforgiving. This guide dives into five common mistakes (plus a bonus!) that even seasoned engineers make, why they matter, and how to avoid the Dark Side with examples, pro tips, and Jedi wisdom.

🎯 TL;DR

Terraform mistakes are more common than lightsaber duels. State files left local, hardcoded secrets, unmodularized code, ignored dependencies, and blind applies—these are the Sith Lords of infra chaos. This guide exposes them all, gives you real-world fixes, and adds a bonus on version pinning (because surprise upgrades are fun only at birthday parties).

1

Neglecting State File Management 🗂️⚠️

Why it Matters

The terraform.tfstate file isn't just a boring JSON blob—it's the single source of truth for your entire infrastructure. Lose it, corrupt it, or expose it, and you're flying the Millennium Falcon blindfolded.

💀

Orphaned Resources

Resources you can't destroy or manage anymore

⚠️

State Drift

Mismatch between Terraform and reality

🔓

Security Breach

Secrets leaked from exposed state files

Classic Blunder

Treating state files like casual docs—kept on laptops, shared in Slack/email, or (gasp) versioned in GitHub. Cue race conditions and expensive investigations into "who ran apply last?"

Pro Tips

  • Use remote state backends (Azure Blob, AWS S3 + DynamoDB, GCP Cloud Storage, Terraform Cloud)
  • Encrypt state at rest and in transit
  • Restrict access with least privilege IAM
  • Enable state locking to prevent concurrent applies
  • Enable versioning/soft-delete for rollback
  • Monitor access logs (CloudTrail, Azure Monitor)

💡 Jedi Tip

Treat your state file like a root password. Lose it, and the Dark Side wins.

2

Hardcoding Values Instead of Using Variables 🤦‍♂️🏷️

Why it Matters

Hardcoding is a shortcut to chaos. Secrets, regions, or IDs sprinkled everywhere lead to broken portability, copy-paste madness, leaked secrets in plain text, and painful onboarding.

Classic Blunder

Embedding location, passwords, and configuration directly in resource blocks.

❌ Bad Examplehcl
resource "azurerm_storage_account" "example" {
  location = "westeurope"  # Hardcoded in 10 files!
}

resource "azurerm_sql_server" "example" {
  administrator_login_password = "SuperSecret123!"  # 😱
}
✅ Good Examplehcl
variable "location" {
  type        = string
  description = "Azure region"
  validation {
    condition     = contains(["westeurope", "eastus"], var.location)
    error_message = "Location must be westeurope or eastus."
  }
}

resource "azurerm_storage_account" "example" {
  location = var.location
}

# Get password from Key Vault
data "azurerm_key_vault_secret" "db_password" {
  name         = "db-admin-password"
  key_vault_id = var.key_vault_id
}

Pro Tips

  • Use variables.tf with validation for env/region
  • Pass secrets via env vars or secret managers (Vault, Azure Key Vault, AWS Secrets Manager)
  • Standardize naming/tagging with locals.tf
  • Fetch IDs dynamically with data sources
  • Run tflint or checkov to block hardcoding in CI/CD

💡 Jedi Tip

Every hardcoded value is future technical debt. Use variables, locals, and data sources like lightsabers to stay elegant.

3

Not Using Terraform Modules for Reusability 🧩

Why it Matters

Terraform without modules = a copy-paste jungle. Updates turn into whack-a-mole across dozens of files. Modules bring DRY (Don't Repeat Yourself) to infra.

❌ Without Modules

  • Copy-paste code everywhere
  • Inconsistent configurations
  • One file missing encryption
  • Painful updates

✅ With Modules

  • Reuse across environments
  • Centralized updates
  • Enforced standards
  • Faster onboarding
Module Examplehcl
module "storage" {
  source  = "Azure/storage/azurerm"
  version = "~> 4.0"

  resource_group_name = var.resource_group_name
  location            = var.location
  account_tier        = "Standard"
  replication_type    = "LRS"

  tags = local.common_tags
}

Pro Tips

  • Use vetted community modules (terraform-aws-vpc, terraform-azurerm-vnet)
  • Create internal modules for VPCs, IAM, storage
  • Pin module versions (ref=v1.2.3) to avoid surprise breakage
  • Test modules in isolation before rollout
  • Keep modules small and composable (network, app, db)

💡 Jedi Tip

Modules are LEGO bricks. Snap them together. Copy-paste infra is glue—it breaks when you need to change.

4

Ignoring Resource Dependencies 🚦

Why it Matters

Terraform builds a dependency graph, but bad references or forced orders lead to race conditions, failed applies, and subtle bugs (dangling security groups, unusable VMs).

Classic Blunder

Creating compute before networks are ready. Instances fail, pipelines stall, engineers rage.

Dependency Examplehcl
resource "azurerm_virtual_network" "main" {
  name                = "my-vnet"
  address_space       = ["10.0.0.0/16"]
  location            = var.location
  resource_group_name = var.rg_name
}

resource "azurerm_subnet" "main" {
  name                 = "my-subnet"
  resource_group_name  = var.rg_name
  virtual_network_name = azurerm_virtual_network.main.name  # Implicit dependency!
  address_prefixes     = ["10.0.1.0/24"]
}

resource "azurerm_network_interface" "main" {
  name                = "my-nic"
  location            = var.location
  resource_group_name = var.rg_name

  ip_configuration {
    subnet_id = azurerm_subnet.main.id  # Implicit dependency!
  }
}

Pro Tips

  • Reference resource attributes for implicit dependencies
  • Use depends_on only when Terraform can't infer
  • Inspect plans for suspicious order/skipped steps
  • Visualize with terraform graph | dot -Tpng > graph.png
  • Design modules with clear input/output contracts

💡 Jedi Tip

Trust the graph, but verify. Overusing depends_on means your design probably needs a rethink.

5

Not Reviewing the Plan—Trusting terraform apply Blindly 🔍

Why it Matters

Terraform's plan is your crystal ball. Skipping it is like deploying DB migrations without reading the diff. Risks include unintentional destroys, misconfigured variables, missed drift, and lost trust after an "innocent" commit nukes prod.

Classic Blunder

Piping terraform apply -auto-approve straight into CI/CD. Congrats—you've automated Russian roulette.

Pro Tips

  • Always run & review plan. Store plan artifacts in CI
  • Require PR reviews & mandatory checks (fmt, validate, tflint, checkov)
  • Ban default auto-approve—limit it to dev/ephemeral envs
  • Flag "diff noise" (unexpected deletes/creates) with policies
  • Post plan summaries in Slack/Teams for transparency

⚠️ Warning

The plan shows you the future. If it looks scary, stop. The Force is literally warning you.

🥇

Bonus: Version Pinning and Upgrades

Why it Matters

Providers and modules evolve fast. Floating on latest is gambling—sudden changes = broken infra. Unpinned versions cause surprise breakage, inconsistent environments, and drift from changed defaults.

❌ Unpinnedhcl
provider "azurerm" {
  features {}
}

module "network" {
  source = "git::https://github.com/my-org/network"
}
✅ Pinnedhcl
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.75"
    }
  }
}

module "network" {
  source = "git::https://github.com/my-org/network?ref=v1.2.1"
}

Pro Tips

  • Pin providers with required_providers + commit .terraform.lock.hcl
  • Pin modules (version = "x.y.z" or ?ref=v1.2.1)
  • Test upgrades in non-prod with plan
  • Schedule upgrades regularly—don't wait years
  • Automate checks with Dependabot/Renovate

💡 Jedi Tip

Version pinning = predictability. Keep surprises for birthday parties, not prod.

🤓 Did You Know?

💡

Terraform's first release was in 2014, same year as Kubernetes!

💡

terraform console helps debug values in real time

💡

Plans can be saved (-out=tfplan) and reused safely

💡

Remote backends support access logging—handy for audits

💡

Terraform Registry has thousands of modules—don't reinvent the (Millennium) Falcon

💡 Key Takeaways

Protect Your State

Treat state files like the Crown Jewels—remote, encrypted, locked

Variables & Modules

Avoid hardcoding, embrace reusability and DRY principles

Respect Dependencies

Trust the graph and verify your resource ordering

Review the Plan

Never skip reviewing what Terraform will do

⁉️ FAQ

Is it ever okay to keep state files local?

Only for experiments. For teams, always use remote backends.

Can I use secrets in variables?

Never hardcode. Use env vars or secret managers.

What's a good tagging standard?

At minimum: Environment, Owner, Project, Purpose, CostCenter. Enforce with policies.

Should I always modularize?

Flat files are fine for POCs. For real projects, modularize from day one.

Why do upgrades break things?

Providers/modules change. Pin versions and test upgrades in isolation first.

🚀 Final Thoughts

Terraform is powerful, but mistakes happen—even to experienced engineers. From state slip-ups to tagging chaos, we've all had "whoops" moments that made us want to hide in a Death Star trash compactor.

The secret isn't perfection—it's guardrails and habits. Remote state, reusable modules, ruthless tagging, careful plan reviews, and pinned versions. These turn chaos into boring, stable infra.

Because in Terraform, boring is beautiful.

And remember: Stay nerdy, keep learning, and keep Talking Nerdy to Me! 🚀