Master Terraform with proven best practices for writing, organizing, and maintaining infrastructure as code that scales with your organization.
After managing infrastructure for dozens of production environments, I've learned that the difference between Terraform that works and Terraform that scales lies in following established best practices. Here's what I've learned the hard way.
Project Structure: Start Right
A well-organized Terraform project is easier to maintain, test, and scale. Here's the structure I recommend:
terraform/
├── modules/
│ ├── networking/
│ ├── compute/
│ └── database/
├── environments/
│ ├── dev/
│ ├── staging/
│ └── production/
├── global/
└── scripts/
Each environment should be isolated with its own state file and variable definitions. This prevents accidental changes from cascading across environments.
State Management: Your Safety Net
Remote State Storage
Never commit state files to version control. Use remote backends:
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "env/production/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
State Locking
Always enable state locking to prevent concurrent modifications. DynamoDB for AWS or Cloud Storage for GCP provide reliable locking mechanisms.
Module Design Principles
Keep Modules Focused
Each module should have a single responsibility. A "networking" module shouldn't create compute instances.
Use Semantic Versioning
Tag your modules with semantic versions:
git tag -a "v1.0.0" -m "First stable release"
Input Validation
Add validation rules to catch errors early:
variable "instance_type" {
type = string
validation {
condition = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
error_message = "Instance type must be t3.micro, t3.small, or t3.medium."
}
}
Resource Naming Conventions
Consistent naming prevents confusion and conflicts:
locals {
name_prefix = "${var.environment}-${var.project}"
}
resource "aws_instance" "web" {
tags = {
Name = "${local.name_prefix}-web-${count.index + 1}"
Environment = var.environment
ManagedBy = "Terraform"
}
}
Security Best Practices
Never Hardcode Secrets
Use environment variables or secret management services:
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "rds-password"
}
Implement Least Privilege
Create specific IAM roles for Terraform with minimal required permissions.
Enable Encryption
Always encrypt sensitive resources:
resource "aws_s3_bucket_server_side_encryption_configuration" "example" {
bucket = aws_s3_bucket.example.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
Testing Your Infrastructure
Validate Before Apply
terraform fmt -check
terraform validate
terraform plan -out=plan.tfplan
Use Terratest for Automated Testing
Write Go tests to validate your infrastructure:
func TestTerraformAwsExample(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../examples/aws",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Add assertions here
}
Collaboration Guidelines
Use Workspaces Carefully
Workspaces are not a replacement for separate environments. Use them for temporary variations, not permanent environment separation.
Document Everything
Include README files with:
- Purpose of the module/configuration
- Required variables
- Example usage
- Any prerequisites
Code Review Process
- Require PR reviews for all changes
- Run
terraform plan
in CI/CD - Use tools like tflint and checkov for static analysis
Common Pitfalls to Avoid
- Avoid Count When Possible: Use
for_each
for better resource tracking - Don't Use Local State: Always use remote state with locking
- Avoid Inline Blocks: Use dynamic blocks for repeated configurations
- Don't Ignore Deprecation Warnings: Address them promptly
- Avoid Hardcoded Values: Use variables and data sources
Advanced Patterns
Blue-Green Deployments
resource "aws_lb_target_group_attachment" "blue" {
count = var.enable_blue ? 1 : 0
target_group_arn = aws_lb_target_group.main.arn
target_id = aws_instance.blue[0].id
}
Multi-Region Deployments
Use providers with aliases:
provider "aws" {
alias = "us_west"
region = "us-west-2"
}
Monitoring and Maintenance
Track Drift
Regularly run terraform plan
to detect drift:
terraform plan -detailed-exitcode
Version Pinning
Pin provider versions to prevent unexpected changes:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
Conclusion
These best practices aren't just recommendations – they're lessons learned from managing production infrastructure. Start implementing them gradually, and you'll find your Terraform code becomes more maintainable, reliable, and scalable.
Remember: Infrastructure as Code is code. Treat it with the same rigor as your application code, and it will serve you well.
Share this article
David Childs
Consulting Systems Engineer with over 10 years of experience building scalable infrastructure and helping organizations optimize their technology stack.