šŸŽ‰ DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

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.

DevOpsBoysMay 19, 20265 min read
Share:Tweet

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:

hcl
# 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:

hcl
# 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

hcl
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

hcl
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

hcl
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):

hcl
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:

hcl
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:

bash
terraform init  # Downloads the module
terraform plan
terraform apply

Module Versioning

When using modules from Git or a registry, always pin the version:

hcl
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

hcl
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:

bash
git tag v1.2.0
git push origin v1.2.0

Then callers pin to: source = "git::...?ref=v1.2.0"


Quick Reference

bash
terraform init    # Download modules referenced in source
terraform get     # Update modules without full init
hcl
# 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.

Newsletter

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

Comments