What a Module Is
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.
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.
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.
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.
- 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.
- 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.tfstructure 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.
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.tfmanifest file in the directory that names and registers it as a module - A special declaration that
terraform initmust 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