Things I Wish I Knew Before Using Terraform
Things I Wish I Knew Before Using Terraform
Terraform is incredibly powerful. No doubt about it. But if you're like me, you probably started using it because you wanted to automate infrastructure, not spend your nights debugging why an aws_subnet refuses to associate with a VPC you swear exists.
This article is a collection of things I wish someone had told me before I dove in. These aren't deep technical gotchas. They're the small architectural and workflow decisions that compound over time and turn a clean setup into a pile of spaghetti. Learn from my pain.
- Open Source Modules Are a Beginner's Trap
When I first started with Terraform, I leaned heavily on open-source modules. Why not? They promised quick infrastructure setup and "battle-tested" code. The problem? Most of them are bloated with conditionals and edge cases to support every scenario except yours.
Once things break (and they will), you'll find yourself neck-deep in someone else's codebase, trying to reverse-engineer why a random flag buried three levels deep caused your RDS instance to behave strangely.
If you're just starting, write your own focused modules. You'll understand your infrastructure better and avoid black-box debugging hell.
- Put Only One Resource in a File
Grouping resources in files like main.tf, compute.tf, or storage.tf seems tidy at first, until your app grows. Then you're stuck figuring out where to put a resource that doesn't cleanly fit into any group.
Even worse, merging changes across branches becomes painful when multiple engineers are modifying the same files. The fix? Put each resource in its own .tf file. It's not overkill. It's a lifesaver. This approach reduces merge conflicts but may create more files; weigh it against your team's preferences for logical grouping. Want to organize? Use folders by provider or service (aws/vpc/, google/cloudrun/), but keep the individual resources separate. It'll keep your diffs clean, your Git merges smooth, and your sanity intact.
- Use locals.tf as a Sanity Buffer
Don't wire outputs from one resource directly into another. It seems efficient, until you're dealing with deeply nested chains of unreadable references.
Instead, use a dedicated locals.tf file to stage data. Transform outputs, assign clean names, and then feed those into other resources. It's your translation layer, a place to reshape what Terraform gives you into something you actually want to work with.
A little upfront investment in locals saves a lot of downstream pain.
- Keep Providers at the Root. Always
Yes, Terraform lets you declare providers inside modules. Don't do it. Just because you can doesn't mean you should.
If you accidentally use different versions of the same provider across modules, you'll start getting subtle differences in outputs or behavior. Good luck debugging that.
Instead, define all providers in the root module and explicitly pass them into your submodules. It's a bit more boilerplate, but you maintain full control, consistency and avoid provider version drift.
- Use Descriptive Names. Not "Primary" or "Secondary"
Early on, I had resources named primary_vpc, main_instance, secondary_db. That felt fine, until I started deploying to multiple regions and environments.
What does primary mean in us-west-2 vs us-east-1? What's main in production vs staging?
Be specific. If you're creating a VPC in us-east-1, name it us_east_1_vpc. If it's for staging, call it staging_us_east_1_vpc. It might seem verbose, but it makes your logs readable, your state understandable, and your team grateful.
- Separate Environment Variables into Dedicated Files
locals.tf is not the place for environment variables. I made that mistake, and it quickly turned into an unreadable dump of hardcoded values, if-else logic, and untraceable overrides.
What works better: create a dedicated file for each environment, like
staging_env.tf or production_env.tf. Each file should define a single map
of environment variables, like env_vars = \{ ... \}, and that's it.
This keeps your environment variables isolated, easy to review, and free from the rest of your Terraform logic. Then reference them in your resources as needed. And trust me, the number of environment-specific variables will always grow.
- Use for_each with Meaningful Keys. Avoid count
Using count to create multiple resources works, until it doesn't. Terraform labels them with indices like [0], [1], and when something breaks, you're left guessing which one is which.
Want readable plans and debuggable logs? Use for_each with a map instead. For example:
resource "awesome_resource" "example" {
for_each = {
for k, v in var.resource_by_name:
k => v if v != null
}
}Now your logs will show:
resource.awesome_resource["resource_a"]
resource.awesome_resource["resource_b"]
resource.awesome_resource["resource_c"]Clear. Descriptive. Easy to trace. It's a no-brainer. Just ensure keys are unique and stable to avoid resource recreation during updates.
Resources
What Terraform pitfalls have you encountered? Share in the comments!