Flat-style split illustration with an orange left side showing a steaming orange mug labeled 'MONDAY', and a blue right side depicting Terraform’s multi-cloud icon branching out to AWS, Azure, and Google Cloud logos, connected to icons for databases, containers, and cost efficiency

Monday Cloud Tip: Terraform Multi-Cloud Module Design – Infrastructure That Works Everywhere

Your weekly dose of actionable cloud wisdom to start the week right

The Problem

Your organisation is locked into one cloud provider because your infrastructure code is a mess of provider-specific resources scattered across hundreds of Terraform files. You want to avoid vendor lock-in and leverage best-of-breed services from different clouds, but your current Terraform setup makes multi-cloud deployment a nightmare. Meanwhile, teams are copying and pasting configurations, creating drift and maintenance headaches.

The Solution

Design reusable Terraform modules that abstract cloud provider differences whilst leveraging each platform’s strengths. Build infrastructure that can deploy consistently across AWS, Azure, and GCP with minimal changes, enabling true multi-cloud flexibility and reducing vendor dependence.

Essential Multi-Cloud Module Patterns:

1. Provider-Agnostic Database Module

# modules/database/variables.tf
variable "name" {
  description = "Database name"
  type        = string
}

variable "cloud_provider" {
  description = "Cloud provider (aws, azure, gcp)"
  type        = string
  validation {
    condition     = contains(["aws", "azure", "gcp"], var.cloud_provider)
    error_message = "Cloud provider must be aws, azure, or gcp."
  }
}

variable "environment" {
  description = "Environment (dev, staging, prod)"
  type        = string
}

variable "instance_class" {
  description = "Database instance size"
  type        = string
  default     = "small"
}

variable "storage_size_gb" {
  description = "Storage size in GB"
  type        = number
  default     = 100
}

variable "backup_retention_days" {
  description = "Backup retention period in days"
  type        = number
  default     = 7
}

variable "multi_az" {
  description = "Enable multi-AZ deployment"
  type        = bool
  default     = false
}

variable "vpc_id" {
  description = "VPC/VNet ID for database deployment"
  type        = string
}

variable "subnet_ids" {
  description = "Subnet IDs for database deployment"
  type        = list(string)
}

variable "allowed_cidr_blocks" {
  description = "CIDR blocks allowed to access the database"
  type        = list(string)
  default     = []
}

# modules/database/locals.tf
locals {
  # Provider-specific instance size mapping
  instance_sizes = {
    aws = {
      small  = "db.t3.micro"
      medium = "db.t3.small" 
      large  = "db.t3.medium"
      xlarge = "db.t3.large"
    }
    azure = {
      small  = "GP_Gen5_2"
      medium = "GP_Gen5_4"
      large  = "GP_Gen5_8"
      xlarge = "GP_Gen5_16"
    }
    gcp = {
      small  = "db-n1-standard-1"
      medium = "db-n1-standard-2"
      large  = "db-n1-standard-4"
      xlarge = "db-n1-standard-8"
    }
  }
  
  # Provider-specific configurations
  provider_config = {
    aws = {
      engine                = "postgres"
      engine_version        = "13.7"
      port                  = 5432
      parameter_group_family = "postgres13"
    }
    azure = {
      engine         = "PostgreSQL"
      engine_version = "13"
      port          = 5432
      sku_name      = local.instance_sizes.azure[var.instance_class]
    }
    gcp = {
      engine         = "POSTGRES_13"
      port          = 5432
      tier          = local.instance_sizes.gcp[var.instance_class]
    }
  }
  
  common_tags = {
    Environment = var.environment
    ManagedBy   = "Terraform"
    Module      = "database"
  }
}

# modules/database/aws.tf
resource "aws_db_subnet_group" "main" {
  count      = var.cloud_provider == "aws" ? 1 : 0
  name       = "${var.name}-${var.environment}"
  subnet_ids = var.subnet_ids
  
  tags = merge(local.common_tags, {
    Name = "${var.name}-${var.environment}-subnet-group"
  })
}

resource "aws_security_group" "db" {
  count       = var.cloud_provider == "aws" ? 1 : 0
  name_prefix = "${var.name}-${var.environment}-db"
  vpc_id      = var.vpc_id
  
  ingress {
    from_port   = local.provider_config.aws.port
    to_port     = local.provider_config.aws.port
    protocol    = "tcp"
    cidr_blocks = var.allowed_cidr_blocks
  }
  
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  tags = merge(local.common_tags, {
    Name = "${var.name}-${var.environment}-db-sg"
  })
}

resource "aws_db_parameter_group" "main" {
  count  = var.cloud_provider == "aws" ? 1 : 0
  family = local.provider_config.aws.parameter_group_family
  name   = "${var.name}-${var.environment}"
  
  parameter {
    name  = "log_statement"
    value = "all"
  }
  
  tags = local.common_tags
}

resource "aws_db_instance" "main" {
  count = var.cloud_provider == "aws" ? 1 : 0
  
  identifier = "${var.name}-${var.environment}"
  
  engine               = local.provider_config.aws.engine
  engine_version       = local.provider_config.aws.engine_version
  instance_class       = local.instance_sizes.aws[var.instance_class]
  allocated_storage    = var.storage_size_gb
  storage_encrypted    = true
  
  db_name  = var.name
  username = "dbadmin"
  password = random_password.db_password.result
  
  vpc_security_group_ids = [aws_security_group.db[0].id]
  db_subnet_group_name   = aws_db_subnet_group.main[0].name
  parameter_group_name   = aws_db_parameter_group.main[0].name
  
  backup_retention_period = var.backup_retention_days
  backup_window          = "03:00-04:00"
  maintenance_window     = "sun:04:00-sun:05:00"
  
  multi_az               = var.multi_az
  publicly_accessible    = false
  storage_type          = "gp2"
  
  skip_final_snapshot = var.environment != "prod"
  final_snapshot_identifier = var.environment == "prod" ? "${var.name}-${var.environment}-final-snapshot" : null
  
  tags = merge(local.common_tags, {
    Name = "${var.name}-${var.environment}"
  })
}

# modules/database/azure.tf
resource "azurerm_postgresql_server" "main" {
  count = var.cloud_provider == "azure" ? 1 : 0
  
  name                = "${var.name}-${var.environment}"
  location            = data.azurerm_resource_group.main[0].location
  resource_group_name = data.azurerm_resource_group.main[0].name
  
  administrator_login          = "dbadmin"
  administrator_login_password = random_password.db_password.result
  
  sku_name   = local.provider_config.azure.sku_name
  version    = local.provider_config.azure.engine_version
  storage_mb = var.storage_size_gb * 1024
  
  backup_retention_days        = var.backup_retention_days
  geo_redundant_backup_enabled = var.multi_az
  auto_grow_enabled           = true
  
  public_network_access_enabled    = false
  ssl_enforcement_enabled          = true
  ssl_minimal_tls_version_enforced = "TLS1_2"
  
  tags = local.common_tags
}

resource "azurerm_postgresql_database" "main" {
  count = var.cloud_provider == "azure" ? 1 : 0
  
  name                = var.name
  resource_group_name = data.azurerm_resource_group.main[0].name
  server_name         = azurerm_postgresql_server.main[0].name
  charset             = "UTF8"
  collation           = "English_United States.1252"
}

resource "azurerm_postgresql_firewall_rule" "allow_subnet" {
  count = var.cloud_provider == "azure" ? length(var.allowed_cidr_blocks) : 0
  
  name                = "allow-subnet-${count.index}"
  resource_group_name = data.azurerm_resource_group.main[0].name
  server_name         = azurerm_postgresql_server.main[0].name
  start_ip_address    = cidrhost(var.allowed_cidr_blocks[count.index], 0)
  end_ip_address      = cidrhost(var.allowed_cidr_blocks[count.index], -1)
}

data "azurerm_resource_group" "main" {
  count = var.cloud_provider == "azure" ? 1 : 0
  name  = split("/", var.vpc_id)[4] # Extract RG name from VNet ID
}

# modules/database/gcp.tf
resource "google_sql_database_instance" "main" {
  count = var.cloud_provider == "gcp" ? 1 : 0
  
  name             = "${var.name}-${var.environment}"
  database_version = local.provider_config.gcp.engine
  region          = data.google_compute_network.main[0].region
  
  settings {
    tier                        = local.provider_config.gcp.tier
    availability_type          = var.multi_az ? "REGIONAL" : "ZONAL"
    disk_size                  = var.storage_size_gb
    disk_type                  = "PD_SSD"
    disk_autoresize           = true
    disk_autoresize_limit     = var.storage_size_gb * 2
    
    backup_configuration {
      enabled                        = true
      start_time                    = "03:00"
      point_in_time_recovery_enabled = true
      backup_retention_settings {
        retained_backups = var.backup_retention_days
      }
    }
    
    maintenance_window {
      day         = 7  # Sunday
      hour        = 4
      update_track = "stable"
    }
    
    database_flags {
      name  = "log_statement"
      value = "all"
    }
    
    ip_configuration {
      ipv4_enabled    = false
      private_network = var.vpc_id
      authorized_networks {
        name  = "allowed-ranges"
        value = join(",", var.allowed_cidr_blocks)
      }
    }
    
    user_labels = local.common_tags
  }
  
  deletion_protection = var.environment == "prod"
}

resource "google_sql_database" "main" {
  count = var.cloud_provider == "gcp" ? 1 : 0
  
  name     = var.name
  instance = google_sql_database_instance.main[0].name
}

resource "google_sql_user" "main" {
  count = var.cloud_provider == "gcp" ? 1 : 0
  
  name     = "dbadmin"
  instance = google_sql_database_instance.main[0].name
  password = random_password.db_password.result
}

data "google_compute_network" "main" {
  count = var.cloud_provider == "gcp" ? 1 : 0
  name  = split("/", var.vpc_id)[5] # Extract network name from VPC ID
}

# modules/database/shared.tf
resource "random_password" "db_password" {
  length  = 16
  special = true
}

# Store password in each provider's secret store
resource "aws_secretsmanager_secret" "db_password" {
  count = var.cloud_provider == "aws" ? 1 : 0
  name  = "${var.name}-${var.environment}-db-password"
  tags  = local.common_tags
}

resource "aws_secretsmanager_secret_version" "db_password" {
  count     = var.cloud_provider == "aws" ? 1 : 0
  secret_id = aws_secretsmanager_secret.db_password[0].id
  secret_string = jsonencode({
    username = "dbadmin"
    password = random_password.db_password.result
  })
}

resource "azurerm_key_vault_secret" "db_password" {
  count        = var.cloud_provider == "azure" ? 1 : 0
  name         = "${var.name}-${var.environment}-db-password"
  value        = random_password.db_password.result
  key_vault_id = var.key_vault_id # Would need to be passed as variable
  tags         = local.common_tags
}

resource "google_secret_manager_secret" "db_password" {
  count     = var.cloud_provider == "gcp" ? 1 : 0
  secret_id = "${var.name}-${var.environment}-db-password"
  
  labels = local.common_tags
  
  replication {
    automatic = true
  }
}

resource "google_secret_manager_secret_version" "db_password" {
  count       = var.cloud_provider == "gcp" ? 1 : 0
  secret      = google_secret_manager_secret.db_password[0].id
  secret_data = random_password.db_password.result
}

# modules/database/outputs.tf
output "connection_string" {
  description = "Database connection string"
  value = var.cloud_provider == "aws" ? "postgresql://dbadmin:${random_password.db_password.result}@${aws_db_instance.main[0].endpoint}/${var.name}" : var.cloud_provider == "azure" ? "postgresql://dbadmin:${random_password.db_password.result}@${azurerm_postgresql_server.main[0].fqdn}:5432/${var.name}" : "postgresql://dbadmin:${random_password.db_password.result}@${google_sql_database_instance.main[0].private_ip_address}:5432/${var.name}"
  sensitive   = true
}

output "endpoint" {
  description = "Database endpoint"
  value = var.cloud_provider == "aws" ? aws_db_instance.main[0].endpoint : var.cloud_provider == "azure" ? azurerm_postgresql_server.main[0].fqdn : google_sql_database_instance.main[0].private_ip_address
}

output "port" {
  description = "Database port"
  value = local.provider_config[var.cloud_provider].port
}

output "database_name" {
  description = "Database name"
  value = var.name
}

2. Multi-Cloud Container Orchestration Module

# modules/container-platform/main.tf
variable "cluster_name" {
  description = "Container cluster name"
  type        = string
}

variable "cloud_provider" {
  description = "Cloud provider (aws, azure, gcp)"
  type        = string
}

variable "node_count" {
  description = "Number of worker nodes"
  type        = number
  default     = 3
}

variable "node_size" {
  description = "Node size (small, medium, large)"
  type        = string
  default     = "medium"
}

locals {
  # Provider-specific node sizes
  node_sizes = {
    aws = {
      small  = "t3.medium"
      medium = "t3.large"
      large  = "t3.xlarge"
    }
    azure = {
      small  = "Standard_D2s_v3"
      medium = "Standard_D4s_v3"
      large  = "Standard_D8s_v3"
    }
    gcp = {
      small  = "e2-standard-2"
      medium = "e2-standard-4"
      large  = "e2-standard-8"
    }
  }
  
  # Kubernetes versions
  k8s_versions = {
    aws   = "1.24"
    azure = "1.24.6"
    gcp   = "1.24.8-gke.2000"
  }
}

# AWS EKS
resource "aws_eks_cluster" "main" {
  count    = var.cloud_provider == "aws" ? 1 : 0
  name     = var.cluster_name
  role_arn = aws_iam_role.eks_cluster[0].arn
  version  = local.k8s_versions.aws
  
  vpc_config {
    subnet_ids = var.subnet_ids
    endpoint_config {
      private_access = true
      public_access  = true
    }
  }
  
  depends_on = [aws_iam_role_policy_attachment.eks_cluster_policy]
}

resource "aws_eks_node_group" "main" {
  count           = var.cloud_provider == "aws" ? 1 : 0
  cluster_name    = aws_eks_cluster.main[0].name
  node_group_name = "${var.cluster_name}-workers"
  node_role_arn   = aws_iam_role.eks_nodes[0].arn
  subnet_ids      = var.subnet_ids
  instance_types  = [local.node_sizes.aws[var.node_size]]
  
  scaling_config {
    desired_size = var.node_count
    max_size     = var.node_count * 2
    min_size     = 1
  }
  
  depends_on = [
    aws_iam_role_policy_attachment.eks_worker_node_policy,
    aws_iam_role_policy_attachment.eks_cni_policy,
    aws_iam_role_policy_attachment.eks_container_registry_policy,
  ]
}

# Azure AKS
resource "azurerm_kubernetes_cluster" "main" {
  count               = var.cloud_provider == "azure" ? 1 : 0
  name                = var.cluster_name
  location            = var.location
  resource_group_name = var.resource_group_name
  dns_prefix          = var.cluster_name
  kubernetes_version  = local.k8s_versions.azure
  
  default_node_pool {
    name       = "default"
    node_count = var.node_count
    vm_size    = local.node_sizes.azure[var.node_size]
    vnet_subnet_id = var.subnet_ids[0]
    
    enable_auto_scaling = true
    min_count          = 1
    max_count          = var.node_count * 2
  }
  
  identity {
    type = "SystemAssigned"
  }
  
  network_profile {
    network_plugin = "azure"
    network_policy = "azure"
  }
}

# GCP GKE
resource "google_container_cluster" "main" {
  count    = var.cloud_provider == "gcp" ? 1 : 0
  name     = var.cluster_name
  location = var.location
  
  # Use the most recent valid version
  min_master_version = local.k8s_versions.gcp
  
  # We can't create a cluster with no node pool defined, but we want to only use
  # separately managed node pools. So we create the smallest possible default
  # node pool and immediately delete it.
  remove_default_node_pool = true
  initial_node_count       = 1
  
  network    = var.vpc_id
  subnetwork = var.subnet_ids[0]
  
  # Enable network policy for security
  network_policy {
    enabled = true
  }
  
  # Enable Workload Identity
  workload_identity_config {
    workload_pool = "${var.project_id}.svc.id.goog"
  }
}

resource "google_container_node_pool" "main" {
  count      = var.cloud_provider == "gcp" ? 1 : 0
  name       = "${var.cluster_name}-workers"
  location   = var.location
  cluster    = google_container_cluster.main[0].name
  node_count = var.node_count
  
  autoscaling {
    min_node_count = 1
    max_node_count = var.node_count * 2
  }
  
  node_config {
    preemptible  = var.environment != "prod"
    machine_type = local.node_sizes.gcp[var.node_size]
    
    # Google recommends custom service accounts with minimal permissions
    service_account = google_service_account.gke_node[0].email
    oauth_scopes = [
      "https://www.googleapis.com/auth/cloud-platform"
    ]
    
    labels = {
      environment = var.environment
    }
    
    tags = ["gke-node", "${var.cluster_name}-node"]
  }
}

3. Module Testing Framework

# test/database_test.go
package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestDatabaseModuleAWS(t *testing.T) {
    t.Parallel()
    
    terraformOptions := &terraform.Options{
        TerraformDir: "../examples/database-aws",
        Vars: map[string]interface{}{
            "name":           "test-db",
            "cloud_provider": "aws",
            "environment":    "test",
            "instance_class": "small",
        },
    }
    
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
    
    // Verify outputs
    endpoint := terraform.Output(t, terraformOptions, "endpoint")
    assert.NotEmpty(t, endpoint)
    
    port := terraform.Output(t, terraformOptions, "port")
    assert.Equal(t, "5432", port)
}

func TestDatabaseModuleAzure(t *testing.T) {
    t.Parallel()
    
    terraformOptions := &terraform.Options{
        TerraformDir: "../examples/database-azure",
        Vars: map[string]interface{}{
            "name":           "test-db",
            "cloud_provider": "azure",
            "environment":    "test",
            "instance_class": "small",
        },
    }
    
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
    
    endpoint := terraform.Output(t, terraformOptions, "endpoint")
    assert.Contains(t, endpoint, ".postgres.database.azure.com")
}

func TestDatabaseModuleGCP(t *testing.T) {
    t.Parallel()
    
    terraformOptions := &terraform.Options{
        TerraformDir: "../examples/database-gcp",
        Vars: map[string]interface{}{
            "name":           "test-db",
            "cloud_provider": "gcp",
            "environment":    "test",
            "instance_class": "small",
        },
    }
    
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
    
    endpoint := terraform.Output(t, terraformOptions, "endpoint")
    assert.NotEmpty(t, endpoint)
}

4. Multi-Cloud Application Deployment

# environments/production/main.tf
module "database" {
  source = "../../modules/database"
  
  name                    = "myapp"
  cloud_provider         = var.primary_cloud
  environment            = "production"
  instance_class         = "large"
  storage_size_gb        = 500
  backup_retention_days  = 30
  multi_az              = true
  
  vpc_id                = local.vpc_configs[var.primary_cloud].vpc_id
  subnet_ids            = local.vpc_configs[var.primary_cloud].private_subnet_ids
  allowed_cidr_blocks   = local.vpc_configs[var.primary_cloud].app_subnet_cidrs
}

module "container_platform" {
  source = "../../modules/container-platform"
  
  cluster_name     = "myapp-prod"
  cloud_provider  = var.primary_cloud
  node_count      = 5
  node_size       = "large"
  
  # Provider-specific configurations
  location             = local.vpc_configs[var.primary_cloud].region
  vpc_id              = local.vpc_configs[var.primary_cloud].vpc_id
  subnet_ids          = local.vpc_configs[var.primary_cloud].private_subnet_ids
  resource_group_name = var.primary_cloud == "azure" ? local.vpc_configs.azure.resource_group : null
  project_id          = var.primary_cloud == "gcp" ? local.vpc_configs.gcp.project_id : null
}

# Deploy to secondary cloud for disaster recovery
module "database_dr" {
  source = "../../modules/database"
  
  name                    = "myapp-dr"
  cloud_provider         = var.secondary_cloud
  environment            = "production"
  instance_class         = "medium"  # Smaller for DR
  storage_size_gb        = 500
  backup_retention_days  = 30
  multi_az              = false      # Single AZ for cost savings
  
  vpc_id                = local.vpc_configs[var.secondary_cloud].vpc_id
  subnet_ids            = local.vpc_configs[var.secondary_cloud].private_subnet_ids
  allowed_cidr_blocks   = local.vpc_configs[var.secondary_cloud].app_subnet_cidrs
}

# locals.tf
locals {
  vpc_configs = {
    aws = {
      vpc_id              = "vpc-12345678"
      region              = "eu-west-1"
      private_subnet_ids  = ["subnet-12345678", "subnet-87654321"]
      app_subnet_cidrs    = ["10.0.1.0/24", "10.0.2.0/24"]
    }
    azure = {
      vpc_id              = "/subscriptions/sub-id/resourceGroups/rg-prod/providers/Microsoft.Network/virtualNetworks/vnet-prod"
      region              = "West Europe"
      resource_group      = "rg-prod"
      private_subnet_ids  = ["/subscriptions/sub-id/resourceGroups/rg-prod/providers/Microsoft.Network/virtualNetworks/vnet-prod/subnets/private-1"]
      app_subnet_cidrs    = ["10.1.1.0/24", "10.1.2.0/24"]
    }
    gcp = {
      vpc_id              = "projects/my-project/global/networks/vpc-prod"
      region              = "europe-west1"
      project_id          = "my-project-prod"
      private_subnet_ids  = ["projects/my-project/regions/europe-west1/subnetworks/private-1"]
      app_subnet_cidrs    = ["10.2.1.0/24", "10.2.2.0/24"]
    }
  }
}

# variables.tf
variable "primary_cloud" {
  description = "Primary cloud provider"
  type        = string
  default     = "aws"
  
  validation {
    condition     = contains(["aws", "azure", "gcp"], var.primary_cloud)
    error_message = "Primary cloud must be aws, azure, or gcp."
  }
}

variable "secondary_cloud" {
  description = "Secondary cloud provider for DR"
  type        = string
  default     = "azure"
  
  validation {
    condition     = contains(["aws", "azure", "gcp"], var.secondary_cloud)
    error_message = "Secondary cloud must be aws, azure, or gcp."
  }
}

Module Governance and Standards

5. Module Versioning and Registry

# .github/workflows/module-release.yml
name: Module Release
on:
  push:
    tags:
      - 'v*'

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        cloud: [aws, azure, gcp]
    steps:
      - uses: actions/checkout@v3
      - uses: hashicorp/setup-terraform@v2
      - name: Run Terratest
        run: |
          cd test
          go test -timeout 30m -parallel 3 ./...
        env:
          CLOUD_PROVIDER: ${{ matrix.cloud }}

  publish:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Publish to Terraform Registry
        run: |
          # Tag follows semantic versioning
          echo "Publishing module version ${{ github.ref_name }}"
          # Registry publishing logic here

# Module metadata
# terraform-registry.tf
terraform {
  required_version = ">= 1.0"
  
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
    google = {
      source  = "hashicorp/google"
      version = "~> 4.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }
}

6. Cost Optimization Across Clouds

# scripts/cost_optimizer.py
import json
import boto3
from azure.identity import DefaultAzureCredential
from azure.mgmt.consumption import ConsumptionManagementClient
from google.cloud import billing

def analyze_multi_cloud_costs():
    """
    Analyze costs across cloud providers and recommend optimizations
    """
    
    costs = {
        'aws': get_aws_costs(),
        'azure': get_azure_costs(), 
        'gcp': get_gcp_costs()
    }
    
    total_cost = sum(costs.values())
    
    print("=== Multi-Cloud Cost Analysis ===")
    for cloud, cost in costs.items():
        percentage = (cost / total_cost) * 100
        print(f"{cloud.upper()}: £{cost:.2f} ({percentage:.1f}%)")
    
    print(f"\nTotal monthly cost: £{total_cost:.2f}")
    print(f"Annual projection: £{total_cost * 12:.2f}")
    
    # Recommendations
    print("\n=== Optimization Recommendations ===")
    
    # Check for unused resources
    unused_resources = find_unused_resources()
    if unused_resources:
        potential_savings = calculate_savings(unused_resources)
        print(f"💰 Remove unused resources: £{potential_savings:.2f}/month savings")
    
    # Right-sizing recommendations
    oversized_resources = find_oversized_resources()
    if oversized_resources:
        rightsizing_savings = calculate_rightsizing_savings(oversized_resources)
        print(f"📏 Right-size resources: £{rightsizing_savings:.2f}/month savings")
    
    # Reserved instances opportunities
    reservation_savings = analyze_reservation_opportunities()
    if reservation_savings:
        print(f"🔒 Reserved instances: £{reservation_savings:.2f}/month savings")
    
    # Multi-cloud workload placement
    placement_recommendations = analyze_workload_placement(costs)
    for rec in placement_recommendations:
        print(f"🌐 {rec['recommendation']}: £{rec['savings']:.2f}/month savings")

def analyze_workload_placement(costs):
    """
    Recommend optimal cloud placement for workloads
    """
    recommendations = []
    
    # Example workload cost comparison
    workloads = {
        'compute_intensive': {
            'aws': 450,    # EC2 compute-optimized instances
            'azure': 420,  # Azure compute-optimized VMs
            'gcp': 380     # GCP compute-optimized instances
        },
        'memory_intensive': {
            'aws': 520,    # EC2 memory-optimized instances
            'azure': 480,  # Azure memory-optimized VMs  
            'gcp': 550     # GCP memory-optimized instances
        },
        'storage_intensive': {
            'aws': 200,    # S3 + EBS costs
            'azure': 180,  # Blob + Managed Disks
            'gcp': 160     # Cloud Storage + Persistent Disks
        }
    }
    
    for workload, cloud_costs in workloads.items():
        cheapest_cloud = min(cloud_costs, key=cloud_costs.get)
        current_cloud = 'aws'  # Assume current deployment
        
        if cheapest_cloud != current_cloud:
            current_cost = cloud_costs[current_cloud]
            optimal_cost = cloud_costs[cheapest_cloud]
            savings = current_cost - optimal_cost
            
            recommendations.append({
                'workload': workload,
                'recommendation': f"Move {workload} from {current_cloud.upper()} to {cheapest_cloud.upper()}",
                'savings': savings
            })
    
    return recommendations

def get_aws_costs():
    """Get AWS costs for the last month"""
    # Implementation for AWS Cost Explorer API
    return 1250.00  # Example cost

def get_azure_costs():
    """Get Azure costs for the last month"""
    # Implementation for Azure Consumption API
    return 950.00   # Example cost

def get_gcp_costs():
    """Get GCP costs for the last month"""
    # Implementation for GCP Billing API
    return 800.00   # Example cost

# Run the analysis
analyze_multi_cloud_costs()

Advanced Multi-Cloud Patterns

7. Data Replication Across Clouds

# modules/data-replication/main.tf
resource "aws_s3_bucket" "primary" {
  count  = var.primary_cloud == "aws" ? 1 : 0
  bucket = "${var.bucket_name}-primary"
}

resource "aws_s3_bucket_replication_configuration" "replication" {
  count = var.primary_cloud == "aws" && var.enable_cross_cloud_replication ? 1 : 0
  
  role   = aws_iam_role.replication[0].arn
  bucket = aws_s3_bucket.primary[0].id
  
  rule {
    id     = "cross-cloud-replication"
    status = "Enabled"
    
    destination {
      bucket        = "arn:aws:s3:::${var.bucket_name}-replica"
      storage_class = "STANDARD_IA"
    }
  }
}

# Azure equivalent
resource "azurerm_storage_account" "primary" {
  count = var.primary_cloud == "azure" ? 1 : 0
  
  name                     = "${var.bucket_name}primary"
  resource_group_name      = var.resource_group_name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = var.enable_cross_cloud_replication ? "GRS" : "LRS"
}

# GCP equivalent
resource "google_storage_bucket" "primary" {
  count    = var.primary_cloud == "gcp" ? 1 : 0
  name     = "${var.bucket_name}-primary"
  location = var.location
  
  dynamic "autoclass" {
    for_each = var.enable_cross_cloud_replication ? [1] : []
    content {
      enabled = true
    }
  }
}

Why It Matters

  • Vendor Independence: Avoid cloud provider lock-in with portable infrastructure
  • Cost Optimization: Leverage best pricing across different clouds for different workloads
  • Risk Mitigation: Distribute risk across multiple providers and regions
  • Best-of-Breed: Use each cloud’s strongest services without being tied to one platform
  • Compliance: Meet data residency and regulatory requirements across jurisdictions

Try This Week

  1. Audit current modules – Identify provider-specific code that could be abstracted
  2. Create your first multi-cloud module – Start with a simple storage or compute module
  3. Implement module testing – Set up Terratest for one module across providers
  4. Calculate multi-cloud costs – Run the cost analysis script for your workloads

Quick Multi-Cloud Readiness Assessment

#!/bin/bash
# Assess current Terraform code for multi-cloud readiness

echo "=== Multi-Cloud Readiness Assessment ==="
echo

# Check for hardcoded provider-specific values
echo "🔍 Checking for hardcoded provider values..."
find . -name "*.tf" -exec grep -l "ami-\|Standard_\|e2-standard\|t3\." {} \; | head -10

echo
echo "📊 Provider usage analysis:"
grep -r "resource \"aws_" . --include="*.tf" | wc -l | xargs echo "AWS resources:"
grep -r "resource \"azurerm_" . --include="*.tf" | wc -l | xargs echo "Azure resources:"
grep -r "resource \"google_" . --include="*.tf" | wc -l | xargs echo "GCP resources:"

echo
echo "🏗️ Module structure analysis:"
find . -name "modules" -type d | wc -l | xargs echo "Module directories:"
find . -name "*.tf" -path "*/modules/*" | wc -l | xargs echo "Module files:"

echo
echo "📝 Variable usage:"
grep -r "var\." . --include="*.tf" | wc -l | xargs echo "Variable references:"
grep -r "locals\." . --include="*.tf" | wc -l | xargs echo "Local references:"

echo
echo "🎯 Multi-cloud readiness recommendations:"
echo "1. Extract hardcoded values into variables"
echo "2. Create provider-agnostic modules for common resources"
echo "3. Implement resource size mapping for different clouds"
echo "4. Add provider validation in variables"
echo "5. Set up module testing across multiple clouds"
echo "6. Implement cost comparison analysis"

Common Multi-Cloud Mistakes

  • Over-abstraction: Creating overly complex modules that are hard to maintain
  • Feature disparity: Assuming all clouds have equivalent services
  • Cost blindness: Not comparing actual costs across implementations
  • Security gaps: Missing provider-specific security best practices
  • Testing neglect: Not validating modules work correctly on all target clouds

Best Practices for Multi-Cloud Modules

  • Start simple: Begin with basic resources before adding complexity
  • Embrace differences: Use each cloud’s strengths rather than forcing uniformity
  • Version carefully: Use semantic versioning and thorough testing
  • Document extensively: Include examples for each supported cloud provider
  • Monitor costs: Track actual spending across cloud implementations

Pro Tip: Start your multi-cloud journey with stateless workloads like containers and static websites. These are easier to make portable and give you experience with the patterns before tackling complex stateful services like databases and message queues.