Multi-Cloud Architecture

Design systems that run across multiple cloud providers to avoid vendor lock-in, improve resilience, and optimize costs. Covers abstraction layers, data sovereignty, multi-cloud networking, and the real-world trade-offs of multi-cloud strategies.

Multi-cloud architecture is a strategy of distributing workloads across two or more public cloud providers—such as AWS, Azure, and GCP—using a consistent operational model, enabling resilience, cost optimization, and vendor independence. It matters when a single cloud provider cannot meet all requirements: when availability, latency, cost, or specific service capabilities vary between regions, or when organizations must avoid vendor lock-in, leverage best-in-class services, or respond to regional compliance or disaster recovery needs.

Designing Workload Placement

To place workloads across clouds, engineers define a set of placement policies that guide where each component runs. These policies are expressed as configuration rules, applied at deployment time or runtime. A common pattern is to use a declarative configuration format like YAML to describe deployment targets.

workloads:
  - name: user-service
    image: registry.example.com/users:v1.3
    location:
      cloud: aws
      region: us-east-1
      availability-zone: us-east-1a
    replica-count: 6
    environment:
      DATABASE_URL: postgres://db-users.prod.aws:5432/users
      LOGGING: "json"
    storage:
      type: s3
      bucket: users-data-prod
      prefix: events/

Each workload specifies its preferred cloud, region, and zone. When deploying, a CI/CD pipeline applies this configuration to the correct cloud provider using native tools.

For AWS, a deployment script might use aws ec2 run-instances with exact flags:

aws ec2 run-instances \
  --image-id ami-12345678 \
  --count 6 \
  --instance-type t3.large \
  --key-name prod-key \
  --security-groups sg-98765432 \
  --subnet-id subnet-abcdef12 \
  --region us-east-1 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Service,Value=user-service},{Key=Environment,Value=prod}]'

For Azure, the same workload might be deployed via az vm create:

az vm create \
  --resource-group users-prod \
  --name user-service-01 \
  --image UbuntuLTS \
  --size Standard_B2s \
  --nsg user-sg \
  --public-ip-address user-pip \
  --location eastus \
  --tags Service=user-service Environment=prod

The key failure mode here is cloud-specific configuration drift: an instance launched on AWS expects AWS_SECRET_ACCESS_KEY in environment variables, but the same service runs on Azure with AZURE_CLIENT_ID and AZURE_TENANT_ID, and no shared configuration layer reconciles the two.

Inter-Cloud Networking and Service Discovery

When services span multiple clouds, they must communicate across network boundaries. The primary challenge is consistent service discovery: a service on AWS must resolve a service on GCP, and vice versa.

The standard approach uses DNS with CNAME records and health checks. A service api.users.example.com resolves to an AWS ELB, but also to a GCP Load Balancer via a CNAME:

api.users.example.com.   IN  CNAME  aws-elb.users.example.com.
aws-elb.users.example.com.  IN  A  52.87.134.22
gcp-lb.users.example.com.   IN  A  35.204.187.199

The resolver selects an endpoint based on the client’s location. For instance, a user in Frankfurt resolves api.users.example.com to gcp-lb.users.example.com via DNS geolocation.

To ensure consistency, a global service registry—such as Consul, etcd, or a custom database—synchronizes service metadata across clouds. Each cloud registers its services via a periodic push of service definitions:

{
  "service": "payment-service",
  "address": "10.42.1.10",
  "port": 8080,
  "cloud": "gcp",
  "region": "europe-west1",
  "health": "healthy",
  "metadata": {
    "version": "v2.1",
    "deployment": "canary"
  }
}

This data is pushed using curl or grpc to a central registry. A service running on AWS fetches the list using:

curl -H "Accept: application/json" \
     http://registry.global:8500/v1/catalog/services?tag=payment

A common failure arises when network latency between clouds exceeds service timeouts. A gRPC call between AWS and GCP can timeout after 10 seconds, but the service discovery cache is updated every 30 seconds. If a new instance starts in GCP but the DNS TTL is 60 seconds, clients continue to route to old instances for a full minute.

To mitigate this, services use adaptive discovery: they query the registry on every request and cache results with a short TTL (e.g., 10 seconds), while background workers update the cache independently.

Cross-Cloud Data Replication and Consistency

Data consistency across clouds is a recurring challenge. A user created on AWS must be immediately available in a GCP-based analytics system, and vice versa.

The most reliable approach is event-driven replication. When a record is created in one cloud, it emits an event to a shared message broker. This broker can be a managed service like AWS SQS, Azure Service Bus, or a self-hosted Kafka cluster.

For example, a user creation on AWS triggers a UserCreated event:

{
  "event": "UserCreated",
  "userId": "u-12345",
  "email": "[email protected]",
  "timestamp": "2024-08-22T12:34:56Z",
  "source": "aws"
}

This event is published to a shared Kafka topic user-events:

kafka-console-producer.sh --broker-list kafka.global:9092 --topic user-events
> {"event":"UserCreated","userId":"u-12345",...}

On GCP, a consumer reads from the same topic and updates a BigQuery dataset:

INSERT INTO `analytics.users` (user_id, email, created_at, source)
VALUES ('u-12345', '[email protected]', '2024-08-22T12:34:56Z', 'aws');

The failure mode here is event loss during cloud migration. If an AWS service fails during a region shift, events accumulate in SQS but are not processed until the new instance is up. A common symptom is data lag: the analytics dashboard shows users created 15 minutes ago, but the source system reports 30 seconds ago.

To detect this, a monitoring system checks for event backlog and processing lag:


aws sqs get-queue-attributes \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/user-events \
  --attribute-names ApproximateNumberOfMessages,ApproximateNumberOfMessagesNotVisible

If ApproximateNumberOfMessages exceeds ApproximateNumberOfMessagesNotVisible by more than 100, the system is falling behind.

A subtle but critical issue is event ordering across clouds. Since each cloud has its own clock, events from AWS and GCP may be unordered. To enforce ordering, systems use timestamped event IDs with a composite key:

{
  "eventId": "e-123456789012-aws-1638524242",
  "timestamp": "2024-12-05T10:10:42.123Z",
  "sequence": 12345,
  "cloud": "aws"
}

Consumers on GCP sort events by timestamp and sequence, ensuring that all events from AWS are ordered relative to those from GCP.

Managing Identity and Access Across Clouds

A multi-cloud system requires unified identity. Users, services, and administrators must authenticate and authorize across all providers.

The standard solution is federated identity, where a central identity provider (IdP) manages user identities and issues tokens that are accepted by all cloud providers.

A common setup uses OpenID Connect (OIDC) with a public IdP like Keycloak, Auth0, or a self-hosted identity.example.com.

When a user logs in to a service on AWS, they are redirected to the IdP:

GET /auth/realms/primary/protocol/openid-connect/auth?
  response_type=code
  &client_id=aws-app
  &redirect_uri=https://aws-app.example.com/callback
  &scope=openid profile email

The IdP returns an ID token:

{
  "iss": "https://identity.example.com",
  "sub": "u-12345",
  "aud": "aws-app",
  "exp": 1701835200,
  "iat": 1701834900,
  "name": "Alice Johnson",
  "email": "[email protected]",
  "groups": ["engineering", "admin"]
}

This token is validated by the AWS service using the IdP’s jwks_uri:

curl -H "Accept: application/json" \
     https://identity.example.com/auth/realms/primary/protocol/openid-connect/certs

The service then configures AWS IAM roles to map claims to policies:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/identity.example.com"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:AssumedRole": "arn:aws:iam::123456789012:role/user-role"
        },
        "StringLike": {
          "identity.example.com:groups": "engineering"
        }
      }
    }
  ]
}

The failure mode is token validation failure due to clock skew. If the IdP and AWS clock differ by more than 15 minutes, exp and iat validation fail, causing login failures. The solution is to set clock-skew to 300 seconds in the validation code.

Another edge case is token refresh. When a token expires, the client must fetch a new one via POST /auth/realms/primary/protocol/openid-connect/token with grant_type=refresh_token. If this request fails, the user is logged out but cannot re-authenticate.

Disaster Recovery and Failover Patterns

Multi-cloud enables robust disaster recovery. When one cloud fails, services can fail over to others.

A key pattern is active-active with global load balancing. Two clouds (AWS and GCP) host the same application, each handling traffic. A global load balancer (e.g., Cloudflare, AWS Route 53, or GCP Global Load Balancer) routes traffic based on health checks.

A health check script runs every 10 seconds:

#!/bin/bash
# health-check.sh
status=$(curl -s -f http://localhost:8080/health)
if [ "$status" = "OK" ]; then
  exit 0
else
  exit 1
fi

This script is registered with the load balancer, which marks a cloud as unhealthy if the check fails for 30 seconds.

When AWS becomes unhealthy, traffic is rerouted to GCP. But failover is not instantaneous: DNS TTLs, load balancer caching, and service startup times delay the shift.

A common failure is partial failover: some services on GCP are healthy, but others are still in AWS, creating a split-brain state. To detect this, a health monitoring system runs periodic end-to-end tests:

curl -H "Host: api.example.com" \
     -H "Accept: application/json" \
     http://load-balancer.global:80/api/v1/users \
     -X POST \
     -d '{"name":"test","email":"[email protected]"}'

If the response is 201 Created but Location header points to https://aws-api.example.com, the system knows AWS is still primary.

To improve failover, systems use circuit-breaking at the edge. A client library on the application layer detects when the primary cloud is unreachable and fails over to the secondary cloud, using a local cache of configuration and user data.

Operational Complexity and Observability

The biggest challenge in multi-cloud architecture is operational visibility. Engineers must monitor across clouds using a unified view.

A standard approach is to collect logs, metrics, and traces into a central observability stack—such as Loki, Prometheus, and Jaeger—using standard formats.

Logs are structured as JSON and streamed to a central system:

{
  "timestamp": "2024-08-22T12:34:56.123Z",
  "level": "info",
  "service": "user-service",
  "cloud": "aws",
  "region": "us-east-1",
  "message": "User created successfully",
  "userId": "u-12345",
  "durationMs": 124
}

Metrics are exposed via HTTP endpoints in Prometheus format:

# HELP user_service_requests_total Total number of requests to user service
# TYPE user_service_requests_total counter
user_service_requests_total{cloud="aws",region="us-east-1"} 12345
user_service_requests_total{cloud="gcp",region="europe-west1"} 8765

Tracing uses OpenTelemetry, with spans reported via grpc or http:

{
  "traceId": "a1b2c3d4e5f6",
  "spanId": "12345678",
  "name": "CreateUser",
  "startTimeUnixNano": 1724347872123456789,
  "endTimeUnixNano": 1724347872247123456,
  "attributes": {
    "userId": "u-12345",
    "cloud": "aws"
  }
}

The key failure mode is inconsistent tracing across clouds. A trace spans AWS and GCP, but the traceId is not propagated correctly through HTTP headers. The solution is to standardize on traceparent and tracestate headers:

traceparent: 00-a1b2c3d4e5f6-12345678-01
tracestate: aws=12345678,gcp=87654321

When these headers are missing, traces are fragmented, making it difficult to diagnose latency between clouds.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.