Skip to main content
Multi-CloudDevOps & IaCintermediate

Terraform Multi-Cloud Patterns

Design Terraform for multi-cloud: provider composition, modules, state management, workspaces, and CI/CD pipelines.

CloudToolStack Team24 min readPublished Mar 14, 2026

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.

hcl
# 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.

hcl
# 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

BackendProviderLockingBest For
S3 + DynamoDBAWSYes (DynamoDB)AWS-primary organizations
Azure StorageAzureYes (native blob lease)Azure-primary organizations
GCSGCPYes (native object lock)GCP-primary organizations
Terraform Cloud / HCPHashiCorpYesMulti-cloud, enterprise features
hcl
# 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.tfstate

Minimize 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

hcl
# 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

hcl
# 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

yaml
# 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 tfplan

Best Practices Summary

PracticeRecommendation
Provider versionsPin major versions, use ~> for minor updates
State isolationSeparate state per provider, layer, and environment
Module versioningSemantic versioning with git tags for internal modules
AuthenticationOIDC federation (never store credentials in CI/CD)
TestingTerratest or tftest for module validation
PolicyOPA/Sentinel for policy-as-code enforcement
Drift detectionScheduled 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.

GCP Terraform GuideCloudFormation vs CDK Guide

Key Takeaways

  1. 1Use provider aliases for multi-region and separate provider blocks for multi-cloud.
  2. 2Design cloud-specific modules with common interfaces rather than abstract multi-cloud modules.
  3. 3Split state files by provider, layer, and environment to minimize blast radius.
  4. 4Use OIDC federation for CI/CD authentication instead of stored credentials.

Frequently Asked Questions

Should I put all clouds in one Terraform state file?
No. A single state touching all providers means one failure blocks all changes. Split by provider, layer, and environment. Use terraform_remote_state to share values.
Should I create abstraction modules that work across clouds?
Generally no. Cloud services rarely map 1:1. Create cloud-specific modules with common variable interfaces for consistency.

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.