Overview
Terraform modules are the building blocks of scalable infrastructure. This guide covers creating reusable modules for the most common AWS infrastructure patterns.
Project Structure
text
terraform/
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── ecs-cluster/
│ ├── rds/
│ └── s3-bucket/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ └── prod/
│ ├── main.tf
│ ├── terraform.tfvars
│ └── backend.tf
└── global/
└── s3-remote-state/
Remote State Setup
global/s3-remote-state/main.tf
# S3 bucket for Terraform state
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-org-terraform-state"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
# DynamoDB for state locking
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
VPC Module
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.name}-vpc"
})
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(var.tags, { Name = "${var.name}-igw" })
}
# Public subnets
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, {
Name = "${var.name}-public-${var.availability_zones[count.index]}"
Tier = "public"
})
}
# Private subnets
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
availability_zone = var.availability_zones[count.index]
tags = merge(var.tags, {
Name = "${var.name}-private-${var.availability_zones[count.index]}"
Tier = "private"
})
}
# NAT Gateway
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? 1 : 0
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? 1 : 0
allocation_id = aws_eip.nat[0].id
subnet_id = aws_subnet.public[0].id
tags = merge(var.tags, { Name = "${var.name}-nat" })
}
modules/vpc/variables.tf
variable "name" {
description = "Name prefix for all resources"
type = string
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
}
variable "enable_nat_gateway" {
description = "Whether to create a NAT Gateway"
type = bool
default = true
}
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {}
}
Using the Module
environments/prod/main.tf
terraform {
backend "s3" {
bucket = "my-org-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
}
module "vpc" {
source = "../../modules/vpc"
name = "prod"
vpc_cidr = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
enable_nat_gateway = true
tags = {
Environment = "production"
Team = "platform"
ManagedBy = "terraform"
}
}
Terraform Workflow
workflow.sh
# Initialize (download providers and modules)
terraform init
# Format code
terraform fmt -recursive
# Validate configuration
terraform validate
# Plan changes (dry run)
terraform plan -var-file=terraform.tfvars -out=tfplan
# Apply approved plan
terraform apply tfplan
# Destroy (with confirmation)
terraform destroy -var-file=terraform.tfvars
Tip
Always save your terraform plan output to a file with -out=tfplan and only apply that exact plan. This prevents drift between what you reviewed and what gets applied.