“Don’t Terraform modules already solve this?”
If you’ve worked on a growing infrastructure codebase, you’ve probably heard that question—maybe even asked it yourself. It usually comes up when someone proposes adding another abstraction layer, introducing a new pattern, or rethinking how infrastructure is organized.
On the surface, Terraform modules seem like the answer to everything: reuse, consistency, cleaner code. But here’s where things get interesting—they solve some problems very well, while quietly introducing others.
Let’s start with the promise
Terraform modules are marketed (accurately, to an extent) as reusable building blocks. Instead of rewriting the same AWS VPC, S3 bucket, or Kubernetes cluster configuration, you package it into a module and reuse it.
A simple example:
1module "vpc" {
2 source = "terraform-aws-modules/vpc/aws"
3 version = "5.0.0"
4
5 name = "my-vpc"
6 cidr = "10.0.0.0/16"
7}
8This is clean, expressive, and avoids duplication. So naturally, teams start asking:
“Why not turn everything into modules?”
Where modules actually shine
There are clear, high-value use cases where Terraform modules genuinely solve real problems:
- Standardization across environments
Ensuring dev, staging, and production follow the same structure. - Encapsulation of complexity
Abstracting away 50+ lines of networking config into a clean interface. - Reusable infrastructure patterns
Things like VPCs, IAM roles, or logging setups.
In these scenarios, modules reduce cognitive load and improve consistency. That’s the ideal case.
But here’s the catch
A common mistake developers make is assuming that reusability always equals simplicity. In Terraform, that’s not always true.
Let’s say your team builds a "universal" module for deploying services. It supports:
- Multiple cloud providers
- Optional autoscaling
- Different networking modes
- Feature flags for logging, monitoring, secrets
Sounds powerful, right? Until you use it:
1module "service" {
2 source = "./modules/service"
3
4 enable_autoscaling = true
5 autoscaling_mode = "cpu"
6 enable_logging = true
7 logging_level = "detailed"
8 network_mode = "private"
9 enable_secrets = false
10 # ...20 more variables
11}
12At this point, the module becomes harder to understand than the raw Terraform it replaced.
So what happened?
The module stopped being a solution and became a mini framework. And frameworks come with trade-offs:
- Hidden behavior
- Harder debugging
- Steeper onboarding
- Coupling across teams
The illusion of “solved problems”
When someone asks, “Don’t modules solve this?”, they’re often assuming:
- The problem is purely about duplication
- Abstraction won’t introduce new complexity
- All use cases can fit a shared interface
That assumption breaks down quickly in real-world systems.
For example, two services might look similar but have subtle differences in:
- Security requirements
- Scaling behavior
- Networking constraints
Forcing both into the same module can create awkward workarounds or bloated configurations.
A more grounded way to think about Terraform modules
Instead of asking whether modules “solve the problem,” it’s more useful to ask:
- What problem are we actually solving?
- Is duplication the real issue, or is it clarity?
- Will this abstraction make future changes easier or harder?
A practical rule of thumb
Use a module when:
- The pattern is stable and well understood
- The inputs and outputs are unlikely to change frequently
- The abstraction reduces more complexity than it introduces
Avoid or delay modules when:
- You’re still experimenting with the architecture
- Different consumers have diverging needs
- The module requires excessive configuration flags
Real-world pattern: start concrete, then abstract
One approach that works well in practice:
- Write plain Terraform for a specific use case
- Repeat it once or twice in different contexts
- Identify what’s truly common
- Extract a module based on real usage—not assumptions
This avoids premature abstraction and keeps modules grounded in reality.
Debugging: where modules can hurt
Here’s something that doesn’t get discussed enough—modules can make debugging harder.
Consider an issue where a resource isn’t behaving as expected. If it’s inside a module:
- You need to trace inputs across multiple layers
- You may not immediately see the resource definition
- Changes require updating shared code, not just local config
In contrast, flat Terraform files are often easier to inspect and modify quickly.
Performance and plan clarity
Terraform itself doesn’t “slow down” because of modules, but large module hierarchies can:
- Make plans harder to read
- Obscure resource relationships
- Increase mental overhead during reviews
When reviewing a plan, clarity matters just as much as correctness.
So… don’t they solve this problem?
Sometimes, yes. But not automatically—and not universally.
Terraform modules are a tool, not a blanket solution. They’re great for codifying known patterns, but they’re not a substitute for thoughtful design.
If anything, the real skill is knowing when not to use them.
A balanced takeaway
Use Terraform modules deliberately:
- Keep them small and focused
- Avoid turning them into feature-heavy abstractions
- Prefer clarity over cleverness
- Continuously refactor based on real usage
And the next time someone asks, “Don’t modules solve this?”, you’ll have a better answer:
“They can—but only if we’re solving the right problem.”