Terraform
Advanced 15 min read Terraform

Terraform Modules

Ship conventions, not copy-paste

Modules package reusable infrastructure: typed input variables, outputs as contracts, and local resources composed into a standalone unit. Registry, private, and in-repo modules are the difference between one team's working answer and an org's repeatable platform.

moduleinputoutputregistryreusecompositiondependencies
Before we start

What you will learn

  • Author a module with inputs/outputs and clear contracts
  • Compose child modules with dependencies
  • Distribute via registry/private and version modules
In 60 seconds

A module is a directory of .tf files exposing variables and outputs. One module call instantiates its resources with a given input set. Sources can be a path, git, or registry, versioned and reusable across stacks and teams.

Introduction

What is this?

Module anatomy: variables.tf (inputs, typed, validation, sensitive), main.tf (resources), outputs.tf (interface/contracts), plus README. Calling: module "name" { source = "..." } with required inputs. Sources: local path, git refs or https, registry module namespace/provider/name/version. Composition: root module per stack; child modules per concern (vpc, app, db). Dependencies: implicit via references; explicit depends_on. Best: module versioning (semver), tests via kitchens/terratest, registry promotion, and a shallow-copy race discipline. Count/for_each of modules for per-env or per-subnet instancing.

Motivation

Why do you need it?

  • Repeated infra solved once instead of copy-pasted into every repo.
  • Encapsulation: users need inputs+outputs, not the 200 internals.
  • Versioning + registry make improvements land everywhere an org adopts them.
  • Teams define a security/compliance baseline once in a module.
Think about it

Mental model

Imagine

Buying a standard equipment kit from a vendor catalog vs assembling from scrap

Think of

Modules from a registry

The catalog kit (module) is versioned, tested, and documented; you configure options (variables) and receive guaranteed connections (outputs). Assembling scrap is your old two-week path.

Mechanics

How it works

  • The module's root calls your chosen sources (local, git, registry) at init.
  • Inputs flow in, the module constructs its own graph, outputs flow out.
  • Root composes modules and wires outputs to further inputs/targeted resources.
  • Version pins lock behavior; upgrades are explicit, reviewable bumps.
  • Plan output expands to every instantiated module resource.

Composing modules.

Root stack
module vpc
module app
module db
Cloud
Hands-on

Examples

A small, well-contracted module

A small, well-contracted module
# modules/label/main.tf
variable "name" { type = string }
variable "labels" { type = map(string) }
output "resource_name" { value = "${var.name}-${join("", values(var.labels))}" }
# callers
module "checkout" {
source = "git::https://github.com/acme/tf-modules//label?ref=v1.2.0"
name = "checkout"
labels = { env = "prod", team = "payments" }
}

Types, a predictable output, and a pinned registry-style source — everything a good module consumer needs.

Real world

In production

  • Org-standard vpc/iam/db modules in a private registry with semver.
  • Per-team stacks composed from the same catalogue → consistent security posture.
  • Module upgrades rolled out as PRs with plan diffs in every consumer.
Guidelines

Best practices

  • Declare typed inputs, sensible defaults, validation, and sensitive for credentials.
  • Give every module a clear README and version contract; output docs are interfaces.
  • Version-pin sources; never @latest in production stacks.
  • Keep modules single-purpose and composable; resist 'one module to rule them all'.
  • Test module upgrades in a scratch stack before promoting a version.
Watch out

Common mistakes

  • Modules with 60 untyped inputs that nobody can verify pre-plan.
  • Copy-paste module forks to 'fix locally' → org drift emerges immediately.
  • Unpinned @latest source → a dark registry upgrade during Friday's apply.
  • Module thinks it owns shared resources → duplicate-creation chaos across stacks.
When things break

Debugging tips

  • Plan breaks on module → read the input contract; add -var-file/tfvars to match types.
  • Registry source unreachable → pin + dry cache; check network/auth.
  • State references a resource the module no longer emits → output change; use state rm/mv deliberately.
  • Version bump surprises → diff the module's changelog before applying the pin move.
Interviews

Interview checkpoint

Practice

Hands-on lab

Publish and Version a Module Locally
35 min · advanced
In progress
Objectives
  • Author a reusable module with inputs/outputs
  • Consume it from two stacks at two versions
  • Evolve the module (new input) and show compatibility

Publish and Version a Module Locally

35 min · advanced

0/4 steps
0%
sandbox · Linux sandbox
$
Try these commands
Test yourself

Quick quiz

0/2
1

Module sources support:

2

Checking a module's API = looking at

Go deeper

Official documentation