Design Terraform Modules like a Software Engineer
Terraform Modules are more than reusable code. A well-designed module makes infrastructure easier to maintain, scale, and reuse across environments. This article explores module design from a software engineering perspective.

Design Terraform Modules like a Software Engineer
In the previous three articles, we explored the three most fundamental concepts of Terraform:
- What Infrastructure as Code is.
- How Terraform works internally.
- Why Terraform State is the heart of the entire workflow.
At this point, you're probably able to write your first Terraform configurations.
But before long, you'll encounter a much more important question.
"How should I organize my Terraform code?"
This is where many engineers start taking the wrong path.
They see Terraform as nothing more than a tool for declaring cloud resources.
They put everything into a few .tf files, copy them into another project, make a few modifications, and repeat the process over and over again.
Everything seems fine in the beginning.
Until the infrastructure starts growing.
That's when you realize something important:
Infrastructure needs software engineering principles just as much as application code does.
Terraform Modules exist to solve exactly this problem.
We Don't Copy Terraform
Imagine your company has three different projects.
project-a/
main.tf
project-b/
main.tf
project-c/
main.tfEvery project needs almost the same infrastructure:
- VPC
- Subnets
- Security Groups
- EC2 Instances
- IAM Roles
The easiest solution?
Copy the entire Terraform directory.
Need another project?
Copy it.
Need another environment?
Copy it.
Need another customer?
Copy it again.
After a few months, you'll end up with dozens of almost identical directories.
Then one day your security team says:
"Every EC2 instance must use Instance Metadata Service Version 2."
It sounds like a small change.
But suddenly you need to:
- Open every project.
- Update every configuration.
- Review every pull request.
- Deploy every environment.
Miss just one project, and your infrastructure is no longer consistent.
This is exactly the kind of problem Software Engineering has been solving for decades.
Infrastructure is no different.
DRY Applies to Infrastructure Too
One of the most famous software engineering principles is:
DRY — Don't Repeat Yourself.
If the same logic appears in multiple places, you'll eventually pay the maintenance cost.
Terraform is no exception.
Consider the following configuration.
resource "aws_instance" "app1" {
...
}
resource "aws_instance" "app2" {
...
}
resource "aws_instance" "app3" {
...
}There's nothing technically wrong with it.
Terraform will happily create all three instances.
However, if these resources are nearly identical, duplicating them everywhere makes your infrastructure increasingly difficult to maintain.
This is exactly why we create:
- Functions in Java.
- Components in React.
- Services in Spring Boot.
Terraform Modules follow the same idea.
They allow us to reuse infrastructure without duplicating implementation.
But if you think Modules exist only to reduce duplicated code, you're still missing their true purpose.
What Is a Terraform Module?
Many Terraform beginners hear a definition like this:
A Terraform Module is simply a directory containing
.tffiles.
Technically, that's correct.
But it doesn't explain what a module really represents.
A much better definition is:
A Terraform Module is a reusable infrastructure component with a well-defined interface.
For example:
module "network"Internally, this module may create:
- VPC
- Public Subnets
- Private Subnets
- Route Tables
- Internet Gateway
- NAT Gateway
The consumer doesn't need to know any of that.
They only need to understand:
- What inputs the module requires.
- What outputs it provides.
The internal implementation can evolve over time without affecting its users.
This is exactly the same concept as Encapsulation in Object-Oriented Programming.
A Terraform Module Is Closer to a Class than a Folder
This was the mindset that completely changed how I design infrastructure.
Let's compare.
A Java class.
public class DatabaseService {
}It has:
- Constructors
- Public methods
- Private methods
- Internal state
Now look at a Terraform Module.
module "database" {
source = "./modules/database"
...
}A module also has:
- Inputs (
variables.tf) - Outputs (
outputs.tf) - Internal resources (
main.tf) - Internal logic (
locals.tf)
Consumers only interact with the public interface.
They don't care how many resources exist inside the module.
Once you start thinking of Terraform Modules as classes instead of folders, the way you design infrastructure changes dramatically.
What Makes a Good Module?
Most Terraform Modules share a similar structure.
modules/
└── network
├── main.tf
├── variables.tf
├── outputs.tf
├── locals.tf
├── versions.tf
├── providers.tf
└── README.mdThe important part isn't the number of files.
It's that each file has a single, well-defined responsibility.
For example:
variables.tfdefines inputs.outputs.tfexposes outputs.locals.tfcontains intermediate values.main.tfdeclares resources.versions.tfdefines Terraform and provider versions.
This is simply Separation of Concerns applied to Infrastructure as Code.
Your Interface Matters More Than Your Implementation
Many engineers spend most of their time optimizing the internals of a module.
Ironically, the most important part is usually the interface.
Imagine a module that requires more than thirty input variables.
module "ec2" {
...
}Before anyone can use it, they have to read pages of documentation.
Now compare that with a simpler module.
module "web_server" {
name = "frontend"
instance_type = "t3.micro"
tags = local.common_tags
}The fewer required inputs a module has, the easier it becomes to understand and reuse.
A simple interface is almost always better than an extremely flexible but overly complicated one.
Don't Let a Module Know Too Much
One mistake I've seen repeatedly in real-world projects is the "mega module."
Imagine a single module responsible for:
Infrastructure Module
├── VPC
├── Subnets
├── EC2
├── IAM
├── ALB
├── Route53
├── CloudWatch
├── Auto Scaling
└── RDSAt first glance, this seems convenient.
One module creates an entire production environment.
In reality, it's an anti-pattern.
The module has accumulated far too many responsibilities.
A small change in one area can unexpectedly affect everything else.
If you've ever heard of the God Object anti-pattern in Object-Oriented Programming, this is its Infrastructure equivalent.
Compose Instead of Building Mega Modules
Rather than creating one enormous module, split your infrastructure into smaller building blocks.
For example:
network
database
compute
monitoringThen compose them together.
production
├── network
├── database
├── compute
└── monitoringThis is called Composition.
Each module has a single responsibility.
A higher-level configuration assembles these independent pieces into a complete infrastructure.
Think of LEGO bricks.
You build large systems by combining many small pieces—not by pouring everything into one giant concrete block.
Don't Forget Versioning
Once multiple projects start consuming the same module, it effectively becomes a shared library.
At that point, versioning becomes essential.
For example:
module "network" {
source = "git::https://github.com/company/iac-modules.git//network?ref=v1.2.0"
}Instead of:
ref=mainUsing version tags ensures every environment knows exactly which module version it is running.
It also makes upgrades, rollbacks, and testing much safer.
Common Mistakes I've Seen
After working on multiple Terraform projects, I've noticed the same mistakes appearing over and over again.
- Designing modules that are too generic.
- Exposing far too many input variables.
- Creating deeply nested modules.
- Copying modules instead of reusing them.
- Hardcoding providers or regions inside modules.
- Forgetting to expose useful outputs.
- Shipping modules without proper documentation.
None of these issues prevent Terraform from working.
But all of them make your infrastructure harder to understand, evolve, and maintain.
Just like software, infrastructure isn't judged only by whether it works today.
It's judged by how easy it is to maintain tomorrow.
Conclusion
Terraform Modules are not about reducing the number of lines of code.
They exist to package infrastructure knowledge into reusable building blocks.
A good module isn't the one with the most features.
It's the one with:
- A clear responsibility.
- A simple interface.
- Easy composition with other modules.
- Long-term maintainability.
Once you start viewing Terraform Modules the same way software engineers design classes, you'll realize that Infrastructure as Code follows many of the same design principles as application development.
In the next article, we'll explore another fundamental concept that is often misunderstood:
How Terraform Providers really work—and why they're much more than simple Terraform plugins.
Continue the Series
Comments
Loading comments…