Terraform Modules: Building Reusable Infrastructure
📅 Published: August 2026
⏱️ Estimated Reading Time: 15 minutes
🏷️ Tags: Terraform, Modules, Infrastructure as Code, Reusability
Introduction: What is a Terraform Module?
A module is a container for multiple resources that are used together. It's like a function in programming—you define it once, give it a clear interface, and reuse it everywhere.
Instead of copying and pasting the same infrastructure code across multiple projects, you package it into a module and reuse it.
Think of modules as blueprints:
A VPC module might contain VPC, subnets, route tables, and internet gateways
A web server module might contain EC2 instances, security groups, and load balancers
A database module might contain RDS instances, parameter groups, and security groups
Part 1: Why Use Modules?
| Without Modules | With Modules |
|---|---|
| Copy-paste same code everywhere | Define once, reuse everywhere |
| 50 lines repeated 10 times | 50 lines in module, 5 lines to call it |
| Change requires updating 10 places | Change requires updating 1 place |
| Hard to share with team | Easy to share and version |
| Inconsistent implementations | Standardized implementations |
# ❌ Without modules - duplicated everywhere resource "aws_vpc" "prod" { cidr_block = "10.0.0.0/16" tags = { Name = "prod-vpc" } } # ... 50 more lines resource "aws_vpc" "dev" { cidr_block = "10.1.0.0/16" tags = { Name = "dev-vpc" } } # ... 50 more lines # ✅ With modules - one call per environment module "prod" { source = "./modules/vpc" name = "prod" cidr = "10.0.0.0/16" } module "dev" { source = "./modules/vpc" name = "dev" cidr = "10.1.0.0/16" }
Part 2: Module Structure
The Standard Layout
modules/
└── s3-bucket/
├── main.tf # Resource definitions
├── variables.tf # Input variables
├── outputs.tf # Output values
├── README.md # Documentation
└── examples/ # Usage examplesvariables.tf
# modules/s3-bucket/variables.tf variable "bucket_name" { description = "Name of the S3 bucket" type = string } variable "environment" { description = "Deployment environment" type = string validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "Environment must be dev, staging, or prod." } } variable "versioning_enabled" { description = "Enable bucket versioning" type = bool default = false } variable "tags" { description = "Tags to apply to resources" type = map(string) default = {} }
main.tf
# modules/s3-bucket/main.tf resource "aws_s3_bucket" "this" { bucket = "${var.bucket_name}-${var.environment}-${random_string.suffix.result}" tags = merge({ Environment = var.environment ManagedBy = "Terraform" }, var.tags) } resource "aws_s3_bucket_versioning" "this" { count = var.versioning_enabled ? 1 : 0 bucket = aws_s3_bucket.this.id versioning_configuration { status = "Enabled" } } resource "random_string" "suffix" { length = 6 special = false upper = false }
outputs.tf
# modules/s3-bucket/outputs.tf output "bucket_id" { description = "Name of the bucket" value = aws_s3_bucket.this.id } output "bucket_arn" { description = "ARN of the bucket" value = aws_s3_bucket.this.arn } output "bucket_name" { description = "Full bucket name with suffix" value = aws_s3_bucket.this.bucket } output "bucket_regional_domain_name" { description = "Regional domain name of the bucket" value = aws_s3_bucket.this.bucket_regional_domain_name }
Part 3: Using Modules
Local Modules
# root/main.tf module "logs_bucket" { source = "./modules/s3-bucket" bucket_name = "logs" environment = "prod" versioning_enabled = true tags = { Purpose = "Application logs" } } module "backup_bucket" { source = "./modules/s3-bucket" bucket_name = "backups" environment = "prod" versioning_enabled = true tags = { Purpose = "Backup storage" } } # Reference outputs output "logs_bucket_arn" { value = module.logs_bucket.bucket_arn }
Remote Modules (Terraform Registry)
# From the public Terraform Registry module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.0.0" name = "my-vpc" cidr = "10.0.0.0/16" azs = ["us-west-2a", "us-west-2b", "us-west-2c"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] }
Remote Modules (Git)
# From a Git repository module "vpc" { source = "git::https://github.com/company/terraform-aws-vpc.git?ref=v1.2.0" name = "my-vpc" cidr = "10.0.0.0/16" } # From a Git repository with SSH module "vpc" { source = "git::ssh://git@github.com/company/terraform-aws-vpc.git?ref=v1.2.0" }
Part 4: Module Composition
Modules can call other modules, building complex infrastructure from reusable pieces.
Example: Web Application Module
Directory structure:
modules/
├── vpc/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── security-groups/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── ec2-instance/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── web-app/ ← Composed module
├── main.tf
├── variables.tf
└── outputs.tfmodules/web-app/main.tf:
# modules/web-app/main.tf locals { name = "${var.app_name}-${var.environment}" } # VPC module "vpc" { source = "../vpc" name = local.name cidr = var.vpc_cidr public_subnet_cidrs = var.public_subnet_cidrs private_subnet_cidrs = var.private_subnet_cidrs } # Security Groups module "security_groups" { source = "../security-groups" vpc_id = module.vpc.vpc_id web_port = var.web_port } # EC2 Instances module "ec2" { source = "../ec2-instance" instance_count = var.instance_count instance_type = var.instance_type subnet_ids = module.vpc.public_subnet_ids security_group_ids = [module.security_groups.web_sg_id] user_data = var.user_data } # Outputs output "load_balancer_dns" { value = module.alb.dns_name } output "instance_ips" { value = module.ec2.public_ips }
Part 5: Module Versioning
Semantic Versioning for Modules
Use semantic versioning (SemVer) for modules:
| Version | When | Example |
|---|---|---|
| MAJOR | Breaking changes | v2.0.0 |
| MINOR | New features, backward compatible | v1.3.0 |
| PATCH | Bug fixes, backward compatible | v1.2.1 |
Publishing Modules
1. Create a Git repository:
terraform-aws-s3-bucket/ ├── main.tf ├── variables.tf ├── outputs.tf ├── README.md └── versions.tf
2. Tag your release:
git tag -a v1.0.0 -m "Initial release" git push origin v1.0.0
3. Use the module with version pinning:
module "s3_bucket" { source = "git::https://github.com/company/terraform-aws-s3-bucket.git?ref=v1.0.0" bucket_name = "my-bucket" environment = "prod" }
Version Constraints
# Exact version version = "1.2.0" # Patch updates only version = "~> 1.2.0" # 1.2.x, not 1.3.0 # Minor updates only version = "~> 1.2" # 1.x, not 2.0.0 # Greater than or equal version = ">= 1.2.0" # Range version = ">= 1.2.0, < 2.0.0"
Part 6: Module Sources
Source Types
| Source Type | Example | Use Case |
|---|---|---|
| Local | ./modules/vpc | Development, simple sharing |
| Git | git::https://github.com/company/module.git | Versioned sharing |
| Terraform Registry | terraform-aws-modules/vpc/aws | Public modules |
| HTTP | https://example.com/module.zip | Direct download |
Module Source Best Practices
✅ Use local modules for development
✅ Use versioned Git modules for team sharing
✅ Use the Terraform Registry for public modules
✅ Pin module versions with
?ref=v1.0.0✅ Document module source locations
Part 7: Module Best Practices
Do's
✅ Keep modules focused: One module, one responsibility
✅ Use descriptive variable names:
vpc_cidrnotcidr✅ Add descriptions: Every variable and output needs a description
✅ Use validation: Validate input values
✅ Set sensible defaults: Make optional variables optional
✅ Document your modules: README with examples
✅ Version your modules: Semantic versioning
✅ Use locals for complex expressions: Keep main.tf clean
Don'ts
❌ Don't create modules for single resources: Not worth it
❌ Don't hardcode provider configurations: Pass providers
❌ Don't include secrets: Use variables or secrets managers
❌ Don't create giant modules: Break into smaller modules
❌ Don't skip testing: Test your modules
Part 8: Module Testing
Static Testing
# Check syntax terraform validate # Check formatting terraform fmt -check # Security scanning tfsec . checkov -d .
Integration Testing
# test/main.tf module "test_bucket" { source = "../modules/s3-bucket" bucket_name = "test-bucket" environment = "test" } output "bucket_created" { value = module.test_bucket.bucket_id != "" ? "Success" : "Failed" }
cd test terraform init terraform apply -auto-approve # Verify resources exist terraform destroy -auto-approve
Module Cheat Sheet
# Module structure modules/ └── service/ ├── main.tf ├── variables.tf ├── outputs.tf └── README.md # Module call module "name" { source = "./modules/service" var1 = "value1" var2 = var.some_value } # Reference outputs output "result" { value = module.name.output_name } # Versioned module call module "name" { source = "git::https://github.com/org/repo.git?ref=v1.0.0" var1 = "value1" }
Summary
| Aspect | Key Points |
|---|---|
| What | Reusable infrastructure components |
| Why | DRY code, consistency, team sharing |
| Structure | main.tf, variables.tf, outputs.tf |
| Sources | Local, Git, Registry |
| Versioning | Semantic versioning |
| Best for | VPCs, databases, web apps, common patterns |
Modules are the foundation of scalable Terraform usage. Start with local modules, then publish to a registry as you mature.
Learn More
Practice Terraform modules with hands-on exercises in our interactive labs:
https://devops.trainwithsky.com/
Comments
Post a Comment