Kubernetes in Depth · Why one cluster stops being enough

Multi-Cluster & Fleet Management

Everything the CKA, CKAD, and CKS blueprints test assumes one cluster: one API server to talk to, one etcd quorum underneath, one CNI, one set of nodes. That assumption is correct for a huge share of real workloads and wrong for the rest — sooner or later a regulator asks where the data physically sits, a bad upgrade takes down every workload at once, or a team needs an isolated blast radius. The moment a second cluster exists, a new problem appears that no single cluster ever poses: keeping many clusters built the same way, configured the same way, and reachable from each other, instead of each one quietly drifting into its own snowflake. This page is the Kubernetes-native tour of that problem — why teams split in the first place, how Cluster API turns "build a cluster" into an object a controller reconciles instead of a hand-run script, how GitOps and policy get pushed to an entire fleet at once, and how a Service in one cluster becomes resolvable from another through the Multi-Cluster Services API. None of it is exam-blueprint material; all of it is what "production Kubernetes" means past a certain scale.

☺ Explain it like I'm 10

Imagine your town has one fire station, and it works great — until the town grows so much that one truck can't reach every fire in time, and if the station itself ever has a bad day (a burst pipe, a broken garage door), the whole town has no fire cover at all. So the town builds more stations, one per neighborhood, all from the identical blueprint — same trucks, same training, same radio channel — so any station can back up any other and a captain in one can call a captain in another by name instead of hunting for a phone number. Fleet management is the job of making sure every new station really is identical to the first one, and that they can all talk to each other the instant they're built, instead of each one slowly becoming its own weird, hard-to-trust exception.

🦉Your host for this topic: Professor Owl — the one who already draws the single-cluster control-plane diagram everyone else's lesson fills in, so multiplying it into a fleet, and drawing the blueprint that keeps every copy honest, is squarely Owl's territory.

Why one cluster stops being enough

☺ Like you're 10: One fire station is simple to run but risky — if it has a bad day, the whole town has no cover. Splitting into a few gives every neighborhood its own safety net.

Kubernetes Architecture and Control Plane Internals both describe one cluster as a single, cohesive unit: one API server surface, one etcd Raft quorum, one CNI, one admission chain. That cohesion is also the risk — a Kubernetes cluster is a shared-fate unit. A botched control-plane upgrade, a runaway admission webhook denying every request, or an etcd disk filling up degrades every workload on that cluster simultaneously, no matter how unrelated they are. Splitting workloads across clusters turns one catastrophic outage into several smaller, independent ones, and it's the single most common honest reason a team runs more than one cluster: blast-radius isolation.

Three more forces show up just as often, and each implies a different split. Hard tenancy: namespaces, RBAC, and NetworkPolicies give real but soft isolation inside one cluster — tenants still share one API server and one kernel per node — so mutually distrusting parties (different SaaS customers, PCI-scoped workloads next to general ones) need a genuinely separate cluster, not just a separate namespace. Data residency and compliance: regulation like GDPR or India's DPDP can require that a region's data never leaves it and that no operator outside that jurisdiction can touch it, which a single global control plane simply cannot promise. Latency, disaster recovery, and the scale ceiling: users on another continent pay real round-trip latency to a single distant cluster, surviving a regional outage means a second region already running rather than one improvised afterward, and every control plane has a real capacity ceiling — you'll feel etcd and API-server pressure well before you hit it.

◆ Key idea

Never add a cluster because "multi-cluster is best practice." Add one because you can name the specific force — blast radius, hard tenancy, residency, latency/DR, or a genuine scale ceiling — that one cluster cannot satisfy, and let that force pick the topology: per-environment, per-team, per-region, a hub-and-spoke fleet, or cells. Platform Engineering's Multi-Cluster & Fleet Management walks every one of those topologies end to end, with the full decision tree and the standing cost tax each one carries — this page assumes that reasoning and goes straight to the Kubernetes-native machinery that makes a fleet real.

Cluster API: making the cluster itself a reconciled object

☺ Like you're 10: Instead of a different crew building each fire station by hand and forgetting how, you write the blueprint down once and a machine builds every station from it — and quietly rebuilds any wall that starts to crack.

Hand-building clusters — clicking through a cloud console, or running a slightly different script each time — is exactly how a fleet accumulates snowflakes: every cluster subtly different, no two upgrades alike, nobody able to recreate one from scratch. Cluster API (CAPI), a Kubernetes SIG Cluster Lifecycle project, fixes this by doing to clusters exactly what the object model already does to workloads: you declare desired state as ordinary Kubernetes objects, and a controller reconciles reality toward it, forever. A small management cluster runs the CAPI controllers; the workload clusters it creates are the ones that actually run your Pods, and ideally know nothing about CAPI at all. Because CAPI objects are just YAML, they live in Git and get applied by the same GitOps controller covered two sections down — "stand up a cluster in ap-south" becomes a reviewed pull request, not a person with cloud-console access at 2 a.m.

CAPI splits cleanly into three provider roles that compose to build one node: an infrastructure provider (CAPA for AWS, CAPZ for Azure, CAPD for local Docker-based clusters) that creates the real VMs, networks, and load balancers; a bootstrap provider — almost always Kubeadm — that turns a raw machine into a node by generating its cloud-init; and a control-plane provider (KubeadmControlPlane) that owns the API-server/etcd tier as a unit, including its own HA and rolling upgrades. Worker nodes are grouped into a MachineDeployment, and the symmetry with what you already know is the entire design: MachineDeploymentMachineSetMachine is the exact same ownership shape as DeploymentReplicaSetPod, and changing a version field triggers the same kind of rolling replacement a Deployment does — except what gets replaced is a whole node, never patched in place.

apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
  name: prod-ap-south
  namespace: fleet
spec:
  clusterNetwork:
    pods: { cidrBlocks: ["10.244.0.0/16"] }      # must not overlap any other cluster in the fleet
  controlPlaneRef:                                # the control-plane provider
    apiVersion: controlplane.cluster.x-k8s.io/v1beta1
    kind: KubeadmControlPlane
    name: prod-ap-south-cp
  infrastructureRef:                              # the infrastructure provider
    apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
    kind: AWSCluster
    name: prod-ap-south
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment                           # a worker pool — clusters' own "Deployment"
metadata:
  name: prod-ap-south-md-0
  namespace: fleet
spec:
  clusterName: prod-ap-south
  replicas: 6                                     # scale the pool like any Deployment
  template:
    spec:
      clusterName: prod-ap-south
      version: v1.30.2                            # bump this → CAPI rolls new, never-patched nodes
      bootstrap:
        configRef: { apiVersion: bootstrap.cluster.x-k8s.io/v1beta1, kind: KubeadmConfigTemplate, name: prod-ap-south-md-0 }
      infrastructureRef:
        apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
        kind: AWSMachineTemplate
        name: prod-ap-south-md-0

This page's job is the concept, not the command line — the full clusterctl workflow, ClusterClass managed topologies, and the failure modes that bite real fleets (provider version skew, a MachineHealthCheck remediation loop shredding a whole pool, the "pivot" that moves CAPI itself between management clusters) all live in Platform Engineering's Cluster API tool guide, written for exactly this depth. What matters here is the shape of the idea: a cluster is no longer a thing a person builds once and remembers — it's a reconciled object, the same way a Pod is, and that's what makes everything in the rest of this page possible.

Git fleet source of truth reconcile 🦉 Management cluster Cluster API · GitOps controller policy · inventory · fleet obs the "hub" create + configure prod · ap-south workload spoke prod · eu-west workload spoke non-prod workload spoke the hub never runs business workloads, and must not be a runtime dependency of the spokes

Fleet-wide GitOps: the same config, pushed to every cluster

☺ Like you're 10: You wrote one rulebook. Now every fire station needs the same rulebook, automatically, with only the small local tweaks each one actually needs.

Cluster API births an empty cluster — a working control plane and joined nodes, nothing else. Something has to fill it with the actual platform (ingress, cert-manager, monitoring agents, tenant namespaces) and keep it in step with every other cluster forever, and that's GitOps stretched across a fleet instead of one cluster. The dominant pattern is a single Argo CD running on the hub with an ApplicationSet driven by the cluster generator: every workload cluster is registered with labels like region=ap-south, the generator produces one Application per matching cluster from a single template, and those label values flow straight into the template so each cluster pulls its own region-specific overlay. Register a brand-new cluster and the platform's whole add-on stack reconciles onto it — no one writes a new manifest.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: platform-addons
  namespace: argocd
spec:
  generators:
    - clusters: { selector: { matchLabels: { fleet: "true" } } }   # one App per registered cluster
  template:
    metadata: { name: 'addons-{{name}}' }
    spec:
      source:
        repoURL: https://git.example.com/fleet-config.git
        path: 'addons/overlays/{{metadata.labels.region}}'         # per-region overlay
      destination: { server: '{{server}}', namespace: platform-system }
      syncPolicy: { automated: { prune: true, selfHeal: true } }

Argo CD isn't the only shape this takes — Flux favors a reconciler running inside each cluster pulling the same repo, which scales further and survives a partition at the cost of no single pane of glass, while Rancher Fleet and Sveltos trade off differently again on where the reconciler runs and how rich the cluster-targeting rules can be. The full engine-by-engine comparison, with a working table of trade-offs, is in Platform Engineering's Multi-Cluster & Fleet Management — the point that matters for a Kubernetes-focused reader is simpler: whichever engine you pick, the same config must reach every cluster from the same source, or the fleet's guardrails start drifting.

And guardrails are exactly where a fleet's real risk lives. Admission policy — the same rules RBAC & Admission Control covers for one cluster — is only as strong as its coverage across all of them: ship the identical policy bundle to every cluster as just another add-on in the same GitOps stream, so "no privileged Pods" and "images only from our registry" hold everywhere by construction, not by memory. Identity should federate the same way — every cluster's API server pointed at the same OIDC provider, so a person's group membership maps to identical RBAC everywhere, provisioned by GitOps rather than hand-edited per cluster. The anti-pattern to watch for is a per-cluster local-admin account or a hand-crafted kubeconfig: invisible, prone to drift, and exactly how a fleet accumulates a snowflake with god-mode access. Platform Engineering's governance & compliance covers the fleet-wide policy-reporting side of this in full.

⚠ The snowflake cluster

Every fleet's real failure mode is the one cluster that's almost like the others — built before the rest existed, patched by hand during an incident and never reconciled back afterward, missing a policy because someone "temporarily" disabled it. It passes a casual glance and fails at the worst possible moment. The structural defense is boring on purpose: every cluster comes from the same Cluster API template and the same GitOps stream, self-heal stays on, and a fleet dashboard flags anything off-version or non-compliant. If you can't recreate a cluster from Git, it's already a snowflake.

Service discovery across clusters: the Multi-Cluster Services API

☺ Like you're 10: Each fire station has its own phone book. If someone at one station needs to call a specific person at another, both stations need to share phone books first — and the phones need a wire between the buildings before any call goes through.

Networking & the CNI covers how, inside one cluster, a Service name resolves through CoreDNS and packets reach any Pod over a flat, no-NAT network. Neither of those things is true by default the instant a second cluster enters the picture: each cluster has its own CoreDNS, its own Pod CIDR, and no route at all to any other cluster's Pods. Closing that gap takes two independent layers — a data plane that makes cross-cluster Pod-to-Pod traffic actually route (and stay encrypted), and a discovery plane that lets a name resolved in one cluster point at endpoints living in another. Kubernetes standardizes the second layer as the Multi-Cluster Services (MCS) API, a SIG-Multicluster project.

You group member clusters into a ClusterSet, then use two CRDs: a ServiceExport, created in the cluster that owns the Service, marks it shareable with the rest of the set; the MCS implementation then auto-creates a matching ServiceImport in every other member. An exported Service becomes resolvable at a well-known name — <service>.<namespace>.svc.clusterset.local — and, crucially, endpoints from every exporting cluster are merged behind that one name, so it naturally load-balances and fails over across clusters without any application code knowing which cluster actually answered.

# In the exporting cluster (ap-south): mark the Service shareable with the ClusterSet
apiVersion: multicluster.x-k8s.io/v1alpha1
kind: ServiceExport
metadata:
  name: catalog
  namespace: shop
---
# From any member cluster (including eu-west), the name now resolves fleet-wide:
#   catalog.shop.svc.clusterset.local  →  merged endpoints from every exporting cluster
# kubectl --context eu-west run probe --rm -it --image=busybox -- \
#   nslookup catalog.shop.svc.clusterset.local

There's a hard prerequisite underneath all of it: Pod and Service CIDRs must not overlap across any two clusters in the set. If ap-south and eu-west both hand out addresses from 10.244.0.0/16, a cross-cluster route is genuinely ambiguous — the exact reason the Cluster API example two sections up pins each cluster's cidrBlocks explicitly. Plan the fleet's address space before the second cluster exists, not after. MCS itself is a vendor-neutral contract, not an implementation — the actual cross-cluster routing underneath it is provided by something else: Cilium Cluster Mesh connects clusters at the CNI/eBPF layer with transparently encrypted (WireGuard or IPsec) traffic, while Istio or Linkerd multi-cluster extends mTLS and traffic policy across clusters via east-west gateways — the mesh side of that trade-off is Service Mesh Fundamentals' territory, and the fleet-scale version of the whole discussion, including global load balancing for user-facing failover, is in Platform Engineering's Multi-Cluster & Fleet Management.

Cluster · ap-south catalog ServiceExport pods · 10.244.0.0/16 unique cluster-id Cluster · eu-west catalog ServiceImport — auto-created pods · 10.32.0.0/16 non-overlapping CIDR cluster mesh encrypted east-west catalog.shop.svc.clusterset.local → merged endpoints in both clusters non-overlapping CIDRs are a hard prerequisite, not a suggestion

When one more cluster isn't the answer

☺ Like you're 10: A new fire station isn't free — someone has to staff it, train it, and inspect it forever. Sometimes a bigger room in the existing station really is enough.

Everything above assumes the extra cluster is justified — but every cluster is a standing bill, paid in managed control-plane fees, per-node overhead for its own CNI and log agents, and a permanent place in on-call. Before reaching for a whole new cluster, escalate only as far as the actual driver demands: a well-configured namespace (quotas, NetworkPolicy, RBAC) handles most team isolation between parties that trust each other; a virtual cluster gives a team its own API server and CRDs inside a shared host cluster, most of the isolation at a fraction of the tax; a real separate cluster is for genuinely distrusting tenants or a hard compliance boundary; a separate region is for latency, DR, or data residency law. Adding a cluster when a namespace would do is the same over-engineering trap covered in Anti-Patterns & Pitfalls; refusing to add one when residency law genuinely demands it is the opposite mistake. Platform Engineering's Multi-Cluster & Fleet Management walks the full cost tax — including the N-clusters × M-add-ons multiplication that makes fleet automation non-optional past a handful of clusters — in detail this page deliberately doesn't repeat. If your own path runs through the full CNCF ladder rather than just Kubernetes' own certifications, the Golden Astronaut course covers the other CNCF projects and LFCS on the way to Golden Kubestronaut.

🦉 Owl's-eye view

"A team swore their cross-cluster Service had 'just stopped resolving' overnight — no config change, no deploy, nothing in the GitOps diff. It hadn't stopped; a brand-new non-prod cluster had joined the fleet an hour earlier reusing the exact same default Pod CIDR as an existing one, because nobody had given the new-cluster template an address range of its own. The MCS controller wasn't confused — the network genuinely was ambiguous. The fix was one field in the Cluster API template, not a single line of application code. I don't trust a fleet's networking until its CIDR plan is written down somewhere more permanent than one engineer's memory."

✎ Try it

Create two local kind clusters with deliberately non-overlapping pod CIDRs — kind create cluster --name ap-south and, from a config specifying a different podSubnet, kind create cluster --name eu-west. Install a CNI that supports Cluster Mesh (Cilium is the common choice) on both, connect them, then apply the ServiceExport above in one cluster for a throwaway Service. From the other cluster, run nslookup against the clusterset.local name and watch it actually resolve. Then, on purpose, rebuild one cluster with the same CIDR as the other and watch cross-cluster routing break — the fastest way to make "non-overlapping CIDRs are a hard prerequisite" stop being an abstract warning.

🎬 At the Pod Squad
🦉

Professor Owl: Finance wants an isolated environment for the new payments team. Before anyone reaches for a third cluster — what's the actual force we can't satisfy with what we already have?

👺

Gizmo the Gremlin: Easy — just hardcode the other cluster's Service ClusterIP into the payments app's config. No MCS, no cluster mesh, no fleet machinery. Ship it today.

🐢

Timmy the Turtle: That ClusterIP isn't even routable outside its own cluster's Pod network — it'll work in a demo and fail the first time either cluster's controller rebuilds a Service and the IP changes underneath you.

🦫

Benny the Beaver: And if payments genuinely needs a separate cluster, I'm not hand-building it either — one more Cluster and MachineDeployment in the same Git repo, same template as every other cluster in the fleet.

🐦

Pip the Hummingbird: And once it exists, a real ServiceExport is what makes it reachable from anywhere else in the fleet by name — not an IP anyone typed in by hand and will forget to update.

🐘

Ellie the Elephant: I'll register it in the fleet dashboard the moment it's born, too. A cluster nobody's tracking is a cluster that's already drifting — I'd rather know on day one.

🐢 Timmy's checkpoint

1. Name three distinct forces that justify a second cluster, and explain why "multi-cluster is best practice" isn't itself a valid reason. 2. In Cluster API, what's the difference between the management cluster and a workload cluster, and what does changing a MachineDeployment's version field actually do to the nodes? 3. What does the Argo CD ApplicationSet cluster generator do, and how do per-cluster values reach its template? 4. Name the two CRDs the Multi-Cluster Services API uses, and what well-known name does an exported Service resolve at? 5. What hard networking prerequisite must hold across every cluster in a ClusterSet before cross-cluster discovery can work at all? 6. What's the difference between the MCS API's job and a cluster mesh's job — which layer does each one solve? 7. Give two cheaper alternatives to try before reaching for a whole new cluster.

Check your answers
  1. Any three of: blast-radius isolation (a shared-fate control plane and etcd quorum), hard tenancy (mutually distrusting parties need more than a namespace), data residency/compliance (regulation dictating where data may sit), latency/disaster recovery, or a genuine single-cluster scale ceiling. "Best practice" isn't a driver because it doesn't tell you which topology to pick or justify the standing cost every extra cluster carries — only a named force does.
  2. The management cluster runs the CAPI controllers and never runs business workloads; a workload cluster is one CAPI provisioned, and runs the actual apps. Changing version on a MachineDeployment triggers a rolling, immutable replacement — new nodes are provisioned at the new version, the old ones are cordoned, drained, and deleted; no node is ever patched in place.
  3. It registers every fleet cluster as a target and produces one Argo CD Application per matching cluster from a single template. Each cluster's own labels (and its name/API server address) are substituted into that template — e.g. {{metadata.labels.region}} selects a per-region config overlay.
  4. ServiceExport (created in the owning cluster to mark a Service shareable) and ServiceImport (auto-created in every other member of the ClusterSet). It resolves at <service>.<namespace>.svc.clusterset.local, with endpoints merged from every exporting cluster.
  5. Pod (and Service) CIDRs must not overlap across any two clusters in the ClusterSet — overlapping ranges make cross-cluster routing genuinely ambiguous, not just misconfigured.
  6. The MCS API is the discovery plane — it lets a name resolved in one cluster point at endpoints living in another. A cluster mesh (Cilium Cluster Mesh, Istio/Linkerd multi-cluster) is the data plane underneath it — the thing that actually routes and encrypts the cross-cluster packets once discovery has told you where to send them.
  7. A well-configured namespace (quotas, NetworkPolicy, RBAC) for teams that already trust each other, and a virtual cluster for stronger isolation — its own API server and CRDs — without the full recurring cost of a separate real cluster.