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).
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.
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.
resource "azurerm_storage_account" "example" {
location = "westeurope" # Hardcoded in 10 files!
}
resource "azurerm_sql_server" "example" {
administrator_login_password = "SuperSecret123!" # 😱
}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.
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 "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.
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.
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.
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.
provider "azurerm" {
features {}
}
module "network" {
source = "git::https://github.com/my-org/network"
}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! 🚀