Modules
A module is a reusable group of Terraform resources packaged together. Every Terraform configuration is technically a module (the root module), and you can call other modules from it.
Why Modules?
Section titled “Why Modules?”- Reuse — Define a VPC once, use it across dev, staging, and prod.
- Encapsulation — Hide complexity behind a clean interface (variables in, outputs out).
- Consistency — Teams use the same tested module instead of copy-pasting resources.
- Versioning — Pin module versions so infrastructure changes are intentional.
Module Structure
Section titled “Module Structure”A module is a directory of .tf files. The convention:
modules/ vpc/ main.tf # resources variables.tf # input variables outputs.tf # output values README.md # documentationvariables.tf
Section titled “variables.tf”variables.tf declares the module’s inputs: values the caller must (or can) pass in when using the module. Each variable block defines a name, optional description, type, and optional default.
Inside the module, you reference these as var.<name> (e.g. var.vpc_cidr). Variables let the same module be reused with different values (e.g. different CIDRs or environments) without changing the module’s resource definitions.
variable "vpc_cidr" { description = "CIDR block for the VPC" type = string}
variable "environment" { description = "Environment name (e.g. dev, staging, prod)" type = string}
variable "public_subnets" { description = "List of public subnet CIDRs" type = list(string) default = []}main.tf
Section titled “main.tf”main.tf defines the module’s resources. These are the infrastructure objects created by the module.
You can reference module inputs (e.g. var.vpc_cidr) and other module outputs (e.g. module.vpc.vpc_id) within these resource blocks.
resource "aws_vpc" "this" { cidr_block = var.vpc_cidr enable_dns_support = true enable_dns_hostnames = true
tags = { Name = "${var.environment}-vpc" Environment = var.environment }}
resource "aws_subnet" "public" { count = length(var.public_subnets) vpc_id = aws_vpc.this.id cidr_block = var.public_subnets[count.index] availability_zone = data.aws_availability_zones.available.names[count.index]
tags = { Name = "${var.environment}-public-${count.index}" }}outputs.tf
Section titled “outputs.tf”outputs.tf declares the module’s outputs: values the caller can use in the calling module. Each output block defines a name, optional description, and the value to expose.
Inside the calling module, you reference these as module.<name>.<output_name> (e.g. module.vpc.vpc_id). Outputs let the caller use the module’s results (e.g. the VPC ID) without knowing how they’re implemented.
output "vpc_id" { description = "ID of the VPC" value = aws_vpc.this.id}
output "public_subnet_ids" { description = "IDs of the public subnets" value = aws_subnet.public[*].id}Calling a Module
Section titled “Calling a Module”To use a module, you add a module block in your root (or another module’s) configuration. You set source to where the module lives (local path, registry, Git, or S3) and pass arguments that map to the module’s input variables. After applying, you reference the module’s outputs as module.<module_label>.<output_name> in other resources or outputs.
Local Module
Section titled “Local Module”A local module lives in your repo, typically under a subdirectory like modules/. Use it when the module is maintained in the same codebase and you don’t need to version it separately.
- source — Relative path from the current
.tffile to the module directory (e.g../modules/vpc). Terraform loads all.tffiles in that directory as the module. - Arguments — Pass values for the module’s variables by name. Required variables must be set; optional ones can be omitted if they have defaults.
- Outputs — After
terraform apply, usemodule.<label>.<output_name>(e.g.module.vpc.public_subnet_ids) in other resources or in your rootoutputblocks. - terraform init — Run
terraform init(orterraform get) whenever you add or change a module’ssourceso Terraform can fetch or link the module.
Example: call the VPC module and pass inputs; then use its output in another resource.
module "vpc" { source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16" environment = "production" public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]}Access the module’s outputs elsewhere in your configuration:
resource "aws_instance" "web" { subnet_id = module.vpc.public_subnet_ids[0] # ...}Terraform Registry
Section titled “Terraform Registry”The Terraform Registry hosts community and official modules:
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.1.0"
name = "my-vpc" cidr = "10.0.0.0/16" azs = ["us-east-1a", "us-east-1b"] public_subnets = ["10.0.1.0/24", "10.0.2.0/24"] private_subnets = ["10.0.3.0/24", "10.0.4.0/24"]}Git Repository
Section titled “Git Repository”Reference a module in a Git repo:
module "vpc" { source = "git::https://github.com/myorg/terraform-modules.git//vpc?ref=v1.2.0"
vpc_cidr = "10.0.0.0/16" environment = "production"}The //vpc points to a subdirectory; ?ref=v1.2.0 pins to a Git tag.
S3 Bucket
Section titled “S3 Bucket”module "vpc" { source = "s3::https://my-bucket.s3.amazonaws.com/modules/vpc.zip"}Versioning
Section titled “Versioning”Always pin module versions to avoid surprises:
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.0" # allows 5.x but not 6.0}| Constraint | Meaning |
|---|---|
= 5.1.0 | Exact version |
>= 5.0 | Minimum version |
~> 5.1 | >= 5.1, < 6.0 (pessimistic) |
>= 5.0, < 6.0 | Range |
For local and Git modules, use Git tags (?ref=v1.2.0) instead of version.
Sharing modules with the organization
Section titled “Sharing modules with the organization”Once a module is stable, you can share it across teams:
- Private Terraform Registry — Host your own registry (e.g. Terraform Cloud private registry, or self-hosted) so teams use
source = "myorg/vpc/aws"andversion = "~> 1.0"with consistent versioning and changelogs. - Git repository with tags — A dedicated
terraform-modulesrepo (or monorepo with amodules/path) and SemVer tags (v1.0.0,v1.1.0) let consumers pin with?ref=v1.0.0. Document inputs/outputs (e.g. with terraform-docs) and keep a CHANGELOG so teams know when to upgrade. - Monorepo with local paths — For a single repo containing many environments,
source = "../../modules/vpc"is simple but couples all consumers to the same ref; use when you want one source of truth and coordinated upgrades.
Improvements over time: add testing (e.g. terraform test, Terratest) and SemVer (bump major for breaking changes, minor for new features, patch for fixes) so the wider org can adopt modules with confidence. See Best Practices — Testing and CI/CD for automation.
Passing Data Between Modules
Section titled “Passing Data Between Modules”Modules communicate through variables (in) and outputs (out):
module "vpc" { source = "./modules/vpc" vpc_cidr = "10.0.0.0/16" environment = "production"}
module "app" { source = "./modules/app" vpc_id = module.vpc.vpc_id # output from vpc module subnet_id = module.vpc.public_subnet_ids[0]}The app module defines vpc_id and subnet_id as input variables; the vpc module exposes them as outputs.
Module Best Practices
Section titled “Module Best Practices”- Keep modules focused — One module = one logical unit (VPC, database, app). Don’t pack everything into one mega-module.
- Document with
description— Every variable and output should have adescription. - Use
validationblocks — Catch bad input early:
variable "environment" { type = string validation { condition = contains(["dev", "staging", "production"], var.environment) error_message = "Environment must be dev, staging, or production." }}- Don’t hardcode providers in modules — Let the root module configure providers; modules inherit them.
- Test modules — Use
terraform planagainst example configs, or tools like Terratest.
Key Takeaways
Section titled “Key Takeaways”- A module is a directory of
.tffiles with variables (in) and outputs (out). - Source from local paths, the Terraform Registry, Git repos, or S3.
- Always pin versions —
version = "~> 5.0"for registry modules,?ref=v1.2.0for Git. - Modules communicate through outputs —
module.vpc.vpc_id. - Keep modules small, focused, and well-documented.