Infrastructure as Code: Terraform Best Practices

After years of managing infrastructure with Terraform across multiple cloud providers and teams, I’ve developed a set of practices that help keep codebases maintainable and deployments reliable.

Project Structure

A well-organized Terraform project is crucial for team collaboration. Here’s the structure I recommend:

infrastructure/
  modules/
    networking/
      main.tf
      variables.tf
      outputs.tf
    compute/
      main.tf
      variables.tf
      outputs.tf
  environments/
    production/
      main.tf
      backend.tf
      terraform.tfvars
    staging/
      main.tf
      backend.tf
      terraform.tfvars

State Management

Remote state is non-negotiable. Use a backend with locking:

terraform {
  backend "s3" {
    bucket         = "company-terraform-state"
    key            = "production/infrastructure.tfstate"
    region         = "eu-west-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

State Management Rules

  1. One state per environment - Never share state between staging and production
  2. Enable state locking - Prevents concurrent modifications
  3. Encrypt state at rest - State files contain sensitive data
  4. Use workspaces sparingly - Prefer separate backend configurations

Module Design

Good modules are reusable, composable, and well-documented:

module "web_cluster" {
  source = "../../modules/compute"

  environment    = var.environment
  instance_type  = "t3.medium"
  min_size       = 2
  max_size       = 10

  tags = merge(var.common_tags, {
    Service = "web"
  })
}

Module Guidelines

  • Keep modules focused - One responsibility per module
  • Use semantic versioning for shared modules
  • Expose only necessary variables - Don’t make everything configurable
  • Always define outputs for inter-module communication
  • Include validation on variables where appropriate
variable "environment" {
  type        = string
  description = "Deployment environment"

  validation {
    condition     = contains(["production", "staging", "development"], var.environment)
    error_message = "Environment must be production, staging, or development."
  }
}

CI/CD Integration

Automate your Terraform workflows:

# GitLab CI example
plan:
  stage: plan
  script:
    - terraform init
    - terraform plan -out=plan.tfplan
  artifacts:
    paths:
      - plan.tfplan

apply:
  stage: deploy
  script:
    - terraform apply plan.tfplan
  when: manual
  only:
    - main

Common Pitfalls

  1. Not using terraform fmt - Enforce formatting in CI
  2. Ignoring drift - Schedule regular terraform plan checks
  3. Hardcoding values - Use variables and data sources
  4. Large monolithic states - Split into smaller, focused states
  5. Missing lifecycle rules - Use prevent_destroy for critical resources

Summary

Infrastructure as Code is only as good as the practices around it. Invest time in structure, automation, and team conventions. Your future self will thank you.