Infrastructure as Code with Terraform
Manage infrastructure as code. Covers Terraform modules, state management, workspace patterns, drift detection, testing IaC, and enterprise-scale Terraform architecture.
Infrastructure as Code (IaC) means managing servers, networks, and services through version-controlled code instead of clicking through cloud consoles. Terraform is the dominant multi-cloud IaC tool, but using it well at enterprise scale requires patterns for state management, module design, and team collaboration that aren’t obvious from the documentation.
Terraform Architecture
├── modules/ # Reusable, versioned modules
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/
│ └── database/
│
├── environments/ # Environment-specific configs
│ ├── dev/
│ │ ├── main.tf # References modules
│ │ ├── terraform.tfvars # Dev-specific values
│ │ └── backend.tf # Dev state backend
│ ├── staging/
│ └── production/
│
└── .github/
└── workflows/
└── terraform.yml # CI/CD pipeline
State Management
| Approach | Best For | Risk |
|---|---|---|
| S3 + DynamoDB lock | AWS teams | Low (standard) |
| Terraform Cloud | Multi-cloud teams | Low (managed) |
| GCS + lock | GCP teams | Low (standard) |
| Local state | Learning only | High (never in production) |
# Remote state with S3 + DynamoDB locking
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "production/networking/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
Module Design
# modules/rds/main.tf
resource "aws_db_instance" "main" {
identifier = "${var.environment}-${var.name}"
engine = var.engine
engine_version = var.engine_version
instance_class = var.instance_class
allocated_storage = var.allocated_storage
max_allocated_storage = var.max_allocated_storage
storage_encrypted = true
multi_az = var.environment == "production"
backup_retention_period = var.environment == "production" ? 30 : 7
deletion_protection = var.environment == "production"
vpc_security_group_ids = [aws_security_group.db.id]
db_subnet_group_name = aws_db_subnet_group.main.name
tags = merge(var.tags, {
Environment = var.environment
ManagedBy = "terraform"
})
}
CI/CD Pipeline
# terraform-ci.yml
on:
pull_request:
paths: ['environments/**', 'modules/**']
jobs:
plan:
steps:
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan -out=tfplan
- name: Post plan to PR comment
run: |
terraform show -no-color tfplan > plan.txt
gh pr comment $PR_NUMBER --body "$(cat plan.txt)"
# Apply only on merge to main
apply:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- run: terraform apply -auto-approve tfplan
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Clicking in console | Drift, no audit trail, unreproducible | All changes through Terraform |
| Monolithic state | Slow plans, blast radius = everything | Split state per environment/service |
| No module versioning | Breaking changes affect all environments | Tag and version modules, pin in consumers |
| Local state | State on developer laptop → data loss | Remote backend with locking |
terraform apply locally | No review, no CI checks | Apply only through CI/CD pipeline |
| Hardcoded values | Can’t reuse across environments | Variables with .tfvars per environment |
Checklist
- Remote state backend with locking
- State split by environment and service
- Modules versioned and reusable
- CI/CD pipeline: format → validate → plan → apply
- Plan output posted to PR for review
- Apply only through CI/CD (never local)
- Drift detection: scheduled plan to detect manual changes
- Tagging policy: all resources tagged (environment, team, managed-by)
- State encryption at rest
- import existing resources before managing them
:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For IaC consulting, visit garnetgrid.com. :::