Terraform Patterns
Project Structure
infrastructure/
├── modules/ # Reusable modules
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── compute/
│ ├── database/
│ └── monitoring/
├── environments/ # Environment-specific configs
│ ├── dev/
│ │ ├── main.tf
│ │ ├── backend.tf
│ │ ├── terraform.tfvars
│ │ └── providers.tf
│ ├── staging/
│ └── production/
├── global/ # Shared resources (IAM, DNS)
└── scripts/
├── plan.sh
└── apply.sh
State Management
# backend.tf — Remote state with S3 + DynamoDB locking
terraform {
backend "s3" {
bucket = "myorg-terraform-state"
key = "production/networking/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
kms_key_id = "alias/terraform-state"
}
}
Module Design
# modules/networking/variables.tf
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "Must be a valid CIDR block."
}
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Must be dev, staging, or production."
}
}
variable "tags" {
description = "Common tags for all resources"
type = map(string)
default = {}
}
Resource Tagging Convention
locals {
common_tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "terraform"
Owner = var.team_name
CostCenter = var.cost_center
CreatedAt = timestamp()
}
}
Best Practices
- Remote state — Always use encrypted remote state with locking
- Modules — Create reusable modules for common patterns
- Variables — Add validation rules and descriptions to all variables
- State isolation — Separate state per environment and component
- Provider versioning — Pin provider versions with
~>constraints - Plan before apply — Always review plans, never auto-apply for production
- Drift detection — Run
terraform planon schedule to detect drift - Import existing — Use
terraform importfor brownfield resources - Workspaces — Use for minor variations; separate configs for major differences
- Data sources — Reference existing resources instead of hardcoding IDs
CI/CD Integration
# GitHub Actions — Terraform plan on PR, apply on merge
- name: Terraform Plan
run: |
terraform init -backend-config=env/${{ env.ENVIRONMENT }}/backend.hcl
terraform plan -var-file=env/${{ env.ENVIRONMENT }}/terraform.tfvars -out=tfplan
terraform show -json tfplan > plan.json
- name: Post Plan to PR
uses: actions/github-script@v7
with:
script: |
// Post plan output as PR comment
Anti-Patterns to Avoid
- ❌ Hardcoded resource IDs
- ❌ Using
latestAMIs without data sources - ❌ Storing secrets in
.tfvarsfiles - ❌ Single monolithic state file
- ❌ Manual console changes alongside Terraform
- ❌ Ignoring provider version constraints
Source: bipinks/ghost-office — distributed by TomeVault.