Terraform

Terraform Best Practices for Multi-Cloud

Lessons learned managing infrastructure across AWS, GCP, and Azure with reusable modules, remote state, and policy-as-code guardrails.

Running infrastructure across multiple cloud providers is increasingly common. Terraform makes this manageable, but without good practices, your codebase can quickly become a tangled mess. Here's what I've learned managing multi-cloud infrastructure at scale.

Project Structure

A clean directory structure is the foundation. I've settled on this layout:

infrastructure/
  modules/
    networking/
    compute/
    database/
    monitoring/
  environments/
    dev/
      aws/
      gcp/
    staging/
      aws/
      gcp/
    production/
      aws/
      gcp/
      azure/

Each environment directory contains a main.tf, variables.tf, outputs.tf, and backend.tf. Modules are shared across all environments and providers.

Remote State

Never store state locally. Use a remote backend with locking:

terraform {
  backend "s3" {
    bucket         = "my-org-terraform-state"
    key            = "production/aws/networking.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

For multi-cloud setups, I keep separate state files per provider and environment. This limits the blast radius when something goes wrong and speeds up plan/apply cycles.

Reusable Modules

Write modules that abstract provider-specific details behind a consistent interface:

module "vpc" {
  source = "../../modules/networking"

  environment = var.environment
  cidr_block  = "10.0.0.0/16"

  availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]

  enable_nat_gateway = true
  single_nat_gateway = var.environment != "production"

  tags = local.common_tags
}

Key rules for modules:

  • Pin versions — Always use version constraints for provider and module sources
  • Sensible defaults — Modules should work out of the box for the common case
  • No hardcoded values — Everything configurable should be a variable
  • Output what consumers need — Think about what the calling code requires

Variable Validation

Terraform supports variable validation blocks. Use them to catch mistakes early:

variable "environment" {
  type = string

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

variable "instance_type" {
  type    = string
  default = "t3.medium"

  validation {
    condition     = can(regex("^t3\\.", var.instance_type))
    error_message = "Only t3 instance types are allowed."
  }
}

Policy as Code

Use tools like OPA (Open Policy Agent) or Sentinel to enforce guardrails:

# policy/no_public_s3.rego
package terraform.s3

deny[msg] {
  resource := input.planned_values.root_module.resources[_]
  resource.type == "aws_s3_bucket"

  acl := resource.values.acl
  acl == "public-read"

  msg := sprintf("S3 bucket '%s' must not be publicly readable", [resource.name])
}

Integrate this into your CI pipeline so policy violations block the apply.

CI/CD Pipeline

A solid pipeline for Terraform looks like this:

  1. terraform fmt -check — Enforce consistent formatting
  2. terraform validate — Catch syntax errors
  3. terraform plan — Generate the execution plan
  4. Policy checks — Run OPA/Sentinel against the plan
  5. Manual approval — For production environments
  6. terraform apply — Apply with the saved plan file
# .github/workflows/terraform.yml
jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform fmt -check
      - run: terraform validate
      - run: terraform plan -out=tfplan
      - run: opa eval -d policies/ -i tfplan.json "data.terraform"

Tagging Strategy

Consistent tagging across clouds is critical for cost tracking and governance:

locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
    Project     = var.project_name
    Team        = var.team
    CostCenter  = var.cost_center
  }
}

Apply these to every resource. Your finance team will thank you.

Key Takeaways

  • Isolate state per environment and provider
  • Write modules that hide provider complexity
  • Validate inputs at the variable level
  • Enforce policies in CI, not through trust
  • Tag everything consistently across clouds

Multi-cloud Terraform is a marathon, not a sprint. Start with good structure and it scales surprisingly well.