← Back to blog

2026-03-20 · 5 min read

Terraform Best Practices: From Chaos to Predictable Infrastructure

How to structure Terraform code for teams of 5-50 engineers, with real patterns that scale and prevent disasters.

#terraform#infrastructure-as-code#aws#devops#iac
Terraform Best Practices: From Chaos to Predictable Infrastructure

Terraform Best Practices: From Chaos to Predictable Infrastructure

I've seen Terraform implementations go from beautiful, maintainable codebases to unmanageable spaghetti. The difference? Following proven patterns and best practices from day one.

After leading Terraform adoption across multiple organizations, I've developed a comprehensive approach that scales from 5 to 50+ engineers. This post covers the patterns that actually work in production.

The Foundation: Repository Structure

Monorepo vs Multi-Repo Debate

Monorepo (Recommended for most teams):

terraform/
├── environments/
│   ├── dev/
│   ├── staging/
│   └── prod/
├── modules/
│   ├── vpc/
│   ├── ecs/
│   ├── rds/
│   └── security/
├── shared/
│   ├── data.tf
│   └── providers.tf
└── scripts/
    ├── plan.sh
    └── apply.sh

When to use Multi-Repo:

  • Highly regulated industries
  • Teams with different deployment cadences
  • Strong organizational boundaries

Environment Separation

Each environment gets its own directory with:

  • main.tf - Environment-specific resources
  • terraform.tfvars - Environment variables
  • backend.tf - State backend configuration

Module Design Principles

1. Single Responsibility Principle

Each module should do one thing well. A VPC module shouldn't create EC2 instances.

Good Module Structure:

modules/vpc/
├── main.tf
├── variables.tf
├── outputs.tf
├── README.md
└── examples/
    └── basic/

2. Composition over Inheritance

Use module composition instead of complex inheritance patterns.

# Bad: Trying to do everything in one module
module "monolith" {
  source = "./modules/everything"
  create_vpc    = true
  create_rds    = true
  create_redis  = true
  # ... 20 more parameters
}

# Good: Compose simple modules
module "network" {
  source = "./modules/vpc"
  # Simple, focused parameters
}

module "database" {
  source = "./modules/rds"
  vpc_id = module.network.vpc_id
  # Clear dependencies
}

State Management

Remote State with Locking

Always use remote state with locking. S3 + DynamoDB is the gold standard.

terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "environments/prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

State File Organization

  • One state file per environment
  • Never share state between environments
  • Use terraform workspace for variations within environments

Variable Management

Variable Definition Patterns

# variables.tf
variable "environment" {
  description = "Environment name"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "instance_count" {
  description = "Number of EC2 instances"
  type        = number
  default     = 1
  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 10
    error_message = "Instance count must be between 1 and 10."
  }
}

Environment-Specific Values

Use .tfvars files for environment-specific values:

# environments/prod/terraform.tfvars
environment   = "prod"
instance_type = "m5.large"
instance_count = 3

# environments/dev/terraform.tfvars
environment   = "dev"
instance_type = "t3.micro"
instance_count = 1

Resource Organization

Resource Naming Conventions

Consistent naming prevents conflicts and improves readability:

# Good naming
resource "aws_instance" "web" {
  # environment-service-purpose-index
  tags = {
    Name = "${var.environment}-web-app-001"
  }
}

# Consistent tagging
resource "aws_instance" "web" {
  tags = merge(local.common_tags, {
    Name        = "${var.environment}-web-app"
    Component   = "application"
    Owner       = "platform-team"
    CostCenter  = "engineering"
  })
}

Resource Dependencies

Be explicit about dependencies:

resource "aws_security_group" "web" {
  # Security group rules depend on VPC
  vpc_id = aws_vpc.main.id
}

resource "aws_instance" "web" {
  # Instance depends on security group
  vpc_security_group_ids = [aws_security_group.web.id]
  depends_on = [aws_security_group.web]
}

Data Sources Best Practices

When to Use Data Sources

  • Reference existing resources
  • Get information from other Terraform configurations
  • Access AWS account information
# Get current AWS account info
data "aws_caller_identity" "current" {}

# Reference existing VPC
data "aws_vpc" "existing" {
  filter {
    name   = "tag:Name"
    values = ["${var.environment}-vpc"]
  }
}

Provider Configuration

Provider Version Pinning

Always pin provider versions:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

Multiple Provider Configurations

# Primary region
provider "aws" {
  region = "us-east-1"
}

# DR region
provider "aws" {
  alias  = "dr"
  region = "us-west-2"
}

Error Handling and Validation

Pre-Conditions and Post-Conditions

resource "aws_instance" "web" {
  instance_type = var.instance_type

  lifecycle {
    precondition {
      condition     = contains(["t3.micro", "t3.small", "m5.large"], var.instance_type)
      error_message = "Instance type must be approved for production use."
    }
  }
}

Validation Rules

Use validation rules extensively:

variable "cidr_block" {
  type = string
  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "CIDR block must be valid."
  }
}

CI/CD Integration

Automated Testing

# .github/workflows/terraform.yml
name: Terraform
on: [pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: hashicorp/setup-terraform@v2
      - run: terraform init
      - run: terraform validate
      - run: terraform plan -no-color

Automated Deployment

# Deploy on merge to main
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v3
      - uses: hashicorp/setup-terraform@v2
      - run: terraform apply -auto-approve

Security Best Practices

Secrets Management

Never hardcode secrets:

# Bad
resource "aws_db_instance" "db" {
  password = "supersecretpassword"
}

# Good
resource "aws_db_instance" "db" {
  password = var.db_password
}

# Even better - use AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db" {
  secret_id = aws_secretsmanager_secret.db.id
}

Least Privilege IAM

Create minimal IAM policies:

resource "aws_iam_role_policy" "ec2" {
  role = aws_iam_role.ec2.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ec2:DescribeInstances",
          "ec2:DescribeImages"
        ]
        Resource = "*"
      }
    ]
  })
}

Performance Optimization

Parallelism Control

# Control parallelism for large deployments
terraform apply -parallelism=10

Targeted Updates

# Only plan changes to specific resources
terraform plan -target=aws_instance.web

Documentation and Knowledge Sharing

README Files

Every module needs documentation:

# VPC Module

This module creates a VPC with public and private subnets.

## Usage

```hcl
module "vpc" {
  source = "./modules/vpc"

  cidr_block = "10.0.0.0/16"
  environment = "prod"
}

Requirements

  • AWS provider >= 4.0

Resources Created

  • VPC
  • Internet Gateway
  • NAT Gateway
  • Public/Private Subnets
  • Route Tables

### Code Comments

```hcl
# This security group allows HTTP/HTTPS traffic from anywhere
# Used for load balancers and public-facing services
resource "aws_security_group" "web" {
  # ...
}

Team Workflow Patterns

Branching Strategy

# Feature branch workflow
git checkout -b feature/add-redis-cluster
terraform plan  # Test changes
terraform apply # Deploy to dev first

Code Review Checklist

  • Variables validated
  • Resources tagged consistently
  • Security groups follow least privilege
  • State backend configured
  • Documentation updated

Monitoring and Alerting

Drift Detection

# Check for configuration drift
terraform plan -detailed-exitcode

Cost Estimation

# Estimate costs before applying
terraform plan -out=tfplan
terraform show -json tfplan | jq '.planned_values'

Migration Strategies

From Manual Infrastructure

  1. Start with low-risk resources (S3 buckets, IAM policies)
  2. Gradually migrate stateful resources
  3. Use terraform import for existing resources

Refactoring Existing Code

  1. Create new modules alongside existing code
  2. Migrate resources gradually
  3. Update state file carefully

Common Anti-Patterns to Avoid

1. Large Monolithic Configurations

Problem: Everything in one file, hard to maintain Solution: Break into modules and environments

2. Hardcoded Values

Problem: No flexibility, hard to reuse Solution: Use variables and locals

3. No State Locking

Problem: Concurrent modifications corrupt state Solution: Use DynamoDB for locking

4. No Version Control

Problem: No history, no collaboration Solution: Git with proper branching

Tools and Ecosystem

Essential Tools

  • terraform-docs: Auto-generate documentation
  • tflint: Linting for Terraform
  • tfsec: Security scanning
  • terragrunt: Wrapper for multi-environment management

IDE Support

  • VS Code with HashiCorp Terraform extension
  • IntelliJ IDEA with Terraform plugin

Measuring Success

Track these metrics:

  • Deployment frequency
  • Mean time to provision
  • Infrastructure incidents
  • Code review turnaround time

Final Thoughts

🔗 See it in action: My terraform-aws-reference-architecture repo applies every pattern in this post, modular structure, remote state, CI validation, and progressive examples from a single EC2 to a full VPC with private RDS. Clone it and adapt to your environment.

Terraform is powerful but requires discipline. The patterns above have helped teams scale from startup chaos to enterprise-grade infrastructure management.

Start with the basics: proper structure, consistent naming, and automated testing. Build from there.

The goal isn't perfect Terraform code, it's reliable, maintainable infrastructure that enables your business to move fast.

What's your biggest Terraform challenge? How do you structure your Terraform code? Share in the comments.

Share:LinkedInXWhatsApp

Related articles

Reactions & comments