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.
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
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.
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.
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.
Mental model
Buying a standard equipment kit from a vendor catalog vs assembling from scrap
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.
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.
Examples
A small, well-contracted module
# modules/label/main.tfvariable "name" { type = string }variable "labels" { type = map(string) }output "resource_name" { value = "${var.name}-${join("", values(var.labels))}" } # callersmodule "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.
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.
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.
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.
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.
Interview checkpoint
Hands-on lab
- 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
Quick quiz
Module sources support:
Checking a module's API = looking at