Terraform Patterns
Best practices for Terraform infrastructure as code.
Project Structure
infrastructure/ ├── modules/ │ ├── networking/ │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ │ └── README.md │ ├── compute/ │ └── database/ ├── environments/ │ ├── dev/ │ │ ├── main.tf │ │ ├── terraform.tfvars │ │ └── backend.tf │ ├── staging/ │ └── prod/ ├── global/ │ ├── iam/ │ └── dns/ └── scripts/ └── init-backend.sh
Module Design
Reusable Module Pattern
modules/vpc/variables.tf
variable "name" { description = "Name prefix for resources" type = string }
variable "environment" { description = "Environment name" type = string validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "Environment must be dev, staging, or prod." } }
variable "cidr_block" { description = "VPC CIDR block" 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) }
variable "tags" { description = "Additional tags" type = map(string) default = {} }
modules/vpc/main.tf
locals { common_tags = merge(var.tags, { Module = "vpc" Environment = var.environment ManagedBy = "terraform" }) }
resource "aws_vpc" "main" { cidr_block = var.cidr_block enable_dns_hostnames = true enable_dns_support = true
tags = merge(local.common_tags, { Name = "${var.name}-vpc" }) }
resource "aws_subnet" "public" { count = length(var.availability_zones) vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.cidr_block, 8, count.index) availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(local.common_tags, { Name = "${var.name}-public-${count.index + 1}" Tier = "public" }) }
resource "aws_subnet" "private" { count = length(var.availability_zones) vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.cidr_block, 8, count.index + 10) availability_zone = var.availability_zones[count.index]
tags = merge(local.common_tags, { Name = "${var.name}-private-${count.index + 1}" Tier = "private" }) }
modules/vpc/outputs.tf
output "vpc_id" { description = "VPC ID" value = aws_vpc.main.id }
output "public_subnet_ids" { description = "List of public subnet IDs" value = aws_subnet.public[*].id }
output "private_subnet_ids" { description = "List of private subnet IDs" value = aws_subnet.private[*].id }
output "vpc_cidr_block" { description = "VPC CIDR block" value = aws_vpc.main.cidr_block }
State Management
Remote Backend Configuration
backend.tf
terraform { backend "s3" { bucket = "company-terraform-state" key = "environments/prod/terraform.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "terraform-locks"
# Assume role for cross-account access
role_arn = "arn:aws:iam::ACCOUNT_ID:role/TerraformStateAccess"
} }
State Locking Table
global/state-backend/main.tf
resource "aws_s3_bucket" "terraform_state" { bucket = "company-terraform-state"
lifecycle { prevent_destroy = true } }
resource "aws_s3_bucket_versioning" "terraform_state" { bucket = aws_s3_bucket.terraform_state.id versioning_configuration { status = "Enabled" } }
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" { bucket = aws_s3_bucket.terraform_state.id rule { apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" } } }
resource "aws_dynamodb_table" "terraform_locks" { name = "terraform-locks" billing_mode = "PAY_PER_REQUEST" hash_key = "LockID"
attribute { name = "LockID" type = "S" } }
Provider Configuration
Multi-Region Setup
provider "aws" { region = var.primary_region
default_tags { tags = { Environment = var.environment Project = var.project_name ManagedBy = "terraform" } } }
provider "aws" { alias = "dr" region = var.dr_region
default_tags { tags = { Environment = var.environment Project = var.project_name ManagedBy = "terraform" } } }
Version Constraints
terraform { required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "> 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "> 2.23"
}
}
}
Data Sources and Lookups
Look up existing resources
data "aws_vpc" "selected" { filter { name = "tag:Environment" values = [var.environment] } }
data "aws_ami" "amazon_linux" { most_recent = true owners = ["amazon"]
filter { name = "name" values = ["amzn2-ami-hvm-*-x86_64-gp2"] } }
data "aws_caller_identity" "current" {} data "aws_region" "current" {}
Conditional Resources
Create resource only in production
resource "aws_cloudwatch_metric_alarm" "high_cpu" { count = var.environment == "prod" ? 1 : 0
alarm_name = "${var.name}-high-cpu" comparison_operator = "GreaterThanThreshold" evaluation_periods = 2 metric_name = "CPUUtilization" namespace = "AWS/EC2" period = 300 statistic = "Average" threshold = 80 }
Dynamic blocks for optional configurations
resource "aws_security_group" "main" { name = "${var.name}-sg" vpc_id = var.vpc_id
dynamic "ingress" { for_each = var.ingress_rules content { from_port = ingress.value.from_port to_port = ingress.value.to_port protocol = ingress.value.protocol cidr_blocks = ingress.value.cidr_blocks } } }
Lifecycle Rules
resource "aws_instance" "web" { ami = data.aws_ami.amazon_linux.id instance_type = var.instance_type
lifecycle { create_before_destroy = true prevent_destroy = var.environment == "prod"
ignore_changes = [
tags["LastModified"],
user_data,
]
} }
Testing with Terratest
// test/vpc_test.go package test
import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert" )
func TestVpcModule(t *testing.T) { terraformOptions := &terraform.Options{ TerraformDir: "../modules/vpc", Vars: map[string]interface{}{ "name": "test", "environment": "dev", "cidr_block": "10.0.0.0/16", "availability_zones": []string{"us-east-1a", "us-east-1b"}, }, }
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
}
CI/CD Integration
.github/workflows/terraform.yml
name: Terraform
on: pull_request: paths: - 'infrastructure/' push: branches: [main] paths: - 'infrastructure/'
jobs: plan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.5.0
- name: Terraform Init
run: terraform init
working-directory: infrastructure/environments/${{ github.event.inputs.environment }}
- name: Terraform Plan
run: terraform plan -out=tfplan
working-directory: infrastructure/environments/${{ github.event.inputs.environment }}
- name: Upload Plan
uses: actions/upload-artifact@v4
with:
name: tfplan
path: infrastructure/environments/${{ github.event.inputs.environment }}/tfplan
References
-
Terraform Documentation
-
Terraform Best Practices
-
AWS Provider Documentation