Challenge ·Medium

CKA Practice: Recover a Broken Static Control-Plane Pod

Every kubectl command against a kubeadm cluster fails, and the workload is still serving. With no API to query, the usual tools tell you nothing. Work from the node itself to find what broke and bring the control plane back.

12:41 in the incident channel:

@here kubectl is failing against the training cluster: The connection to the server 172.16.0.2:6443 was refused. Deploys are blocked and the dashboard is blank. The VMs are all up, and nobody changed anything (they say). Can someone take a look?

The control plane on cplane-01 is unhealthy. Cluster operations are failing and workloads are affected. Restore the control plane and return the cluster to a healthy state.

The plan:

  1. Work out what is broken without the API server to help you.
  2. Get the API server answering again.
  3. Confirm the rest of the control plane came back with it.
  4. Confirm the nodes and the workload are healthy.

Constraints:

  • Repair the control plane. Do not recreate the cluster. A rebuild takes the evidence with it.
  • Do not delete or edit the web-backend deployment in the web namespace.
  • Keep the existing cluster CA. Reissuing it fails the attempt.

Pre-flight

Work on cplane-01 as root (sudo -i).

kubectl talks to the API server, and the API server is what is missing, so every kubectl command will fail for the whole first half of this challenge. Everything you need is on the node:

  • systemctl and journalctl answer for the kubelet, which is a plain systemd unit.
  • crictl talks to the container runtime over its own socket, so it keeps working.
  • /etc/kubernetes/manifests/ and /var/log/ are files on disk.

Each step below carries its own hints. Open them only as far as you need.

What runs the control plane

On a kubeadm cluster the control plane components are not scheduled. The kubelet on cplane-01 runs them directly from manifest files in /etc/kubernetes/manifests/. They are called static pods, and the kubelet owns their whole lifecycle.

That has two consequences worth holding on to:

  • The kubelet does not need the API server to run them. If the kubelet is up, it is still trying to run the control plane right now.
  • The mirror pods you normally see with kubectl get pods -n kube-system are only read-only copies published to the API. With the API down, they do not exist as far as you are concerned.

So the first question is whether the kubelet is healthy: systemctl status kubelet.

Step 1: Get the API server answering again

Find the component that is failing, work out why from its own output, and correct it.

This step passes when kubectl get --raw=/readyz returns ok.

Seeing containers without the API

crictl speaks to the container runtime over its socket, so it works with the API server down:

crictl ps            # running containers
crictl ps -a         # also containers that have exited

Compare the two. A component that starts and dies keeps appearing in crictl ps -a with a climbing attempt count, and never appears in crictl ps.

crictl pods is a different list. It shows pod sandboxes, which hold the network and IPC namespaces. The container runs inside the sandbox and has its own ID. The two lists never share an ID, and most crictl subcommands take one or the other, not both. Reach for crictl ps, not crictl pods.

On this playground crictl prints two warnings on every call, because /etc/crictl.yaml does not exist and it falls back to probing the default sockets. It still works. Silence it once:

crictl config --set runtime-endpoint=unix:///run/containerd/containerd.sock
Reading the logs of a container that will not stay up
crictl logs <container-id>

logs takes a container ID. Passing it a pod ID from crictl pods, or a pod name, returns NotFound, which reads like the container is gone when it is only the wrong identifier. Take the ID from crictl ps -a, or skip the copying:

crictl logs "$(crictl ps -a --name kube-apiserver -q | head -1)"

A process that exits immediately usually explains itself in its last line.

The kubelet garbage-collects exited containers, so that ID can disappear mid-incident. The logs on disk outlive it:

ls /var/log/pods/kube-system_kube-apiserver-*/
tail -50 /var/log/containers/kube-apiserver-*.log

Know where the kubelet writes container logs. It is the more reliable path.

From the error to the manifest

The component is failing on its own configuration, before it does any real work. Read its static pod manifest and compare it line by line against what the error message named:

grep -n -- '--etcd' /etc/kubernetes/manifests/kube-apiserver.yaml
Applying the fix

Edit the manifest in place. You do not need to restart anything by hand.

The kubelet watches /etc/kubernetes/manifests/ and also rescans it on a timer set by fileCheckFrequency, 20 seconds by default, so an edit is picked up shortly after you save it. Changing the file is how you deploy a change to a static pod.

Give it a moment after that. An API server that answers requests is not yet an API server that is ready: /readyz reports failures for a few seconds while its post-start hooks finish.

Step 2: Confirm the control plane came back whole

The API server is one of four components on this node. Check that all of them are running, and meet the objects that represent them:

kubectl get pods -n kube-system -l tier=control-plane
Mirror pods, and why kubectl cannot manage them

The control plane pods are back in that listing now. They are mirror pods: the kubelet publishes a read-only copy of each static pod to the API server so that you can see it. The API server does not manage them. The practical difference:

kubectl -n kube-system delete pod kube-apiserver-cplane-01

That command appears to succeed. It deletes the mirror, the kubelet notices, and the mirror comes straight back. It never touched the running container. To change or stop a static pod you edit or move its manifest file, which is what you did in Step 1.

In the exam: kubectl edit on a mirror pod is rejected, and kubectl delete on one is close to useless.

Step 3: Confirm the nodes and the workload

Getting the process running is not the same as the cluster being healthy:

kubectl get nodes
kubectl -n web get deployment web-backend

The workload never stopped serving during the outage. This step confirms it, and confirms the kubelets have re-reported since the API server returned.

Here kubectl was dead, so the investigation had to start at the runtime. When the API server is up and a single node has gone quiet, the investigation runs the other way, from the control plane down. The sibling challenge in this series, Recover a NotReady Node After a Kubelet Configuration Error, starts from that side.

Other people's work on the same terrain, worth doing alongside this one:

References