Suppose one Terraform module must build a network across several availability zones, with a choice of subnet layouts. A high-level module can expose the choice and call submodules to build each location’s network. This keeps the interface simple while moving topology-specific resources into smaller components. section 10.2.2
A submodule is a module distributed inside a larger parent module, rather than separately. The parent’s modules directory is the strong convention for storing it, though not a requirement. A submodule has the structure and documentation expected of any module; it can also be called and tested individually. Its purpose is to divide the parent’s functionality into manageable pieces. section 3.1 section 3.1.4
Build a network location first
For a two-segment location, a submodule accepts a CIDR block, VPC ID, and availability zone. It uses cidrsubnet to split the address range into public and private subnets, and creates a NAT gateway so the private subnet can reach the internet. A three-segment variant also creates an isolated subnet. Build and test these lower-level components before wrapping them in the high-level module, but do not mistake an individually callable submodule for a complete deployment: the high-level module is responsible for the internet gateway. section 10.2.3
Listing 10.6 Public and private subnet module
variable "cidr_block" {
description = "The CIDR block for the network."
type = string
}
variable "vpc_id" {
description = "The ID of the VPC."
type = string
}
variable "availability_zone" {
description = "The availability zone."
type = string
}
locals {
private_subnet_cidr_block = cidrsubnet(var.cidr_block, 1, 0) #1
public_subnet_cidr_block = cidrsubnet(var.cidr_block, 1, 1) #1
}
#
# Public Network
#
resource "aws_subnet" "public" {
vpc_id = var.vpc_id
cidr_block = local.public_subnet_cidr_block #2
map_public_ip_on_launch = true #3
availability_zone = var.availability_zone
tags = {
Network = "Public" #4
}
}
resource "aws_eip" "nat" {
tags = {}
}
resource "aws_nat_gateway" "main" { #5
allocation_id = aws_eip.nat.id #6
subnet_id = aws_subnet.public.id #7
tags = {}
}
#
# Private Network
#
resource "aws_subnet" "private" { #8
vpc_id = var.vpc_id
cidr_block = local.private_subnet_cidr_block #9
availability_zone = var.availability_zone #10
tags = {
Network = "Private"
}
}
resource "aws_route_table" "private" { #11
vpc_id = var.vpc_id
}
resource "aws_route_table_association" "private" { #12
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private.id
}
resource "aws_route" "internet_access" { #13
route_table_id = aws_route_table.private.id
nat_gateway_id = aws_nat_gateway.main.id
destination_cidr_block = "0.0.0.0/0" #14
}
output "private_subnet_id" { #15
value = aws_subnet.private.id
}
output "public_subnet_id" { #16
value = aws_subnet.public.id
}
The public-and-private subnet configuration shows the submodule’s inputs, cidrsubnet calculations, and AWS resources together. section 10.2.3
Keep the parent-child boundary deliberate. Publishing the location components as standalone modules offers more direct control, while keeping them behind a high-level module favors ease of use. Avoid adding submodules inside submodules merely to split work further: deeper nesting raises project complexity. Module depth also appears in Terraform resource addresses used in command output and state, so a clear hierarchy helps when navigating resources. section 10.2.2 section 3.1.4 section 10.1.2
