Skip to main content

Capstone — Build the Colony

Time for the real thing. You'll build a reusable "villa" module, instantiate it across three cities (dev, staging, prod), wire in variables and outputs, and run the complete lifecycle — plan, apply, show, destroy. Plan for about an hour.

Goal

By the end, one module definition has produced three separate S3 storage sheds with correct names, tags, and outputs — and every resource is cleanly destroyed.

Step 1 · Scaffold the Project​

mkdir colony && cd colony
mkdir modules/villa

Project layout:

colony/
├── main.tf # calls the module three times
├── variables.tf # city names, project name
├── outputs.tf # bucket names + ARNs
└── modules/
└── villa/
├── main.tf # the reusable blueprint
├── variables.tf
└── outputs.tf

Step 2 · Write the Villa Module​

modules/villa/main.tf:

modules/villa/main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}

resource "aws_s3_bucket" "shed" {
bucket = var.bucket_name
tags = var.tags
}

modules/villa/variables.tf:

modules/villa/variables.tf
variable "bucket_name" {
type = string
description = "Globally unique bucket name for this villa"
}

variable "tags" {
type = map(string)
default = {}
description = "Tags applied to the bucket"
}

modules/villa/outputs.tf:

modules/villa/outputs.tf
output "shed_arn" {
value = aws_s3_bucket.shed.arn
}
Remember

A module is just a folder of .tf files that other configurations can call — the reusable blueprint. Everything a caller must provide is a variable; everything a caller needs back is an output.

Step 3 · Call the Module for Three Cities​

main.tf:

main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
}

provider "aws" {
region = var.region
}

resource "random_string" "suffix" {
length = 6
special = false
}

module "villa" {
source = "./modules/villa"

for_each = toset(var.cities)

bucket_name = "campus-library-${each.key}-${random_string.suffix.result}"
tags = {
Project = var.project
City = each.key
ManagedBy = "Terraform"
}
}

variables.tf:

variables.tf
variable "project" {
type = string
default = "Campus Library"
}

variable "region" {
type = string
default = "eu-west-1"
}

variable "cities" {
type = list(string)
default = ["dev", "staging", "prod"]
}

outputs.tf:

outputs.tf
output "shed_arns" {
description = "ARN of each city's shed"
value = { for city in var.cities : city => module.villa[city].shed_arn }
}
Remember

for_each on a module creates one instance per city. The { for ... in ... } output builds a map of city → ARN in one expression.

Step 4 · The Full Lifecycle​

terraform init
terraform fmt
terraform validate
terraform plan

Expected:

Plan: 3 to add, 0 to change, 0 to destroy.
terraform apply
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

(Three buckets + one random suffix.)

terraform output
shed_arns = {
"dev" = "arn:aws:s3:::campus-library-dev-a1b2c3"
"prod" = "arn:aws:s3:::campus-library-prod-a1b2c3"
"staging" = "arn:aws:s3:::campus-library-staging-a1b2c3"
}

Step 5 · Inspect and Admire​

terraform state list
terraform state show "module.villa[\"prod\"].aws_s3_bucket.shed"

You're reading the register of a three-city colony, all built from one module.

Step 6 · Extend It (Optional)​

Pick one:

  • Add a versioning argument to the bucket
  • Add a server_side_encryption_configuration block
  • Move state to a remote S3 backend + DynamoDB lock
  • Add a fourth city: terraform plan -var cities=...

Step 7 · Demolition​

terraform destroy

Type yes, then confirm in the AWS console that all three buckets are gone.

Check Yourself​

  • A module built and called from a parent config
  • Module instantiated 3× via for_each
  • Variables with types and defaults
  • A computed map output
  • init → fmt → validate → plan → apply → show → destroy

Done — you've built (and demolished) a colony from a reusable blueprint. If any term felt fuzzy, check the Glossary. To see how real teams organize this in code review, revisit the workflow ritual and the Git Deep Dive.