AWS-Specific Modules
Creating reusable modules for AWS infrastructure patterns accelerates development and ensures consistency across projects. However, AWS-specific modules need to handle the complexity of AWS services, regional differences, and the various configuration options that make AWS both powerful and complicated.
This part covers patterns for building robust, reusable AWS modules that encapsulate best practices while remaining flexible enough for different use cases.
VPC Module with Best Practices
A comprehensive VPC module that handles common networking patterns:
# modules/aws-vpc/variables.tf
variable "name" {
description = "Name prefix for all resources"
type = string
}
variable "cidr_block" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "Must be a valid CIDR block."
}
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
default = []
}
variable "enable_nat_gateway" {
description = "Enable NAT Gateway for private subnets"
type = bool
default = true
}
variable "single_nat_gateway" {
description = "Use a single NAT Gateway for all private subnets"
type = bool
default = false
}
variable "enable_vpn_gateway" {
description = "Enable VPN Gateway"
type = bool
default = false
}
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 = "Additional tags for all resources"
type = map(string)
default = {}
}
# modules/aws-vpc/main.tf
data "aws_availability_zones" "available" {
state = "available"
}
locals {
# Use provided AZs or default to first 3 available
azs = length(var.availability_zones) > 0 ? var.availability_zones : slice(data.aws_availability_zones.available.names, 0, 3)
# Calculate subnet CIDRs
public_subnet_cidrs = [
for i, az in local.azs :
cidrsubnet(var.cidr_block, 8, i + 1)
]
private_subnet_cidrs = [
for i, az in local.azs :
cidrsubnet(var.cidr_block, 8, i + 11)
]
database_subnet_cidrs = [
for i, az in local.azs :
cidrsubnet(var.cidr_block, 8, i + 21)
]
common_tags = merge(var.tags, {
ManagedBy = "terraform"
})
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = var.enable_dns_hostnames
enable_dns_support = var.enable_dns_support
tags = merge(local.common_tags, {
Name = "${var.name}-vpc"
})
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(local.common_tags, {
Name = "${var.name}-igw"
})
}
# Public Subnets
resource "aws_subnet" "public" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = local.public_subnet_cidrs[count.index]
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = merge(local.common_tags, {
Name = "${var.name}-public-${count.index + 1}"
Type = "public"
Tier = "public"
})
}
# Private Subnets
resource "aws_subnet" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = local.private_subnet_cidrs[count.index]
availability_zone = local.azs[count.index]
tags = merge(local.common_tags, {
Name = "${var.name}-private-${count.index + 1}"
Type = "private"
Tier = "application"
})
}
# Database Subnets
resource "aws_subnet" "database" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = local.database_subnet_cidrs[count.index]
availability_zone = local.azs[count.index]
tags = merge(local.common_tags, {
Name = "${var.name}-database-${count.index + 1}"
Type = "private"
Tier = "database"
})
}
# NAT Gateways
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(local.azs)) : 0
domain = "vpc"
depends_on = [aws_internet_gateway.main]
tags = merge(local.common_tags, {
Name = "${var.name}-nat-eip-${count.index + 1}"
})
}
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(local.azs)) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = merge(local.common_tags, {
Name = "${var.name}-nat-${count.index + 1}"
})
depends_on = [aws_internet_gateway.main]
}
# Route Tables
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = merge(local.common_tags, {
Name = "${var.name}-public-rt"
Type = "public"
})
}
resource "aws_route_table" "private" {
count = var.enable_nat_gateway ? length(local.azs) : 1
vpc_id = aws_vpc.main.id
dynamic "route" {
for_each = var.enable_nat_gateway ? [1] : []
content {
cidr_block = "0.0.0.0/0"
nat_gateway_id = var.single_nat_gateway ? aws_nat_gateway.main[0].id : aws_nat_gateway.main[count.index].id
}
}
tags = merge(local.common_tags, {
Name = "${var.name}-private-rt-${count.index + 1}"
Type = "private"
})
}
# Route Table Associations
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
count = length(aws_subnet.private)
subnet_id = aws_subnet.private[count.index].id
route_table_id = var.enable_nat_gateway ? aws_route_table.private[count.index].id : aws_route_table.private[0].id
}
# VPN Gateway (optional)
resource "aws_vpn_gateway" "main" {
count = var.enable_vpn_gateway ? 1 : 0
vpc_id = aws_vpc.main.id
tags = merge(local.common_tags, {
Name = "${var.name}-vpn-gateway"
})
}
# modules/aws-vpc/outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "vpc_cidr_block" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
output "public_subnet_ids" {
description = "IDs of the public subnets"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "IDs of the private subnets"
value = aws_subnet.private[*].id
}
output "database_subnet_ids" {
description = "IDs of the database subnets"
value = aws_subnet.database[*].id
}
output "internet_gateway_id" {
description = "ID of the Internet Gateway"
value = aws_internet_gateway.main.id
}
output "nat_gateway_ids" {
description = "IDs of the NAT Gateways"
value = aws_nat_gateway.main[*].id
}
output "availability_zones" {
description = "List of availability zones used"
value = local.azs
}
Application Load Balancer Module
A comprehensive ALB module with security best practices:
# modules/aws-alb/variables.tf
variable "name" {
description = "Name for the load balancer"
type = string
}
variable "vpc_id" {
description = "VPC ID where the load balancer will be created"
type = string
}
variable "subnet_ids" {
description = "List of subnet IDs for the load balancer"
type = list(string)
}
variable "certificate_arn" {
description = "ARN of the SSL certificate"
type = string
default = null
}
variable "enable_deletion_protection" {
description = "Enable deletion protection"
type = bool
default = true
}
variable "idle_timeout" {
description = "Connection idle timeout in seconds"
type = number
default = 60
}
variable "enable_http2" {
description = "Enable HTTP/2"
type = bool
default = true
}
variable "ip_address_type" {
description = "IP address type (ipv4 or dualstack)"
type = string
default = "ipv4"
}
variable "target_groups" {
description = "Map of target group configurations"
type = map(object({
port = number
protocol = string
target_type = string
health_check_path = string
health_check_matcher = string
health_check_timeout = number
health_check_interval = number
healthy_threshold = number
unhealthy_threshold = number
}))
default = {}
}
variable "listeners" {
description = "Map of listener configurations"
type = map(object({
port = number
protocol = string
certificate_arn = string
ssl_policy = string
default_action = object({
type = string
target_group_name = string
redirect_config = object({
status_code = string
protocol = string
port = string
})
})
}))
default = {}
}
variable "tags" {
description = "Additional tags"
type = map(string)
default = {}
}
# modules/aws-alb/main.tf
locals {
common_tags = merge(var.tags, {
ManagedBy = "terraform"
})
}
# Security Group for ALB
resource "aws_security_group" "alb" {
name_prefix = "${var.name}-alb-"
vpc_id = var.vpc_id
description = "Security group for ${var.name} ALB"
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${var.name}-alb-sg"
})
lifecycle {
create_before_destroy = true
}
}
# Application Load Balancer
resource "aws_lb" "main" {
name = var.name
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.subnet_ids
enable_deletion_protection = var.enable_deletion_protection
idle_timeout = var.idle_timeout
enable_http2 = var.enable_http2
ip_address_type = var.ip_address_type
access_logs {
bucket = aws_s3_bucket.alb_logs.bucket
prefix = "alb-logs"
enabled = true
}
tags = merge(local.common_tags, {
Name = var.name
})
}
# S3 bucket for ALB access logs
resource "aws_s3_bucket" "alb_logs" {
bucket = "${var.name}-alb-logs-${random_id.bucket_suffix.hex}"
force_destroy = true
tags = local.common_tags
}
resource "random_id" "bucket_suffix" {
byte_length = 4
}
resource "aws_s3_bucket_lifecycle_configuration" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
rule {
id = "delete_old_logs"
status = "Enabled"
expiration {
days = 90
}
}
}
resource "aws_s3_bucket_policy" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
AWS = data.aws_elb_service_account.main.arn
}
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.alb_logs.arn}/*"
},
{
Effect = "Allow"
Principal = {
Service = "delivery.logs.amazonaws.com"
}
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.alb_logs.arn}/*"
Condition = {
StringEquals = {
"s3:x-amz-acl" = "bucket-owner-full-control"
}
}
}
]
})
}
data "aws_elb_service_account" "main" {}
# Target Groups
resource "aws_lb_target_group" "main" {
for_each = var.target_groups
name = "${var.name}-${each.key}"
port = each.value.port
protocol = each.value.protocol
vpc_id = var.vpc_id
target_type = each.value.target_type
health_check {
enabled = true
healthy_threshold = each.value.healthy_threshold
unhealthy_threshold = each.value.unhealthy_threshold
timeout = each.value.health_check_timeout
interval = each.value.health_check_interval
path = each.value.health_check_path
matcher = each.value.health_check_matcher
port = "traffic-port"
protocol = each.value.protocol
}
tags = merge(local.common_tags, {
Name = "${var.name}-${each.key}-tg"
})
}
# Listeners
resource "aws_lb_listener" "main" {
for_each = var.listeners
load_balancer_arn = aws_lb.main.arn
port = each.value.port
protocol = each.value.protocol
certificate_arn = each.value.certificate_arn
ssl_policy = each.value.ssl_policy
default_action {
type = each.value.default_action.type
dynamic "target_group_arn" {
for_each = each.value.default_action.type == "forward" ? [1] : []
content {
target_group_arn = aws_lb_target_group.main[each.value.default_action.target_group_name].arn
}
}
dynamic "redirect" {
for_each = each.value.default_action.type == "redirect" ? [1] : []
content {
port = each.value.default_action.redirect_config.port
protocol = each.value.default_action.redirect_config.protocol
status_code = each.value.default_action.redirect_config.status_code
}
}
}
tags = local.common_tags
}
RDS Module with High Availability
A production-ready RDS module with backup and monitoring:
# modules/aws-rds/main.tf
resource "aws_db_subnet_group" "main" {
name = "${var.name}-db-subnet-group"
subnet_ids = var.subnet_ids
tags = merge(var.tags, {
Name = "${var.name}-db-subnet-group"
})
}
resource "aws_security_group" "rds" {
name_prefix = "${var.name}-rds-"
vpc_id = var.vpc_id
description = "Security group for ${var.name} RDS instance"
ingress {
description = "Database access"
from_port = var.port
to_port = var.port
protocol = "tcp"
security_groups = var.allowed_security_groups
}
tags = merge(var.tags, {
Name = "${var.name}-rds-sg"
})
lifecycle {
create_before_destroy = true
}
}
resource "aws_db_instance" "main" {
identifier = var.name
# Engine configuration
engine = var.engine
engine_version = var.engine_version
instance_class = var.instance_class
# Storage configuration
allocated_storage = var.allocated_storage
max_allocated_storage = var.max_allocated_storage
storage_type = var.storage_type
storage_encrypted = var.storage_encrypted
kms_key_id = var.kms_key_id
# Database configuration
db_name = var.database_name
username = var.username
password = var.password
port = var.port
# Network configuration
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
publicly_accessible = var.publicly_accessible
# Backup configuration
backup_retention_period = var.backup_retention_period
backup_window = var.backup_window
maintenance_window = var.maintenance_window
# High availability
multi_az = var.multi_az
# Monitoring
monitoring_interval = var.monitoring_interval
monitoring_role_arn = var.monitoring_interval > 0 ? aws_iam_role.rds_monitoring[0].arn : null
# Performance Insights
performance_insights_enabled = var.performance_insights_enabled
performance_insights_kms_key_id = var.performance_insights_kms_key_id
performance_insights_retention_period = var.performance_insights_retention_period
# Deletion protection
deletion_protection = var.deletion_protection
skip_final_snapshot = var.skip_final_snapshot
final_snapshot_identifier = var.skip_final_snapshot ? null : "${var.name}-final-snapshot-${formatdate("YYYY-MM-DD-hhmm", timestamp())}"
tags = merge(var.tags, {
Name = var.name
})
}
# IAM role for enhanced monitoring
resource "aws_iam_role" "rds_monitoring" {
count = var.monitoring_interval > 0 ? 1 : 0
name = "${var.name}-rds-monitoring-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "monitoring.rds.amazonaws.com"
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "rds_monitoring" {
count = var.monitoring_interval > 0 ? 1 : 0
role = aws_iam_role.rds_monitoring[0].name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"
}
# CloudWatch alarms
resource "aws_cloudwatch_metric_alarm" "database_cpu" {
alarm_name = "${var.name}-database-cpu-utilization"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/RDS"
period = "300"
statistic = "Average"
threshold = "80"
alarm_description = "This metric monitors RDS CPU utilization"
dimensions = {
DBInstanceIdentifier = aws_db_instance.main.id
}
alarm_actions = var.alarm_actions
tags = var.tags
}
resource "aws_cloudwatch_metric_alarm" "database_connections" {
alarm_name = "${var.name}-database-connection-count"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "DatabaseConnections"
namespace = "AWS/RDS"
period = "300"
statistic = "Average"
threshold = var.max_connections_threshold
alarm_description = "This metric monitors RDS connection count"
dimensions = {
DBInstanceIdentifier = aws_db_instance.main.id
}
alarm_actions = var.alarm_actions
tags = var.tags
}
ECS Fargate Module
A complete ECS Fargate module for containerized applications:
# modules/aws-ecs-fargate/main.tf
resource "aws_ecs_cluster" "main" {
name = var.cluster_name
configuration {
execute_command_configuration {
kms_key_id = var.kms_key_id
logging = "OVERRIDE"
log_configuration {
cloud_watch_encryption_enabled = true
cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs_exec.name
}
}
}
setting {
name = "containerInsights"
value = var.enable_container_insights ? "enabled" : "disabled"
}
tags = var.tags
}
resource "aws_ecs_cluster_capacity_providers" "main" {
cluster_name = aws_ecs_cluster.main.name
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
default_capacity_provider_strategy {
base = 1
weight = 100
capacity_provider = "FARGATE"
}
}
# Task Definition
resource "aws_ecs_task_definition" "main" {
family = var.service_name
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = var.cpu
memory = var.memory
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([
{
name = var.container_name
image = var.container_image
portMappings = [
{
containerPort = var.container_port
protocol = "tcp"
}
]
environment = [
for key, value in var.environment_variables : {
name = key
value = value
}
]
secrets = [
for key, value in var.secrets : {
name = key
valueFrom = value
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.app.name
awslogs-region = data.aws_region.current.name
awslogs-stream-prefix = "ecs"
}
}
healthCheck = var.health_check_command != null ? {
command = var.health_check_command
interval = 30
timeout = 5
retries = 3
startPeriod = 60
} : null
essential = true
}
])
tags = var.tags
}
# ECS Service
resource "aws_ecs_service" "main" {
name = var.service_name
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.main.arn
desired_count = var.desired_count
capacity_provider_strategy {
capacity_provider = "FARGATE"
weight = var.fargate_weight
base = var.fargate_base
}
capacity_provider_strategy {
capacity_provider = "FARGATE_SPOT"
weight = var.fargate_spot_weight
}
network_configuration {
security_groups = concat([aws_security_group.ecs_service.id], var.additional_security_groups)
subnets = var.subnet_ids
assign_public_ip = var.assign_public_ip
}
dynamic "load_balancer" {
for_each = var.target_group_arn != null ? [1] : []
content {
target_group_arn = var.target_group_arn
container_name = var.container_name
container_port = var.container_port
}
}
deployment_configuration {
maximum_percent = 200
minimum_healthy_percent = 100
}
enable_execute_command = var.enable_execute_command
tags = var.tags
depends_on = [
aws_iam_role_policy_attachment.ecs_execution,
aws_cloudwatch_log_group.app
]
}
# Auto Scaling
resource "aws_appautoscaling_target" "ecs_target" {
count = var.enable_autoscaling ? 1 : 0
max_capacity = var.max_capacity
min_capacity = var.min_capacity
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "ecs_policy_cpu" {
count = var.enable_autoscaling ? 1 : 0
name = "${var.service_name}-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs_target[0].resource_id
scalable_dimension = aws_appautoscaling_target.ecs_target[0].scalable_dimension
service_namespace = aws_appautoscaling_target.ecs_target[0].service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = var.cpu_target_value
}
}
What’s Next
AWS-specific modules provide the building blocks for consistent, well-architected infrastructure, but monitoring and maintaining that infrastructure requires comprehensive observability and compliance automation.
In the next part, we’ll explore monitoring and compliance patterns that provide visibility into your AWS infrastructure, automate compliance checks, and integrate with AWS native monitoring services.