Stage 7 · Master
Advanced Topics & Future of Platform Engineering
Multi-Cloud Platform Strategy
Abstraction layers: Crossplane, Terraform, Cluster API. Avoid vendor lock-in. Consistent APIs across AWS, Azure, GCP.
Why Multi-Cloud?
- Resilience: Avoid single-cloud outage (AWS us-east-1, Azure, GCP all had major outages)
- Compliance: Data sovereignty (GDPR, data must stay in EU/Germany/region)
- Cost Optimization: Leverage spot/preemptible across clouds, negotiate better pricing
- Best-of-Breed: Use AWS for ML (SageMaker), GCP for data (BigQuery), Azure for enterprise (AD)
- Vendor Leverage: Avoid lock-in, maintain negotiating power
- Disaster Recovery: Cross-cloud failover for critical services
Multi-cloud adds significant complexity: networking, identity, data gravity, operations, skills. Only adopt when: clear business driver, platform team mature, abstraction layer exists. Start with single-cloud + DR in second cloud.
Abstraction Layers
| Layer | Tool | Scope | Strength | Weakness |
|---|---|---|---|---|
| Infrastructure | Crossplane | Cloud resources (DB, bucket, VPC) | K8s-native, composable, GitOps | Learning curve, provider maturity varies |
| Infrastructure | Terraform | All cloud resources | Mature, huge ecosystem, state management | Not K8s-native, separate workflow |
| Cluster | Cluster API | K8s clusters (control plane, workers) | K8s-native, declarative, multi-cloud | Provider-specific templates |
| Application | Helm/Kustomize | K8s workloads | Standard, GitOps-friendly | Not multi-cloud aware |
Crossplane for Multi-Cloud
Crossplane extends Kubernetes to provision cloud resources. Compositions define how a platform capability (e.g., PostgreSQL) maps to cloud-specific managed resources (AWS RDS, Azure PostgreSQL, GCP CloudSQL).
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: postgresqlinstances.database.platform.example.com
spec:
group: database.platform.example.com
names:
kind: PostgreSQLInstance
plural: postgresqlinstances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
engineVersion:
type: string
enum: ["14", "15", "16"]
default: "15"
instanceClass:
type: string
default: "db.t3.medium"
storageGB:
type: integer
minimum: 20
maximum: 65536
default: 100
multiAZ:
type: boolean
default: true
backupRetentionDays:
type: integer
default: 7
region:
type: string
description: "Target region (cloud-agnostic)"
provider:
type: string
enum: ["aws", "azure", "gcp"]
default: "aws"
---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: postgresqlinstance-multi-cloud
spec:
compositeTypeRef:
apiVersion: database.platform.example.com/v1alpha1
kind: PostgreSQLInstance
mode: Pipeline
pipeline:
- step: select-provider
functionRef:
name: function-go-templating
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
- name: provider-config
base:
apiVersion: v1
kind: ConfigMap
metadata:
name: provider-config
data:
provider: "{{ .spec.provider }}"
region: "{{ .spec.region }}"
- step: create-rds
functionRef:
name: function-go-templating
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: DBInstance
spec:
forProvider:
region: "{{ .spec.region }}"
engine: postgres
engineVersion: "{{ .spec.engineVersion }}"
instanceClass: "{{ .spec.instanceClass }}"
allocatedStorage: "{{ .spec.storageGB }}"
multiAZ: "{{ .spec.multiAZ }}"
backupRetentionPeriod: "{{ .spec.backupRetentionDays }}"
deletionProtection: true
publiclyAccessible: false
vpcSecurityGroupIdRefs:
- name: rds-sg
dbSubnetGroupNameRef:
name: rds-subnet-group
tags:
platform.example.com/managed-by: crossplane
platform.example.com/instance: "{{ .metadata.name }}"
patches:
- fromFieldPath: "metadata.uid"
toFieldPath: "metadata.name"
transforms:
- type: string
string:
fmt: "pg-%s"
percentSpecifiers:
- fromFieldPath: "metadata.uid"
- name: rds-instance-azure
base:
apiVersion: postgresql.azure.upbound.io/v1beta1
kind: FlexibleServer
spec:
forProvider:
location: "{{ .spec.region }}"
version: "{{ .spec.engineVersion }}"
skuName: "{{ .spec.instanceClass }}"
storageMB: "{{ mul .spec.storageGB 1024 }}"
highAvailability:
mode: "{{ if .spec.multiAZ }}ZoneRedundant{{ else }}Disabled{{ end }}"
backup:
retentionDays: "{{ .spec.backupRetentionDays }}"
administratorLogin: postgres
administratorLoginPasswordSecretRef:
name: pg-password
namespace: crossplane-system
key: password
condition: "{{ eq .spec.provider "azure" }}"
- name: rds-instance-gcp
base:
apiVersion: sql.gcp.upbound.io/v1beta1
kind: DatabaseInstance
spec:
forProvider:
region: "{{ .spec.region }}"
databaseVersion: "POSTGRES_{{ .spec.engineVersion }}"
settings:
tier: "{{ .spec.instanceClass }}"
dataDiskSizeGb: "{{ .spec.storageGB }}"
availabilityType: "{{ if .spec.multiAZ }}REGIONAL{{ else }}ZONAL{{ end }}"
backupConfiguration:
enabled: true
binaryLogEnabled: true
startTime: "03:00"
ipConfiguration:
authorizedNetworks: []
requireSsl: true
condition: "{{ eq .spec.provider "gcp" }}"
Crossplane Composition uses pipeline with go-templating to create cloud-specific resources based on provider field. Single API, multi-cloud implementation.
Terraform for Multi-Cloud
Terraform excels at foundational infrastructure: VPCs, IAM, shared services. Use Terraform for cloud-agnostic modules, Crossplane for K8s-integrated resources.
# modules/vpc/main.tf
variable "cloud" {
type = string
validation {
condition = contains(["aws", "azure", "gcp"], var.cloud)
error_message = "Cloud must be aws, azure, or gcp."
}
}
variable "region" {
type = string
}
variable "cidr_block" {
type = string
default = "10.0.0.0/16"
}
# AWS VPC
resource "aws_vpc" "main" {
count = var.cloud == "aws" ? 1 : 0
cidr_block = var.cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "platform-vpc"
Environment = var.environment
Cloud = "aws"
}
}
resource "aws_subnet" "private" {
count = var.cloud == "aws" ? length(var.private_subnet_cidrs) : 0
for_each = toset(var.private_subnet_cidrs)
vpc_id = aws_vpc.main[0].id
cidr_block = each.value
availability_zone = each.key
tags = {
Name = "platform-private-${each.key}"
Environment = var.environment
Type = "private"
}
}
# Azure VNet
resource "azurerm_virtual_network" "main" {
count = var.cloud == "azure" ? 1 : 0
name = "platform-vnet"
location = var.region
resource_group_name = azurerm_resource_group.main[0].name
address_space = [var.cidr_block]
tags = {
Environment = var.environment
Cloud = "azure"
}
}
resource "azurerm_subnet" "private" {
count = var.cloud == "azure" ? length(var.private_subnet_cidrs) : 0
for_each = toset(var.private_subnet_cidrs)
name = "platform-private-${each.key}"
resource_group_name = azurerm_resource_group.main[0].name
virtual_network_name = azurerm_virtual_network.main[0].name
address_prefixes = [each.value]
}
# GCP VPC
resource "google_compute_network" "main" {
count = var.cloud == "gcp" ? 1 : 0
name = "platform-vpc"
auto_create_subnetworks = false
routing_mode = "GLOBAL"
description = "Platform VPC for ${var.environment}"
}
resource "google_compute_subnetwork" "private" {
count = var.cloud == "gcp" ? length(var.private_subnet_cidrs) : 0
for_each = toset(var.private_subnet_cidrs)
name = "platform-private-${each.key}"
ip_cidr_range = each.value
region = var.region
network = google_compute_network.main[0].id
private_ip_google_access = true
}
# Outputs (cloud-agnostic)
output "vpc_id" {
value = var.cloud == "aws" ? aws_vpc.main[0].id :
0].id :
var.cloud == "azure" ? azurerm_virtual_network.main[0].id :
google_compute_network.main[0].id
}
output "private_subnet_ids" {
value = var.cloud == "aws" ? [for k, v in aws_subnet.private : v.id] :
var.cloud == "azure" ? [for k, v in azurerm_subnet.private : v.id] :
[for k, v in google_compute_subnetwork.private : v.id]
}
Terraform module abstracts VPC creation across AWS, Azure, GCP. Single interface, cloud-specific implementation.
Cluster API for Multi-Cloud
Cluster API (CAPI) provides Kubernetes-native cluster lifecycle management. Providers for AWS (CAPA), Azure (CAPZ), GCP (CPG), vSphere, etc. Platform uses CAPI templates for consistent cluster creation.
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
name: platform-cluster
namespace: platform-system
spec:
clusterNetwork:
services:
cidrBlocks: ["10.128.0.0/12"]
pods:
cidrBlocks: ["192.168.0.0/16"]
serviceDomain: "cluster.local"
topology:
class: platform-cluster-class
version: v1.28.0
controlPlane:
replicas: 3
workers:
machineDeployments:
- class: platform-worker
name: md-0
replicas: 3
failureDomains:
- us-east-1a
- us-east-1b
- us-east-1c
variables:
- name: cloudProvider
value: "aws"
- name: region
value: "us-east-1"
- name: instanceType
value: "m6i.xlarge"
- name: sshKeyName
value: "platform-ssh-key"
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: ClusterClass
metadata:
name: platform-cluster-class
spec:
infrastructure:
selector:
matchLabels:
platform.example.com/cluster-class: "true"
controlPlane:
ref:
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KubeadmControlPlaneTemplate
name: platform-control-plane
machineInfrastructure:
selector:
matchLabels:
platform.example.com/cluster-class: "true"
workers:
machineDeployments:
- class: platform-worker
template:
bootstrap:
configSelector:
matchLabels:
platform.example.com/cluster-class: "true"
infrastructure:
selector:
matchLabels:
platform.example.com/cluster-class: "true"
variables:
- name: cloudProvider
required: true
schema:
openAPIV3Schema:
type: string
enum: ["aws", "azure", "gcp"]
- name: region
required: true
schema:
openAPIV3Schema:
type: string
- name: instanceType
required: true
schema:
openAPIV3Schema:
type: string
- name: sshKeyName
required: true
schema:
openAPIV3Schema:
type: string
ClusterClass defines reusable cluster template. Variables allow cloud-specific values. InfrastructureRef points to cloud-specific templates (AWSClusterTemplate, AzureClusterTemplate, GCPClusterTemplate).
Consistent Platform APIs
Platform API presents unified interface. Implementation delegates to Crossplane/Terraform/CAPI based on resource type and cloud.
Avoiding Vendor Lock-In
- Abstraction Layer: Platform API hides cloud specifics. Teams never call AWS/Azure/GCP SDKs directly.
- Portable Data: Use open formats (Parquet, Avro, Protobuf). Avoid cloud-specific services for core data.
- Portable Compute: K8s + Knative/KEDA runs anywhere. Avoid Lambda/Cloud Functions/Fargate for portable workloads.
- Multi-Cloud CI/CD: Build once, deploy anywhere. Container images in multi-region registry (GHCR, ECR, GAR, ACR).
- Terraform State: Store in cloud-agnostic backend (S3 + DynamoDB, Azure Blob, GCS, or Consul).
- Secrets: External Secrets Operator + Vault/Key Vault/Secrets Manager. App reads from K8s Secret.
- Networking: Cilium/Submariner for cluster mesh. Avoid cloud-specific load balancers where possible.
- Observability: OpenTelemetry + Prometheus/Grafana/Loki/Tempo. Avoid CloudWatch/Stackdriver/Cloud Monitoring lock-in.
- Exit Strategy: Document migration path for each cloud service. Test quarterly.
Data gravity pulls compute to where data lives. Multi-cloud strategy must address: where does primary data live? How to replicate? Cross-cloud egress costs? Start with single-cloud data, multi-cloud compute for stateless workloads.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.