Best practices for composable, maintainable, and scalable Terraform infrastructure.
*Datanest Digital β datanest.dev*
6. Drift Detection & Remediation
7. Cost Estimation & Optimization
10. Troubleshooting
These patterns follow three core principles:
1. Composable: Each pattern is a self-contained unit that can be used independently or combined with others. A VPC pattern doesn't assume you'll use ECS β it outputs everything downstream patterns need.
2. Environment-aware: Every pattern accepts an environment variable and adjusts defaults accordingly. Dev gets cost-optimized settings; prod gets HA and monitoring.
3. Opinionated defaults, flexible overrides: Patterns ship with production-ready defaults but expose variables for every meaningful configuration point.
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Terragrunt Root β
β (state, provider, common tags) β
ββββββββββββββββ¬βββββββββββββββ¬ββββββββββββββββββββ€
β Dev β Staging β Prod β
β (2 AZ, min) β (2 AZ, mid)β (3 AZ, HA, mon) β
ββββββββββββββββ΄βββββββββββββββ΄ββββββββββββββββββββ€
β Pattern Library β
β βββββββ ββββββββββββ βββββββ ββββββββ ββββββββ
β β VPC β βECS Farg. β β RDS β βLambdaβ βSite ββ
β ββββ¬βββ ββββββ¬ββββββ ββββ¬βββ ββββ¬ββββ ββββ¬ββββ
β ββββββββββββ΄βββββββββββ΄ββββββββ β β
β Shared Modules β β
β ββββββββ β β
β β Tags βββββββββββββββββββββββββββββββββββββ β
β ββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
A single module that provisions "everything" becomes unmaintainable:
# DON'T: Monolithic "platform" module
module "platform" {
source = "./modules/platform"
# 200+ variables covering VPC, ECS, RDS, Lambda...
}Break infrastructure into independent patterns connected by outputs:
# DO: Compose independent patterns
module "network" {
source = "../patterns/vpc-three-tier"
environment = var.environment
vpc_cidr = "10.0.0.0/16"
}
module "database" {
source = "../patterns/rds-aurora"
vpc_id = module.network.vpc_id # β composed
environment = var.environment
}
module "app" {
source = "../patterns/ecs-fargate-service"
vpc_id = module.network.vpc_id # β composed
subnet_ids = module.network.private_subnet_ids
db_endpoint = module.database.cluster_endpoint
}Create a shared module when:
# Good shared module: consistent tagging
module "tags" {
source = "../../modules/tags"
project = var.project
environment = var.environment
team = "platform"
}Every external dependency should be a variable, never a hard-coded data source lookup inside the module:
# DON'T: Hidden dependency
data "aws_vpc" "main" {
tags = { Name = "main" } # Assumes a VPC named "main" exists
}
# DO: Explicit input
variable "vpc_id" {
description = "VPC ID where resources will be deployed"
type = string
}Use variable validation blocks to catch misconfigurations before plan:
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}Use count or for_each for optional features, not separate modules:
# Optional read replicas
resource "aws_rds_cluster_instance" "readers" {
count = var.reader_count # 0 in dev, 2 in prod
...
}Outputs are your module's API. Be generous β it's easier to ignore an output than to add one later:
output "cluster_endpoint" { value = aws_rds_cluster.main.endpoint }
output "cluster_arn" { value = aws_rds_cluster.main.arn }
output "security_group_id" { value = aws_security_group.db.id }
output "kms_key_arn" { value = aws_kms_key.db.arn }One state file per environment per pattern:
s3://my-terraform-state/
βββ dev/
β βββ vpc/terraform.tfstate
β βββ ecs/terraform.tfstate
β βββ rds/terraform.tfstate
βββ staging/
β βββ ...
βββ prod/
βββ ...
Always use DynamoDB locking. The Terragrunt root config handles this automatically, but for standalone Terraform:
backend "s3" {
bucket = "my-terraform-state"
key = "prod/vpc/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}Use terraform_remote_state or SSM Parameter Store for cross-pattern data:
# Option A: Remote state (tightly coupled)
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "${var.environment}/vpc/terraform.tfstate"
region = "us-east-1"
}
}
# Option B: SSM Parameter Store (loosely coupled, preferred)
data "aws_ssm_parameter" "vpc_id" {
name = "/${var.project}/${var.environment}/vpc/id"
}Use the tf-wrapper.sh script for all operations β it enforces:
deletion_protection = falsedeletion_protection = true# Plan against staging with prod-like settings
./scripts/tf-wrapper.sh plan -e staging
# Review the plan carefully
less .logs/plan-staging.log
# Apply to staging
./scripts/tf-wrapper.sh apply -e staging
# After validation, plan and apply to prod
./scripts/tf-wrapper.sh plan -e prod
./scripts/tf-wrapper.sh apply -e prodDrift occurs when real infrastructure diverges from Terraform state. Common causes:
# Check for drift
./scripts/tf-wrapper.sh drift -e prod
# Terraform will show a plan with unexpected changes
# Exit code 2 = drift detected1. Accept drift: Import the change into state
terraform import aws_instance.web i-1234567890abcdef02. Reject drift: Apply to revert to desired state
./scripts/tf-wrapper.sh plan -e prod
./scripts/tf-wrapper.sh apply -e prod3. Prevent drift: Use lifecycle { ignore_changes } for expected drift
lifecycle {
ignore_changes = [desired_count] # Managed by auto-scaling
}Run drift detection in CI/CD on a schedule:
# .github/workflows/drift-detection.yml
on:
schedule:
- cron: '0 8 * * 1-5' # Weekdays at 8 AM UTC
jobs:
drift-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: |
./scripts/tf-wrapper.sh drift -e prod
if [ $? -eq 2 ]; then
# Send Slack notification about drift
curl -X POST "$SLACK_WEBHOOK" \
-d '{"text":"β οΈ Infrastructure drift detected in prod"}'
fi# Estimate costs before applying
./scripts/tf-wrapper.sh cost -e prod
# Compare costs between branches
infracost diff \
--path . \
--compare-to infracost-base.json| Resource | Dev Optimization | Prod Optimization |
|---|---|---|
| RDS | Serverless v2 (min 0.5 ACU) | Right-size with Performance Insights data |
| NAT Gateway | Single NAT ($32/mo saved) | Keep multi-AZ for HA |
| ECS | 0.25 vCPU / 512 MB | Auto-scale based on actual metrics |
| CloudFront | PriceClass_100 | PriceClass_200 (covers most users) |
| S3 | No versioning | Lifecycle rules for old versions |
The tags module ensures every resource has Project, Environment, Team, and CostCenter tags, enabling AWS Cost Explorer filtering.
.tfvars files or staterandom_password + Secrets Manager (as in the RDS pattern)All patterns encrypt data at rest by default:
dynamodb:*)* resource ARNs in production policiesname: Terraform
on:
pull_request:
paths: ['terraform/**']
push:
branches: [main]
paths: ['terraform/**']
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Init
run: terraform init
- name: Validate
run: terraform validate
- name: Plan
run: terraform plan -out=tfplan
env:
TF_VAR_environment: ${{ github.base_ref == 'main' && 'prod' || 'dev' }}
- name: Comment PR
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
script: |
// Post plan output as PR comment
apply:
needs: plan
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production # Requires manual approval
steps:
- run: terraform apply tfplan# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.88.0
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
- id: terraform_docs
- id: terraform_checkov
args: ['--args=--quiet']State lock stuck
# List locks
aws dynamodb scan --table-name terraform-locks
# Force unlock (use with caution)
terraform force-unlock LOCK_IDProvider version conflicts
# Upgrade providers
terraform init -upgrade
# Pin to specific version in required_providersResource already exists
# Import existing resource into state
terraform import aws_s3_bucket.site my-bucket-nameCycle dependencies
depends_on explicitly# Show current state
terraform state list
# Show specific resource
terraform state show aws_rds_cluster.main
# Move resource in state (rename without destroy)
terraform state mv aws_instance.old aws_instance.new
# Remove from state without destroying
terraform state rm aws_instance.imported
# Refresh state from real infrastructure
terraform refresh| Pattern | Use Case | Key Features |
|---|---|---|
| VPC Three-Tier | Network foundation | Public/private/database subnets, NAT, flow logs |
| ECS Fargate | Container workloads | ALB, auto-scaling, health checks, rolling deploys |
| RDS Aurora | Relational databases | Encryption, read replicas, automated backups |
| Lambda API | Serverless APIs | API Gateway, DynamoDB, X-Ray, throttling |
| Static Site | Frontend hosting | S3, CloudFront, security headers, SPA support |
*Part of the Infrastructure as Code Patterns collection by Datanest Digital.*
*For support: hello@datanest.dev*
Production-ready Terraform patterns and Terragrunt configurations for AWS infrastructure.

![Terraform]()
![AWS]()
Stop reinventing the wheel. Battle-tested IaC patterns for VPC, ECS, RDS, Lambda, and more.
Get the full Infrastructure As Code Patterns and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.