Terraform & Infrastructure as Code›04 · Modules
Learning Hub / Delivery & Infrastructure as Code / Terraform & Infrastructure as Code

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.

Practitioner
Key wordsmodulesroot modulechild modulecompositionversioningregistrygit refmodule design

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_providers and 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)

  1. Turn your file-creating configuration from lesson 03 into a module modules/files with typed inputs and an output map.
  2. Call it twice from a root module (module "dev_files", module "prod_files") with different inputs.
  3. Put the module in a separate Git repository, tag v1.0.0, and reference it with ?ref=v1.0.0.
  4. Change the module (a new default mode), tag v1.1.0, and upgrade only dev_files. Compare the plans for both calls.
  5. 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 moved blocks 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.