Stage 4 · Provision
Modules & Composition
Module Composition Patterns
Root modules, child modules, for_each composition, dependency inversion, and wrapper modules — structuring modules for reuse.
Composition Overview
Module composition is how you combine small, focused modules into larger infrastructure stacks. Good composition keeps modules independent while allowing them to work together. The patterns you choose determine how easy it is to reuse, test, and maintain modules.
Root vs Child Modules
| Aspect | Root Module | Child Module |
|---|---|---|
| Has backend | Yes | No |
| Has variables | Yes | Yes |
| Has outputs | Yes | Yes |
| Directly applied | Yes | No |
| Can use data sources | Yes | No |
| Can use providers | Yes | No |
module "networking" {
source = "./modules/networking"
cidr = var.vpc_cidr
env = var.environment
}
module "compute" {
source = "./modules/compute"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.private_subnet_ids
}
module "database" {
source = "./modules/database"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.database_subnet_ids
}The root module composes child modules by passing outputs as inputs. The networking module creates the VPC. The compute and database modules use the VPC outputs. This creates a clear dependency chain.
for_each Composition
variable "services" {
default = {
web = {
instance_type = "t3.micro"
min_count = 2
}
api = {
instance_type = "t3.small"
min_count = 3
}
}
}
module "service" {
source = "./modules/service"
for_each = var.services
name = each.key
instance_type = each.value.instance_type
min_count = each.value.min_count
vpc_id = module.networking.vpc_id
}for_each creates multiple module instances from a map. Each key creates a separate module instance with its own state. This eliminates duplication when deploying multiple similar services.
Dependency Inversion
# Module does not depend on specific implementations
variable "subnet_ids" {
type = list(string)
description = "Subnet IDs for the deployment"
}
variable "security_group_ids" {
type = list(string)
description = "Security group IDs to attach"
}
# Consumer provides the values
module "app" {
source = "./modules/app"
subnet_ids = module.networking.private_subnet_ids
security_group_ids = module.security.app_sg_ids
}The app module accepts generic inputs (subnet_ids, security_group_ids) rather than depending on specific networking or security modules. This makes the app module reusable across different infrastructure setups.
Wrapper Modules
# modules/kubernetes-cluster/main.tf
module "vpc" {
source = "../vpc"
cidr = var.vpc_cidr
}
module "eks" {
source = "../eks"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
}
module "node_groups" {
source = "../node-groups"
cluster_name = module.eks.cluster_name
subnet_ids = module.vpc.private_subnet_ids
}
# Outputs aggregate child module outputs
output "cluster_endpoint" {
value = module.eks.cluster_endpoint
}
output "cluster_name" {
value = module.eks.cluster_name
}A wrapper module combines multiple child modules into a cohesive unit. It hides the complexity of composing individual modules. Consumers use the wrapper instead of wiring modules together.
Composition Patterns
- Layer pattern — networking, compute, database in separate layers.
- Service pattern — each service has its own module composition.
- Platform pattern — shared infrastructure modules for multiple teams.
- Environment pattern — different module configurations per environment.
Child modules should do one thing well. A VPC module creates VPCs. An EKS module creates EKS clusters. Compose them in root modules. Do not create mega-modules that do everything.
Module composition is primarily about data flow — passing outputs from one module as inputs to another. Design modules with clear input/output interfaces. Data flows from networking to compute to database.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.