Modules Introduction
Terraform starts feeling very different once a configuration grows beyond a handful of resources. The problem is no longer just creating infrastructure; it is deciding how that infrastructure should be organized so that the same ideas can be reused without turning every project into a maze of copied configuration.
That is where modules come in. A module lets you put an infrastructure implementation behind a well-defined interface. Instead of making the caller understand every resource inside it, you can expose the inputs it needs and the outputs it provides.
What Is A Terraform Module?
A Terraform module is a collection of Terraform configuration files in a directory.
That definition is deliberately simple because there is an important point to understand:
You are already using a module whenever you run Terraform from a configuration directory.
The directory from which you run Terraform is the root module. A module that is called by another module is a child module.
For example:
infrastructure/
βββ main.tf
βββ variables.tf
βββ outputs.tfThis directory is a module. If main.tf calls another module, that other directory becomes a child module.
A useful mental model is:
flowchart TD
R[Root Module]
R --> N[Network Module]
R --> C[Compute Module]
R --> D[Database Module]
N --> I[Infrastructure]
C --> I
D --> I
The root module is the composition layer. Child modules encapsulate pieces of the implementation.
A module is not a separate Terraform installation and it is not a separate Terraform execution. Terraform evaluates the root module and the child modules as one configuration during the same plan and apply.
Why Do Modules Exist?
Imagine that a configuration starts with one web server. You might have a few resources and everything is easy to understand.
Later, you add:
- networking
- several application servers
- storage
- monitoring
- databases
- security-related resources
The configuration can become difficult to reason about.
One response is to split files:
main.tf
network.tf
compute.tf
database.tf
storage.tfThat can improve organization, but it does not automatically create a reusable component. All those files still belong to the same module.
A module gives you another boundary:
Root Module
β
βββ Network Module
βββ Compute Module
βββ Storage ModuleNow the root module can say, conceptually:
Build me a network with these characteristics.
It does not necessarily need to know how the network module implements every resource.
This is the same reason functions and libraries are useful in programming: you can work with a defined interface without constantly dealing with the implementation underneath.
The Root Module And Child Modules
Consider this structure:
terraform-project/
βββ main.tf
βββ modules/
βββ network/
β βββ main.tf
βββ compute/
βββ main.tfIf the root main.tf contains:
module "network" {
source = "./modules/network"
}
module "compute" {
source = "./modules/compute"
}then Terraform sees two child modules.
The source argument tells Terraform where the module’s configuration comes from. Here, both modules are local directories.
The caller is the root module, while network and compute are child module instances.
The names network and compute are labels chosen by the caller. They let you refer to the module instance elsewhere:
module.network
module.computeThe module label does not have to match the directory name, although matching names usually make a configuration easier to read.
Modules Create Boundaries
A useful way to think about a module is as a boundary around implementation.
Inside a module, you might have several resources:
Network Module
βββ network resource
βββ subnet resource
βββ routing resource
βββ security resourceThe caller should not need to know all of those implementation details.
Instead, the module can expose an interface:
Module Interface
ββββββββββββββββββββββββββ
Inputs ββΊ βββΊ Outputs
β implementation β
β inside β
ββββββββββββββββββββββββββThe inputs describe what the module needs.
The outputs describe what the module provides.
This distinction becomes increasingly important as infrastructure grows. A module is useful when the boundary represents a meaningful piece of infrastructure rather than simply another directory.
A Module Is More Than A Folder
It is tempting to define a module as:
“A folder containing
main.tf,variables.tf, andoutputs.tf.”
That is a common layout, but those filenames are conventions, not requirements.
Terraform loads the configuration files in a module directory. You can organize those files however you like.
For example:
modules/
βββ network/
βββ main.tf
βββ variables.tf
βββ outputs.tf
βββ versions.tfThe important thing is the module boundary, not the number of files.
Similarly, creating a module does not automatically make infrastructure reusable. A badly designed module can simply hide complexity and make a configuration harder to understand.
Modules As Architecture
The real value of modules appears when you start thinking about architecture.
Instead of seeing Terraform as one large collection of resources:
Resource
Resource
Resource
Resource
Resource
Resourceyou can organize infrastructure into meaningful components:
Root Module
β
ββββββββββββΌβββββββββββ
βΌ βΌ βΌ
Network Compute Database
Module Module Module
β β β
ββββββββββββ΄βββββββββββ
β
βΌ
InfrastructureThe root module composes the system.
Each child module owns a coherent piece of infrastructure.
That ownership matters. If a module represents networking, its implementation can change without forcing the root module to understand every internal change.
When Should You Create A Module?
Do not create a module simply because a directory can be created.
A module is a good candidate when a group of resources:
- represents a meaningful infrastructure component
- has a clear responsibility
- can be described through a small interface
- is likely to be reused
- benefits from hiding implementation details
- has a lifecycle or ownership boundary that makes sense
For example, a reusable network module can be sensible because “network” is a meaningful infrastructure concept.
On the other hand, wrapping a single resource in a module merely because it is possible often adds indirection without providing much value.
The important question is not:
“Can I turn this into a module?”
It is:
“Does this have a useful boundary?”
Tip
A good module should make the caller’s job easier. If calling a module requires understanding almost as much implementation detail as writing the resources directly, the abstraction may not be helping.
The Caller And The Implementation
The caller and the child module have different responsibilities.
The caller generally decides what infrastructure it wants.
The module decides how that infrastructure is implemented.
For example:
Root Module
β
β desired configuration
βΌ
Network Module
β
β implementation details
βββ network
βββ subnets
βββ routesThis does not mean every implementation detail must be hidden. Sometimes callers genuinely need control over a particular behavior.
The goal is to expose the decisions that belong at the module’s boundary while keeping internal details internal.
That is why module design is ultimately an architecture problem, not merely a syntax problem.
What Happens During Terraform Operations?
A child module does not run Terraform independently.
When you execute Terraform from the root module, Terraform loads the root configuration and its module tree.
Conceptually:
terraform plan
β
βΌ
Root Module
β
βββ Child Module A
βββ Child Module B
βββ Child Module C
β
βΌ
Terraform evaluates the complete configurationThe module boundaries help organize the configuration, but Terraform still builds one dependency graph for the configuration being planned.
This connects directly to the dependency concepts you learned earlier: references across module boundaries can participate in the same graph.
For example, if a compute module consumes an output from a network module, Terraform can understand that the compute configuration depends on the network value.
The Main Idea To Keep
The most useful mental model for this entire section is:
A module is an interface around infrastructure implementation.
When you use a module, ask two questions:
What does this module need?
What does this module provide?The answers form the boundary between the caller and the implementation.
Once you start seeing modules this way, the next step is natural: design a small module whose interface is simple enough to understand and useful enough to reuse.