Terraform Best Practices for Production Infrastructure
Essential patterns and practices for managing production infrastructure with Terraform.
On this page +
After years of managing cloud infrastructure with Terraform, we’ve learned what works and what doesn’t. Here are our battle-tested best practices.
State Management
Your Terraform state is the source of truth for your infrastructure. Protect it.
Use Remote State
Never store state locally for production infrastructure:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/infrastructure.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
State Locking
Always enable state locking to prevent concurrent modifications. DynamoDB works great with S3 backends.
Module Structure
Organize your Terraform code into reusable modules:
infrastructure/
├── modules/
│ ├── vpc/
│ ├── ec2/
│ └── rds/
├── environments/
│ ├── dev/
│ ├── staging/
│ └── prod/
└── global/
└── iam/
Variables and Outputs
Use Descriptive Variable Names
variable "environment" {
description = "Deployment environment (dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
Export Important Outputs
Make it easy to reference resources across modules:
output "vpc_id" {
description = "The ID of the VPC"
value = aws_vpc.main.id
}
Tagging Strategy
Consistent tagging is crucial for cost allocation and resource management:
locals {
common_tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "terraform"
Owner = var.team_email
}
}
Plan Before Apply
Always review your changes:
terraform plan -out=tfplan
terraform apply tfplan
This ensures you apply exactly what you reviewed.
Ready to Level Up?
Whether you’re starting fresh or migrating existing infrastructure to Terraform, we can help. Contact us for a consultation.
Related notes
Pymantic Release, Part 2: module_versions.json, or How We Stopped Find-And-Replacing Production
Fifty-seven independently versioned modules met twenty subscription directories. Someone had to blink.
Pymantic Release, Part 1: One Repo, Sixty Modules, and the Lie of a Single Version Number
How a 300-line Python script replaced our entire release process, and the four separate ways it was quietly wrong.
Peering, Part 3: Overrides, Exclusions, and Importing a Legacy Estate
Generating uniform infrastructure is easy. Generating infrastructure that matches what is already deployed is the job.