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.
Questions & Comments
Loading commentsโฆ