As Terraform configurations grow, repeating the same infrastructure patterns across environments and projects becomes tedious and error-prone. Modules solve this — they are the primary unit of reuse in Terraform.
What is a Module?
Every directory containing .tf files is a module. Your main working directory is the root module. You can call other modules (child modules) from it using module blocks.
project/
main.tf # root module
variables.tf
outputs.tf
modules/
vpc/ # child module
main.tf
variables.tf
outputs.tf
ec2/
main.tf
variables.tf
outputs.tf
Writing a Module
# modules/vpc/variables.tf
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "environment" {
type = string
}
variable "public_subnet_cidrs" {
type = list(string)
}
# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
tags = { Name = "${var.environment}-vpc" }
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidrs[count.index]
}
# modules/vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.this.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
Calling a Module
# main.tf (root module)
module "vpc" {
source = "./modules/vpc" # local path
vpc_cidr = "10.0.0.0/16"
environment = var.environment
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
}
# Reference module outputs
resource "aws_lb" "main" {
subnets = module.vpc.public_subnet_ids
}
output "vpc_id" {
value = module.vpc.vpc_id
}
Module Sources
| Source type | Example |
|---|---|
| Local path | ./modules/vpc |
| Terraform Registry | terraform-aws-modules/vpc/aws |
| GitHub | git::https://github.com/org/repo.git//modules/vpc |
| Git with tag | git::https://github.com/org/repo.git?ref=v2.0.0 |
| S3 bucket | s3::https://s3.amazonaws.com/bucket/module.zip |
Using Registry Modules
The Terraform Registry hosts community-maintained modules. The terraform-aws-modules organisation is particularly well-maintained:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
}
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.0"
cluster_name = "my-cluster"
cluster_version = "1.30"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
}
Module Versioning Best Practices
- Always pin module versions with
version = "~> x.y" - Run
terraform init -upgradeto update to the latest allowed version - For local modules in a monorepo, use relative paths
- Document all module inputs and outputs with
descriptionfields
Next: Backends and Remote State — how to configure where Terraform stores state for production team workflows.