Modules

Terraform / Infrastructure as Code

Chapter 7 · Modules

Everything so far has lived in a single, flat set of files. Real infrastructure repeats the same shapes — a web server, a VPC, a database tier — over and over, across environments and projects. Modules package a shape once and reuse it.

What a Module Is

Any directory of .tf files is a module. Whichever one terraform commands are run from is the root module — every configuration in this course so far has technically already been one, just never explicitly calling any others.

Child Modules

A module block calls another module from the root (or from another module), passing inputs as arguments matching that module's own declared variables, and reading its declared outputs back.

module "web_server" { source = "./modules/web-server" instance_type = "t3.micro" environment = var.environment } output "web_ip" { value = module.web_server.public_ip }

Module Structure & Composition

A module's own directory follows the same variables.tf/main.tf/outputs.tf convention as any configuration — inputs come in through its variable blocks, results go out through its output blocks. Everything else inside stays private to the module; the caller never sees its internal resource names.

# modules/web-server/variables.tf variable "instance_type" { type = string } variable "environment" { type = string } # modules/web-server/outputs.tf output "public_ip" { value = aws_instance.this.public_ip }

The Public Terraform Registry

registry.terraform.io hosts community and vendor-maintained modules — a well-tested VPC module, for instance, covers subnet layout, route tables, NAT gateways, and dozens of edge cases a first attempt at writing one from scratch would likely miss.

module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.0" cidr = "10.0.0.0/16" }

Module Versioning

Registry modules use the same ~> version-constraint syntax as providers (Chapter 2) — pinning a module version protects against an upstream update introducing a breaking change without warning, exactly the same reproducibility guarantee the provider lock file already provides.

DRY Infrastructure

Module blocks accept for_each and count too — calling the same module multiple times with different inputs, rather than copy-pasting a resource shape once per environment.

module "web_server" { for_each = toset(["dev", "staging"]) source = "./modules/web-server" environment = each.key }
A module's variables.tf is its API
Anyone calling a module only interacts with its declared inputs and outputs — treat that contract with the same care as a public function signature. Changing a required variable's meaning breaks every caller, exactly like a breaking API change would.
Don't modularize a single resource with no real logic
A module wrapping one plain resource, with no meaningful composition or reuse behind it, adds a layer of indirection without earning it back — matching the general principle that an abstraction should be introduced because the task genuinely needs it, not by default. Reach for a module once a shape is actually reused, or genuinely complex enough to be worth naming.

Hands-On Exercises

Exercise 1

Explain the difference between the root module and a child module, and explain in what sense every Terraform configuration in this course so far has technically already "been" a module.

📄 View solution
Exercise 2

A team needs a VPC matching a very common, well-established pattern. Explain why using a vetted community module from the Terraform Registry is often preferable to writing the VPC resources from scratch, and what version pinning specifically protects against.

📄 View solution
Exercise 3

A colleague wraps a single resource block, with no meaningful added logic, in its own module. Explain why this might be premature abstraction, and what tradeoff is actually being made.

📄 View solution

Chapter 7 Quick Reference

  • Root module — where commands are run from; child module — called via a module block
  • Every configuration is technically a module — the root module, specifically
  • Registryregistry.terraform.io, vetted community/vendor modules
  • Module version — same ~> syntax as providers, protects against breaking upstream changes
  • for_each/count on a module block — DRY, reused across environments
  • Don't modularize a single resource with no real logic behind it