Terraform Nested Modules
A nested module (also called a sub-module or child module) is a module that calls another module. This composition pattern lets you build higher-level abstractions from smaller, focused components — a production-environment module that calls networking, database, and application modules, which each call even more focused modules.
What Nested Modules Enable
Root Module (environments/production/main.tf)└── module "production" (modules/environment/main.tf) ├── module "networking" (modules/networking/main.tf) │ ├── aws_vpc │ ├── aws_subnet (multiple) │ └── aws_nat_gateway ├── module "database" (modules/database/main.tf) │ ├── aws_db_instance │ └── aws_security_group └── module "application" (modules/application/main.tf) ├── aws_ecs_service └── aws_lbEach layer knows only about the layer directly below it. The root module doesn’t know about VPCs — it just calls the environment module and provides high-level config.
Basic Nested Module Example
# modules/networking/main.tf — low-level networking moduleresource "aws_vpc" "this" { cidr_block = var.cidr_block tags = { Name = "${var.name}-vpc" }}
resource "aws_subnet" "private" { count = length(var.availability_zones) vpc_id = aws_vpc.this.id cidr_block = cidrsubnet(var.cidr_block, 4, count.index) availability_zone = var.availability_zones[count.index]}output "vpc_id" { value = aws_vpc.this.id }output "private_subnet_ids" { value = aws_subnet.private[*].id }# modules/application-stack/main.tf — calls networking as a sub-modulemodule "networking" { source = "../networking" # Relative path from this module name = var.name cidr_block = var.vpc_cidr availability_zones = var.availability_zones}
module "database" { source = "../database" vpc_id = module.networking.vpc_id # Pass networking output to database subnet_ids = module.networking.private_subnet_ids name = var.name}
module "application" { source = "../ecs-service" vpc_id = module.networking.vpc_id subnet_ids = module.networking.private_subnet_ids db_host = module.database.endpoint # Pass database output to application name = var.name image = var.image}# modules/application-stack/outputs.tf — bubble up what callers needoutput "vpc_id" { value = module.networking.vpc_id }output "app_endpoint" { value = module.application.load_balancer_dns }Root Module Using the Stack
module "production" { source = "../../modules/application-stack"
name = "production" vpc_cidr = "10.0.0.0/16" availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"] image = "mycompany/app:v3.2.1"}
output "production_url" { value = module.production.app_endpoint}This root module is clean — it describes intent, not implementation.
Referencing Nested Module Outputs
When a sub-module’s output needs to reach the root, each layer must explicitly pass it up:
output "vpc_id" { value = aws_vpc.this.id }
# Layer 2 (stack): modules/application-stack/outputs.tfoutput "vpc_id" { value = module.networking.vpc_id } # Re-exported
# Layer 1 (root): environments/production/outputs.tfoutput "vpc_id" { value = module.production.vpc_id } # Re-exported againThis explicit propagation is verbose but intentional — it makes module interfaces clear and prevents callers from depending on internal implementation details.
Using count and for_each with Nested Modules
# modules/environment/main.tf — create a full environment per regionvariable "regions" { type = list(string) default = ["us-east-1", "eu-west-1"]}
module "networking" { for_each = toset(var.regions)
source = "../networking" name = "prod-${each.key}"
providers = { aws = aws.by_region[each.key] # Pass region-specific provider }}Depth Guidelines
Nested modules are powerful but composition depth adds cognitive overhead:
Recommended: 2-3 levels maxRoot → Stack Module → Component Module ✅
Getting complex: 4 levelsRoot → Environment → Stack → Component → Sub-component ⚠️
Too deep: 5+ levelsRoot → ... → ... → ... → ... → Primitive ❌ (hard to follow data flow)Signs you’re nesting too deep:
- Output values flow through 4+ files before being used
- You forget which module defines a resource
terraform state listis hard to navigate
Flatten when composition becomes a burden — separate configurations with remote state data sources may be cleaner than deep nesting.
Module Dependency Graph
Terraform tracks dependencies between nested modules automatically:
# Generate visual dependency graphterraform graph | dot -Tsvg > graph.svg
# Or view in terminal (requires graphviz)terraform graph | dot -Tpng > graph.pngThe graph shows how modules depend on each other and in what order Terraform will apply them.