Terraform Nested Modules and Composition
Large buildings come from standard parts: walls, doors, windows, and pipes. Builders do not invent each part again for every house. Terraform modules follow the same idea. Small modules do one job well, and a larger configuration joins them together like building blocks. This style is called composition. It keeps big projects tidy and lets teams reuse proven parts.
What Nested Modules Mean
A nested module is a module called from inside another module. The top folder you run Terraform from is the root module. A child module is any module the root calls. A grandchild module sits below a child.
Diagram: Module Tree
Root Module (you run terraform here)
|
+-- module "network"
| +-- module "subnets" (grandchild)
|
+-- module "compute"
|
+-- module "database"Two Ways to Combine Modules
Nesting: A Module Calls a Module
The network module hides its subnet details by calling a subnet module internally. The root module never sees the inner workings.
Composition: The Root Calls Several Modules Side by Side
The root module calls network, compute, and database modules at the same level. The root passes outputs from one module into the inputs of another. Composition keeps each module independent and easy to test.
Composition in Action
A network module creates a virtual network and exposes its ID and subnet IDs as outputs. A compute module needs those values to place servers. The root module connects the two.
Diagram: Data Flowing Between Modules
+----------------+ vpc_id, subnet_ids +----------------+
| module network | ----------------------> | module compute |
+----------------+ +--------+-------+
| |
| vpc_id instance_ids
v v
+----------------+ +----------------+
| module database| | root output |
+----------------+ +----------------+Root Module Code
module "network" {
source = "./modules/network"
cidr_block = "10.0.0.0/16"
}
module "compute" {
source = "./modules/compute"
vpc_id = module.network.vpc_id
subnet_ids = module.network.public_subnet_ids
}
module "database" {
source = "./modules/database"
vpc_id = module.network.vpc_id
subnet_ids = module.network.private_subnet_ids
}Each reference follows the pattern module.NAME.OUTPUT. Terraform reads those references and builds the network first, then the compute and database modules.
Network Module Outputs
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}The Folder Layout
project/
main.tf
variables.tf
outputs.tf
modules/
network/
main.tf
variables.tf
outputs.tf
compute/
main.tf
variables.tf
outputs.tf
database/
main.tf
variables.tf
outputs.tfRules for Healthy Module Design
Keep Modules Small and Focused
A module should answer one question: how do I build a network, or how do I build a database. A module that creates everything becomes hard to change and hard to reuse.
Limit Nesting Depth
Two levels deep is comfortable. Four levels deep turns debugging into a treasure hunt. When a module tree grows deep, flatten it and compose at the root.
Expose Only What Others Need
Child modules do not share internal resources. A parent reads only declared outputs. Declare outputs for values other modules must use and keep the rest private.
Pass Values Down Through Variables
A child module cannot read variables from the root. The root must pass each value explicitly. Explicit inputs make every dependency visible.
Passing Providers to Modules
Child modules inherit the default provider from the root. A module that must use a different region needs an explicit provider map.
module "backup" {
source = "./modules/backup"
providers = {
aws = aws.west
}
}Nested Module Versus Composition
| Approach | Strength | Risk |
|---|---|---|
| Nesting | Hides complex details behind one call | Deep trees are hard to trace |
| Composition | Every connection is visible in the root | The root file grows longer |
Key Points
- The root module is the folder where you run Terraform.
- A nested module is a module called from inside another module.
- Composition places modules side by side and links them through outputs and inputs.
- References use the pattern
module.NAME.OUTPUT. - Children read only the values the parent passes in.
- Keep modules small and keep the tree shallow.
