Lesson 04 of 7 · Modules
Modules
Package infrastructure into reusable modules: layout and conventions, inputs and outputs, composing modules into stacks, versioning with Git tags or a registry, and the design choices that separate helpful modules from painful ones.
What a module is
Every Terraform directory is a module. The one you run terraform apply in is the root module; the ones it calls are child modules. A module packages resources behind an interface (variables in, outputs out).
A module is a LEGO kit with its own instructions: "fire station", "bridge", "tower". You don't rebuild a fire station brick by brick every time. You pick the kit, choose a few options (colour, size), and snap it into your city next to other kits.
A small, opinionated module
modules/bucket/
├── main.tf # the resources
├── variables.tf # inputs
├── outputs.tf # outputs
├── versions.tf # required_providers and versions
└── README.md # what it does, inputs/outputs, examples
# modules/bucket/main.tf
resource "aws_s3_bucket" "this" {
bucket = var.name
tags = var.tags
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration { status = "Enabled" } # opinion: always versioned
}
resource "aws_s3_bucket_public_access_block" "this" {
bucket = aws_s3_bucket.this.id
block_public_acls = true # opinion: never public
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# live/prod/main.tf
module "logs_bucket" {
source = "git::https://github.com/acme/tf-modules.git//bucket?ref=v1.4.0"
name = "acme-prod-logs"
tags = local.common_tags
}
output "logs_bucket_arn" {
value = module.logs_bucket.arn
}
The module encodes decisions (versioning on, public access blocked) so every caller gets them for free.
Composition
Stacks are built by composing modules: a network module's outputs feed a cluster module, whose outputs feed platform modules. Keep modules small and focused; compose them in root modules per environment.
module "vpc" { source = "…/vpc?ref=v2.1.0" cidr = "10.20.0.0/16" }
module "eks" {
source = "…/eks?ref=v3.0.2"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
}
Versioning and sources
| Source | Versioning |
|---|---|
Local path (./modules/x) |
Same commit as the caller; fine inside one repository |
Git (?ref=v1.4.0) |
Tags; semantic versioning by convention |
| Public or private registry | version = "~> 1.4" constraints |
Release modules with a changelog; bump environments in separate PRs (dev first).
Module design rules of thumb
- Encode opinions: secure defaults, naming, tagging. Don't mirror every provider argument.
- Don't configure providers inside reusable modules; declare
required_providersand let the root pass them in. - Prefer
for_each-friendly interfaces (maps of objects) for collections. - Give every variable and output a description; generate docs (e.g.
terraform-docs). - Well-known community modules (e.g.
terraform-aws-modules/*) are excellent starting points. Read their changelogs before major upgrades.
Try it: build, version and compose (free)
- Turn your file-creating configuration from lesson 03 into a module
modules/fileswith typed inputs and an output map. - Call it twice from a root module (
module "dev_files",module "prod_files") with different inputs. - Put the module in a separate Git repository, tag
v1.0.0, and reference it with?ref=v1.0.0. - Change the module (a new default mode), tag
v1.1.0, and upgrade onlydev_files. Compare the plans for both calls. - Add a README with an example, and generate the inputs/outputs table with
terraform-docs markdown table .(if installed).
Going deeper: module ecosystems
- Treat a shared module library like a product: owners, versioning, changelogs, deprecation notices and CI tests (lesson 07).
- Use
movedblocks inside modules when refactoring internal addresses, so callers upgrade without replacements. - Avoid deep nesting (modules calling modules calling modules); two levels is usually plenty.
- Consider stack tools (Terragrunt, Terraform Stacks, or plain CI orchestration) once you have many environments × layers.
Recap
- A module = resources behind variables and outputs; the root module composes child modules.
- Good modules encode decisions (secure defaults, naming, tags), not every argument.
- Pin versions (Git tags or registry constraints) and upgrade environments deliberately.
- Keep providers in the root; describe every input and output.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.