Terraform Modules: Building Reusable Infrastructure
Terraform Modules: Building Reusable Infrastructure
Once your Terraform configs pass a few hundred lines, copy-paste becomes the enemy. Modules are Terraform's answer: package a set of resources behind a clean interface of inputs and outputs, then reuse it everywhere.
Table of Contents
- Module anatomy
- Calling the module
- Sources: local, git, and registry
- Design rules for good modules
- Iterating without pain
- Modules vs. just repeating yourself
Module anatomy
A module is just a directory of .tf files:
modules/vpc/
main.tf # resources
variables.tf # inputs
outputs.tf # exports
# modules/vpc/variables.tf
variable "cidr_block" { type = string }
variable "environment" { type = string }
# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
tags = { Environment = var.environment }
}
# modules/vpc/outputs.tf
output "vpc_id" { value = aws_vpc.this.id }
Calling the module
module "vpc_prod" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
environment = "prod"
}
module "vpc_staging" {
source = "./modules/vpc"
cidr_block = "10.1.0.0/16"
environment = "staging"
}
# consume outputs
resource "aws_subnet" "web" {
vpc_id = module.vpc_prod.vpc_id
# ...
}
Two environments, one implementation — drift between them becomes structurally impossible.
Sources: local, git, and registry
source = "./modules/vpc" # local
source = "git::https://github.com/org/tf-modules.git//vpc?ref=v1.2.0"
source = "terraform-aws-modules/vpc/aws" # public registry
version = "~> 5.0" # required for registry modules
The Terraform Registry hosts battle-tested community modules (the terraform-aws-modules/* family especially). Pin versions — ref=v1.2.0 or version — always.
Design rules for good modules
- Small and opinionated. A module that "does everything" is a framework nobody can debug. One module = one concept (a VPC, a database, a service).
- Sensible defaults. variable "enable_flow_logs" { default = true } makes the secure path the easy path.
- Export only what's consumed. Outputs are your API contract; exporting internals invites coupling.
- No provider blocks inside modules. The caller configures providers; modules declare required_providers versions only.
- Version your modules. Git tags let callers upgrade deliberately instead of breaking on every push.
Iterating without pain
terraform init -upgrade # refresh module sources
terraform plan -target=module.vpc_prod # scope a plan
terraform state mv aws_vpc.old module.vpc_prod.aws_vpc.this
state mv is the under-appreciated superpower: refactor resources into a module without destroying anything.
Modules vs. just repeating yourself
Repeat code is sometimes fine in Terraform — explicit beats clever. Reach for a module when the same pattern appears a third time, when teams need a paved-road standard, or when versioning infra changes matters. Below that threshold, plain resources stay more readable.
Well-built modules are how infra scales past "one person who knows where everything is." They're the difference between a codebase and a platform.
Related Articles
- Terraform 101: Managing Cloud Infrastructure as Code
- Terraform State Explained: Backends, Locking, and Surgery
Last Updated: October 2026 Author: CloudOpsGuide Team Difficulty: Intermediate Estimated Reading Time: 11 minutes