DEV Community

Cover image for To The Moon Terraform Ep.6

To The Moon Terraform Ep.6

"The Saturn V was not one thing. It was three stages, a command module, a service module, and a lunar module — each designed independently, each tested independently, each capable of being replaced independently. The genius was not in any single component. The genius was in the interface between them."


🌕 Episode 6 — The Modular Rocket

Let us consider, briefly, the engineering absurdity of building everything from scratch every time.

Imagine if NASA, having built the first Saturn V rocket, responded to the second Moon mission by saying: "Right. Let's design a new rocket from first principles." The fuel tank: reinvented. The guidance system: rewritten. The escape tower: reconsidered.

No organisation on Earth would do this. And yet, without Terraform Modules, this is precisely what infrastructure engineers do.

They write the same VPC configuration in every project. They copy-paste security group rules. They duplicate S3 bucket configurations with subtle differences that introduce subtle bugs. They re-invent, again and again, components that were already correct the first time.

Modules are how you build a rocket once and fly it many times.


🧱 What Is a Module?

A Terraform Module is simply a directory of .tf files. Any directory of Terraform configuration is a module — including the directory you have been working in, which is called the root module.

What makes modules powerful is calling one module from another — passing inputs in, receiving outputs back, hiding the internal complexity.

mission-apollo/                      ← Root module (your project)
├── main.tf                          ← Calls child modules
├── variables.tf
├── outputs.tf
│
└── modules/                         ← Child modules (reusable components)
    ├── network/                     ← VPC, subnets, gateways
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    ├── compute/                     ← EC2 instances, launch templates
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    └── security/                    ← Security groups, IAM roles
        ├── main.tf
        ├── variables.tf
        └── outputs.tf
Enter fullscreen mode Exit fullscreen mode

Each module is a stage of the rocket: designed independently, tested independently, assembled into the complete vehicle.


🔧 Writing a Module: The Network Stage

# modules/network/variables.tf

variable "mission_name" {
  description = "Mission name prefix for all resources"
  type        = string
}

variable "vpc_cidr" {
  description = "CIDR block for the VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "public_subnet_count" {
  description = "Number of public subnets"
  type        = number
  default     = 2
}

variable "environment" {
  description = "Deployment environment"
  type        = string
}
Enter fullscreen mode Exit fullscreen mode
# modules/network/main.tf

locals {
  name_prefix = "${var.mission_name}-${var.environment}"
}

resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = { Name = "${local.name_prefix}-vpc" }
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id
  tags   = { Name = "${local.name_prefix}-igw" }
}

resource "aws_subnet" "public" {
  count             = var.public_subnet_count
  vpc_id            = aws_vpc.this.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true
  tags = { Name = "${local.name_prefix}-public-${count.index + 1}" }
}

data "aws_availability_zones" "available" {
  state = "available"
}
Enter fullscreen mode Exit fullscreen mode
# modules/network/outputs.tf

output "vpc_id" {
  description = "The ID of the VPC"
  value       = aws_vpc.this.id
}

output "public_subnet_ids" {
  description = "List of public subnet IDs"
  value       = aws_subnet.public[*].id
}

output "internet_gateway_id" {
  description = "The ID of the Internet Gateway"
  value       = aws_internet_gateway.this.id
}
Enter fullscreen mode Exit fullscreen mode

🚀 Calling a Module: The Assembly

# main.tf — Root module: assembly of the rocket stages

# Stage 1: The Network (VPC and subnets)
module "network" {
  source = "./modules/network"   # Local path to the module

  mission_name        = var.mission_name
  environment         = var.environment
  vpc_cidr            = "10.0.0.0/16"
  public_subnet_count = 3
}

# Stage 2: The Compute (using network outputs as inputs)
module "compute" {
  source = "./modules/compute"

  mission_name   = var.mission_name
  environment    = var.environment
  vpc_id         = module.network.vpc_id              # Output from network → input to compute
  subnet_ids     = module.network.public_subnet_ids   # Output from network → input to compute
  instance_type  = var.instance_type
}

# Stage 3: The Security (IAM roles and policies)
module "security" {
  source = "./modules/security"

  mission_name = var.mission_name
  environment  = var.environment
}
Enter fullscreen mode Exit fullscreen mode

The elegance: module.network.vpc_id — the output of one stage feeds directly into the input of the next. The interfaces between stages are explicit, typed, and documented.


🌐 Public Modules: The Supplier Network

You do not need to write every module yourself. The Terraform Registry (registry.terraform.io) hosts thousands of community and verified modules — pre-built rocket stages from trusted suppliers.

# Using official AWS VPC module from the Terraform Registry
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"   # Pin the version — always

  name = "${var.mission_name}-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = true
  enable_vpn_gateway = false

  tags = {
    Mission     = var.mission_name
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}
Enter fullscreen mode Exit fullscreen mode

This single module call replaces hundreds of lines of VPC configuration — and it has been tested by thousands of teams worldwide.


📊 The SIPOC of Episode 6

🔵 Supplier 🟡 Input 🟢 Process 🟠 Output 🔴 Consumer
Root module module {} block with source + input variables Terraform resolves module source path or downloads from Registry Module resource definitions merged into plan Terraform planning and apply engine
Module's variables.tf Values passed in module {} block Input validation and type checking Populated var.* namespace within module Module's internal main.tf resources
Module's main.tf Provider configuration (inherited from root) Resources planned and applied Real cloud resources Module's outputs.tf value resolution
Module's outputs.tf Attributes of resources created within module Value extraction and exposure Named outputs (module.<name>.<output>) Root module and other modules referencing them

🌕 Next episode: **Mission Control Systems* — Remote State and Backends. Because a flight recorder that exists on only one engineer's laptop is not a flight recorder.*

Top comments (0)