Terraform Modules
Turn a working configuration into a network module and a compute module, called twice with different inputs.
- Time
- 50 min
- Level
- Intermediate
- Objectives
- 5 objectives
- Cost
- Low cost
Where this fits in the platform
Already built
This lab adds
- A reusable module with inputs and outputs
Before you start
You will need
- Terraform >= 1.6
- AWS CLI v2, configured
You do not need these already — the lab environment below provides them.
You will be able to
- Extract a module with a deliberate input/output contract
- Call one module from another and let the graph order the work
- Know when a module is not worth writing
Cost — Low cost
— a VPC, subnets and one `t3.micro`. No NAT Gateway in this lab, deliberately: it is the one resource here that would bill hourly.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
The scenario#
The configuration from the previous lab works, and now a second environment needs the same shape with different addresses. Copying the directory is the obvious move and the wrong one — two copies drift, and the drift is discovered during an incident.
A module is how the same definition serves both.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on AWS
This lab builds real cloud infrastructure, so it needs your own AWS account. Follow the cost and cleanup notes above — the resources are yours, and so is the bill.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
The network module
Step 1 of 4
The contract#
A module is a directory of .tf files with exactly three surfaces:
modules/network/
main.tf the resources it owns
variables.tf the inputs — its public API
outputs.tf the outputs — what callers may depend onAnything a caller needs must leave through an output. There is no reaching
inside for a resource, and that restriction is precisely what makes a module
safe to change later.
What you are proving: You can write a module that creates a network from inputs and exposes the ids others need
Marking this settles success criterion 1.
# modules/network/variables.tf
variable "name" { type = string }
variable "cidr_block" { type = string }
variable "azs" { type = list(string) }# modules/network/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
tags = { Name = var.name }
}
resource "aws_subnet" "public" {
for_each = { for i, az in var.azs : az => i }
vpc_id = aws_vpc.this.id
availability_zone = each.key
cidr_block = cidrsubnet(var.cidr_block, 8, each.value)
tags = { Name = "${var.name}-public-${each.key}" }
}cidrsubnet(var.cidr_block, 8, 0) carves 10.0.0.0/24 out of 10.0.0.0/16.
Computing subnets rather than listing them means the module works for any CIDR
it is given.
# modules/network/outputs.tf
output "vpc_id" { value = aws_vpc.this.id }
output "subnet_ids" { value = [for s in aws_subnet.public : s.id] }What you are proving: You can write a module that places an instance into a subnet it did not create
Marking this settles success criteria 2 and 5.
# modules/compute/variables.tf
variable "name" { type = string }
variable "subnet_id" { type = string }
variable "instance_type" {
type = string
default = "t3.micro"
}Note what is not here: no vpc_id, no reference to aws_vpc. The compute
module is handed a subnet id and does not care where it came from. That is the
whole point — it could be given a subnet from a different module, or one that
already existed.
What you are proving: You can wire modules together with no hardcoded ids anywhere
Marking this settles success criterion 3.
# main.tf
module "network" {
source = "./modules/network"
name = "demo"
cidr_block = "10.20.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
}
module "app" {
source = "./modules/compute"
name = "demo-app"
subnet_id = module.network.subnet_ids[0]
}Because module.app references module.network.subnet_ids, Terraform knows the
network must exist first. Nobody wrote an ordering; the reference is the
ordering.
terraform init # required again — a new module must be installed
terraform plan
terraform applyterraform init after adding a module trips everyone up once. A new or moved
module source is not picked up until you re-init.
What you are proving: You can call one module twice and get two independent environments
Marking this settles success criterion 4.
module "network_staging" {
source = "./modules/network"
name = "staging"
cidr_block = "10.30.0.0/16"
azs = ["us-east-1a"]
}One definition, two networks that cannot drift apart. That is the return on the directory structure.
When not to write a module#
A module costs a directory, two extra files and a layer of indirection. It earns that when the same shape is built more than once, or when it hides genuine complexity behind a small interface.
Wrapping a single aws_s3_bucket in a module buys nothing and forces the next
reader to open two files to understand one resource. The useful test:
would a second caller ever exist? If not, write the resource directly and
extract it the day the second caller appears.
Module not installed after adding a module block
Run terraform init again. A new module source is only fetched at init.
Error: Unsupported attribute on module.network.something
That value has no output. A module exposes nothing by default — add the output explicitly.
Both networks got the same CIDR
The second module call reused the default. Pass cidr_block explicitly to each.
cidrsubnet errors with 'prefix extension too large'
You asked for more subnet bits than the parent CIDR has room for. A /16 with 8 gives /24s; a /24 with 8 does not fit.
Clean up#
Run this even if you did not finish.
Destructive — This removes real resources. Check which environment you are in first.
terraform destroy -auto-approve
aws ec2 describe-instances --filters Name=instance-state-name,Values=running --query 'Reservations[].Instances[].InstanceId'Cost of this lab: Free tier — a VPC, subnets and one t3.micro. No NAT Gateway in this lab, deliberately: it is the one resource here that would bill hourly.
Success criteria
0 of 5
The concept behind it
Next up
Lab 21 of 59 on the project path