Overview
Terraform modules are reusable packages of Terraform configurations. Create custom modules to standardize patterns across environments.
Module structure
Follow this standard structure:
modules/
└── your-module/
├── main.tf # Resource definitions
├── variables.tf # Input variables
├── outputs.tf # Output values
└── versions.tf # Provider requirements
Example: VPC module
1. Create the module directory
mkdir -p modules/vpc
2. Define resources (main.tf)
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = var.enable_dns_hostnames
enable_dns_support = var.enable_dns_support
tags = merge(var.tags, {
Name = var.name
})
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, {
Name = "${var.name}-public-${count.index + 1}"
})
}
data "aws_availability_zones" "available" {
state = "available"
}
3. Define variables (variables.tf)
variable "name" {
description = "Name prefix for VPC resources"
type = string
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "VPC CIDR must be a valid IPv4 CIDR block."
}
}
variable "public_subnet_cidrs" {
description = "List of CIDR blocks for public subnets"
type = list(string)
default = []
}
variable "enable_dns_hostnames" {
description = "Enable DNS hostnames in the VPC"
type = bool
default = true
}
variable "enable_dns_support" {
description = "Enable DNS support in the VPC"
type = bool
default = true
}
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {}
}
4. Define outputs (outputs.tf)
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "vpc_cidr" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
output "public_subnet_ids" {
description = "IDs of public subnets"
value = aws_subnet.public[*].id
}
5. Set version requirements (versions.tf)
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
Using the module
Add the module to your environment:
# environments/staging/main.tf
module "vpc" {
source = "../../modules/vpc"
name = "staging-vpc"
vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
tags = {
Environment = "staging"
}
}
Reference module outputs:
resource "aws_instance" "app" {
ami = "ami-12345678"
instance_type = "t3.micro"
subnet_id = module.vpc.public_subnet_ids[0]
tags = {
Name = "app-server"
}
}
Validating the module
make validate-env ENV=staging # Validate configuration
make plan ENV=staging # Preview changes
make apply ENV=staging # Deploy infrastructure
Best practices
Use descriptive variable names
# Good
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
}
# Avoid
variable "cidr" {
type = string
}
Add validation rules
variable "instance_type" {
description = "EC2 instance type"
type = string
validation {
condition = can(regex("^t[23]\\.", var.instance_type))
error_message = "Instance type must be t2 or t3 family."
}
}
Provide sensible defaults
variable "enable_monitoring" {
description = "Enable detailed monitoring"
type = bool
default = false
}
Use merge for tags
resource "aws_instance" "app" {
# ...
tags = merge(var.tags, {
Name = "app-server"
Role = "application"
})
}
Export useful outputs
# Individual values
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
# Grouped values
output "database" {
description = "Database connection information"
value = {
endpoint = aws_db_instance.main.endpoint
port = aws_db_instance.main.port
}
sensitive = true
}
Next steps
- OIDC Provider - Reference implementation
- Environments - Using modules across environments
- Local Development - Testing workflows