Best Practices

Terraform Best Practices 2025: Structure, State, Security, and CI/CD

By CloudFormation Team ยท 2025-06-10 ยท 13 min read

Project Structure

A consistent project structure prevents confusion as your team and codebase grow. Use this pattern for medium to large projects:

project/
โ”œโ”€โ”€ main.tf          # Root module โ€” call child modules
โ”œโ”€โ”€ variables.tf     # All input variable declarations
โ”œโ”€โ”€ outputs.tf       # All output declarations
โ”œโ”€โ”€ providers.tf     # Provider and version constraints
โ”œโ”€โ”€ versions.tf      # terraform {} block with required_version
โ”œโ”€โ”€ locals.tf        # Local values (avoid inline locals)
โ”œโ”€โ”€ data.tf          # Data source lookups
โ”œโ”€โ”€ terraform.tfvars.example  # Example values (no secrets!)
โ””โ”€โ”€ modules/
    โ”œโ”€โ”€ networking/
    โ”œโ”€โ”€ compute/
    โ””โ”€โ”€ database/

State Management

State is the most operationally sensitive part of Terraform. Follow these rules:

  • Always use remote backends โ€” S3+DynamoDB, Azure Blob, GCS, or Terraform Cloud
  • Enable state locking โ€” DynamoDB for S3, always enabled for Terraform Cloud
  • Enable state encryption โ€” encrypt = true in your S3 backend
  • Never commit state files โ€” add *.tfstate and *.tfstate.backup to .gitignore
  • Separate state per environment โ€” dev, staging, prod must never share state
terraform {
  backend "s3" {
    bucket         = "company-tf-state-prod"
    key            = "services/api/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-locks"
  }
}

Module Design

Modules should be small, focused, and reusable. Follow these principles:

  • One module per logical resource group (VPC, compute cluster, database)
  • Never nest more than 3 levels deep
  • Always document input/output variables with description
  • Pin module versions when using the Terraform Registry
  • Use count or for_each instead of duplicating resources
variable "instance_count" {
  description = "Number of EC2 instances to launch"
  type        = number
  default     = 2
  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 20
    error_message = "Instance count must be between 1 and 20."
  }
}

Security Best Practices

  • Use OIDC for CI/CD authentication โ€” no long-lived AWS access keys in GitHub Secrets
  • Least-privilege IAM โ€” your Terraform IAM user/role should have only what it needs
  • Secrets via Secrets Manager โ€” never put passwords or API keys in .tfvars or state
  • Run tfsec or Checkov โ€” catch misconfigurations before they reach production
  • Enable S3 versioning on state bucket โ€” allows rolling back accidental state corruption

Testing Terraform Code

Treat infrastructure code with the same rigour as application code:

  • terraform validate โ€” syntax and type checking, run in every PR
  • tflint โ€” provider-specific linting rules for AWS, Azure, GCP
  • Checkov / tfsec โ€” security scanning before apply
  • Terratest (Go) โ€” write unit and integration tests for modules
  • Infracost โ€” cost estimation before merge to prevent surprise cloud bills

Try CloudFormation Free

Design infrastructure visually and generate production-ready Terraform in seconds โ€” 693 resources across 9 cloud providers.

๐Ÿš€ Open the Designer โ†’