Terraform Best Practices

Following Terraform best practices ensures your infrastructure is secure, maintainable, and scalable. This guide covers production-ready practices for Terraform.

File Organization

Standard Project Structure

terraform-project/
├── main.tf              # Main configuration
├── variables.tf         # Input variables
├── outputs.tf           # Output values
├── terraform.tfvars     # Variable values
├── versions.tf          # Provider requirements
├── README.md            # Documentation
├── .gitignore           # Git ignore rules
└── modules/             # Reusable modules
    ├── vpc/
    ├── ec2/
    └── rds/

Separate Environments

environments/
├── dev/
│   ├── main.tf
│   └── terraform.tfvars
├── staging/
│   ├── main.tf
│   └── terraform.tfvars
└── production/
    ├── main.tf
    └── terraform.tfvars

Version Management

Pin Terraform Version

# versions.tf
terraform {
  required_version = ">= 1.0"
  
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
💡 Tip: Pin to specific versions in production: version = "5.0.0"

State Management

Use Remote State

terraform {
  backend "s3" {
    bucket         = "terraform-state-bucket"
    key            = "terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}
✅ Best Practices:
  • ✅ Always use remote state in production
  • ✅ Enable state locking (DynamoDB, etc.)
  • ✅ Enable encryption for state files
  • ✅ Enable versioning on state storage
  • ✅ Never commit state files to Git

Security

Never Commit Secrets

# .gitignore
*.tfstate
*.tfstate.*
.terraform/
.terraform.lock.hcl
*.tfvars
!terraform.tfvars.example
*.tfplan

Use Environment Variables

# For AWS credentials
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."

# Or use AWS profiles
export AWS_PROFILE="production"

Use Secret Management

# Use AWS Secrets Manager
data "aws_secretsmanager_secret" "db_password" {
  name = "prod/db/password"
}

data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = data.aws_secretsmanager_secret.db_password.id
}

Variables Best Practices

Always Provide Descriptions

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t2.micro"
  
  validation {
    condition     = can(regex("^t[23]\\.", var.instance_type))
    error_message = "Instance type must be t2 or t3 family."
  }
}

Use Sensitive Variables

variable "db_password" {
  description = "Database password"
  type        = string
  sensitive   = true
}

Resource Management

Use Tags Consistently

locals {
  common_tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
    Owner       = var.team_name
  }
}

resource "aws_instance" "web" {
  tags = merge(local.common_tags, {
    Name = "${var.environment}-web-server"
  })
}

Use Data Sources

# Instead of hardcoding AMI IDs
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

resource "aws_instance" "web" {
  ami = data.aws_ami.amazon_linux.id
}

Module Best Practices

✅ Module Guidelines:
  • ✅ One module per logical component
  • ✅ Use descriptive module names
  • ✅ Document modules with README.md
  • ✅ Pin module versions in production
  • ✅ Expose all important outputs
  • ✅ Use variables for all configurable values

CI/CD Integration

Terraform Cloud/Enterprise

# terraform.yml (GitHub Actions)
name: Terraform

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v1
    
    - name: Terraform Init
      run: terraform init
    
    - name: Terraform Validate
      run: terraform validate
    
    - name: Terraform Plan
      run: terraform plan
      continue-on-error: true
    
    - name: Terraform Apply
      if: github.ref == 'refs/heads/main'
      run: terraform apply -auto-approve

Common Mistakes to Avoid

⚠️ Common Mistakes:
  • ❌ Committing state files to Git
  • ❌ Hardcoding credentials
  • ❌ Not using remote state
  • ❌ Not pinning versions
  • ❌ Not using modules for reusable code
  • ❌ Not documenting variables
  • ❌ Ignoring terraform plan output
  • ❌ Not testing changes in dev/staging first

Next Steps