| name | terraform-patterns |
| description | Terraform infrastructure as code patterns and best practices. Use when writing Terraform configurations, creating modules, managing state, or designing IaC architectures. |
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