Understanding Terraform Modules
Terraform modules are self-contained packages of Terraform configurations that are managed as a group. They enable you to reuse code, reduce duplication, and maintain consistency across your infrastructure.
Why Use Modules?
Benefits:
- Reusability: Write once, use everywhere
- Organization: Group related resources together
- Consistency: Standardize infrastructure patterns
- Maintainability: Update in one place, affect all consumers
- Abstraction: Hide complexity behind simple interfaces
Module Structure
Basic Module Layout
modules/
├── vpc/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── README.md
└── ec2/
├── main.tf
├── variables.tf
├── outputs.tf
└── README.mdCreating Your First Module
Module Structure:
modules/
└── web-server/
├── main.tf
├── variables.tf
├── outputs.tf
└── README.mdmain.tf (Module Code):
resource "aws_instance" "web" {
count = var.instance_count
ami = var.ami_id
instance_type = var.instance_type
subnet_id = var.subnet_ids[count.index % length(var.subnet_ids)]
vpc_security_group_ids = [var.security_group_id]
user_data = var.user_data
tags = merge(
var.common_tags,
{
Name = "${var.instance_name}-${count.index}"
}
)
}
resource "aws_lb_target_group_attachment" "web" {
count = var.instance_count
target_group_arn = var.target_group_arn
target_id = aws_instance.web[count.index].id
port = 80
}variables.tf (Module Inputs):
variable "instance_count" {
description = "Number of instances to create"
type = number
default = 1
}
variable "ami_id" {
description = "AMI ID to use for instances"
type = string
}
variable "instance_type" {
description = "Instance type"
type = string
default = "t2.micro"
}
variable "subnet_ids" {
description = "List of subnet IDs"
type = list(string)
}
variable "security_group_id" {
description = "Security group ID"
type = string
}
variable "instance_name" {
description = "Base name for instances"
type = string
}
variable "user_data" {
description = "User data script"
type = string
default = null
}
variable "common_tags" {
description = "Common tags to apply to resources"
type = map(string)
default = {}
}
variable "target_group_arn" {
description = "Target group ARN for load balancer"
type = string
}outputs.tf (Module Outputs):
output "instance_ids" {
description = "IDs of created instances"
value = aws_instance.web[*].id
}
output "instance_public_ips" {
description = "Public IPs of instances"
value = aws_instance.web[*].public_ip
}
output "instance_private_ips" {
description = "Private IPs of instances"
value = aws_instance.web[*].private_ip
}Using Modules
Local Module Usage
Calling a Local Module:
# main.tf
module "web_server" {
source = "./modules/web-server"
instance_count = 3
ami_id = var.ami_id
instance_type = "t2.micro"
subnet_ids = module.vpc.public_subnet_ids
security_group_id = module.security_group.sg_id
instance_name = "web"
common_tags = var.common_tags
target_group_arn = module.load_balancer.target_group_arn
user_data = file("${path.module}/scripts/web-setup.sh")
}Remote Module Usage
Public Registry Module:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
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
tags = {
Terraform = "true"
Environment = "dev"
}
}GitHub Source:
module "network" {
source = "github.com/myorg/terraform-modules//network"
vpc_cidr = "10.0.0.0/16"
aws_region = "us-east-1"
}S3 Source:
module "storage" {
source = "s3::https://s3.amazonaws.com/my-bucket/modules/storage.zip"
bucket_prefix = "my-app-"
environment = "production"
}Module Versions
Version Constraints
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = ">= 3.0, < 4.0"
}Version Operators
>= 3.0: Any version 3.0 or higher~> 3.0: Any version 3.x.x= 3.0.0: Exact version 3.0.0> 3.0, < 4.0: Any version between 3.0 and 4.0
Module Patterns
1. Complete Infrastructure Module
# modules/web-infrastructure/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = var.tags
}
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, {
Name = "${var.name}-public-${count.index}"
})
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = var.tags
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = var.tags
}
resource "aws_route_table_association" "public" {
count = length(var.availability_zones)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}2. Multi-Environment Module
# modules/environment/main.tf
module "vpc" {
source = "../vpc"
name = "${var.environment}-vpc"
vpc_cidr = var.vpc_cidr
tags = var.common_tags
}
module "ec2" {
source = "../ec2"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnet_ids
instance_count = var.instance_count
instance_type = var.instance_type
environment = var.environment
common_tags = var.common_tags
}
module "rds" {
source = "../rds"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
engine = var.database_engine
engine_version = var.database_version
instance_class = var.database_instance_class
environment = var.environment
common_tags = var.common_tags
}3. Composite Module Pattern
# modules/web-app-cluster/main.tf
module "asg" {
source = "../autoscaling-group"
vpc_id = var.vpc_id
subnet_ids = var.subnet_ids
instance_type = var.instance_type
min_size = var.min_size
max_size = var.max_size
desired_capacity = var.desired_capacity
}
module "alb" {
source = "../application-load-balancer"
vpc_id = var.vpc_id
subnet_ids = var.subnet_ids
}
module "dns" {
source = "../route53"
domain_name = var.domain_name
alb_dns_name = module.alb.dns_name
}Module Development Best Practices
1. Document Your Modules
README.md Template:
# Web Server Module
## Description
Creates a scalable web server infrastructure with Auto Scaling.
## Requirements
| Name | Version |
|------|---------|
| terraform | >= 1.0 |
| aws | >= 4.0 |
## Providers
| Name | Version |
|------|---------|
| aws | >= 4.0 |
## Inputs
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|----------|
| instance_count | Number of instances | number | 1 | no |
| instance_type | EC2 instance type | string | t2.micro | no |
## Outputs
| Name | Description |
|------|-------------|
| instance_ids | List of instance IDs |
| alb_dns_name | DNS name of the load balancer |
## Usagemodule "web" { source = "./modules/web-server" instance_count = 3 }
2. Use Validation Rules
variable "instance_type" {
description = "EC2 instance type"
type = string
validation {
condition = can(regex("^t[23]\.(micro|small|medium)$", var.instance_type))
error_message = "Instance type must be t2.micro, t2.small, t2.medium, t3.micro, t3.small, or t3.medium."
}
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}3. Provide Sensible Defaults
variable "enable_monitoring" {
description = "Enable detailed monitoring"
type = bool
default = true
}
variable "common_tags" {
description = "Common tags for all resources"
type = map(string)
default = {
ManagedBy = "Terraform"
}
}4. Use Local Values
locals {
name_prefix = "${var.project}-${var.environment}"
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "Terraform"
}
}
resource "aws_instance" "web" {
tags = merge(local.common_tags, var.extra_tags)
}Module Registry
Publishing to Terraform Registry
- 1Structure Your Repository
terraform-aws-module/
├── modules/
│ └── example/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── examples/
│ └── complete/
│ └── main.tf
├── README.md
├── LICENSE
└── terraform.yml- 1Create terraform.yml
name: my_awesome_module
namespace: myorg
description: An awesome Terraform module
provider: aws
version: 1.0.0- 1Publish
- Push to GitHub/GitLab
- Register at registry.terraform.io
Using Registry Modules
module "db" {
source = "terraform-aws-modules/rds/aws"
version = "5.0.0"
identifier = "mydb"
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t2.micro"
allocated_storage = 20
name = "mydb"
username = "admin"
password = var.db_password
vpc_security_group_ids = [module.security_group.security_group_id]
tags = {
Owner = "me"
}
}Module Testing
1. Structure Tests
# test/terraform/modules/example_test.rb
describe 'Modules' do
describe 'VPC Module' do
it 'creates a VPC' do
expect(vpc).to exist
end
it 'has correct CIDR block' do
expect(vpc.cidr_block).to eq('10.0.0.0/16')
end
end
end2. Integration Tests
# examples/complete/main.tf
module "test" {
source = "../../modules/vpc"
vpc_cidr = "10.0.0.0/16"
tags = {
Test = "true"
}
}cd examples/complete
terraform init
terraform apply -auto-approve
terraform destroy -auto-approveAdvanced Module Techniques
1. Dynamic Blocks
module "security_group" {
source = "./modules/security-group"
ingress_rules = [
{
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
}2. Optional Resources
variable "enable_nat_gateway" {
description = "Enable NAT Gateway"
type = bool
default = false
}
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? 1 : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[0].id
}3. Module Composition
module "production" {
source = "./modules/environment"
environment = "production"
vpc_cidr = "10.0.0.0/16"
}
module "staging" {
source = "./modules/environment"
environment = "staging"
vpc_cidr = "10.1.0.0/16"
}Common Pitfalls
1. Hardcoded Values
Avoid:
resource "aws_instance" "web" {
ami = "ami-12345678" # Hardcoded
}Prefer:
variable "ami_id" {
description = "AMI ID"
type = string
}
resource "aws_instance" "web" {
ami = var.ami_id
}2. Over-Abstraction
Too Generic:
variable "resource_properties" {
type = map(string)
}Better:
variable "instance_type" {
type = string
}
variable "root_volume_size" {
type = number
}3. Tight Coupling
Design modules to be loosely coupled and composable.
Module Maintenance
Versioning Strategy
terraform init -upgrade
terraform plan
terraform applyBreaking Changes
- 1Increment major version
- 2Document changes in CHANGELOG.md
- 3Provide migration guide
Real-World Examples
Multi-Region Deployment
module "us-east" {
source = "./modules/infrastructure"
region = "us-east-1"
name = "production"
}
module "eu-west" {
source = "./modules/infrastructure"
region = "eu-west-1"
name = "production"
}Blue-Green Deployment
module "blue" {
source = "./modules/deployment"
color = "blue"
instance_id = "i-blue123"
}
module "green" {
source = "./modules/deployment"
color = "green"
instance_id = "i-green456"
}Conclusion
Terraform modules are essential for building maintainable infrastructure as code. They promote reuse, consistency, and collaboration. Start simple, document well, and iterate based on usage patterns.
Remember: Good modules are focused, well-documented, and thoroughly tested. They abstract complexity while maintaining flexibility.