Chapter 6: Modules
Topic 35

What a Module Is

ConceptModules

A module is a reusable container of Terraform resources with defined inputs and outputs — the unit of abstraction and reuse in the language. There is nothing special marking a directory as a module: it is a directory of .tf files, and it becomes a module the moment another configuration calls it with values. Every configuration is already one.

The reason to care is duplication. The moment the same shape of infrastructure — a VPC with its subnets and routes, a security group with a standard rule set — appears in a second place, you have a choice: copy the resource blocks and maintain the copy forever, or extract a module and call it twice. A module is how a fix gets made in one place instead of ten.

A module as a black box
Inputs
The public API — input variables the caller sets to configure the module.
Internals (hidden)
A directory of resources, locals, and data sources the caller never sees or touches.
Outputs
Return values the module exposes — read elsewhere as module.network.vpc_id.

Root and Child Modules

The directory you run terraform plan in is the root module. Anything it calls with a module block is a child module. That is the whole hierarchy — there is no global registry of modules, no install step that makes a directory "a module." The root module is where variables get their real values and where state lives; child modules are the reusable pieces it wires together.

Calling a child module looks like the block below. The source points at the directory, the arguments inside set its inputs, and the module's outputs become available as module.network.vpc_id. The child has no idea who called it or with what — it only sees the inputs it was handed.

A root module calling a child
module "network" {
  source   = "./modules/vpc"
  cidr_block = "10.0.0.0/16"
  az_count   = 3
}

resource "aws_instance" "web" {
  subnet_id = module.network.public_subnet_ids[0]
  ami       = "ami-0abc123"
  instance_type = "t3.micro"
}

The Module Interface

A module communicates through exactly three things: input variables coming in, resources doing the work inside, and outputs going back out. The inside is encapsulated. A caller passes inputs and reads outputs; it does not reach into the module's resources, and it should not need to. That boundary is the entire point — it lets the module's internals change without touching any caller, as long as the inputs and outputs hold.

A minimal module is just three files by convention: variables.tf for the inputs, main.tf for the resources, and outputs.tf for the return values. Terraform does not require those names — it merges every .tf file in the directory regardless — but the convention makes any module navigable in seconds, which matters when the module is something other people consume.

modules/vpc/outputs.tf — the return surface
output "vpc_id" {
  value = aws_vpc.this.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

Why Modules Beat Copy-Paste

Four things come from extracting a module. Reuse: call the same VPC definition in dev, staging, and prod from one source. Consistency: every caller gets the same tagging, the same flow-log setup, the same defaults, because they share one implementation. Encapsulation: a caller works against a small interface instead of forty raw resource blocks. A reviewable boundary: a change to the module is one diff in one place, reviewed once, rather than a sweep across every project that copied it.

Copy-paste loses all four. The copies drift — someone fixes a subnet tag in one project and not the other six — and a security change has to be hunted down everywhere it was pasted. The cost of a module is the indirection of one extra directory; the cost of copy-paste is paid every time the shape needs to change.

Granularity

Modules span a spectrum. At one end, a tiny module wraps a single resource with a few sensible defaults — useful but easy to overdo. At the other, a large opinionated module like the community VPC module provisions dozens of resources behind one call. Most useful modules sit in the middle: a focused, single-purpose component (a VPC, an RDS instance with its parameter group and subnet group) with a clean interface.

The failure mode at the small end is fragmentation — every pair of resources becomes its own thin wrapper, and a configuration turns into a maze of one-line modules nobody can trace. Extract a module when the same shape actually repeats or needs a consistent contract, not on principle. Granularity is covered in depth when this chapter reaches composition and reusable design.

Common Mistakes
  • Wrapping every group of two resources in its own module, fragmenting a configuration into dozens of thin wrappers nobody can navigate — indirection that hides the structure instead of clarifying it.
  • Building a module that expects callers to reference its internal resources (module.x.aws_instance.this) instead of declared outputs, so an internal refactor breaks every caller.
  • Copy-pasting the same resource blocks across projects rather than extracting a module, so a single tag or security fix has to be repeated in ten places and one gets missed.
  • Treating "module" as a special framework construct and looking for an install or registration step, when a module is just a directory of configuration called with inputs.
  • Extracting a speculative module before the shape repeats anywhere, abstracting a single use case into an interface that the second caller then has to fork.
Best Practices
  • Extract a module when the same shape of infrastructure appears in more than one place or needs a consistent contract across callers — not on the first occurrence.
  • Design every module as a black box: inputs and outputs are the contract, the resources and locals inside are private.
  • Keep the variables.tf / main.tf / outputs.tf structure so any reader finds the interface in seconds, even though Terraform ignores the filenames.
  • Start from the resources you actually repeat, then generalize, rather than designing a speculative abstraction first.
  • Reference a child module's values only through its outputs, so the module's internals stay free to change.
Comparable tools CloudFormation nested stacks and modules play the same reuse role Pulumi component resources bundle resources behind an interface Ansible roles are the configuration-management analog

Knowledge Check

What actually distinguishes a module directory from any other directory of .tf files?

  • Nothing intrinsic — it becomes a module when another configuration calls it with inputs
  • A required module.tf manifest file in the directory that names and registers it as a module
  • A special declaration that terraform init must install and record before the directory can be used
  • The presence of a backend block configuring where the directory stores its state

How can a root module read a value from inside a child module it calls?

  • Only through the child's declared outputs, referenced as module.name.output
  • By referencing the child's internal resources directly, like module.name.aws_vpc.this.id
  • By reading the child's state file path and parsing it
  • Every resource in a child module is automatically exposed to the parent

What is the trade-off of making every pair of resources its own module?

  • It fragments the config into many thin wrappers, adding indirection that hides the structure rather than clarifying it
  • Terraform refuses to plan a configuration with more than ten module blocks and errors during init
  • Each tiny module needs its own separate state file and a backend block to store it
  • Module calls run strictly one after another in sequence, so many small modules slow the overall apply down linearly with their count

Why does a module beat copy-pasting the same resource blocks across projects?

  • A fix is made once in the module instead of repeated across every copy, where one inevitably gets missed
  • Copy-pasted resource blocks cannot reference variables, while a module's blocks can
  • A module applies measurably faster than the equivalent inline resource blocks copy-pasted into each project
  • Copy-pasted resources are left out of the state file entirely, so they drift

You got correct