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:
terraform fmt -check— Enforce consistent formattingterraform validate— Catch syntax errorsterraform plan— Generate the execution plan- Policy checks — Run OPA/Sentinel against the plan
- Manual approval — For production environments
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.