What Is a Terraform Module? Explained Simply (2026)
Terraform modules let you reuse infrastructure code instead of copying and pasting. Here's what a module is, how to write one, how to use one, and why every Terraform project beyond the basics needs them.
You've written some Terraform. It works. But now you need the same S3 bucket setup in three different projects, or the same VPC structure across five environments.
You could copy-paste. But then a change means updating five places.
That's what Terraform modules solve.
What Is a Terraform Module?
A Terraform module is a reusable package of Terraform configuration ā a folder of .tf files that can be called from other Terraform code, just like a function in programming.
Instead of writing the same code again:
# Repeated in project A
resource "aws_s3_bucket" "data" {
bucket = "project-a-data"
# ... 30 lines of config
}
# Repeated in project B
resource "aws_s3_bucket" "data" {
bucket = "project-b-data"
# ... same 30 lines
}You write it once as a module and call it:
# Project A
module "data_bucket" {
source = "./modules/s3-bucket"
bucket_name = "project-a-data"
}
# Project B
module "data_bucket" {
source = "./modules/s3-bucket"
bucket_name = "project-b-data"
}One definition. Called twice with different inputs.
Every Terraform Project Is Already a Module
Here's something that surprises beginners: every Terraform directory is already a module. The folder where you run terraform apply is called the root module.
When you write:
my-project/
āāā main.tf
āāā variables.tf
āāā outputs.tf
That's a module. The difference is that the root module is called directly by Terraform, while child modules are called by other modules.
Module Structure
A module is just a folder with .tf files. The three important files:
modules/s3-bucket/
āāā main.tf # The actual resources
āāā variables.tf # Inputs the module accepts
āāā outputs.tf # Values the module exposes to callers
variables.tf ā Inputs
variable "bucket_name" {
type = string
description = "Name of the S3 bucket"
}
variable "enable_versioning" {
type = bool
description = "Enable S3 versioning"
default = true
}
variable "tags" {
type = map(string)
description = "Tags to apply to the bucket"
default = {}
}main.tf ā Resources
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
tags = var.tags
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = var.enable_versioning ? "Enabled" : "Suspended"
}
}outputs.tf ā Outputs
output "bucket_id" {
value = aws_s3_bucket.this.id
description = "The S3 bucket ID"
}
output "bucket_arn" {
value = aws_s3_bucket.this.arn
description = "The S3 bucket ARN"
}How to Call a Module
From your root module (or another module):
module "logs_bucket" {
source = "./modules/s3-bucket" # Local path
bucket_name = "my-app-logs"
enable_versioning = false
tags = {
Environment = "production"
Team = "platform"
}
}
# Use the module's output
resource "aws_cloudwatch_log_group" "app" {
# reference module output
}
output "logs_bucket_arn" {
value = module.logs_bucket.bucket_arn
}The source can be:
- Local path:
"./modules/s3-bucket" - Terraform Registry:
"terraform-aws-modules/s3-bucket/aws" - Git repo:
"git::https://github.com/myorg/tf-modules.git//s3-bucket?ref=v1.0.0" - S3:
"s3::https://s3.amazonaws.com/my-tf-modules/s3-bucket.zip"
Using Public Modules from the Terraform Registry
The Terraform Registry has thousands of community-maintained modules. For AWS, the terraform-aws-modules organization has high-quality modules for almost everything.
Example ā VPC:
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
single_nat_gateway = true
}This creates a full VPC with subnets, route tables, NAT gateways, and internet gateway ā in 15 lines instead of 300+.
After adding a module, run:
terraform init # Downloads the module
terraform plan
terraform applyModule Versioning
When using modules from Git or a registry, always pin the version:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.2" # Pin exact version
}Why: if you don't pin, a terraform init after a module update might break your infrastructure. Pin versions the same way you pin Docker image tags.
When Should You Write Your Own Module?
Write a module when:
- You're repeating the same resource config in multiple places
- You want to enforce standards (every S3 bucket must have encryption, every EC2 must have specific tags)
- Your root module is getting too large (100+ lines is a sign to modularize)
- Multiple teams need the same infrastructure pattern
Don't write a module for:
- One-off resources you'll never reuse
- Simple single-resource configs (a module with one resource adds complexity without value)
Module Best Practices
1. One module = one responsibility
A networking module that creates VPC + subnets + security groups is fine. A module that creates VPC + RDS + S3 + Lambda is too broad.
2. Always document variables
Every variable and output should have a description. Your future self will thank you.
3. Use default = null for optional values
variable "kms_key_arn" {
type = string
description = "KMS key for encryption. Leave null to use AWS managed key."
default = null
}4. Don't hardcode environment or region
Pass these as variables. Modules should be environment-agnostic.
5. Version your own modules
If multiple teams use your module, tag releases in Git:
git tag v1.2.0
git push origin v1.2.0Then callers pin to: source = "git::...?ref=v1.2.0"
Quick Reference
terraform init # Download modules referenced in source
terraform get # Update modules without full init# Local module
source = "./modules/my-module"
# Registry module (with version)
source = "hashicorp/consul/aws"
version = "~> 0.1"
# Git module (with tag)
source = "git::https://github.com/org/repo.git//path?ref=v1.0"Modules are the difference between Terraform that works and Terraform that scales. Once you start using them, you'll wonder how you managed without them.
Keep learning Terraform: Check out our guides on Terraform Remote State and Terraform vs Pulumi to go deeper.
Affiliate note: HashiCorp Terraform Cloud offers free remote state, private module registry, and team collaboration ā a great complement to using modules at scale.
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam ā just practical engineering content.
Related Articles
How to Use AI Agents to Automate Terraform Infrastructure Changes in 2026
AI agents can now plan, review, and apply Terraform changes from natural language. Here's how agentic AI is transforming infrastructure-as-code workflows.
Build an AI Terraform Cost Estimator Using Claude (2026)
Before you run terraform apply, wouldn't you want to know how much it'll cost? Build an AI cost estimator that reads your Terraform plan output and gives you a detailed cost breakdown using Claude as the reasoning engine.
Pulumi vs Crossplane: Which Infrastructure Tool to Use in 2026?
Pulumi vs Crossplane comparison ā architecture, use cases, team fit, and when to use each for managing cloud infrastructure in 2026.