Terraform Modules
Modules are containers for multiple resources that are used together. They allow you to organize your code, reuse infrastructure, and create abstractions.
What are Modules?
Benefits of Modules
- ✅ Reusability - Write once, use many times
- ✅ Organization - Group related resources together
- ✅ Abstraction - Hide complexity behind simple interfaces
- ✅ Maintainability - Update in one place, affects all uses
- ✅ Testing - Test modules independently
Module Structure
Standard Module Structure
modules/
└── vpc/
├── main.tf # Main resources
├── variables.tf # Input variables
├── outputs.tf # Output values
├── README.md # Documentation
└── versions.tf # Provider requirements
Creating a Module
Example: VPC Module
modules/vpc/variables.tf
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
}
variable "environment" {
description = "Environment name"
type = string
}
variable "tags" {
description = "Tags to apply to resources"
type = map(string)
default = {}
}
modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(var.tags, {
Name = "${var.environment}-vpc"
Environment = var.environment
})
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = merge(var.tags, {
Name = "${var.environment}-public-subnet-${count.index + 1}"
})
}
data "aws_availability_zones" "available" {
state = "available"
}
modules/vpc/outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = aws_subnet.public[*].id
}
output "vpc_cidr" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
Using Modules
Local Modules
module "vpc" {
source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16"
environment = "production"
tags = {
Project = "web-app"
ManagedBy = "Terraform"
}
}
# Access module outputs
resource "aws_instance" "web" {
subnet_id = module.vpc.public_subnet_ids[0]
# ...
}
Git Repository Modules
module "vpc" {
source = "git::https://github.com/example/terraform-aws-vpc.git"
vpc_cidr = "10.0.0.0/16"
}
# Use specific branch/tag
module "vpc" {
source = "git::https://github.com/example/terraform-aws-vpc.git?ref=v1.2.0"
}
# Use specific branch
module "vpc" {
source = "git::https://github.com/example/terraform-aws-vpc.git?ref=main"
}
Terraform Registry Modules
# Public registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
enable_vpn_gateway = true
tags = {
Terraform = "true"
Environment = "production"
}
}
Module Versioning
💡 Best Practice: Always pin module versions in production to ensure consistency.
# Pin to specific version
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
}
# Use version constraints
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0" # >= 5.0, < 6.0
}
Module Composition
Nested Modules
module "vpc" {
source = "./modules/vpc"
# ...
}
module "ec2" {
source = "./modules/ec2"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnet_ids
security_groups = module.vpc.security_group_ids
}
Module Outputs
Accessing Module Outputs
# Module definition
output "instance_id" {
value = aws_instance.web.id
}
# Using module output
resource "aws_eip" "web_ip" {
instance = module.ec2.instance_id
}
# Pass output to another module
module "monitoring" {
source = "./modules/monitoring"
instance_id = module.ec2.instance_id
}
Module Best Practices
✅ Best Practices:
- ✅ One module per logical component (VPC, EC2, RDS)
- ✅ Use variables for all configurable values
- ✅ Expose outputs for important resource attributes
- ✅ Document modules with README.md
- ✅ Use version constraints for module sources
- ✅ Keep modules focused and single-purpose
- ✅ Use local values for computed values
- ✅ Follow consistent naming conventions
Module Examples
Complete Module Example
# modules/ec2/main.tf
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "subnet_id" {
description = "Subnet ID for EC2 instance"
type = string
}
variable "tags" {
description = "Tags for EC2 instance"
type = map(string)
default = {}
}
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = var.subnet_id
tags = var.tags
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web.id
}
output "public_ip" {
description = "Public IP of the EC2 instance"
value = aws_instance.web.public_ip
}
Next Steps
- Best Practices - Production-ready practices
- AWS Setup - Complete AWS setup guide
- Core Concepts - Deep dive into fundamentals