Creating Modules

Create and use custom Terraform modules for reusable infrastructure patterns.


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