Cloud  /  Terraform

IaC Terraform 50 guides · updated 2026

Infrastructure as code done right — providers, state, reusable modules, and the workflow patterns that keep multi-cloud deployments sane in 2026.

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_lb

Each 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 module
resource "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]
}
modules/networking/outputs.tf
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-module
module "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 need
output "vpc_id" { value = module.networking.vpc_id }
output "app_endpoint" { value = module.application.load_balancer_dns }

Root Module Using the Stack

environments/production/main.tf
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:

modules/networking/outputs.tf
output "vpc_id" { value = aws_vpc.this.id }
# Layer 2 (stack): modules/application-stack/outputs.tf
output "vpc_id" { value = module.networking.vpc_id } # Re-exported
# Layer 1 (root): environments/production/outputs.tf
output "vpc_id" { value = module.production.vpc_id } # Re-exported again

This 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 region
variable "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 max
Root → Stack Module → Component Module ✅
Getting complex: 4 levels
Root → Environment → Stack → Component → Sub-component ⚠️
Too deep: 5+ levels
Root → ... → ... → ... → ... → Primitive ❌ (hard to follow data flow)

Signs you’re nesting too deep:

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:

Terminal window
# Generate visual dependency graph
terraform graph | dot -Tsvg > graph.svg
# Or view in terminal (requires graphviz)
terraform graph | dot -Tpng > graph.png

The graph shows how modules depend on each other and in what order Terraform will apply them.