TL;DR — Multi-cluster Kubernetes operators are the missing layer between declarative intent and distributed reality. This post covers the architecture patterns, controller design strategies, and production hardening techniques needed to build operators that work reliably across hybrid environments — spanning AWS EKS, Azure AKS, on-prem OpenShift, and bare metal — without fragmenting your operational model.
The Problem: Why Single-Cluster Operators Break at Scale
Most Kubernetes operators are built with a single-cluster assumption baked in. They watch a namespace, reconcile a custom resource, and manage pods on the local API server. This works beautifully until your organization needs to deploy a managed database across three regions, fail over a streaming pipeline from AWS to Azure, or keep a staging cluster in sync with production on-premise.
The limitations are concrete:
- API server affinity: A controller using the default
client-goconfiguration connects to a single kubeconfig context. There is no built-in awareness of sibling clusters. - State fragmentation: When a cluster goes partition-separated, the operator loses visibility into workloads running on the other side. Reconciliation loops diverge, and drift becomes silent.
- Network topology ignorance: Operators assume low-latency, high-bandwidth communication between control plane and data plane. In hybrid setups, inter-cluster VPN latency or cross-region egress costs introduce real failure modes.
- Secret and credential sprawl: Each cluster has its own identity provider, service accounts, and secrets store. A single-cluster operator has no unified mechanism to propagate credentials across boundaries.
These are not edge cases. They are the daily reality for platforms running at scale — think a financial services firm running compliance-bound workloads on-premise while bursting into GKE for compute-intensive batch jobs, or a SaaS vendor serving global customers across three cloud providers.
Core Architecture Patterns for Multi-Cluster Operators
Building a multi-cluster operator requires rethinking the controller pattern from the ground up. The good news is that several proven architectural patterns have emerged in the community.
The Hub-and-Spoke Controller Model
In this pattern, a central “hub” controller maintains global state and dispatches intents to per-cluster “spoke” controllers running inside each target cluster. The hub does not directly manage pods — it communicates through a declarative interface.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Hub │ │ Spoke │ │ Spoke │
│ Cluster │──────▶│ Cluster A │──────▶│ Cluster B │
│ │ │ (EKS) │ │ (On-Prem) │
│ Global State│ │ Local Ctrl │ │ Local Ctrl │
└──────────────┘ └──────────────┘ └──────────────┘
The hub controller watches a multi-cluster custom resource definition (CRD) and translates global intent into cluster-specific objects. Each spoke controller — a lightweight operator deployed inside its respective cluster — watches for its assigned slice of the global state and reconciles locally.
This model has several advantages:
- Network boundary respect: The hub never needs direct API access to every cluster. Spokes pull configuration from the hub or a shared store.
- Graceful degradation: If the hub loses connectivity to a spoke, the spoke continues operating on its last-known configuration.
- Credential isolation: Each spoke uses its own service account and RBAC context. No shared credentials cross cluster boundaries.
The primary trade-off is increased operational complexity — you are deploying and maintaining a controller in every cluster, not just one.
The Federated Multi-Cluster Controller
A federated controller runs a single process but maintains multiple client-go configurations, each pointing to a different cluster. It watches CRDs across all clusters and reconciles globally.
# Simplified illustration of a multi-cluster controller loop
from kubernetes import client, config
clusters = {
"production-east": "https://eks-east.example.com",
"production-west": "https://eks-west.example.com",
"on-prem-staging": "https://openshift-lan.example.com",
}
for name, endpoint in clusters.items():
context = f"cluster-{name}"
config.load_kube_config(context=context)
api = client.CustomObjectsApi()
# Watch and reconcile per-cluster resources
This approach is simpler to deploy but introduces a critical risk: a single controller process becomes a blast radius. If it crashes, all clusters lose reconciliation simultaneously. It also concentrates credential exposure — one process holds tokens for every cluster.
In practice, the federated model works best when the operator is stateless and the number of clusters is small (under five), and when you can pair it with a leader-election mechanism and a persistent queue for reconciliation events.
The Event-Driven Multi-Cluster Mesh
This is the most advanced pattern and the one that scales to dozens or hundreds of clusters. Rather than a controller polling multiple API servers, the architecture uses an event bus — typically Apache Kafka or a cloud-native equivalent like Amazon EventBridge or Google Pub/Sub — to propagate state changes across clusters.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Cluster A│───▶│ Kafka │◀───│ Cluster B│
│ │ │ Topic │ │ │
│ Events │ │ Stream │ │ Events │
└──────────┘ └────┬─────┘ └──────────┘
│
┌────▼─────┐
│ Cluster C│
│ (GKE) │
└──────────┘
Each cluster runs a lightweight agent that publishes reconciliation events to the bus and subscribes to events relevant to its workloads. A separate processing layer — which could be another operator or a serverless function — handles cross-cluster coordination logic: conflict resolution, ordering guarantees, and eventual consistency enforcement.
This pattern is what platforms like Crossplane and Karmada use at their core. It introduces operational overhead in managing the event infrastructure, but the payoff is linear scalability and true decoupling between clusters.
Implementation Strategies: Building the Controller
Using kubebuilder and controller-runtime for Multi-Cluster Support
The controller-runtime library that underpins kubebuilder is single-cluster by default, but it supports multiple managers and clients. Here is the pattern for wiring up a multi-cluster reconciler:
// Create managers for each cluster
mgrA, _ := ctrl.NewManager(ctrl.Config{Context: ctxA}, ctrl.Options{})
mgrB, _ := ctrl.NewManager(ctrl.Config{Context: ctxB}, ctrl.Options{})
// Register the same reconciler on both managers
reconciler := &MultiClusterReconciler{}
if err := reconciler.SetupWithManager(mgrA); err != nil {
log.Error(err, "unable to setup controller on cluster A")
}
if err := reconciler.SetupWithManager(mgrB); err != nil {
log.Error(err, "unable to setup controller on cluster B")
}
The key insight is that each manager owns its own client.Client, watch configuration, and event queue. The reconciler function must be idempotent across clusters — it cannot assume that a resource it just created in Cluster A exists in Cluster B.
CRD Design for Multi-Cluster State
Your custom resource definitions must encode cluster topology explicitly. A naive CRD that only specifies “deploy three replicas” is insufficient. You need a structure that maps intent to clusters:
apiVersion: platform.example.com/v1
kind: DistributedWorkload
metadata:
name: payment-processor
spec:
replicas: 3
placement:
- cluster: production-east
replicas: 2
topology:
zone: us-east-1
- cluster: on-prem-dr
replicas: 1
topology:
zone: dr-site
failoverPolicy:
primary: production-east
secondary: on-prem-dr
trigger: health-check-failure
sync:
configMap: true
secret: encrypted
Notice the explicit placement array, failover policy, and sync configuration. This structure lets the operator make informed decisions about where to schedule workloads and how to handle failures. Without this, you are building a custom scheduler from scratch — which is almost always the wrong call.
Patterns in Production: Hardening Multi-Cluster Operators
Failure Detection and Automatic Failover
Multi-cluster environments introduce a new class of failure: cluster partition. When a cluster becomes unreachable, the operator must decide whether to wait, fail over, or degrade gracefully.
The production pattern is to implement a tiered health check:
- Liveness probe: Is the API server responding? (Every few seconds)
- Readiness probe: Can the cluster accept new workloads? (Every 30 seconds)
- Application-level health: Are the managed resources actually healthy? (Every 60 seconds, via custom metrics)
When a cluster fails the liveness check for a configurable threshold (commonly three consecutive failures), the operator triggers the failover policy defined in the CRD. This typically involves promoting a secondary cluster’s resources to primary and updating DNS or load balancer configurations.
func (r *MultiClusterReconciler) checkClusterHealth(clusterID string) HealthStatus {
_, err := r.clients[clusterID].Discovery().ServerVersion()
if err != nil {
return HealthUnreachable
}
// Additional application-level checks...
return HealthOK
}
State Synchronization and Conflict Resolution
When multiple clusters can modify the same logical resource, you need a conflict resolution strategy. The two dominant approaches are:
- Last-write-wins with vector clocks: Each cluster attaches a logical timestamp to its changes. The operator resolves conflicts by comparing vector clock entries rather than wall-clock time. This is the approach used by etcd and is well-suited for configuration state.
- Operational transform with primary-first semantics: One cluster is designated as the primary for a given resource. Secondary clusters can propose changes, but the primary’s decision is final. This is simpler to reason about but introduces a single point of coordination.
In practice, most production multi-cluster operators use a hybrid: primary-first for critical state (credentials, networking rules) and vector clocks for less critical state (workload configuration, scaling parameters).
Secret Management Across Cluster Boundaries
Secrets are the hardest problem in multi-cluster operations. Each cluster has its own secrets store, and cross-cluster secret propagation creates security and compliance risks.
The recommended pattern is to use an external secrets manager — such as HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault — as the single source of truth. Each cluster’s operator fetches secrets at runtime using its own identity, and secrets never persist in Kubernetes etcd across cluster boundaries.
# ExternalSecrets CRD pattern
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: payment-db-creds
data:
- secretKey: username
remoteRef:
key: database/credentials
property: username
- secretKey: password
remoteRef:
key: database/credentials
property: password
This approach, implemented through tools like External Secrets Operator, decouples secret lifecycle management from cluster topology. The operator in each cluster simply watches for ExternalSecret resources and syncs them from the central store.
Networking and Service Mesh Integration
Cross-cluster service discovery is non-trivial. When a workload in Cluster A needs to call a service in Cluster B, you need a reliable network path and a discovery mechanism.
Service meshes like Istio and Linkerd provide multi-cluster modes that handle this transparently:
- Istio multi-primary: Each cluster runs its own control plane, and they are connected via a shared root CA. Services discover each other through DNS entries that resolve across clusters.
- Istio primary-remote: One cluster runs the control plane; others run only data plane proxies. Simpler to operate but creates a dependency on the primary cluster’s availability.
For operators that need to orchestrate cross-cluster services, integrating with the service mesh control plane is essential. The operator should watch mesh-specific CRDs (like Istio’s ServiceEntry or DestinationRule) and update them as cluster topology changes.
Observability and Debugging at Multi-Cluster Scale
When something breaks across five clusters, logs alone are insufficient. You need correlated observability:
- Unified tracing: Propagate trace context across cluster boundaries using W3C trace headers. Tools like Jaeger or AWS X-Ray can aggregate traces from multiple clusters into a single view.
- Centralized metrics: Each cluster’s operator exports Prometheus metrics with a
clusterlabel. A centralized Prometheus server or Thanos instance aggregates these for global dashboards. - Event audit trails: Every reconciliation event — successful or failed — should be logged with a structured format including the cluster ID, resource version, and reconciliation duration. This creates a forensic trail for post-incident analysis.
# Prometheus metric example
- name: multi_cluster_reconciliation_duration_seconds
help: "Time spent reconciling a multi-cluster resource"
labels:
- cluster_id
- resource_kind
- resource_name
- status # success, failure, timeout
Without these observability primitives, debugging a multi-cluster operator is guesswork. The failure surface area grows quadratically with the number of clusters — two clusters have one inter-cluster link to debug, five clusters have ten, and so on.
Key Takeaways
- Choose your architecture pattern based on cluster count and network topology: Hub-and-spoke for 3–10 clusters with clear boundaries, federated for fewer clusters with simple requirements, and event-driven mesh for 10+ clusters or dynamic environments.
- Never assume cluster symmetry: Each cluster has its own API server version, resource quotas, node pools, and network configuration. Your operator must handle heterogeneity gracefully.
- Externalize secrets and state: Do not replicate secrets across etcd clusters. Use an external secrets manager as the single source of truth and let each cluster’s operator fetch at runtime.
- Implement tiered health checks and automatic failover: Cluster partition is not a theoretical concern — it is a regular operational event. Your operator must detect, decide, and act within configurable time bounds.
- Invest in correlated observability early: The cost of retrofitting tracing, metrics, and audit trails into a production multi-cluster operator is significantly higher than building them in from day one.
- Leverage existing frameworks: Crossplane, Karmada, and Cluster API provide battle-tested multi-cluster primitives. Building everything from scratch is rarely justified unless your requirements are truly unique.
Further Reading
- Crossplane: Cloud-Native Infrastructure as Code — The leading open-source project for composing cloud infrastructure using Kubernetes CRDs, with built-in multi-cluster support.
- Karmada: Kubernetes Armada for Multi-Cluster Orchestration — A production-grade multi-cluster orchestration system built on Kubernetes primitives, supporting federated scheduling and policy-based placement.
- KubeVirt and Multi-Cluster Operator Patterns — The Cluster API project provides a declarative way to create, configure, and manage Kubernetes clusters, including multi-cluster topologies.
- Istio Multi-Cluster Deployment Documentation — Official guide to deploying Istio across multiple clusters with both primary-remote and multi-primary topologies.
- External Secrets Operator — A Kubernetes operator that syncs secrets from external systems (Vault, AWS, Azure, GCP) into Kubernetes, essential for multi-cluster secret management.
- The Operator Pattern: Kubernetes Documentation — The foundational Kubernetes documentation on the operator pattern, including controller-runtime internals and best practices.
- Designing Multi-Cluster Systems: Lessons from Production — Google’s research paper on multi-cluster system design, covering state consistency, failure models, and scheduling strategies that inform modern Kubernetes operator design.