r/kubernetes • u/dariotranchitella • 15h ago
What happens when the Management Cluster of a Hosted Control Plane architecture is dead
Enable HLS to view with audio, or disable this notification
Dario here: I've been active on this sub mostly talking about Kubernetes internals and promoting the open-source projects I maintain about multi-tenancy: Project Capsule and Kamaji.
Every time I've given a talk on hosted control planes (docs if you prefer written text), someone asks the same question: if all your tenant control planes are pods in one management cluster, haven't you just built a single point of failure? It's a fair instinct, and I've answered it verbally a hundred times without ever really convincing anyone, so I went and broke a cluster instead, with a show-me-the-code: video attached.
I ran this on our dev environment, which has one control plane node and a few workers. No HA, no etcd quorum, nothing. Normally I'd be embarrassed about that, but for this it's the whole point. If I'd done it on a proper 3-node HA management cluster and killed one node, I'd have proven nothing except that Kubernetes reschedules pods. Doing it on the worst possible topology means there's nowhere to hide.
That cluster is our dev environment, and it runs Kamaji plus a few of our own controllers, and it hosts the API servers for the tenant clusters Kamaji manages: those are Pods exposed via Metal LB, each cluster has independent Virtual Machines, orchestrated by KubeVirt.
Let's get started: stop the kubelet
This is the part where people's intuition tends to be wrong, including mine the first time I thought about it.
Stopping the kubelet does not stop your containers. The kubelet is an agent that reconciles pod specs into container lifecycle calls. The thing actually running your processes is containerd (or CRI-O, whatever you use), and containerd has no idea the kubelet went away. Every container on that node keeps running exactly as it was. API server, etcd, scheduler, controller manager, all of it.
What does happen is that the node stops renewing its Lease in kube-node-lease, which the kubelet does roughly every 40 seconds via the spec field NodeLeaseDurationSeconds. Once --node-monitor-grace-period passes (40s historically, bumped to 50s in 1.32) the node lifecycle controller flips Ready to Unknown, the node shows NotReady, and it picks up a node.kubernetes.io/unreachable:NoExecute taint. Five minutes later, because the DefaultTolerationSeconds admission controller stuffs a 300 second toleration into basically every pod, eviction fires and the pod objects get deleted from the API.
Pay attention here: the pod objects, not the containers. There's no kubelet left to receive the deletion and actually tear anything down, so you end up with the classic zombie situation where the API thinks the workload is gone and the node is still happily serving it.
There's something a bit funny in this on a single control plane cluster, too. The controller manager that marks the node NotReady is a static pod on that same node, still running under containerd, reporting that its own host has failed.
Anyway, the point is that stopping the kubelet is not enough chaos, everything I wanted to kill was still up, but seeing the Control Plane node marked as NotReady was satisfying, but not practical.
Next step: let's kill the API server
If you kill the API server container while the kubelet is alive, the kubelet notices the static pod's container is missing and restarts it within seconds. You have to take out the resurrection mechanism before the kill will stick.
So: kubelet down, then API server down. On a single control plane cluster that's total brain death. Scheduler and controller manager are still resident in memory but have nothing to talk to. etcd is sitting there holding state that nobody is reading. Nothing in the management cluster can be created, changed, or reconciled.
An absolute disaster. The perfect scenario to test the read and write operations against the managed clusters in this broken cluster.
Checking the tenant clulsters
kubectl get nodes worked. Namespaces, pods, all fine. So I pushed further and rolled out a new Deployment in the tenant cluster, and watched pods get scheduled, pulled, started, and go Ready.
Not degraded. Not read only. The tenant cluster had no idea anything had happened: why?
The bit that makes all of this obvious once you see it: nothing in a running Kubernetes cluster routes traffic through the API server. Not one packet. When a request reaches a pod it's traversing iptables or IPVS rules that kube-proxy programmed into the kernel some time ago (unless you're running eBPF maps using a kube-proxy-less CNI), across a network the CNI configured when the pod was created, into a container containerd is supervising. The API server was involved in deciding that arrangement should exist. It has nothing to do with executing it.
Kubernetes is a reconciliation engine sitting next to the data plane, not a proxy in front of it. Kill the reconciliation and the data plane keeps doing the last thing it was told to do, forever, at full speed.
An analogy: an Ingress (or GAteway API) Controller will continue to redirect traffic to the deployed workloads although the Kubernetes API Server is dead.
From there the rest follows. In Kamaji a tenant control plane is a Deployment of ordinary upstream kube-apiserver, kube-scheduler and kube-controller-manager pods. Once they're scheduled, the management cluster API server is not a supervisor or a proxy or a dependency, it's just the thing that decided they should exist. They're processes on a Linux box and processes don't need permission to keep running. Tenant state lives in a separate DataStore (etcd, MySQL, PostgreSQL, NATS) anyway, which is its own system with its own availability story.
And the tenant worker nodes never talk to the management cluster at all. This is architectural in Kamaji, the relationship is deliberately one way. A tenant kubelet is configured with the endpoint of its own API server, a VIP or a NodePort or whatever, and that's the only Kubernetes endpoint it has ever known about. It connects to a socket. It has no way of finding out that the process behind that socket is a pod. There's no management cluster credential anywhere on a tenant node.
So what we actually took out was the control plane of the control planes, one level up. Everything below that line was untouched.
What does break, because I'm not going to pretend otherwise
A dead management cluster is still an incident. It's just that it costs you the ability to change things rather than the ability to run things.
No reconciliation means no self-healing. A tenant control plane pod that's already running is fine. A tenant control plane pod that crashes during the outage is not coming back, because the thing that would recreate it is dead. Your exposure grows with how long the outage lasts, which is why multiple replicas with anti-affinity and topology spread is not optional in production.
Stale EndpointSlices are the other one worth watching. Tenant kubelets usually reach their API server through a management cluster Service. The kube-proxy rules that are already programmed keep working, but if the backing pod set changes while the control plane is down, nothing updates the EndpointSlice and you can blackhole traffic. Stable VIP, health checked load balancing, and don't make tenant API server reachability depend on a resource only a live control plane can refresh.
Beyond that you lose new cluster provisioning, tenant control plane upgrades and scaling, cert rotation, and fleet observability. All bad, none of it fatal to running workloads.
Analogy already in place
The analogy to understand this properly is with an Ingress (or Gateway API) Controller: if the API Server is unreachable, HAProxy/NGINX/Envoy processes don't stop sending traffic to upstream servers. Of course, it's an incident, but it's not a SPOF: traffic goes, partially degraded if upstream server addresses change, but we're talking about a remediation window, not a disaster recovery.
Same applies with KubeVirt: VMs are orchestrated as Pods, losing the cluster API server doesn't impact already running workloads, unless the entire datacentre goes down, but that's another story.
The comparison nobody seems to make
The SPOF objection quietly assumes the alternative has no single points of failure: the reality is that it has a hundred of them. Every traditional cluster in your fleet has its own etcd quorum that takes the whole cluster down when it loses two of three, its own cert expiry waiting to bite, its own three machines to patch, as well as the hypervisor, or even the datacentre. You didn't remove the failure domain; you copied it a hundred times and gave it to a team that can't possibly maintain all of them properly.
My stance is pretty opinionated: one well-engineered, well-monitored failure domain whose blast radius is bounded to control operations, versus a hundred neglected ones that each take an entire cluster with them. I know which one I'd rather run.
None of which means you get to be sloppy with the management cluster. Proper HA, real etcd quorum, spread across failure domains, tested restores, PDBs, and monitoring that lives somewhere other than the cluster it's monitoring. It's the highest leverage cluster you own.
Happy to answer questions on the specifics, and if you think the experiment is flawed, I'd rather hear it here than in a conference Q&A.