Terraform Multi-Cloud Patterns
Design Terraform for multi-cloud: provider composition, modules, state management, workspaces, and CI/CD pipelines.
Prerequisites
- Intermediate Terraform knowledge (modules, state, providers)
- Experience with at least one cloud provider
Terraform for Multi-Cloud Infrastructure
Terraform is the de facto standard for multi-cloud Infrastructure as Code (IaC). Its provider-based architecture supports AWS, Azure, GCP, OCI, Kubernetes, and hundreds of other platforms through a single workflow: write HCL, plan changes, apply infrastructure. Unlike cloud-specific IaC tools (CloudFormation, Bicep, Deployment Manager), Terraform provides a consistent language and workflow across all providers.
However, multi-cloud Terraform is not simply writing resources for each provider in one configuration. Production multi-cloud Terraform requires careful patterns for provider composition, module design, state management, workspace organization, and CI/CD integration. This guide covers these patterns with real-world examples.
Terraform vs OpenTofu
OpenTofu is an open-source fork of Terraform maintained by the Linux Foundation, created after HashiCorp changed Terraform's license to BSL. The two tools are largely compatible. All patterns in this guide work with both Terraform and OpenTofu. Choose based on your licensing requirements and ecosystem preferences.
Provider Composition Patterns
Multi-cloud Terraform configurations need multiple providers configured simultaneously. Use provider aliases for multi-region deployments within a single cloud, and separate provider blocks for cross-cloud resources.
# Multi-cloud provider configuration
terraform {
required_version = ">= 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
# AWS provider with multi-region aliases
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
ManagedBy = "terraform"
Environment = var.environment
}
}
}
provider "aws" {
alias = "us_west_2"
region = "us-west-2"
default_tags {
tags = {
ManagedBy = "terraform"
Environment = var.environment
}
}
}
# Azure provider
provider "azurerm" {
features {}
subscription_id = var.azure_subscription_id
}
# GCP provider
provider "google" {
project = var.gcp_project_id
region = "us-central1"
}
# Example: Cross-cloud VPN between AWS and GCP
resource "aws_vpn_gateway" "to_gcp" {
vpc_id = aws_vpc.main.id
}
resource "google_compute_ha_vpn_gateway" "to_aws" {
name = "aws-vpn-gateway"
network = google_compute_network.main.id
region = "us-central1"
}Module Design for Multi-Cloud
Effective module design is the foundation of maintainable multi-cloud Terraform. There are two philosophies: cloud-specific modules (one module per provider, exposed through a common interface) and abstraction modules (one module that wraps multiple providers behind a unified interface). Cloud-specific modules are more practical because cloud services rarely map 1:1.
# Module structure for multi-cloud project
# modules/
# ├── aws/
# │ ├── vpc/ # AWS VPC module
# │ ├── eks/ # AWS EKS module
# │ ├── rds/ # AWS RDS module
# │ └── s3/ # AWS S3 module
# ├── azure/
# │ ├── vnet/ # Azure VNet module
# │ ├── aks/ # Azure AKS module
# │ ├── sql/ # Azure SQL module
# │ └── storage/ # Azure Storage module
# ├── gcp/
# │ ├── vpc/ # GCP VPC module
# │ ├── gke/ # GCP GKE module
# │ ├── cloudsql/ # GCP Cloud SQL module
# │ └── gcs/ # GCP GCS module
# └── shared/
# ├── dns/ # Cross-cloud DNS
# ├── vpn/ # Cross-cloud VPN
# └── monitoring/ # Cross-cloud monitoring
# Example: Cloud-specific VPC module with common interface
# modules/aws/vpc/main.tf
variable "name" { type = string }
variable "cidr" { type = string }
variable "azs" { type = list(string) }
resource "aws_vpc" "main" {
cidr_block = var.cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = var.name }
}
output "vpc_id" { value = aws_vpc.main.id }
output "cidr" { value = aws_vpc.main.cidr_block }
# modules/gcp/vpc/main.tf
variable "name" { type = string }
variable "cidr" { type = string }
resource "google_compute_network" "main" {
name = var.name
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "main" {
name = "${var.name}-subnet"
ip_cidr_range = var.cidr
network = google_compute_network.main.id
}
output "network_id" { value = google_compute_network.main.id }
output "cidr" { value = google_compute_subnetwork.main.ip_cidr_range }State Management
Terraform state stores the mapping between your HCL configuration and real infrastructure. For multi-cloud, the state management strategy determines how teams collaborate, how changes are isolated, and how blast radius is minimized.
State Backend Options
| Backend | Provider | Locking | Best For |
|---|---|---|---|
| S3 + DynamoDB | AWS | Yes (DynamoDB) | AWS-primary organizations |
| Azure Storage | Azure | Yes (native blob lease) | Azure-primary organizations |
| GCS | GCP | Yes (native object lock) | GCP-primary organizations |
| Terraform Cloud / HCP | HashiCorp | Yes | Multi-cloud, enterprise features |
# S3 backend for multi-cloud state
terraform {
backend "s3" {
bucket = "myorg-terraform-state"
key = "multi-cloud/production/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
kms_key_id = "alias/terraform-state"
}
}
# State file organization pattern:
# s3://myorg-terraform-state/
# ├── aws/
# │ ├── networking/production/terraform.tfstate
# │ ├── compute/production/terraform.tfstate
# │ └── databases/production/terraform.tfstate
# ├── azure/
# │ ├── networking/production/terraform.tfstate
# │ └── compute/production/terraform.tfstate
# ├── gcp/
# │ ├── networking/production/terraform.tfstate
# │ └── compute/production/terraform.tfstate
# └── cross-cloud/
# ├── vpn/terraform.tfstate
# └── dns/terraform.tfstateMinimize State File Scope
Never put all your multi-cloud infrastructure in a single state file. A single Terraform apply that touches AWS, Azure, and GCP simultaneously means a failure in one provider blocks all changes. Split state files by provider, layer (networking, compute, data), and environment. Use terraform_remote_state data sources or outputs to share values between state files.
Workspace Organization
# Using Terraform workspaces for environments
# terraform workspace new staging
# terraform workspace new production
# terraform workspace select production
locals {
env = terraform.workspace
config = {
staging = {
aws_instance_type = "t3.small"
azure_vm_size = "Standard_B2s"
gcp_machine_type = "e2-small"
min_replicas = 1
max_replicas = 5
}
production = {
aws_instance_type = "m5.xlarge"
azure_vm_size = "Standard_D4s_v5"
gcp_machine_type = "n2-standard-4"
min_replicas = 3
max_replicas = 50
}
}
current = local.config[local.env]
}Cross-State References
# Reference outputs from another state file
data "terraform_remote_state" "aws_networking" {
backend = "s3"
config = {
bucket = "myorg-terraform-state"
key = "aws/networking/production/terraform.tfstate"
region = "us-east-1"
}
}
data "terraform_remote_state" "gcp_networking" {
backend = "s3"
config = {
bucket = "myorg-terraform-state"
key = "gcp/networking/production/terraform.tfstate"
region = "us-east-1"
}
}
# Use outputs from both states
resource "aws_vpn_connection" "to_gcp" {
vpn_gateway_id = data.terraform_remote_state.aws_networking.outputs.vpn_gateway_id
customer_gateway_id = aws_customer_gateway.gcp.id
type = "ipsec.1"
}
resource "aws_customer_gateway" "gcp" {
bgp_asn = 65002
ip_address = data.terraform_remote_state.gcp_networking.outputs.vpn_gateway_ip
type = "ipsec.1"
}CI/CD Pipeline for Multi-Cloud Terraform
# GitHub Actions workflow for multi-cloud Terraform
name: Multi-Cloud Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
plan:
runs-on: ubuntu-latest
strategy:
matrix:
layer: [aws-networking, azure-networking, gcp-networking, cross-cloud-vpn]
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
# AWS OIDC authentication
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/TerraformRole
aws-region: us-east-1
# Azure OIDC authentication
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# GCP OIDC authentication
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
- name: Terraform Plan
run: |
cd layers/${{ matrix.layer }}
terraform init
terraform plan -out=tfplan
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: |
cd layers/${{ matrix.layer }}
terraform apply -auto-approve tfplanBest Practices Summary
| Practice | Recommendation |
|---|---|
| Provider versions | Pin major versions, use ~> for minor updates |
| State isolation | Separate state per provider, layer, and environment |
| Module versioning | Semantic versioning with git tags for internal modules |
| Authentication | OIDC federation (never store credentials in CI/CD) |
| Testing | Terratest or tftest for module validation |
| Policy | OPA/Sentinel for policy-as-code enforcement |
| Drift detection | Scheduled terraform plan to detect manual changes |
Use Terragrunt for DRY Multi-Cloud
Terragrunt is a wrapper around Terraform that helps keep configurations DRY (Don't Repeat Yourself). It provides features for managing multiple Terraform modules, environments, and state files with minimal boilerplate. Terragrunt is especially valuable for multi-cloud setups where you have similar configurations across providers and environments.
Key Takeaways
- 1Use provider aliases for multi-region and separate provider blocks for multi-cloud.
- 2Design cloud-specific modules with common interfaces rather than abstract multi-cloud modules.
- 3Split state files by provider, layer, and environment to minimize blast radius.
- 4Use OIDC federation for CI/CD authentication instead of stored credentials.
Frequently Asked Questions
Should I put all clouds in one Terraform state file?
Should I create abstraction modules that work across clouds?
Written by CloudToolStack Team
Cloud engineers and architects with hands-on experience across AWS, Azure, and GCP. We write guides based on real-world production patterns, not just documentation rewrites.
Disclaimer: This guide is for educational purposes. Cloud services change frequently; always refer to official documentation for the latest information. AWS, Azure, and GCP are trademarks of their respective owners.