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
- Audit current modules – Identify provider-specific code that could be abstracted
- Create your first multi-cloud module – Start with a simple storage or compute module
- Implement module testing – Set up Terratest for one module across providers
- 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.








