Overview
Terraform modules are reusable packages of Terraform configurations. This guide shows you how to create custom modules and use them in your environments.
Module structure
Follow this standard structure for new modules:
modules/
└── your-module/
├── main.tf # Resource definitions
├── variables.tf # Input variables
├── outputs.tf # Output values
└── versions.tf # Provider requirements
Example: Creating a 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 in an environment
Add the module to your environment configuration:
# environments/staging/main.tf
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = "staging"
ManagedBy = "terraform"
Repository = "towardsthecloud/aws-terraform-starter-kit-demo"
}
}
}
# OIDC Provider Module
module "oidc_provider" {
source = "../../modules/oidc-provider"
use_existing_oidc_provider = var.use_existing_oidc_provider
github_repo = var.github_repo
role_name = var.role_name
managed_policy_arns = var.managed_policy_arns
}
# VPC Module
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"
}
}
Using module outputs
Reference module outputs in your resources:
# Use VPC outputs in other resources
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
Test your module:
# Validate
make validate-env ENV=staging
# Plan
make plan ENV=staging
# Apply
make apply ENV=staging
Module best practices
1. Use descriptive variable names
# Good
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
}
# Avoid
variable "cidr" {
type = string
}
2. 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."
}
}
3. Provide sensible defaults
variable "enable_monitoring" {
description = "Enable detailed monitoring"
type = bool
default = false
}
4. Use merge for tags
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {}
}
resource "aws_instance" "app" {
# ...
tags = merge(var.tags, {
Name = "app-server"
Role = "application"
})
}
5. Export useful outputs
# Export individual values
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
# Or export objects
output "database" {
description = "Database connection information"
value = {
endpoint = aws_db_instance.main.endpoint
port = aws_db_instance.main.port
}
sensitive = true
}
Next steps
- Review the OIDC Provider Module reference
- Learn about Environments for using modules across environments
- Explore Local Development for testing workflows