Using Terraform Modules for Reusable Infrastructure Components on AWS
Learn how to create and use Terraform modules to build reusable, maintainable, and scalable infrastructure components on AWS
Using Terraform Modules for Reusable Infrastructure Components on AWS
Terraform modules are containers for multiple resources that are used together. This guide shows you how to create and use modules to build reusable infrastructure components on AWS.
Video Tutorial
Learn more about using Terraform Modules in AWS in this comprehensive video tutorial:
Prerequisites
- AWS CLI configured with appropriate permissions
- Terraform installed (version 1.0.0 or later)
- Basic understanding of Terraform and AWS
- Git for version control
Project Structure
terraform-modules/
├── main.tf
├── variables.tf
├── outputs.tf
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── ec2/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── rds/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── alb/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── terraform.tfvars
Creating Reusable Modules
VPC Module
Create 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.project_name}-vpc"
}
)
}
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_subnet" "public" {
count = var.az_count
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]
map_public_ip_on_launch = true
tags = merge(
var.tags,
{
Name = "${var.project_name}-public-${count.index + 1}"
Type = "Public"
}
)
}
resource "aws_subnet" "private" {
count = var.az_count
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + var.az_count)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = merge(
var.tags,
{
Name = "${var.project_name}-private-${count.index + 1}"
Type = "Private"
}
)
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(
var.tags,
{
Name = "${var.project_name}-igw"
}
)
}
resource "aws_eip" "nat" {
count = var.az_count
vpc = true
tags = merge(
var.tags,
{
Name = "${var.project_name}-nat-eip-${count.index + 1}"
}
)
}
resource "aws_nat_gateway" "main" {
count = var.az_count
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = merge(
var.tags,
{
Name = "${var.project_name}-nat-${count.index + 1}"
}
)
depends_on = [aws_internet_gateway.main]
}
Create modules/vpc/variables.tf:
variable "project_name" {
description = "Name of the project"
type = string
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
}
variable "az_count" {
description = "Number of AZs to use"
type = number
}
variable "tags" {
description = "Tags to apply to resources"
type = map(string)
default = {}
}
Create modules/vpc/outputs.tf:
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "IDs of public subnets"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "IDs of private subnets"
value = aws_subnet.private[*].id
}
output "nat_gateway_ids" {
description = "IDs of NAT Gateways"
value = aws_nat_gateway.main[*].id
}
EC2 Module
Create modules/ec2/main.tf:
resource "aws_security_group" "main" {
name = "${var.project_name}-${var.name}-sg"
description = "Security group for ${var.name}"
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.ingress_rules
content {
description = ingress.value.description
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = lookup(ingress.value, "cidr_blocks", null)
security_groups = lookup(ingress.value, "security_groups", null)
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(
var.tags,
{
Name = "${var.project_name}-${var.name}-sg"
}
)
}
resource "aws_launch_template" "main" {
name_prefix = "${var.project_name}-${var.name}-lt"
image_id = var.ami_id
instance_type = var.instance_type
network_interfaces {
associate_public_ip_address = var.associate_public_ip
security_groups = [aws_security_group.main.id]
}
user_data = var.user_data != null ? base64encode(var.user_data) : null
tag_specifications {
resource_type = "instance"
tags = merge(
var.tags,
{
Name = "${var.project_name}-${var.name}"
}
)
}
}
resource "aws_autoscaling_group" "main" {
count = var.create_asg ? 1 : 0
name = "${var.project_name}-${var.name}-asg"
desired_capacity = var.desired_capacity
max_size = var.max_size
min_size = var.min_size
target_group_arns = var.target_group_arns
vpc_zone_identifier = var.subnet_ids
launch_template {
id = aws_launch_template.main.id
version = "$Latest"
}
dynamic "tag" {
for_each = merge(
var.tags,
{
Name = "${var.project_name}-${var.name}"
}
)
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
}
Create modules/ec2/variables.tf:
variable "project_name" {
description = "Name of the project"
type = string
}
variable "name" {
description = "Name of the EC2 component"
type = string
}
variable "vpc_id" {
description = "ID of the VPC"
type = string
}
variable "subnet_ids" {
description = "IDs of subnets"
type = list(string)
}
variable "ami_id" {
description = "ID of the AMI"
type = string
}
variable "instance_type" {
description = "Instance type"
type = string
}
variable "associate_public_ip" {
description = "Whether to associate public IP"
type = bool
default = false
}
variable "ingress_rules" {
description = "Ingress rules for security group"
type = list(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = optional(list(string))
security_groups = optional(list(string))
}))
}
variable "user_data" {
description = "User data script"
type = string
default = null
}
variable "create_asg" {
description = "Whether to create an ASG"
type = bool
default = false
}
variable "desired_capacity" {
description = "Desired capacity for ASG"
type = number
default = 1
}
variable "min_size" {
description = "Minimum size for ASG"
type = number
default = 1
}
variable "max_size" {
description = "Maximum size for ASG"
type = number
default = 1
}
variable "target_group_arns" {
description = "ARNs of target groups"
type = list(string)
default = []
}
variable "tags" {
description = "Tags to apply to resources"
type = map(string)
default = {}
}
Using the Modules
Create main.tf:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
provider "aws" {
region = var.aws_region
}
locals {
common_tags = {
Environment = var.environment
Project = var.project_name
Terraform = "true"
}
}
module "vpc" {
source = "./modules/vpc"
project_name = var.project_name
vpc_cidr = var.vpc_cidr
az_count = var.az_count
tags = local.common_tags
}
module "web_servers" {
source = "./modules/ec2"
project_name = var.project_name
name = "web"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
ami_id = var.web_ami_id
instance_type = var.web_instance_type
associate_public_ip = false
create_asg = true
desired_capacity = 2
min_size = 2
max_size = 4
ingress_rules = [
{
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [module.alb.security_group_id]
}
]
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
EOF
tags = local.common_tags
}
Module Best Practices
-
Module Structure
- Keep modules focused and single-purpose
- Use consistent file structure
- Include README documentation
-
Input Variables
- Use descriptive variable names
- Provide default values when appropriate
- Use variable validation
-
Outputs
- Export necessary values
- Use descriptive output names
- Document output usage
-
Versioning
- Use semantic versioning
- Tag releases
- Document changes
Module Examples
Simple VPC Usage
module "vpc" {
source = "./modules/vpc"
project_name = "my-app"
vpc_cidr = "10.0.0.0/16"
az_count = 2
tags = {
Environment = "prod"
Team = "infrastructure"
}
}
Web Server with ASG
module "web_servers" {
source = "./modules/ec2"
project_name = "my-app"
name = "web"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
ami_id = "ami-0735c191cf914754d"
instance_type = "t3.micro"
create_asg = true
desired_capacity = 2
min_size = 2
max_size = 4
ingress_rules = [
{
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
tags = {
Environment = "prod"
Service = "web"
}
}
Module Organization
-
Local Modules
- Store in
modules/directory - Use relative paths
- Good for project-specific modules
- Store in
-
Remote Modules
- Store in Git repositories
- Use version tags
- Good for shared modules
-
Registry Modules
- Publish to Terraform Registry
- Follow registry guidelines
- Good for public modules
Testing Modules
- Kitchen-Terraform
driver:
name: terraform
provisioner:
name: terraform
platforms:
- name: aws
verifier:
name: terraform
systems:
- name: default
backend: local
suites:
- name: default
- Terratest
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
)
func TestVpcModule(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../examples/vpc",
Vars: map[string]interface{}{
"project_name": "test",
"vpc_cidr": "10.0.0.0/16",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
}
Conclusion
You’ve learned how to create and use Terraform modules for AWS infrastructure. This approach provides:
- Reusable infrastructure components
- Consistent configurations
- Maintainable code
- Scalable architecture
Remember to:
- Follow module best practices
- Document your modules
- Test thoroughly
- Version control your modules