Terraform Isn’t Application Code: Why Simplicity Beats ‘Cleverness’ in IaC
One of the most important (and overlooked) truths in infrastructure as code is this: Terraform is not application code.That might sound obvious, but I’ve seen countless developers — especially those coming from strong software engineering backgrounds — carry over habits from app development that don’t translate well to infrastructure.
And I get it. If you’re used to crafting elegant abstractions, designing highly modular systems, and refactoring for reusability, then seeing repeated code blocks in a Terraform repo can feel… wrong. You might feel compelled to “clean it up.” But before you do, let’s talk about why applying application development best practices to Terraform can backfire — and what to do instead.
The Appeal of Abstraction
In application development, abstraction is power. It gives us reusable components, cleaner logic, testability, and easier evolution over time. Object-oriented and functional paradigms thrive on abstraction. So when we start writing Terraform, we reach for the same tools: modules, variables, reusable patterns. We refactor aggressively. We eliminate repetition.
But here’s the rub: infrastructure code has a very different job. Terraform doesn’t compile down into optimized machine instructions. It doesn’t need to be fast or efficient. It’s a model of your infrastructure, and the most important thing it can be is understandable.
“Other architectural aspects such as performance, efficiency, data integrity, and especially technical elegance are not important distinctions for Infrastructure as Code.”
Let that sink in. Terraform code doesn’t need to be clever — it needs to be boring. Predictable. Readable. Debuggable.
The Pitfalls of Over-Abstraction
Let me give you a real-world example. I’ve seen teams build elaborate module hierarchies to enforce consistency across cloud environments. They create a root module that wraps a child module that wraps a reusable “core” module with dozens of input variables — many of which are just passed straight through. On paper, this looks DRY and reusable. In practice? It’s a nightmare to debug.
When something breaks — or worse, silently misbehaves — you now have to:
- Track values across multiple layers of modules
- Decode naming conventions that were abstracted for “flexibility”
- Guess where a particular resource is defined (or overridden)
You end up doing more infrastructure archaeology than infrastructure development.
Now compare that to a simple block like this:
resource "azurerm_storage_account" "logs" {
name = "stlogs${random_string.suffix.result}"
resource_group_name = var.resource_group_name
location = var.location
account_tier = "Standard"
account_replication_type = "LRS"
}
Yes, you could wrap this in a module. You could parameterize it further. But maybe you don’t need to. Maybe repeating a few lines is better than creating an abstraction you have to carry around forever.
“A beautifully DRY module system that’s impossible to debug is worse than some copy-pasted resource blocks that are dead simple to understand.”
Prioritize Clarity Over Cleverness
This isn’t an argument against using modules. Modules are great — when used with care. The key is understanding the tradeoffs between reuse and readability.
If a module is going to be reused in multiple environments or projects, or encapsulates a non-trivial set of resources (like a full AKS cluster setup), it makes sense to extract it. But if you’re abstracting just to reduce line count or avoid repetition in a single repo, you’re likely adding more complexity than you’re saving.
Here’s the principle I follow:
Infrastructure code should optimize for clarity, not elegance.
When someone reads your code — six months from now, or after you’ve left the team — they should be able to understand what’s happening without tracing through three levels of indirection.
Terraform is a Model, Not a System
At its core, Terraform is a declarative model of desired infrastructure state. It’s not a dynamic runtime system. There are no “hot paths” to optimize, no performance bottlenecks in how your code is written. The Terraform engine handles that.
This frees you to focus on what really matters:
- Is your infrastructure code predictable?
- Can others quickly scan it and understand what it’s doing?
- Are your plans and diffs clear and accurate?
- Can you debug issues without spelunking into abstractions?
If yes, then you’re doing it right — even if your code has some duplication.
Conclusion
One of the best pieces of advice I can give is this: write Terraform for your future self.
When you come back to a module six months later, will you understand how it works? Will you remember the subtle differences in how a wrapper module configures VNets in staging vs. production? Or will you have to unravel a pile of clever abstractions to figure it out?
“Your future self will thank you for prioritizing clarity over cleverness.”
Keep your Terraform code as simple as it can be — and no simpler. Use modules where it helps. Avoid indirection where it hurts. And above all, write for the humans reading your code, not the tools running it.
If you’re someone who’s deeply invested in clean, elegant code — great. Just remember that Terraform isn’t a system you’re building. It’s a blueprint. And blueprints should be clear, explicit, and boring in the best possible way. Want to dive deeper into Terraform module structure, naming conventions, and practical design patterns? Keep an eye out for my upcoming book — where this topic gets the full deep-dive treatment it deserves.