Infrastructure as Code with Terraform
Project Structure
terraform/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ └── terraform.tfvars
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── ecs-service/
│ └── rds/
└── shared/
└── backend.tf
Module Design
# modules/ecs-service/main.tf
variable "name" { type = string }
variable "image" { type = string }
variable "cpu" { type = number; default = 256 }
variable "memory" { type = number; default = 512 }
variable "environment_variables" {
type = map(string)
default = {}
}
resource "aws_ecs_task_definition" "this" {
family = var.name
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.cpu
memory = var.memory
container_definitions = jsonencode([{
name = var.name
image = var.image
portMappings = [{ containerPort = 3000 }]
environment = [
for k, v in var.environment_variables : { name = k, value = v }
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/${var.name}"
"awslogs-region" = data.aws_region.current.name
"awslogs-stream-prefix" = "ecs"
}
}
}])
}
output "task_definition_arn" {
value = aws_ecs_task_definition.this.arn
}
Using Modules
# environments/prod/main.tf
module "api_service" {
source = "../../modules/ecs-service"
name = "api-prod"
image = "123456789.dkr.ecr.us-east-1.amazonaws.com/api:${var.image_tag}"
cpu = 512
memory = 1024
environment_variables = {
NODE_ENV = "production"
DB_HOST = module.rds.endpoint
REDIS_URL = "redis://${module.redis.endpoint}:6379"
}
}
Remote State with S3
# backend.tf
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock" # Prevent concurrent applies
}
}
# Create the lock table
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute { name = "LockID"; type = "S" }
}
Workspaces for Environments
# Create workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
# Switch workspace
terraform workspace select prod
# Use in config
locals {
env_config = {
dev = { instance_type = "t3.small"; min_count = 1; max_count = 3 }
staging = { instance_type = "t3.medium"; min_count = 2; max_count = 5 }
prod = { instance_type = "t3.large"; min_count = 3; max_count = 20 }
}
config = local.env_config[terraform.workspace]
}
CI/CD Integration
# .github/workflows/terraform.yml
- name: Terraform Init
run: terraform init -backend-config="key=${{ github.ref_name }}/terraform.tfstate"
- name: Terraform Plan
run: terraform plan -out=tfplan -var="image_tag=${{ github.sha }}"
- name: Terraform Apply (main branch only)
if: github.ref == 'refs/heads/main'
run: terraform apply tfplan
Drift Detection
# Detect config drift (manual changes in console)
terraform plan -detailed-exitcode
# Exit code 0: no changes
# Exit code 1: error
# Exit code 2: diff found (drift!)
Terraform modules enable DRY infrastructure code that can be reused across environments and teams.