DevOps Nimbus Kubernetes

Kubernetes errors and how to fix them

66 problems you will meet in real clusters. Each one gives the exact error, why it happens, the commands that confirm it, the fix and how to prevent it, then a scenario to break and fix it yourself. Replace names in angle brackets with your own. To search by pasting an error, use the interactive reference.

Pod will not start

ImagePullBackOff / ErrImagePull

Error or output

Failed to pull image "ngnix:1.27": rpc error: code = Unknown desc = failed to pull and unpack image "docker.io/library/ngnix:1.27": failed to resolve reference "docker.io/library/ngnix:1.27": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
Error: ErrImagePull
Back-off pulling image "ngnix:1.27"
Error: ImagePullBackOff

What it means

The kubelet asked the container runtime to download the image and the registry said no, so the container never starts. ErrImagePull is the first failure; ImagePullBackOff means the kubelet is now waiting longer between tries. The text after failed to pull and unpack image is the registry's own answer, and it tells the causes apart.

Causes, most likely first

  1. The image name or tag is wrong, or the tag was never pushed. The registry has nothing under that name: not found, and on Docker Hub often pull access denied, repository does not exist or may require authorization.

    Check
    kubectl get pod <pod> -o jsonpath='{.spec.containers[*].image}'   # compare it letter by letter with what you pushed
    docker manifest inspect <image>:<tag> >/dev/null && echo exists   # from a machine logged in to that registry: fails here too = the name or tag is wrong
    Fix

    Correct the image on the Deployment, not the Pod: kubectl set image deployment/<name> <container>=<image>:<tag>.

  2. The image is private and no pull Secret reaches the Pod. The registry refuses an anonymous pull with 401 Unauthorized or 403 Forbidden; Docker Hub gives the same pull access denied as for a typo.

    Check
    kubectl get pod <pod> -o jsonpath='{.spec.imagePullSecrets}'   # empty: no pull Secret reaches this Pod
    docker manifest inspect <image>:<tag> >/dev/null && echo exists   # works from a logged-in machine: the image exists, access is missing
    Fix

    Create a pull Secret in the Pod's namespace (kubectl create secret docker-registry <name> --docker-server=<registry> ...) and list it under imagePullSecrets, or on the namespace's ServiceAccount.

  3. A pull Secret exists but is not used: it is in another namespace, or written for another registry host. The kubelet only sends credentials whose host matches the image's registry, so the pull is still anonymous.

    Check
    kubectl get secret <pull-secret> -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq -r '.auths | keys[]'   # registry hosts only, no passwords: none matches the image's registry = this cause
    kubectl get secret <pull-secret> -n <pod-namespace>   # NotFound: it lives in another namespace
    Fix

    Recreate it in the Pod's namespace with --docker-server set to the exact registry host in the image name.

  4. The registry is rate-limiting the node. Docker Hub answers with 429 Too Many Requests and toomanyrequests.

    Check
    kubectl describe pod <pod> | grep -i toomanyrequests   # present: this cause
    Fix

    See ImagePullBackOff: 429 toomanyrequests: pull through a mirror, or authenticate the pulls.

  5. The tag has no image for the node's CPU architecture. The runtime finds the tag but no matching platform: no match for platform in manifest.

    Check
    kubectl get nodes -L kubernetes.io/arch
    docker buildx imagetools inspect <image>:<tag>   # the node's architecture missing from Platform: this cause
    Fix

    Build and push the image for every architecture you run: docker buildx build --platform linux/amd64,linux/arm64 -t <image> --push ..

Prevent it

  • Deploy only tags your pipeline has just pushed, and fail the pipeline if the push fails.
  • Attach pull Secrets to the namespace's ServiceAccount, so every Pod there gets them.

Now learn it: break and fix it yourself in ImagePullBackOff · Private registry, then take Level 01 · Pods that will not start on a real cluster. Open this in the interactive reference.

CreateContainerConfigError

Error or output

Warning  Failed  kubelet  Error: configmap "menu-config" not found
Warning  Failed  kubelet  Error: couldn't find key DB_PASSWORD in Secret kitchen/db
Warning  Failed  kubelet  Error: container has runAsNonRoot and image will run as root

What it means

The kubelet could not build the container's configuration, so it did not start it and there are no logs. The Failed event names what was wrong: a ConfigMap or Secret that does not exist, a key that is not in it, or a security setting the image cannot satisfy.

Causes, most likely first

  1. The ConfigMap or Secret does not exist in the Pod's namespace. A Pod can only use its own namespace's objects, so one in another namespace counts as missing: configmap "<name>" not found.

    Check
    kubectl describe pod <pod>   # the Failed event names it
    kubectl get configmap,secret -n <pod-namespace>   # not listed: this cause
    Fix

    Create it in the Pod's namespace, or correct the name the Pod references.

  2. The object exists but not with that key. Keys are case sensitive, so DB_PASSWORD and db-password differ: couldn't find key DB_PASSWORD in Secret.

    Check
    kubectl describe secret <name>   # key names and sizes, never the values: the key is not there
    kubectl get configmap <name> -o yaml   # for a ConfigMap
    Fix

    Use the key exactly as stored, or add it to the Secret or ConfigMap. Mark a reference optional: true only when the app can run without it.

  3. runAsNonRoot is set and the image runs as root. The kubelet refuses to start it: container has runAsNonRoot and image will run as root.

    Check
    kubectl get pod <pod> -o jsonpath='{.spec.securityContext}{.spec.containers[0].securityContext}'   # runAsNonRoot true, no runAsUser
    Fix

    Set a numeric runAsUser (and fix file ownership in the image), or use an image that runs as a non-root user.

  4. runAsNonRoot is set and the image's USER is a name, not a number. The kubelet cannot tell whether the name is root: image has non-numeric user (nginx), cannot verify user is non-root.

    Check
    kubectl describe pod <pod>   # non-numeric user in the event
    Fix

    Set runAsUser to that user's number, or change USER in the Dockerfile to a number.

Prevent it

  • Ship the ConfigMaps and Secrets in the same release as the Deployment that reads them.
  • Mark optional references optional: true, and only those.

Now learn it: break and fix it yourself in Missing ConfigMap · Missing Secret key · Pod Security, then take Level 05 · Missing configuration on a real cluster. Open this in the interactive reference.

StartError, exit code 128, empty logs

Error or output

Error: failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "ngnix": executable file not found in $PATH: unknown

What it means

The runtime created the container but could not start its process, so nothing ran and there is nothing to log. Exit code 128 is the runtime's, not your app's. The Failed event says why, for example exec: "ngnix": executable file not found in $PATH.

Causes, most likely first

  1. A typo in command or args. The runtime looks for that executable and it does not exist: executable file not found in $PATH.

    Check
    kubectl get deploy <name> -o jsonpath='{.spec.template.spec.containers[0].command}'   # the misspelt name
    Fix

    Correct command or args on the Deployment to a binary that exists in the image.

  2. A script without execute permission, or with Windows line endings. The runtime cannot run it: permission denied, or no such file (for the interpreter with a hidden \r).

    Check
    kubectl describe pod <pod>   # permission denied, or no such file or directory
    Fix

    Make it executable in the image (chmod +x in the Dockerfile) and save it with Unix line endings.

  3. The binary is not in the image, for example a slim or distroless base without a shell.

    Check
    docker run --rm --entrypoint ls <image> /usr/local/bin   # the binary missing (fails on images with no ls at all)
    Fix

    Use the image's own ENTRYPOINT, or add the binary to the image.

Prevent it

  • Start the image with its real command in CI before deploying it.
  • Prefer the image's own ENTRYPOINT over overriding command in the manifest.

Now learn it: break and fix it yourself in Wrong command, then take Level 01 · Pods that will not start on a real cluster. Open this in the interactive reference.

exec format error

Error or output

exec /usr/local/bin/invoice: exec format error

What it means

The kernel refused to run the container's program because it is not in a format this machine can run. Usually the binary was built for another CPU architecture (arm64 against amd64). The container exits at once with this as its only log line.

Causes, most likely first

  1. The image was built for another architecture, for example on an Apple Silicon laptop (arm64) and run on amd64 nodes.

    Check
    kubectl get nodes -L kubernetes.io/arch
    docker buildx imagetools inspect <image>   # the node's architecture not under Platform: this cause
    Fix

    Build for the nodes, or for both: docker buildx build --platform linux/amd64,linux/arm64 -t <image> --push ..

  2. A single-architecture build was pushed over a multi-arch tag. Nodes of the other architecture now get an image they cannot run.

    Check
    docker buildx imagetools inspect <image>   # one platform where there were two
    Fix

    Push the multi-arch build again, and never push a single-architecture build to that tag.

  3. A mixed cluster without a nodeSelector. The Pod lands on a node of an architecture the image lacks.

    Check
    kubectl get pod <pod> -o wide; kubectl get nodes -L kubernetes.io/arch   # failing Pods only on one architecture
    Fix

    Select the right nodes with a nodeSelector on kubernetes.io/arch, or build for both.

  4. The command is a script with no #! line. The kernel cannot run it on any architecture.

    Check
    docker run --rm --entrypoint head <image> -n 1 <script>   # platforms match and no #!/bin/sh: this cause
    Fix

    Add #!/bin/sh (or its interpreter) as the first line, or run it through the interpreter in command.

Prevent it

  • Build images in CI, for every architecture you run, never from a laptop.
  • Never push a single-architecture build over a multi-arch tag.

Now learn it: break and fix it yourself in Wrong CPU architecture, then take Level 01 · Pods that will not start on a real cluster. Open this in the interactive reference.

Init:0/1 (or Init:CrashLoopBackOff)

Error or output

menu-5b8d9c6f7-tw4kp   0/1   Init:0/1                0             5m
menu-5b8d9c6f7-tw4kp   0/1   Init:CrashLoopBackOff   4 (30s ago)   5m

What it means

Init containers run one at a time, and each must exit 0 before the next one, and then the app, can start. Init:0/1 means none of one has finished yet; Init:CrashLoopBackOff means one keeps failing and being restarted.

Causes, most likely first

  1. It waits for a Service or DNS name that does not exist. A wait loop such as until nslookup db never ends, so the Pod stays at Init:0/1.

    Check
    kubectl logs <pod> -c <init-container>   # the same wait message over and over
    kubectl get svc -A | grep <name>   # missing, or in another namespace
    Fix

    Create the Service, or correct the name it waits for (<svc>.<namespace> across namespaces).

  2. It waits for a dependency that exists but is down. The Service is there with no Ready endpoints, so the check never succeeds.

    Check
    kubectl get endpointslices -l kubernetes.io/service-name=<svc>   # no ready endpoints: fix that app first
    Fix

    Fix the dependency first; the init container moves on by itself once it answers.

  3. Its own command fails. It exits with an error and the kubelet restarts it: Init:CrashLoopBackOff or Init:Error.

    Check
    kubectl logs <pod> -c <init-container> --previous   # an error and a non-zero exit
    kubectl describe pod <pod>   # Init Containers: Last State, Exit Code
    Fix

    Fix the command or what it reads; init containers must exit 0 for the Pod to go on.

Prevent it

  • Give wait loops a timeout and a log line that names what they wait for.
  • Prefer retries in the app over init containers that block on other services.

Now learn it: break and fix it yourself in Init container, then take Level 16 · The menu never loads on a real cluster. Open this in the interactive reference.

ContainerCreating (for minutes)

Error or output

Warning  FailedMount  kubelet  MountVolume.SetUp failed for volume "tls" : secret "menu-tls" not found
Warning  FailedMount  kubelet  Unable to attach or mount volumes: unmounted volumes=[tls], unattached volumes=[tls kube-api-access-8x2lp]: timed out waiting for the condition

What it means

The Pod has a node, but the kubelet cannot finish preparing it: mounting its volumes or setting up its network. No container has started, so kubectl logs is empty and the reason is only in the events. timed out waiting for the condition follows the real error and adds nothing.

Causes, most likely first

  1. A ConfigMap or Secret mounted as a volume does not exist. The kubelet retries the mount and never finishes: FailedMount, secret "<name>" not found.

    Check
    kubectl describe pod <pod>   # FailedMount names the volume and the object
    kubectl get secrets,configmaps   # in the Pod's namespace: not listed = this cause
    Fix

    Create the Secret or ConfigMap in the Pod's namespace, or correct its name in the volume.

  2. A ReadWriteOnce volume is still attached to another node. It can attach to one node at a time: FailedAttachVolume, Multi-Attach error.

    Check
    kubectl describe pod <pod>   # Multi-Attach error or FailedAttachVolume
    Fix

    See Multi-Attach error for volume: the old Pod or node still holds the volume.

  3. The network plugin cannot give the Pod an IP. The kubelet cannot create the Pod sandbox: FailedCreatePodSandBox.

    Check
    kubectl describe pod <pod>   # FailedCreatePodSandBox, failed to setup network for sandbox
    Fix

    See FailedCreatePodSandBox (CNI): look at the CNI Pod on that node.

Prevent it

  • Deploy the objects a Pod mounts before, or together with, the Pod.
  • Alert on Pods in ContainerCreating for more than a few minutes.

Now learn it: break and fix it yourself in Secret volume not found · Multi-Attach volume, then take Level 14 · Boss level: the Friday night incident on a real cluster. Open this in the interactive reference.

Multi-Attach error for volume

Error or output

Warning  FailedAttachVolume  attachdetach-controller  Multi-Attach error for volume "pvc-<uid>" Volume is already used by pod(s) orders-6c9b7f5d4-qx8zm

What it means

A ReadWriteOnce volume can be attached to one node at a time. The new Pod is on a different node from the Pod that still holds the volume, so the attach-detach controller refuses to attach it there and the new Pod waits in ContainerCreating.

Causes, most likely first

  1. A RollingUpdate starts the new Pod before the old one stops. The old Pod keeps the volume on its node.

    Check
    kubectl get pods -o wide   # old and new Pod on different nodes, both present
    Fix

    For a single-replica app on a ReadWriteOnce volume, use Recreate: kubectl patch deployment <name> -p '{"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}'.

  2. The old node died, and the volume is not detached yet. The controller waits about six minutes before forcing it.

    Check
    kubectl get nodes   # the old Pod's node NotReady
    Fix

    Wait, or once the node is shut down for good, kubectl taint node <node> node.kubernetes.io/out-of-service=nodeshutdown:NoExecute (Kubernetes 1.28 and later) so its volumes detach at once.

  3. Several replicas share one ReadWriteOnce claim. Only the replicas on one node can mount it.

    Check
    kubectl get deploy <name>   # replicas above 1, one claimName in the template
    Fix

    Use a StatefulSet with one claim per replica, or ReadWriteMany storage.

Prevent it

  • Use StatefulSets for apps that own a disk.
  • Never share one ReadWriteOnce claim between replicas.

Now learn it: break and fix it yourself in Multi-Attach volume, then take Level 08 · Data that survives on a real cluster. Open this in the interactive reference.

ImagePullBackOff: 429 toomanyrequests

Error or output

Failed to pull image "nginx:1.27": ... 429 Too Many Requests - Server message: toomanyrequests: You have reached your unauthenticated pull rate limit. https://www.docker.com/increase-rate-limit

What it means

Docker Hub limits how many pulls it serves per hour: per public IP address for anonymous pulls, and per account for logged-in ones. 429 Too Many Requests and toomanyrequests mean that limit is used up. unauthenticated in the message means the pull was anonymous.

Causes, most likely first

  1. Many nodes behind one NAT address pull at once (new nodes, a scale-out, an upgrade). They share one anonymous limit.

    Check
    kubectl get events -A | grep -c toomanyrequests   # many at the same time, on many nodes
    Fix

    Pull through a mirror or your own registry: kubectl set image deployment/<name> <container>=<mirror>/library/<image>:<tag>.

  2. A pull Secret that is not used, so pulls stay anonymous. The message says unauthenticated pull rate limit even though you have an account.

    Check
    kubectl get pod <pod> -o jsonpath='{.spec.imagePullSecrets}'   # empty: the login never reaches the Pod
    Fix

    Add the docker-registry Secret (for docker.io) to imagePullSecrets, or to the namespace's ServiceAccount.

  3. Images are referenced without a registry, so they come from Docker Hub. Each new node pulls them again.

    Check
    kubectl get pods -A -o jsonpath='{..image}' | tr ' ' '\n' | sort -u   # names without a registry host
    Fix

    Reference images by your mirror's name. Deleting Pods to retry only spends more of the limit.

  4. imagePullPolicy: Always on Pods that restart often. Each restart asks the registry again.

    Check
    kubectl get deploy <name> -o jsonpath='{.spec.template.spec.containers[*].imagePullPolicy}'   # Always
    Fix

    Use IfNotPresent with immutable tags.

Prevent it

  • Mirror the public images you use into your own registry, and reference them by that name.
  • Use imagePullPolicy IfNotPresent with immutable tags.

Now learn it: break and fix it yourself in Docker Hub rate limit, then take Level 01 · Pods that will not start on a real cluster. Open this in the interactive reference.

CreateContainerError: apparmor profile not found

Error or output

Warning  Failed  kubelet  Error: failed to create containerd container: apparmor profile not found k8s-deny-write

What it means

The Pod asks for a Localhost AppArmor profile: one that must already be loaded into the kernel of the node it runs on. The runtime could not find it, so it refused to create the container. It fails only on the nodes that lack the profile.

Causes, most likely first

  1. The profile was loaded on some nodes only. Pods work where it is and fail where it is not.

    Check
    kubectl get pods -o wide   # the failing Pods share nodes
    ssh <node> sudo aa-status | grep <profile>   # missing on those nodes: this cause
    Fix

    Load it on every node: sudo apparmor_parser -q /etc/apparmor.d/<profile>.

  2. The node rebooted and the profile is not loaded at boot. It was loaded by hand once.

    Check
    ssh <node> 'ls /etc/apparmor.d/<profile>; sudo aa-status | grep <profile>'   # file present, not loaded
    Fix

    Install it where the AppArmor service loads it at boot, through the node image or a DaemonSet.

  3. A typo in localhostProfile. The name asked for is not the one loaded.

    Check
    kubectl get pod <pod> -o jsonpath='{.spec.containers[0].securityContext.appArmorProfile}'   # compare with aa-status
    Fix

    Correct localhostProfile to the profile's exact name.

Prevent it

  • Install profiles with the node image or a DaemonSet, so every node has them at boot.
  • Label the nodes that have the profile, and select them, if not all do.

Now learn it: break and fix it yourself in AppArmor profile, then take Level 17 · Rejected at the door on a real cluster. Open this in the interactive reference.

FailedCreatePodSandBox (CNI)

Error or output

Warning  FailedCreatePodSandBox  kubelet  Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox "<id>": plugin type="calico" failed (add): stat /var/lib/calico/nodename: no such file or directory: check that the calico/node container is running and has mounted /var/lib/calico/
Warning  FailedCreatePodSandBox  kubelet  Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox "<id>": plugin type="flannel" failed (add): failed to load flannel 'subnet.env' file: open /run/flannel/subnet.env: no such file or directory
container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized

What it means

Before any container starts, the kubelet asks the runtime to create the Pod sandbox, and the runtime calls the CNI plugin to give it a network and an IP. That call failed, so the Pod stays in ContainerCreating. The event names the plugin (plugin type="calico") and its error.

Causes, most likely first

  1. The CNI plugin's own Pod (calico-node, kube-flannel, cilium, aws-node) is not running on that node. Its files are missing, so the plugin call fails.

    Check
    kubectl -n kube-system get pods -o wide | grep -Ei 'calico|flannel|cilium|aws-node'   # not Running on that node: this cause
    Fix

    Read its logs (kubectl -n kube-system logs <cni-pod>) and delete it so its DaemonSet starts a fresh one.

  2. No CNI installed yet. On a new kubeadm cluster the nodes stay NotReady: cni plugin not initialized.

    Check
    kubectl describe node <node> | grep -i network   # NetworkPluginNotReady
    Fix

    Install the plugin the cluster was built for, with a Pod CIDR that matches kubeadm's --pod-network-cidr.

  3. Plugin binaries are missing from /opt/cni/bin, or the config in /etc/cni/net.d is missing or broken.

    Check
    ls /etc/cni/net.d /opt/cni/bin   # on the node: empty or missing the plugin
    sudo journalctl -u kubelet | grep -i cni
    Fix

    Restart or reinstall the plugin's DaemonSet, which writes both on each node.

  4. The plugin has run out of Pod IPs for that node.

    Check
    kubectl describe pod <pod>   # no IP addresses available, or failed to allocate
    Fix

    Free addresses or give the plugin a bigger pool, as its documentation describes.

Prevent it

  • Alert when the CNI DaemonSet has fewer ready Pods than nodes.
  • Upgrade the CNI plugin together with the cluster, and never edit /etc/cni/net.d by hand.

Now learn it: break and fix it yourself in Node NotReady, then take Level 14 · Boss level: the Friday night incident on a real cluster. Open this in the interactive reference.

Restarting or not Ready

CrashLoopBackOff

Error or output

orders-6c9b7f5d4-qx8zm   0/1   CrashLoopBackOff   6 (2m ago)   10m
Warning  BackOff  kubelet  Back-off restarting failed container orders in pod orders-6c9b7f5d4-qx8zm_kitchen(<uid>)

What it means

The container starts, its main process exits, and the kubelet starts it again, waiting longer each time (up to five minutes). The RESTARTS count climbs. CrashLoopBackOff only says that it keeps exiting; the exit code and the previous run's logs say why.

Causes, most likely first

  1. The app fails at start: a missing setting, a bad config file, a database it cannot reach. It logs an error and exits, usually with code 1 or 2.

    Check
    kubectl logs <pod> --previous   # an error just before the end: this cause
    kubectl describe pod <pod>   # Last State: Terminated, Exit Code 1
    Fix

    Fix the configuration it names on the Deployment (kubectl set env deployment/<name> KEY=value, or the ConfigMap), so a new rollout replaces the Pods.

  2. The command or args are wrong. The process cannot even start, so the logs are empty: StartError with exit code 128, or 127 when a shell cannot find the command.

    Check
    kubectl describe pod <pod>   # Reason StartError, Exit Code 128, executable file not found
    kubectl get deploy <name> -o jsonpath='{.spec.template.spec.containers[0].command}'
    Fix

    Correct command or args; see StartError, exit code 128, empty logs.

  3. The container ran out of memory. The kernel killed it at its memory limit, so it exits with 137 and Reason OOMKilled.

    Check
    kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState}'   # reason OOMKilled, exitCode 137
    Fix

    Raise the memory limit above the real working set; see OOMKilled.

  4. A liveness probe kills a healthy app. The kubelet restarts the container when the probe fails, so it looks like a crash.

    Check
    kubectl describe pod <pod>   # Liveness probe failed and Killing events
    kubectl logs <pod> --previous   # a clean shutdown, not an error
    Fix

    Fix the probe, not the app; see Liveness probe failed.

Prevent it

  • Make the app fail fast with a clear message when a required setting is missing.
  • Run the image with production-like configuration in CI, and keep maxUnavailable low so a crashing rollout never takes every replica.

Now learn it: break and fix it yourself in CrashLoopBackOff · Wrong command · Wrong CPU architecture · Liveness probe · Slow start killed · OOMKilled, then take Level 01 · Pods that will not start on a real cluster. Open this in the interactive reference.

Liveness probe failed, container restarted

Error or output

Warning  Unhealthy  kubelet  Liveness probe failed: Get "http://10.244.1.7:8080/healthz": dial tcp 10.244.1.7:8080: connect: connection refused
Normal   Killing    kubelet  Container web failed liveness probe, will be restarted

What it means

The kubelet calls the liveness probe, and after failureThreshold failures in a row it kills the container and starts it again. connection refused means nothing listened on that port; a status code means the app answered, but not with 200 to 399. The app may be perfectly healthy: the probe decides, not the app.

Causes, most likely first

  1. The probe uses the wrong port or path. It fails every time, so the container restarts on a steady rhythm.

    Check
    kubectl get pod <pod> -o yaml | grep -A8 livenessProbe   # compare port and path with what the app serves
    kubectl exec <pod> -- wget -qO- -T 2 http://localhost:<port><path>   # fails here too: wrong port or path
    Fix

    Point the probe at the port and path the app really serves, in the Deployment's Pod template.

  2. No startupProbe, so the liveness probe starts before a slow app is up. It is killed during start and never gets there.

    Check
    kubectl describe pod <pod>   # Killing within the first minute, every run
    kubectl logs <pod> --previous   # ends while still starting up
    Fix

    Add a startupProbe with enough failureThreshold x periodSeconds for the slowest start; liveness waits until it passes.

  3. timeoutSeconds or failureThreshold is too strict for a busy app. Under load the answer takes longer than the timeout (1 second by default).

    Check
    kubectl describe pod <pod>   # context deadline exceeded or Client.Timeout in the Unhealthy event, mostly under load
    Fix

    Raise timeoutSeconds and failureThreshold so a slow answer under load does not count as dead.

  4. The probe checks a dependency (a database, another service) instead of the process. When the dependency is down, every replica is killed at once.

    Check
    kubectl exec <pod> -- wget -qO- http://localhost:<port><path>   # the response mentions the database or another service
    Fix

    Make liveness check only the process itself; put dependency checks in the readiness probe.

Prevent it

  • Let liveness check only the process itself; put dependency checks in readiness.
  • Test probes against the image locally (curl localhost:<port><path>) before shipping them.

Now learn it: break and fix it yourself in Liveness probe · Slow start killed, then take Level 06 · Never ready, always restarting on a real cluster. Open this in the interactive reference.

OOMKilled

Error or output

    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137

What it means

The container's memory went over its limit, so the Linux kernel killed it (exit code 137 = killed by signal 9) and the kubelet restarted it. Last State: Terminated, Reason: OOMKilled is the proof. The app usually logs nothing about it, because it is killed without warning.

Causes, most likely first

  1. The memory limit is lower than the app's real working set. Each time use reaches the limit, the kernel kills it.

    Check
    kubectl describe pod <pod>   # Last State OOMKilled, Exit Code 137
    kubectl top pod <pod>   # use climbing towards the limit before each restart
    Fix

    Raise the limit above the measured peak: kubectl set resources deployment/<name> --limits=memory=<size> --requests=memory=<size>.

  2. The manifest sets no limit, so the namespace's LimitRange default applies, often a small one. The limit came from the namespace, not you.

    Check
    kubectl describe limitrange   # a Default Limit for memory
    kubectl describe pod <pod>   # an annotation saying LimitRanger set the limit
    Fix

    Set requests and limits explicitly in the manifest.

  3. A runtime heap (JVM, Node.js) is set larger than the container limit. The runtime grows past the limit before it collects garbage.

    Check
    kubectl exec <pod> -- printenv JAVA_TOOL_OPTIONS JAVA_OPTS NODE_OPTIONS   # -Xmx or --max-old-space-size above the limit: this cause
    Fix

    Cap the heap below the limit, for example -XX:MaxRAMPercentage=75 for the JVM.

  4. A memory leak. Use grows steadily until the limit, whatever size you give it.

    Check
    kubectl top pod <pod>   # rises without levelling off, run after run
    Fix

    Raise the limit only to buy time; find the leak with the runtime's heap tools.

  5. The node ran out of memory, not the container. When Pods use far more than they request, the kernel can kill a container below its own limit.

    Check
    kubectl get events -A --field-selector reason=SystemOOM   # node-level OOM: this cause
    Fix

    Set memory requests close to real use, so the scheduler stops packing the node beyond its memory.

Prevent it

  • Size limits from measured peaks (kubectl top pod, a VPA recommendation) with headroom.
  • Alert on container restarts with reason OOMKilled, and on memory close to its limit.

Now learn it: break and fix it yourself in OOMKilled · OOMKilled, no limit set · VPA recommendation, then take Level 07 · Stuck in Pending on a real cluster. Open this in the interactive reference.

Running but 0/1 Ready

Error or output

Warning  Unhealthy  kubelet  Readiness probe failed: HTTP probe failed with statuscode: 404
Warning  Unhealthy  kubelet  Readiness probe failed: Get "http://10.244.2.5:8080/ready": dial tcp 10.244.2.5:8080: connect: connection refused

What it means

The container runs, but its readiness probe fails, so the Pod is taken out of its Services' endpoints and gets no traffic. Nothing restarts: readiness only controls traffic. A status code means the app answered with an error; connection refused means nothing listened on that port.

Causes, most likely first

  1. The probe's path or port is wrong. The app answers 404, or nothing listens on that port.

    Check
    kubectl get pod <pod> -o yaml | grep -A8 readinessProbe   # compare with the app's real port and path
    kubectl describe pod <pod>   # statuscode: 404 or connection refused
    Fix

    Correct the probe in the Deployment's Pod template (your manifest, or kubectl edit deployment/<name>), so new Pods roll out with it.

  2. The app needs longer to start than the probe allows. It fails until the app is up; that is normal for the first seconds, not for minutes.

    Check
    kubectl get pod <pod> -w   # becomes 1/1 after a while: this cause
    kubectl logs <pod>   # still starting up
    Fix

    Add a startupProbe for slow starts, rather than a long initialDelaySeconds.

  3. A dependency the readiness check needs is down. The app reports itself not ready on purpose (often 503).

    Check
    kubectl exec <pod> -- wget -qO- http://localhost:<port><path>   # names a database or service
    kubectl logs <pod>   # if the app logs requests, the probe's show up here
    Fix

    Fix the dependency. If every replica goes unready together, consider whether readiness should depend on it at all.

Prevent it

  • Give every app a cheap readiness endpoint, and test it in CI.
  • Watch for Pods that stay unready; they shrink capacity without any restart.

Now learn it: break and fix it yourself in Readiness probe, then take Level 06 · Never ready, always restarting on a real cluster. Open this in the interactive reference.

Config changed, app still uses the old value

Error or output

MENU_FILE=/etc/menu/summer.json

What it means

Updating a ConfigMap or Secret changes the object, not what running containers already read. Environment variables are set once, when the container starts. Mounted files do change, after a delay, but only if the app reads them again.

Causes, most likely first

  1. The value reaches the app through env or envFrom. Environment variables are fixed when the container starts.

    Check
    kubectl get pod <pod> -o jsonpath='{.status.startTime}'   # started before the change: this cause
    kubectl exec <pod> -- printenv <KEY>   # a ConfigMap value: still the old one
    Fix

    Restart the Pods: kubectl rollout restart deployment/<name>, then kubectl rollout status deployment/<name>.

  2. The file is mounted and updates after the kubelet's sync (often a minute or more), but the app only reads it at start.

    Check
    kubectl exec <pod> -- cat /etc/<app>/<file>   # a ConfigMap file: new content, old behaviour = the app does not reread
    Fix

    Restart the Pods, or make the app watch the file.

  3. The file is mounted with subPath. subPath mounts never update.

    Check
    kubectl get deploy <name> -o yaml | grep -B2 subPath
    Fix

    Mount the whole volume as a directory instead, or restart the Pods on each change.

  4. For a Secret, checking the value would print it. Compare lengths instead.

    Check
    kubectl exec <pod> -- sh -c 'printf %s "$KEY" | wc -c'   # differs from the new value's length: the Pod has the old one
    Fix

    Restart the Pods as above; do not print Secret values to check them.

Prevent it

  • Put a hash of the config in a Pod template annotation (Helm and Kustomize can), so a config change rolls out by itself.
  • Or use immutable, versioned ConfigMaps (menu-config-v2) and change the reference.

Now learn it: break and fix it yourself in ConfigMap not reloaded, then take Level 05 · Missing configuration on a real cluster. Open this in the interactive reference.

Password rejected though the Secret is right

Error or output

FATAL:  password authentication failed for user "accounts"

What it means

The database rejected the password the app sent. Kubernetes passes a Secret's value to the app exactly as stored, so if the value is wrong, it was stored wrong. But the database gives the same message when the password is right and the user or its settings are not, so check which side is wrong before changing anything.

Causes, most likely first

  1. The value was encoded twice: a base64 value put under stringData:, or passed to --from-literal, which encode it again. The app sends a base64 string as its password.

    Check
    kubectl get secret <name> -o jsonpath='{.data.<KEY>}' | base64 -d | wc -c   # length only: about 4/3 of the password's, often ending in =, means encoded twice
    Fix

    Recreate it from the plain value: kubectl create secret generic <name> --from-file=KEY=<file> --dry-run=client -o yaml | kubectl apply -f -, with the file written by printf %s (no newline), which also keeps the password out of shell history.

  2. echo without -n added a newline before encoding. The password the app sends ends in a newline.

    Check
    kubectl get secret <name> -o jsonpath='{.data.<KEY>}' | base64 -d | tail -c 1 | od -c   # \n: a trailing newline
    Fix

    Recreate it as above, and use echo -n or printf %s when encoding by hand.

  3. The Secret was changed but the app read it at start. It still sends the old password.

    Check
    kubectl exec <pod> -- sh -c 'printf %s "$DB_PASSWORD" | wc -c'   # differs from the stored value's length: the Pod has the old one
    Fix

    Restart the Pods: kubectl rollout restart deployment/<name>.

  4. The Secret is right and the database disagrees. PostgreSQL gives this same message for a wrong password, a user that does not exist, and a user with no SCRAM password where pg_hba.conf asks for scram-sha-256; only its server log says which.

    Check
    kubectl logs <postgres-pod> | grep -A1 'password authentication failed'   # DETAIL: Password does not match, Role does not exist, or no valid SCRAM secret
    Fix

    Fix the database side the DETAIL line names: create the user, set its password, or match pg_hba.conf's method.

Prevent it

  • Put plain values under stringData, base64 only under data, and use echo -n when encoding.
  • Create Secrets from files or a secret manager, not by hand.

Now learn it: break and fix it yourself in Secret encoded twice, then take Level 05 · Missing configuration on a real cluster. Open this in the interactive reference.

Read-only file system

Error or output

nginx: [emerg] mkdir() "/var/cache/nginx/client_temp" failed (30: Read-only file system)

What it means

The app tried to write a file and the kernel refused because that file system is mounted read-only (error 30, EROFS). With readOnlyRootFilesystem: true, every path that is not on a writable volume is read-only.

Causes, most likely first

  1. Hardening turned on readOnlyRootFilesystem for an image that writes caches, pid files or /tmp at start.

    Check
    kubectl logs <pod> --previous   # the path it tried to write
    kubectl get deploy <name> -o yaml   # readOnlyRootFilesystem: true, no volume at that path
    Fix

    Keep the root read-only, and mount an emptyDir at each path the app writes (for nginx: /var/cache/nginx, /var/run, /tmp).

  2. A volume is mounted read-only where the app writes.

    Check
    kubectl get deploy <name> -o jsonpath='{range .spec.template.spec.containers[*].volumeMounts[*]}{.mountPath} {.readOnly}{"\n"}{end}'   # the failing path under a mount marked true: this cause
    Fix

    Remove readOnly from that mount, or write somewhere else. ConfigMap and Secret volumes are always read-only.

Prevent it

  • Find the paths an image writes before hardening it, by running it with a read-only root locally.
  • Prefer images built to run read-only and as non-root.

Now learn it: break and fix it yourself in Read-only root breaks nginx, then take Level 17 · Rejected at the door on a real cluster. Open this in the interactive reference.

Evicted

Error or output

Status:   Failed
Reason:   Evicted
Message:  The node was low on resource: ephemeral-storage.

What it means

The kubelet stopped the Pod to protect its node, which was running low on memory, disk or process IDs. The Pod stays in the list as Failed with Reason Evicted, and its controller creates a replacement elsewhere. The Message names the resource that ran low.

Causes, most likely first

  1. The node's disk is nearly full, often with old images under /var/lib/containerd. The kubelet evicts Pods to free ephemeral storage: The node was low on resource: ephemeral-storage.

    Check
    kubectl describe node <node>   # DiskPressure True, and the disk-pressure taint
    ssh <node> sudo df -h   # / or /var near 100%
    Fix

    Free the disk: sudo crictl rmi --prune on the node removes unused images; clear large logs.

  2. The node is low on memory. Pods using more than they request go first, BestEffort Pods first of all.

    Check
    kubectl describe node <node>   # MemoryPressure True
    kubectl get pod <pod> -o jsonpath='{.status.message}'   # low on resource: memory
    Fix

    Set memory requests close to real use, so critical Pods are not BestEffort and nodes are not overpacked.

  3. One Pod used more ephemeral storage than its own limit. Only that Pod is evicted, even with a healthy node.

    Check
    kubectl get pod <pod> -o jsonpath='{.status.message}'   # ephemeral local storage usage exceeds the total limit
    Fix

    Raise its ephemeral-storage limit, or write the data to a volume instead.

Prevent it

  • Set requests (and ephemeral-storage limits) so critical Pods are not BestEffort.
  • Alert on node disk use well before the kubelet's eviction threshold.

Now learn it: break and fix it yourself in Node DiskPressure, then take Level 07 · Stuck in Pending on a real cluster. Open this in the interactive reference.

Scheduling and storage

Pending

Error or output

Warning  FailedScheduling  default-scheduler  0/3 nodes are available: 3 Insufficient memory. preemption: 0/3 nodes are available: 3 No preemption victims found for incoming pod.
0/3 nodes are available: 1 node(s) had untolerated taint {dedicated: gpu}, 2 node(s) didn't match Pod's node affinity/selector.

What it means

The scheduler tried every node and none passed all its checks, so the Pod has no node (NODE <none> in kubectl get pods). 0/3 nodes are available counts the nodes it tried, and each reason after it says how many nodes failed which check. The preemption: part says whether evicting lower-priority Pods could help.

Causes, most likely first

  1. The Pod asks for more CPU or memory than any node has free. The scheduler counts requests, not real use, so a node can look idle and still be full: Insufficient cpu or Insufficient memory.

    Check
    kubectl describe pod <pod>   # Insufficient memory in FailedScheduling: this cause
    kubectl describe nodes | grep -A8 'Allocated resources'   # requests near 100% on every node
    Fix

    Lower the request to what the app really uses (kubectl set resources deployment/<name> --requests=memory=<size>), or add capacity.

  2. A nodeSelector or node affinity names a label no node has. The scheduler then skips every node: didn't match Pod's node affinity/selector.

    Check
    kubectl get pod <pod> -o jsonpath='{.spec.nodeSelector}'   # the labels it needs
    kubectl get nodes --show-labels   # no node with that label: this cause
    Fix

    Correct the label in the Pod template, or label the node meant for it: kubectl label node <node> <key>=<value>.

  3. A node has a taint the Pod does not tolerate. Taints keep Pods off a node unless they opt in: had untolerated taint {key: value}.

    Check
    kubectl describe node <node> | grep Taints   # the taint named in the event
    Fix

    Add a matching toleration only if the Pod belongs on that node; otherwise remove the selector that points it there.

  4. A PersistentVolumeClaim the Pod uses is not Bound. The scheduler waits for the volume before it places the Pod: unbound immediate PersistentVolumeClaims.

    Check
    kubectl get pvc   # STATUS Pending: see PersistentVolumeClaim Pending
    Fix

    Get the claim bound first (a StorageClass that exists, a running provisioner); the Pod schedules once it is.

  5. The node already runs as many Pods as its kubelet allows (maxPods, 110 by default). It is full by count, not by CPU: Too many pods.

    Check
    kubectl get pods -A -o wide --field-selector spec.nodeName=<node> | wc -l   # close to the node's pod capacity
    kubectl get node <node> -o jsonpath='{.status.allocatable.pods}'
    Fix

    Add nodes, or raise maxPods in the kubelet config where the node and its Pod network have room for more.

Prevent it

  • Set requests from measured use (kubectl top pods, a VPA recommendation) rather than guesses.
  • Alert on Pods that stay Pending for more than a few minutes, and on nodes running out of allocatable memory.

Now learn it: break and fix it yourself in Pending · Node selector · Untolerated taint · Too many pods · PVC Pending · Scheduler down, then take Level 07 · Stuck in Pending on a real cluster. Open this in the interactive reference.

Pending with no events at all

Error or output

Events:  <none>
kube-scheduler-control-plane   0/1   CrashLoopBackOff   5 (40s ago)   3m

What it means

Every scheduling attempt writes an event, even a failed one (FailedScheduling). Events: <none> on a Pending Pod therefore means no scheduler has looked at it at all.

Causes, most likely first

  1. kube-scheduler is down: a broken static Pod manifest, a missing kubeconfig, a bad flag. Nobody picks a node, so nobody writes events.

    Check
    kubectl -n kube-system get pods   # kube-scheduler-<node> CrashLoopBackOff or missing
    kubectl -n kube-system logs kube-scheduler-<node>   # the startup error
    Fix

    Fix /etc/kubernetes/manifests/kube-scheduler.yaml; the kubelet restarts it.

  2. spec.schedulerName names a scheduler that is not running. The default scheduler ignores Pods meant for another one.

    Check
    kubectl get pod <pod> -o jsonpath='{.spec.schedulerName}'   # not default-scheduler
    Fix

    Run that scheduler, or remove the field.

  3. spec.nodeName is set to a node that does not exist. The Pod skips the scheduler, and no kubelet claims it.

    Check
    kubectl get pod <pod> -o jsonpath='{.spec.nodeName}'; kubectl get nodes   # a name not in the list
    Fix

    Remove nodeName from the template and use a nodeSelector instead.

Prevent it

  • Back up the control-plane manifests before editing them, and change one thing at a time.
  • Alert on control-plane Pods that are not Running.

Now learn it: break and fix it yourself in Scheduler down, then take Level 14 · Boss level: the Friday night incident on a real cluster. Open this in the interactive reference.

Pending: didn't match pod anti-affinity rules

Error or output

Warning  FailedScheduling  default-scheduler  0/3 nodes are available: 3 node(s) didn't match pod anti-affinity rules. preemption: 0/3 nodes are available: 3 No preemption victims found for incoming pod.

What it means

Required pod anti-affinity says: never put this Pod in the same topology (a node, or a zone) as a matching Pod. When every node or zone already has one, the scheduler has nowhere left: didn't match pod anti-affinity rules. Evicting Pods of the same priority is not allowed, hence No preemption victims found.

Causes, most likely first

  1. More replicas than nodes, with requiredDuringScheduling anti-affinity on kubernetes.io/hostname. One per node is the most it can place.

    Check
    kubectl get pods -l app=<app> -o wide   # one replica on every schedulable node already
    kubectl get deploy <name> -o yaml | grep -A8 podAntiAffinity   # requiredDuringScheduling
    Fix

    Use preferredDuringSchedulingIgnoredDuringExecution so extra replicas may share a node, or topologySpreadConstraints with whenUnsatisfiable: ScheduleAnyway.

  2. A node was drained or removed, so the rule cannot be met any more. Its replica has nowhere to go.

    Check
    kubectl get nodes   # SchedulingDisabled nodes do not count
    Fix

    Uncordon the node or add one, or keep the hard rule and lower replicas to the number of nodes.

  3. The topologyKey is a zone, with fewer zones than replicas.

    Check
    kubectl get nodes -L topology.kubernetes.io/zone   # fewer zones than replicas
    Fix

    Spread by zone with topologySpreadConstraints and maxSkew instead of a hard anti-affinity rule.

Prevent it

  • Use hard anti-affinity only when two replicas on one node is really unacceptable.
  • Remember drains and node failures: a hard rule with replicas equal to nodes cannot reschedule.

Now learn it: break and fix it yourself in Anti-affinity blocks scaling, then take Level 12 · The GPU node on a real cluster. Open this in the interactive reference.

Pending: No preemption victims found

Error or output

Warning  FailedScheduling  default-scheduler  0/3 nodes are available: 3 Insufficient cpu. preemption: 0/3 nodes are available: 3 No preemption victims found for incoming pod.

What it means

The Pod does not fit on any node (Insufficient cpu), so the scheduler looked for lower-priority Pods it could evict to make room. No preemption victims found for incoming pod means it found none: everything running has the same or a higher priority. This differs from Preemption is not helpful for scheduling, which is printed for nodes that fail on something eviction cannot fix.

Causes, most likely first

  1. Critical services and batch jobs both run at the default priority 0. Nothing is lower than the Pending Pod, so nothing may be evicted.

    Check
    kubectl get priorityclass
    kubectl get pods -A -o custom-columns=NAME:.metadata.name,PRIORITY:.spec.priority   # all 0: this cause
    Fix

    Create a PriorityClass for critical workloads (kubectl create priorityclass <name> --value=100000) and set it on the Deployment: kubectl patch deployment <name> -p '{"spec":{"template":{"spec":{"priorityClassName":"<name>"}}}}'.

  2. A priorityClassName that does not exist. The Pod is then not created at all (FailedCreate), so the priority never applies.

    Check
    kubectl describe rs <replicaset>   # no PriorityClass with name ... was found
    Fix

    Create the PriorityClass, or correct the name.

  3. Requests fill the nodes completely, and everything running really matters. Preemption cannot help.

    Check
    kubectl describe nodes | grep -A8 'Allocated resources'   # requests near 100%
    Fix

    Add capacity, or lower requests that are larger than real use.

  4. Not this case: Preemption is not helpful for scheduling is printed for nodes that fail on something eviction cannot fix, such as an untolerated taint, a node selector or an unbound PersistentVolumeClaim.

    Check
    kubectl describe pod <pod>   # Preemption is not helpful: see Pending
    Fix

    See Pending: fix the taint, selector or claim the event names.

Prevent it

  • Give critical services a higher PriorityClass than batch work from day one.
  • Use ResourceQuotas per priority class so batch work cannot fill the cluster.

Now learn it: break and fix it yourself in Critical Pod can't schedule, then take Level 07 · Stuck in Pending on a real cluster. Open this in the interactive reference.

PersistentVolumeClaim Pending

Error or output

Warning  ProvisioningFailed  persistentvolume-controller  storageclass.storage.k8s.io "fast-ssd" not found
Normal   FailedBinding       persistentvolume-controller  no persistent volumes available for this claim and no storage class is set
Normal   WaitForFirstConsumer  persistentvolume-controller  waiting for first consumer to be created before binding

What it means

A claim is Pending until a PersistentVolume is bound to it, either an existing one or one the StorageClass's provisioner creates. Pods that use the claim wait with it. The claim's events say which step is stuck.

Causes, most likely first

  1. The storageClassName does not exist, or the claim names no class and the cluster has no default. Nothing can create a volume: storageclass "fast-ssd" not found, or no storage class is set.

    Check
    kubectl describe pvc <name>   # ProvisioningFailed or FailedBinding
    kubectl get storageclass   # the class missing, or none marked (default)
    Fix

    Use a class that exists, or mark one default: kubectl patch storageclass <name> -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'.

  2. The class uses WaitForFirstConsumer. It binds only once a Pod using the claim is scheduled, so the volume is made in the right zone. This is normal.

    Check
    kubectl describe pvc <name>   # waiting for first consumer to be created before binding
    Fix

    Nothing to fix: create the Pod. If the Pod is Pending too, fix the Pod's scheduling first.

  3. The class's CSI driver is not installed or not running. The claim keeps waiting for a volume to be created by the external provisioner.

    Check
    kubectl describe pvc <name>   # Waiting for a volume to be created ... external provisioner
    kubectl get csidriver; kubectl get pods -A | grep -i csi   # the class's PROVISIONER not running
    Fix

    Install or repair the CSI driver the StorageClass names, and read its controller Pod's logs.

  4. The provisioner cannot satisfy the size or access mode. Most block storage is ReadWriteOnce only, so a ReadWriteMany claim fails.

    Check
    kubectl describe pvc <name>   # ProvisioningFailed with the provisioner's reason
    kubectl get pvc <name> -o jsonpath='{.spec.accessModes}'
    Fix

    Ask for an access mode and size the provisioner supports, or use storage that offers ReadWriteMany.

Prevent it

  • Keep one default StorageClass in every cluster, and the same class names across environments.
  • Claims cannot change class later: choose it before the first deploy.

Now learn it: break and fix it yourself in PVC Pending, then take Level 08 · Data that survives on a real cluster. Open this in the interactive reference.

no space left on device

Error or output

inventory: fatal: write /data/inventory.db-wal: no space left on device

What it means

A write failed because the file system it went to is full. In a Pod, that is usually its PersistentVolume, sometimes the node's own disk. The path in the message says which file system it was.

Causes, most likely first

  1. The PersistentVolumeClaim is too small for the data. The volume fills up and the next write fails.

    Check
    kubectl exec <pod> -- df -h /data   # Use% 100%: this cause
    Fix

    Grow the claim if its StorageClass allows it (kubectl get storageclass, ALLOWVOLUMEEXPANSION): kubectl patch pvc <name> -p '{"spec":{"resources":{"requests":{"storage":"<bigger>"}}}}'. Then check kubectl describe pvc <name>: FileSystemResizePending means it grows when the Pod restarts.

  2. Logs or temporary files are written to the volume and never cleaned.

    Check
    kubectl exec <pod> -- du -sh /data/*   # a log or tmp directory taking most of it
    Fix

    Delete what does not belong there, and rotate or ship the logs.

  3. Out of inodes, not bytes: many small files. df -h shows free space but no new file can be created.

    Check
    kubectl exec <pod> -- df -i /data   # IUse% 100%: this cause
    Fix

    Remove the small files, or use a file system with more inodes.

  4. The node's own disk is full, for a path that is not on a volume.

    Check
    kubectl describe node <node>   # DiskPressure True
    Fix

    See Evicted: free the node's disk.

Prevent it

  • Use StorageClasses with allowVolumeExpansion: true.
  • Alert on kubelet_volume_stats_used_bytes above 80% of capacity.

Now learn it: break and fix it yourself in Volume full, then take Level 08 · Data that survives on a real cluster. Open this in the interactive reference.

DaemonSet not on every node

Error or output

NAME        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
log-agent   2         2         2       2            2           <none>          30d

What it means

A DaemonSet runs one Pod on every node it counts as eligible. DESIRED is that count, so if it is lower than the number of nodes, the DaemonSet does not consider the missing node at all, and it can still report every desired Pod ready.

Causes, most likely first

  1. The node has a taint the DaemonSet does not tolerate. Untolerated NoSchedule taints exclude the node, so DESIRED does not count it.

    Check
    kubectl get ds <name>   # DESIRED below the node count
    kubectl describe node <node> | grep Taints   # a taint the DaemonSet does not tolerate
    Fix

    Add a matching toleration to the DaemonSet's Pod template (agents often tolerate every taint: operator: Exists).

  2. A nodeSelector or affinity on the DaemonSet excludes the node. DESIRED does not count it either.

    Check
    kubectl get ds <name> -o wide   # NODE SELECTOR
    kubectl get node <node> --show-labels   # the label missing
    Fix

    Label the node, or widen the selector.

  3. The node is out of resources for the agent. Then DESIRED counts the node, but its Pod stays Pending, so READY is below DESIRED.

    Check
    kubectl get pods -l app=<name> -o wide   # one Pending on that node
    Fix

    Free room on the node, or give the agent a higher PriorityClass so it can preempt.

Prevent it

  • Give node agents tolerations for every taint you use, including control-plane ones if they belong there.
  • Alert when a DaemonSet's DESIRED count differs from the number of nodes it should cover.

Now learn it: break and fix it yourself in DaemonSet missing a node, then take Level 19 · One agent per node on a real cluster. Open this in the interactive reference.

StatefulSet stuck: only <name>-0 exists

Error or output

NAME      READY   AGE
records   0/3     10m
records-0   0/1   CrashLoopBackOff   5 (1m ago)   10m

What it means

With the default podManagementPolicy, OrderedReady, a StatefulSet creates <name>-1 only after <name>-0 is Running and Ready, and so on. If the first Pod never gets Ready, the others are never created.

Causes, most likely first

  1. <name>-0 cannot start: a missing ConfigMap, an unbound claim, a crash. The rest wait for it.

    Check
    kubectl describe pod <name>-0   # its own error: fix that first
    kubectl logs <name>-0
    Fix

    Fix the first Pod; the others follow once it is Ready.

  2. <name>-0 runs but its readiness probe never passes. Running is not enough for OrderedReady.

    Check
    kubectl get pod <name>-0   # Running, READY 0/1
    Fix

    Fix the probe so it passes as soon as the Pod can serve.

  3. Its PersistentVolumeClaim is Pending, or the headless Service is missing.

    Check
    kubectl get pvc -l app=<name>   # Pending
    kubectl get svc <serviceName>   # NotFound
    Fix

    Get the claim bound and create the headless Service the StatefulSet names. If replicas do not depend on each other, a new StatefulSet can use podManagementPolicy: Parallel.

Prevent it

  • Give StatefulSets a readiness probe that passes as soon as the Pod can serve.
  • Test a scale-up from zero in staging.

Now learn it: break and fix it yourself in StatefulSet stuck at 0, then take Level 15 · Every replica is the same, except it is not on a real cluster. Open this in the interactive reference.

Networking and traffic

Service has no endpoints

Error or output

Endpoints:         <none>
NAME       ENDPOINTS   AGE
menu-svc   <none>      2h

What it means

A Service sends traffic to the Ready Pods its selector matches. Endpoints: <none> means that list is empty: no Pod matches the selector, or none of those that match is Ready. Calls fail at once.

Causes, most likely first

  1. The Service's selector and the Pods' labels differ, often by one letter. The selector matches nothing.

    Check
    kubectl describe svc <svc>   # Selector
    kubectl get pods -l <key>=<value>   # the Service's selector exactly: No resources found = this cause
    Fix

    Correct the selector, or the labels in the Deployment's Pod template (not on running Pods).

  2. The Pods match but are not Ready. Only Ready Pods become endpoints.

    Check
    kubectl get pods -l <key>=<value>   # Pods listed, READY 0/1: this cause
    Fix

    Fix their readiness probe; see Running but 0/1 Ready.

  3. The Service is in a different namespace from the Pods. A selector only matches Pods in its own namespace.

    Check
    kubectl get svc -A | grep <svc>; kubectl get pods -A -l <key>=<value>   # different namespaces
    Fix

    Create the Service in the Pods' namespace, and call it as <svc>.<namespace> from elsewhere.

Prevent it

  • Generate the selector and the Pod labels from one value (a Helm value, a Kustomize label).
  • Alert on Services with zero ready endpoints.

Now learn it: break and fix it yourself in No endpoints · Readiness probe, then take Level 02 · The Service that routes nowhere on a real cluster. Open this in the interactive reference.

Connection refused / timed out

Error or output

wget: can't connect to remote host (10.96.112.40): Connection refused
wget: download timed out

What it means

Refused and timed out are different failures. Refused means something answered and said no: nothing listens on that port, or the Service has no endpoints. Timed out means nothing answered at all: the packets were dropped, usually by a NetworkPolicy.

Causes, most likely first

  1. The Service's targetPort is not the port the app listens on. kube-proxy forwards to a closed port, so the Pod refuses.

    Check
    kubectl exec <pod> -- wget -qO- -T 2 http://<pod-ip>:<container-port>   # works by Pod IP but not by Service: this cause
    kubectl get svc <svc> -o yaml | grep -A4 ports   # targetPort
    Fix

    Make targetPort the port the container listens on, or name the container port and use the name.

  2. The Service has no endpoints. With nothing to forward to, the connection is refused at once.

    Check
    kubectl get endpointslices -l kubernetes.io/service-name=<svc>   # no endpoints: this cause
    Fix

    See Service has no endpoints.

  3. The app listens on 127.0.0.1, not 0.0.0.0. It is reachable only from inside its own Pod.

    Check
    kubectl exec <pod> -- netstat -tln   # 127.0.0.1:<port>, not 0.0.0.0 or :::<port> (or ss -tln)
    Fix

    Make the app listen on 0.0.0.0.

  4. A NetworkPolicy drops the traffic. Dropped packets get no answer, so the client times out instead of being refused.

    Check
    kubectl get networkpolicy -n <target-namespace>   # a policy selecting the target Pods
    kubectl exec <pod> -- wget -qO- -T 2 http://<pod-ip>:<container-port>   # times out by Pod IP too: this cause
    Fix

    Add an allow rule for the client's labels and the port to the policy that selects the target Pods.

Prevent it

  • Name container ports and use the name as targetPort, so a port change cannot break the Service.
  • Add NetworkPolicies together with the allow rules each app needs, and test them in CI.

Now learn it: break and fix it yourself in Wrong targetPort · NetworkPolicy, then take Level 11 · Everyone can reach the database on a real cluster. Open this in the interactive reference.

Some requests fail, some work

Error or output

Endpoints:         10.244.1.4:80,10.244.2.6:80,10.244.3.9:8081

What it means

A Service spreads requests across all the Pods its selector matches. When some requests fail or show another version, some of those Pods are not the ones you meant, or cannot answer. The endpoint list shows which Pods are in.

Causes, most likely first

  1. Another Deployment (a canary, an old version) carries the same labels. The Service sends part of the traffic to it.

    Check
    kubectl get pods -l <selector> --show-labels -o wide   # Pods from two Deployments: this cause
    Fix

    Make the selector specific: kubectl set selector service <svc> app=<app>,track=stable.

  2. Some Pods listen on a different port from targetPort. Requests that reach them are refused.

    Check
    kubectl get endpointslices -l kubernetes.io/service-name=<svc>   # an endpoint on an odd port, such as 8081
    Fix

    Make every Pod behind the Service serve the targetPort, or give the odd ones their own Service.

  3. One node's Pods are unreachable: a NetworkPolicy, or a broken CNI on that node.

    Check
    kubectl get pods -l <selector> -o wide   # failures line up with one node
    Fix

    Look at that node's CNI Pod and the NetworkPolicies that select those Pods.

Prevent it

  • Give canaries their own label (track=canary) and select it explicitly.
  • Watch error rates per Pod, not only per Service.

Now learn it: break and fix it yourself in Canary leaking, then take Level 02 · The Service that routes nowhere on a real cluster. Open this in the interactive reference.

Could not resolve host (DNS)

Error or output

wget: bad address 'menu-svc'
curl: (6) Could not resolve host: menu-svc
** server can't find menu-svc.kitchen.svc.cluster.local: NXDOMAIN
;; connection timed out; no servers could be reached

What it means

The Pod asked the cluster DNS (CoreDNS) for a name and got no address. NXDOMAIN (server can't find, bad address) means DNS answered that the name does not exist. no servers could be reached or a timeout means DNS did not answer at all.

Causes, most likely first

  1. The name is wrong, or the Service is in another namespace. A short name only works inside the same namespace, so DNS answers NXDOMAIN.

    Check
    kubectl get svc -A | grep <svc>   # the namespace it really is in
    kubectl exec <pod> -- nslookup <svc>.<namespace>.svc.cluster.local   # resolves: use the longer name
    Fix

    Use the right name; across namespaces it is <svc>.<namespace>.

  2. An egress NetworkPolicy does not allow DNS. Queries to kube-dns on port 53 are dropped, so lookups time out.

    Check
    kubectl get networkpolicy -n <ns>   # a policy with policyTypes Egress
    kubectl exec <pod> -- nslookup kubernetes.default   # times out too: this cause
    Fix

    Allow egress to kube-dns in kube-system on port 53, UDP and TCP, in the default-deny policy.

  3. CoreDNS is not running. Nothing answers the queries, so every lookup times out.

    Check
    kubectl get pods -n kube-system -l k8s-app=kube-dns   # none Running, or 0 replicas
    Fix

    Bring it back: kubectl -n kube-system scale deployment coredns --replicas=2, and read its logs if it crashes.

Prevent it

  • Ship a DNS allow rule with every default-deny egress policy.
  • Run at least two CoreDNS replicas with a PodDisruptionBudget.

Now learn it: break and fix it yourself in DNS across namespaces · CoreDNS down · Egress lockdown breaks DNS, then take Level 04 · Connection refused on a real cluster. Open this in the interactive reference.

Ingress returns 404 or 503

Error or output

HTTP/1.1 404 Not Found
HTTP/1.1 503 Service Temporarily Unavailable

What it means

The ingress controller received the request, so DNS and the load balancer work. 404 means no Ingress rule matched the host and path. 503 means a rule matched, but its backend Service has no Ready Pods to send to.

Causes, most likely first

  1. 404: no rule matches the request's host or path, or the Ingress has an ingressClassName this controller does not serve. The controller falls back to its default backend.

    Check
    kubectl describe ingress <name>   # Rules: the host and paths
    kubectl get ingressclass   # the class the Ingress names exists
    curl -i -H 'Host: <host>' http://<ingress-address>/   # 404 with the right Host too: rule or class
    Fix

    Make host, path and ingressClassName match the request and the controller.

  2. 404 on sub-paths only: pathType is Exact where Prefix was meant. /menu matches, /menu/today does not.

    Check
    kubectl get ingress <name> -o yaml   # pathType: Exact
    Fix

    Use pathType: Prefix for a path with pages under it.

  3. Every page shows the home page, or the app's own 404: a rewrite-target drops or keeps the wrong part of the path. The app gets a different path from the one the user asked for.

    Check
    kubectl get ingress <name> -o yaml | grep rewrite-target
    kubectl logs <app-pod>   # the path the app received
    Fix

    Fix the rewrite-target so the app gets the path it expects.

  4. 503: the backend Service has no Ready endpoints, or the Ingress names a wrong Service or port. The controller has nowhere to send the request.

    Check
    kubectl describe ingress <name>   # Backends: <error: services "x" not found> or no endpoints
    kubectl get endpointslices -l kubernetes.io/service-name=<backend-svc>
    Fix

    Correct the Service name and port in the Ingress, or give the Service Ready Pods.

Prevent it

  • Test each Ingress with curl and its Host header in CI or a smoke test after deploy.
  • Keep the Ingress, Service and Deployment in one release so names and ports stay in step.

Now learn it: break and fix it yourself in Ingress 404 · Ingress 503 · Ingress path too exact · Ingress path not rewritten, then take Level 04 · Connection refused on a real cluster. Open this in the interactive reference.

Ingress serves the fake certificate

Error or output

*  subject: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate
curl: (60) SSL certificate problem: self-signed certificate

What it means

ingress-nginx answers HTTPS with its built-in self-signed certificate, Kubernetes Ingress Controller Fake Certificate, whenever it has no valid certificate for the host the client asked for. Browsers and curl reject it. Something stopped the controller from using your Secret.

Causes, most likely first

  1. The Secret named in spec.tls does not exist, or is in another namespace. The controller can only read TLS Secrets in the Ingress's namespace.

    Check
    kubectl describe ingress <name>   # TLS: <secret> terminates <host>
    kubectl get secret <secret> -n <ingress-namespace>   # NotFound: this cause
    Fix

    Create it in the Ingress's namespace with the name spec.tls uses: kubectl create secret tls <name> --cert=<file.crt> --key=<file.key>.

  2. The Secret is not of type kubernetes.io/tls, or the key does not match the certificate. The controller skips it.

    Check
    kubectl get secret <secret> -o jsonpath='{.type}'   # not kubernetes.io/tls
    kubectl -n ingress-nginx logs deploy/ingress-nginx-controller | grep <secret>   # why it was skipped
    Fix

    Recreate it with kubectl create secret tls, which checks the pair.

  3. The certificate does not name the host. The controller will not serve it for that host.

    Check
    openssl x509 -in <file.crt> -noout -ext subjectAltName   # the host missing
    Fix

    Issue a certificate that names the host.

  4. The host is not under spec.tls hosts, or the client sends no SNI (it connects by IP). The controller cannot tell which certificate to use, so it answers with its default.

    Check
    openssl s_client -connect <ingress-address>:443 -servername <host> </dev/null 2>/dev/null | openssl x509 -noout -subject   # right certificate with the name, fake without: this cause
    Fix

    List the host under spec.tls hosts, and connect by name.

Prevent it

  • Issue certificates automatically (cert-manager) and alert on their expiry.
  • Test HTTPS for each host after deploy: curl -v https://<host>/.

Now learn it: break and fix it yourself in Ingress TLS, then take Level 04 · Connection refused on a real cluster. Open this in the interactive reference.

HTTPRoute not attached (Gateway API)

Error or output

  Conditions:
    Type:     Accepted
    Status:   False
    Reason:   NotAllowedByListeners

What it means

An HTTPRoute only takes effect when a Gateway accepts it. The route's status then shows, per Gateway, Accepted (the Gateway took it) and ResolvedRefs (its backends are valid), each with a reason. A route nobody accepted gets 404 from the Gateway.

Causes, most likely first

  1. parentRefs names a Gateway that does not exist, or one in another namespace without saying so. No Gateway considers the route, so it often has no status at all.

    Check
    kubectl describe httproute <name>   # no Parents in Status, or NoMatchingParent
    kubectl get gateway -A   # the name and namespace it should name
    Fix

    Make parentRefs name the Gateway and its namespace exactly.

  2. The Gateway's listener does not allow routes from this namespace. allowedRoutes defaults to the Gateway's own namespace: NotAllowedByListeners.

    Check
    kubectl describe httproute <name>   # Reason NotAllowedByListeners
    kubectl get gateway <name> -n <ns> -o yaml | grep -A4 allowedRoutes
    Fix

    Allow the route's namespace in the listener's allowedRoutes, or move the route.

  3. The route's hostname matches no listener's hostname: NoMatchingListenerHostname.

    Check
    kubectl describe httproute <name>   # NoMatchingListenerHostname
    Fix

    Use a hostname the listener accepts, or add a listener for it.

  4. No controller runs for the GatewayClass. Nothing programs the Gateway or writes status.

    Check
    kubectl get gatewayclass   # ACCEPTED not True
    kubectl describe gateway <name>   # Programmed not True
    Fix

    Install the Gateway controller the GatewayClass names.

  5. Accepted, but a backendRef points at a Service in another namespace without a ReferenceGrant. The backend is refused (ResolvedRefs False, RefNotPermitted) and requests get 500, not 404.

    Check
    kubectl describe httproute <name>   # ResolvedRefs False, RefNotPermitted
    kubectl get referencegrant -A
    Fix

    Add a ReferenceGrant in the Service's namespace that allows HTTPRoutes from the route's namespace.

Prevent it

  • Check every route's Accepted condition after deploy.
  • Document which namespaces each shared Gateway accepts routes from.

Now learn it: break and fix it yourself in Gateway API route, then take Level 04 · Connection refused on a real cluster. Open this in the interactive reference.

LoadBalancer EXTERNAL-IP <pending>

Error or output

NAME      TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
shop-lb   LoadBalancer   10.96.45.12   <pending>     80:31380/TCP   20m

What it means

A Service of type LoadBalancer only asks for a load balancer; something else must create it, normally the cloud controller manager. <pending> means nothing has, yet. The Service still works through its node port in the meantime.

Causes, most likely first

  1. The cluster has nothing that provides load balancers: kind, minikube or bare metal without MetalLB or cloud-provider-kind.

    Check
    kubectl describe svc <name>   # no events from a service controller: this cause
    curl http://<node-ip>:<nodePort>/   # works: only the load balancer is missing
    Fix

    Use the node port, or install a load balancer such as MetalLB or cloud-provider-kind; or change the type: kubectl patch svc <name> -p '{"spec":{"type":"NodePort"}}'.

  2. On a cloud, the cloud controller manager is missing or lacks permissions. It tries and fails.

    Check
    kubectl describe svc <name>   # SyncLoadBalancerFailed with a permission error
    Fix

    Give the cloud controller the permissions its provider documents for load balancers.

  3. The cloud account hit its load-balancer or IP quota.

    Check
    kubectl describe svc <name>   # SyncLoadBalancerFailed with a quota or limit error
    Fix

    Raise the quota, or put many Services behind one Ingress or Gateway.

Prevent it

  • Use an Ingress or Gateway with one load balancer for many Services.
  • Keep cloud-only Service types out of local overlays.

Now learn it: break and fix it yourself in LoadBalancer pending, then take Level 04 · Connection refused on a real cluster. Open this in the interactive reference.

Deployments and jobs

Deployment 0/N with no Pods (FailedCreate)

Error or output

Warning  FailedCreate  replicaset-controller  Error creating: pods "orders-6c9b7f5d4-qx8zm" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "orders" must set securityContext.allowPrivilegeEscalation=false)
Warning  FailedCreate  replicaset-controller  Error creating: pods "orders-6c9b7f5d4-qx8zm" is forbidden: exceeded quota: kitchen-quota, requested: limits.memory=512Mi, used: limits.memory=1792Mi, limited: limits.memory=2Gi

What it means

The Deployment's ReplicaSet asked the API server to create Pods and was refused, so no Pod exists and kubectl get pods has nothing to describe. The reason is on the ReplicaSet: Error creating: pods ... is forbidden: and then who refused and why.

Causes, most likely first

  1. Pod Security admission rejects the Pod. The namespace enforces a level (restricted, baseline) and the template lacks fields it requires: violates PodSecurity.

    Check
    kubectl describe rs <new-replicaset>   # violates PodSecurity and the fields it wants
    kubectl get ns <ns> --show-labels   # pod-security.kubernetes.io/enforce
    Fix

    Add the securityContext fields the message lists to the Pod template.

  2. A ResourceQuota is full. The new Pod would take the namespace over its limit: exceeded quota, with requested, used and limited.

    Check
    kubectl describe resourcequota   # Used at or near Hard
    Fix

    Lower the requests and limits, free room in the namespace, or raise the quota with its owner.

  3. A quota on limits or requests exists, and the Pod sets none. The quota cannot count a Pod without them: failed quota: <name>: must specify limits.memory.

    Check
    kubectl describe rs <new-replicaset>   # must specify
    Fix

    Set requests and limits on every container, or add a LimitRange with defaults.

  4. An admission webhook or ValidatingAdmissionPolicy denies it. The message then names the webhook or policy.

    Check
    kubectl describe rs <new-replicaset>   # admission webhook or ValidatingAdmissionPolicy in the message
    Fix

    See failed calling webhook or FailedCreate: denied by ValidatingAdmissionPolicy.

  5. The ServiceAccount the template names does not exist. Pods cannot be created for it: error looking up service account.

    Check
    kubectl get serviceaccount <name> -n <ns>   # NotFound: this cause
    Fix

    Create the ServiceAccount, or correct serviceAccountName.

Prevent it

  • Run kubectl apply --dry-run=server in CI against a namespace with the same Pod Security level and quota.
  • Alert on Deployments whose available replicas stay below the desired count.

Now learn it: break and fix it yourself in Pod Security · Quota exceeded, then take Level 17 · Rejected at the door on a real cluster. Open this in the interactive reference.

Rollout stuck

Error or output

Waiting for deployment "orders" rollout to finish: 1 out of 3 new replicas have been updated...
error: deployment "orders" exceeded its progress deadline

What it means

A rolling update replaces old Pods only as new ones become Ready. If the new Pods never get Ready, the rollout waits, old Pods keep serving, and after progressDeadlineSeconds (600 by default) the Deployment reports ProgressDeadlineExceeded. It does not roll back by itself.

Causes, most likely first

  1. The new image or configuration is broken. New Pods crash or stay unready, so the rollout cannot go on.

    Check
    kubectl get rs   # the new ReplicaSet has Pods, none Ready
    kubectl describe pod <new-pod>   # CrashLoopBackOff, ImagePullBackOff or Readiness probe failed
    Fix

    Undo while you fix it: kubectl rollout undo deployment/<name>, then fix the image or config and roll out again.

  2. The new Pods cannot be created or scheduled: a quota, a taint, no room. They never exist, or stay Pending.

    Check
    kubectl get pods   # new Pods Pending, or none
    kubectl describe rs <new-replicaset>   # FailedCreate
    Fix

    Fix the scheduling or quota problem the events name; see Pending or Deployment 0/N with no Pods.

Prevent it

  • Set progressDeadlineSeconds and fail the pipeline when kubectl rollout status fails.
  • Use readiness probes, so a bad version never receives traffic.

Now learn it: break and fix it yourself in Bad rollout, then take Level 03 · A deploy went wrong on a real cluster. Open this in the interactive reference.

Rollout paused: UP-TO-DATE stays 0

Error or output

menu   2/2   0   2   1d
  Progressing    Unknown  DeploymentPaused

What it means

A paused Deployment records changes to its template but does not act on them: no new ReplicaSet, no new Pods. UP-TO-DATE stays 0 and the Progressing condition says DeploymentPaused.

Causes, most likely first

  1. Someone ran kubectl rollout pause during an incident and never resumed.

    Check
    kubectl describe deploy <name>   # Progressing Unknown, DeploymentPaused
    kubectl get deploy <name> -o jsonpath='{.spec.paused}'   # true
    Fix

    Resume it: kubectl rollout resume deployment/<name>, then kubectl rollout status deployment/<name>.

  2. A tool that batches changes paused it and failed before resuming.

    Check
    kubectl get deploy <name> -o yaml --show-managed-fields | grep -B12 'f:paused'   # the manager that set it
    Fix

    Resume it as above, and fix the tool so it always resumes.

  3. Deleting Pods does not help. They are recreated from the old ReplicaSet, with the old image.

    Check
    kubectl get rs   # only the old ReplicaSet
    Fix

    Resume instead of deleting Pods.

Prevent it

  • Resume in the same runbook step that paused, and note pauses in the incident channel.
  • Alert on Deployments paused for more than an hour.

Now learn it: break and fix it yourself in Rollout paused, then take Level 03 · A deploy went wrong on a real cluster. Open this in the interactive reference.

Helm release failed

Error or output

Error: UPGRADE FAILED: context deadline exceeded
Error: UPGRADE FAILED: another operation (install/upgrade/rollback) is in progress

What it means

Helm records each upgrade as a numbered revision. When an upgrade fails, the revision is marked failed, but the objects it already changed stay changed. context deadline exceeded means --wait timed out; another operation is in progress means the last revision is still marked pending.

Causes, most likely first

  1. Bad values: an image tag that does not exist, a misspelt key. The objects apply, but the new Pods never become Ready, so --wait times out.

    Check
    helm history <release>   # the failed revision
    helm get values <release> --revision <n>   # compare with the last good one
    kubectl get pods   # new Pods failing
    Fix

    Roll back (helm rollback <release> <good-revision>), fix the values, then upgrade again with --wait.

  2. An earlier upgrade was interrupted, so the release is stuck in pending-upgrade. Helm refuses a new operation: another operation (install/upgrade/rollback) is in progress.

    Check
    helm history <release>   # last revision pending-upgrade, and no helm process running
    Fix

    Roll back to the last deployed revision to clear it, then upgrade again.

  3. Someone changed the objects with kubectl. Helm's record and the cluster differ, so the upgrade conflicts.

    Check
    helm get manifest <release> | kubectl diff -f -   # changes Helm did not make
    Fix

    Put the change in the chart's values, and change Helm-managed objects only through Helm.

Prevent it

  • Run helm template and helm lint in CI, and upgrade with --wait --atomic (--rollback-on-failure in Helm 4) so a failure rolls back on its own.
  • Change Helm-managed objects only through Helm.

Now learn it: break and fix it yourself in Helm failed upgrade, then take Level 18 · One app, many environments on a real cluster. Open this in the interactive reference.

Job failed: BackoffLimitExceeded

Error or output

Warning  BackoffLimitExceeded  job-controller  Job has reached the specified backoff limit

What it means

A Job retries failed Pods, and after backoffLimit failures (6 by default) it gives up and marks itself Failed with reason BackoffLimitExceeded. Nothing runs again until you create a new Job. The answer is in the failed Pods, not the Job.

Causes, most likely first

  1. The task itself fails. Each Pod exits non-zero, so each one counts as a failure.

    Check
    kubectl get pods -l job-name=<name>   # several in Error
    kubectl logs job/<name>   # the task's error (or kubectl logs <pod>)
    Fix

    Fix the cause, then delete the Job and create it again: a finished Job does not rerun.

  2. Pods are killed from outside: OOMKilled, or evicted. Those count as failures too.

    Check
    kubectl describe pod <failed-pod>   # Last State OOMKilled, or Reason Evicted
    Fix

    Give the Pod the memory it needs; use podFailurePolicy to ignore disruptions (DisruptionTarget) if evictions are expected.

  3. backoffLimit is too low for a task that fails now and then. A few transient errors use it up.

    Check
    kubectl get job <name> -o jsonpath='{.spec.backoffLimit}'
    Fix

    Raise backoffLimit, and make the task retry its own transient errors.

  4. Not this case: a Job that runs past activeDeadlineSeconds fails with DeadlineExceeded (Job was active longer than specified deadline), not BackoffLimitExceeded.

    Check
    kubectl get job <name> -o jsonpath='{.status.conditions[?(@.type=="Failed")].reason}'   # BackoffLimitExceeded or DeadlineExceeded
    Fix

    DeadlineExceeded: raise activeDeadlineSeconds if the task only needs longer.

Prevent it

  • Alert on failed Jobs, and keep failed Pods (or ship their logs) so the cause can be read.
  • Use podFailurePolicy to stop retrying errors that will never succeed.

Now learn it: break and fix it yourself in Failed Job, then take Level 09 · The cleanup that never ran on a real cluster. Open this in the interactive reference.

CronJob does not run

Error or output

NAME      SCHEDULE    TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
cleanup   0 2 * * *   <none>     True      0        12d             90d
Warning  JobAlreadyActive  cronjob-controller  Not starting job because prior execution is running and concurrency policy is Forbid

What it means

The CronJob controller creates a Job at each scheduled time unless something stops it. LAST SCHEDULE is when it last did. SUSPEND, ACTIVE and the events say what is stopping it.

Causes, most likely first

  1. The CronJob is suspended. The controller skips every run while suspend is true.

    Check
    kubectl get cronjob <name>   # SUSPEND True
    Fix

    Resume it: kubectl patch cronjob <name> -p '{"spec":{"suspend":false}}'.

  2. concurrencyPolicy is Forbid and a previous run hangs. Each new run is skipped while it is still active: JobAlreadyActive.

    Check
    kubectl get cronjob <name>   # ACTIVE 1
    kubectl get jobs   # one Running for days
    Fix

    Delete the stuck Job, and set activeDeadlineSeconds in the jobTemplate so a run cannot hang again.

  3. The schedule is read in the controller's time zone. Without spec.timeZone it runs at a different hour from the one you meant.

    Check
    kubectl get cronjob <name>   # TIMEZONE <none>
    Fix

    Set spec.timeZone, for example Europe/London.

  4. Too many starts were missed (over 100), for example after the controller was down. The controller stops scheduling: too many missed start time.

    Check
    kubectl describe cronjob <name>   # too many missed start
    Fix

    Set startingDeadlineSeconds, so only recent missed starts are counted.

Prevent it

  • Alert when a CronJob's last successful time is older than its schedule allows.
  • Give every CronJob an activeDeadlineSeconds and a timeZone.

Now learn it: break and fix it yourself in CronJob not running · CronJob blocked by a hung Job, then take Level 09 · The cleanup that never ran on a real cluster. Open this in the interactive reference.

HPA TARGETS <unknown>

Error or output

menu   Deployment/menu   cpu: <unknown>/70%   2   6   2   5m
Warning  FailedGetResourceMetric  horizontal-pod-autoscaler  failed to get cpu utilization: missing request for cpu in container menu of Pod menu-5b8d9c6f7-tw4kp
unable to get metrics for resource cpu: unable to fetch metrics from resource metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)

What it means

The HorizontalPodAutoscaler scales on utilisation: the Pods' current CPU (or memory) as a percentage of their request. <unknown> under TARGETS means it could not work out that number, so it does not scale at all. The FailedGetResourceMetric event says which half is missing: the request, or the metrics.

Causes, most likely first

  1. A container in the target has no CPU (or memory) request. Utilisation is a percentage of the request, so without one there is nothing to divide by: missing request for cpu.

    Check
    kubectl describe hpa <name>   # missing request for cpu in container <name>: this cause
    kubectl get deploy <name> -o jsonpath='{.spec.template.spec.containers[*].resources.requests}'   # one container without cpu
    Fix

    Set a CPU request on every container of the target, sidecars too: kubectl set resources deployment/<name> --requests=cpu=<m>.

  2. metrics-server is not installed or not Ready. The metrics API does not exist, so the HPA has no numbers: the server could not find the requested resource (get pods.metrics.k8s.io).

    Check
    kubectl get apiservice v1beta1.metrics.k8s.io   # missing, or AVAILABLE False: this cause
    kubectl top pods   # error: Metrics API not available
    Fix

    Install metrics-server, and wait until the APIService is Available.

  3. metrics-server runs but cannot scrape the kubelets. The API exists, but has no numbers for these Pods.

    Check
    kubectl -n kube-system logs deploy/metrics-server   # x509 or timeout errors scraping nodes: this cause
    Fix

    Give metrics-server the kubelet CA, or turn on signed kubelet serving certificates; --kubelet-insecure-tls is for kind and labs only.

  4. The Pods have just started or are not Ready. The HPA skips Pods without fresh metrics for a short while.

    Check
    kubectl get hpa <name> -w   # a percentage appears within a minute or two: nothing to fix
    Fix

    Wait a minute after a rollout before reading TARGETS.

Prevent it

  • Require requests on every container (a LimitRange, or an admission policy).
  • Alert on HPAs with a ScalingActive condition of False.

Now learn it: break and fix it yourself in HPA not scaling, then take Level 13 · Ready for the dinner rush on a real cluster. Open this in the interactive reference.

VPA recommends, nothing changes

Error or output

  Update Policy:
    Update Mode:  Off
  Recommendation:
    Container Recommendations:
      Container Name:  oven

What it means

The VerticalPodAutoscaler has three parts: the recommender computes the numbers, the updater evicts Pods so they restart, and the admission controller writes the new requests into the new Pods. A recommendation with no change means the recommender works and one of the other two does not act.

Causes, most likely first

  1. updateMode is Off (recommend only) or Initial (new Pods only). The VPA is doing exactly what it was told.

    Check
    kubectl describe vpa <name>   # Update Mode: Off or Initial
    Fix

    Set updateMode: Recreate (or Auto) to let it apply the recommendation, or copy the numbers into the manifest yourself.

  2. The workload has fewer replicas than the updater's minReplicas (2 by default). The updater will not evict the only replica.

    Check
    kubectl get deploy <name>   # 1 replica
    Fix

    Run at least two replicas, or lower minReplicas in the VPA's updatePolicy if a short outage is acceptable.

  3. The updater or admission controller is not installed or not running. Recommendations are written, but nothing applies them.

    Check
    kubectl get pods -n kube-system | grep vpa   # recommender only, or one CrashLoopBackOff
    Fix

    Install or repair the missing component.

Prevent it

  • Do not let a VPA in Auto mode and an HPA on CPU or memory control the same Deployment.
  • Review recommendations regularly, even in Off mode, and copy them into the manifests.

Now learn it: break and fix it yourself in VPA recommendation, then take Level 13 · Ready for the dinner rush on a real cluster. Open this in the interactive reference.

New version deployed, old code running

Error or output

shop: version 2.4.0

What it means

A tag such as latest is a name that can point at a new image at any time. A node that already has an image under that tag keeps using it unless it is told to pull again, and nothing restarts the Pods when the tag moves.

Causes, most likely first

  1. A mutable tag with imagePullPolicy IfNotPresent. Nodes keep the image they pulled before.

    Check
    kubectl describe pod <pod>   # Image ID: the digest it runs
    docker buildx imagetools inspect <image>   # Digest: what the tag points to now; different = this cause
    Fix

    Deploy an immutable tag or a digest: kubectl set image deployment/<name> <container>=<image>:<version>.

  2. The Deployment did not change, so nothing rolled out. Pushing a tag does not restart anything.

    Check
    kubectl rollout history deployment/<name>   # no new revision
    Fix

    Change the tag in the Deployment, so a rollout happens.

  3. A registry mirror serves a stale copy of the tag.

    Check
    docker buildx imagetools inspect <mirror>/<image>   # a different digest from the source registry
    Fix

    Refresh the mirror, or deploy by digest so a stale copy cannot match.

Prevent it

  • Tag every build with a version or commit and never reuse a tag; deploy by that tag or by digest.
  • Block :latest in production with an admission policy.

Now learn it: break and fix it yourself in Old code on :latest, then take Level 03 · A deploy went wrong on a real cluster. Open this in the interactive reference.

Terminating forever

Error or output

menu-5b8d9c6f7-tw4kp   0/1   Terminating   0   3h
["kitchen.example/cleanup"]

What it means

Deleting an object sets a deletion time and then waits for every finalizer on it to be removed, and for a Pod, for its kubelet to confirm the containers stopped. Terminating for a long time means one of those never happens.

Causes, most likely first

  1. A finalizer is set and no controller removes it, often because its operator was uninstalled. The API server keeps the object until the list is empty.

    Check
    kubectl get pod <pod> -o jsonpath='{.metadata.finalizers}'   # a finalizer listed: this cause
    Fix

    Bring back the controller that removes it. Only if it is gone for good: kubectl patch pod <pod> -p '{"metadata":{"finalizers":null}}'.

  2. The Pod's node is unreachable. No kubelet can confirm the containers stopped.

    Check
    kubectl get pod <pod> -o wide; kubectl get nodes   # its node NotReady
    Fix

    Delete the node object if it is gone, or as a last resort kubectl delete pod <pod> --grace-period=0 --force: only when the node is really down, since a StatefulSet could otherwise run two copies.

  3. The app ignores SIGTERM and the grace period is long. The kubelet waits for terminationGracePeriodSeconds before it kills it.

    Check
    kubectl get pod <pod> -o jsonpath='{.spec.terminationGracePeriodSeconds}'   # a large number
    Fix

    Handle SIGTERM in the app, and keep the grace period as short as a clean shutdown needs.

Prevent it

  • Uninstall operators only after deleting the objects they finalize.
  • Handle SIGTERM in the app and keep terminationGracePeriodSeconds short.

Now learn it: break and fix it yourself in Stuck Terminating, then take Level 14 · Boss level: the Friday night incident on a real cluster. Open this in the interactive reference.

kubectl and manifests

The connection to the server was refused

Error or output

The connection to the server localhost:8080 was refused - did you specify the right host or port?
The connection to the server 127.0.0.1:6443 was refused - did you specify the right host or port?
Unable to connect to the server: dial tcp: lookup api.cluster.example.internal: no such host

What it means

kubectl could not open a connection to the address in your kubeconfig, so Kubernetes never saw the command. refused means the address answered that nothing listens on that port; no such host means the name does not resolve. localhost:8080 is kubectl's fallback when it found no kubeconfig at all.

Causes, most likely first

  1. No kubeconfig. kubectl then tries localhost:8080; under sudo it reads root's kubeconfig, not yours.

    Check
    echo $KUBECONFIG; ls -l ~/.kube/config   # neither set nor present: this cause
    kubectl config current-context   # error: current-context is not set
    Fix

    Point kubectl at one (export KUBECONFIG=<file>, or copy it to ~/.kube/config), and run kubectl as your own user.

  2. The current context points at a cluster that is gone or stopped, such as a kind cluster after Docker restarted.

    Check
    kubectl config view --minify | grep server   # the address it uses
    docker ps --filter name=control-plane   # kind: node container not running
    Fix

    Switch context (kubectl config use-context <name>), or start the kind node again (docker start <cluster>-control-plane), or recreate it with ./nimbus up.

  3. The API server is down on the control-plane node: a broken static Pod manifest, or etcd not running.

    Check
    sudo crictl ps -a | grep kube-apiserver   # Exited, on a kubeadm control-plane node
    Fix

    Read why: sudo crictl logs <container-id> and sudo journalctl -u kubelet, then fix the manifest or etcd.

  4. A VPN, proxy or firewall between you and a remote cluster drops or refuses the connection.

    Check
    nc -vz <host> 6443   # fails from your machine, works from inside the network
    Fix

    Connect the VPN, or add the API server to NO_PROXY.

Prevent it

  • Show the current context in your shell prompt, and keep one kubeconfig per cluster.
  • Monitor the API server's /readyz endpoint, so you hear it is down before kubectl tells you.

Now learn it: break and fix it yourself in Wrong namespace, then take Level 00 · Hello, cluster on a real cluster. Open this in the interactive reference.

x509: certificate signed by unknown authority

Error or output

Unable to connect to the server: tls: failed to verify certificate: x509: certificate signed by unknown authority
Unable to connect to the server: x509: certificate is valid for 10.96.0.1, 172.18.0.2, not 203.0.113.10

What it means

kubectl connected to the API server and checked its TLS certificate against the CA in your kubeconfig. signed by unknown authority means that CA did not sign it. certificate is valid for A, B, not C means the certificate is genuine but does not name the address C you connected to.

Causes, most likely first

  1. The kubeconfig holds the CA of an earlier cluster at the same address, since recreated (kind delete and create, kubeadm reset and init). The new cluster has a new CA.

    Check
    kubectl config view --minify --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d | openssl x509 -noout -subject -enddate
    openssl s_client -connect <host>:6443 </dev/null 2>/dev/null | openssl x509 -noout -issuer   # issuer differs from your CA's subject: this cause
    Fix

    Take a fresh kubeconfig: kind export kubeconfig --name <cluster>, or copy /etc/kubernetes/admin.conf on a kubeadm control plane.

  2. A proxy or VPN that inspects TLS answers with its own certificate.

    Check
    openssl s_client -connect <host>:6443 </dev/null 2>/dev/null | openssl x509 -noout -issuer   # your company's proxy as issuer
    Fix

    Bypass the proxy for the API server (add it to NO_PROXY) rather than trusting the proxy's CA.

  3. certificate is valid for ..., not ...: you connect by an IP or name that is not in the API server certificate, such as a public IP or a load balancer added later.

    Check
    sudo openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -ext subjectAltName   # on a kubeadm control-plane node: the address missing
    Fix

    Connect by a listed name, or add it: list it under apiServer.certSANs, move the old apiserver.crt and apiserver.key aside, run sudo kubeadm init phase certs apiserver --config <file>, and restart the API server.

  4. A merged kubeconfig pairs the context with the wrong cluster entry.

    Check
    kubectl config view --minify   # the server and cluster entry the context uses
    Fix

    Point the context at the right cluster entry. Do not set insecure-skip-tls-verify: it accepts any server, including the wrong one.

Prevent it

  • Export a fresh kubeconfig every time a cluster is recreated, and keep one file per cluster.
  • Plan the API server's names before you build: kubeadm init --control-plane-endpoint <name> --apiserver-cert-extra-sans <name>.

Now learn it: break and fix it yourself in Certificates expired, then take Level 14 · Boss level: the Friday night incident on a real cluster. Open this in the interactive reference.

error converting YAML to JSON

Error or output

error: error parsing deploy.yaml: error converting YAML to JSON: yaml: line 19: mapping values are not allowed in this context
error: error parsing service.yaml: error converting YAML to JSON: yaml: line 8: found character that cannot start any token
error: error parsing deploy.yaml: error converting YAML to JSON: yaml: line 23: did not find expected key

What it means

kubectl could not read the file as YAML, so nothing was sent to the cluster. The line number is where the parser gave up, which is often just after the real mistake.

Causes, most likely first

  1. Indentation that does not line up: a key one space off, or a list item not under its parent.

    Check
    sed -n '15,19p' deploy.yaml   # the reported line and the few before it
    Fix

    Indent each level by the same two spaces. YAML X-ray on this site names each line's path, so a key at the wrong depth stands out.

  2. A tab. YAML indents with spaces only: found character that cannot start any token.

    Check
    grep -nP '\t' deploy.yaml   # any line: this cause
    Fix

    Replace tabs with spaces.

  3. A missing space after a colon, or an unquoted value that contains : : mapping values are not allowed in this context.

    Check
    sed -n '<line>p' deploy.yaml   # a second : in the value
    Fix

    Quote the value, such as "*" or "@daily".

  4. A - out of place in a list, or keys of one item at different depths: did not find expected key.

    Check
    kubectl apply --dry-run=client -f deploy.yaml   # still fails after each fix: look at the next reported line
    Fix

    Line up the keys of each list item under its first key.

Prevent it

  • Use an editor that shows indentation and tabs, and run a YAML linter such as yamllint in CI.
  • Check manifests with kubectl apply --dry-run=server -f in CI, before they reach a cluster.

Now learn it: paste the manifest into YAML X-ray, which explains each line and marks the fields it does not know, then take Level 00 · Hello, cluster on a real cluster. Open this in the interactive reference.

strict decoding error: unknown field

Error or output

Error from server (BadRequest): error when creating "deploy.yaml": Deployment in version "v1" cannot be handled as a Deployment: strict decoding error: unknown field "spec.template.spec.containers[0].imagePullpolicy"
Error from server (BadRequest): error when creating "deploy.yaml": Deployment in version "v1" cannot be handled as a Deployment: json: cannot unmarshal string into Go struct field DeploymentSpec.spec.replicas of type int32

What it means

The YAML is valid, but the API server checked it against the kind's schema and found a field that does not exist there (unknown field), or a value of the wrong type (cannot unmarshal string into ... of type int32). It rejected the whole object.

Causes, most likely first

  1. A misspelt field or the wrong case. Fields are case sensitive: imagePullPolicy, not imagePullpolicy.

    Check
    kubectl explain deployment.spec.template.spec.containers   # the fields that exist, with their types
    Fix

    Correct the field the message names (the path after unknown field).

  2. A field at the wrong level, such as replicas under template. It exists, just not there.

    Check
    kubectl explain <kind>.<path> --recursive | grep <field>
    Fix

    Move it to the level kubectl explain shows.

  3. A value of the wrong type, such as replicas: "3" or a port in quotes: cannot unmarshal string.

    Check
    kubectl explain deployment.spec.replicas   # FIELD: replicas <integer>
    Fix

    Give it the type shown, for example replicas: 3 without quotes.

  4. A field from a newer Kubernetes version than the cluster runs.

    Check
    kubectl version   # Server Version older than the docs you followed
    Fix

    Remove the field, or upgrade the cluster. Do not use --validate=false: the field is then dropped silently, not applied.

Prevent it

  • Validate manifests in CI with kubectl apply --dry-run=server, or against the schema with a tool such as kubeconform.
  • Use an editor with the Kubernetes schema, so unknown fields are marked as you type.

Now learn it: paste the manifest into YAML X-ray, which explains each line and marks the fields it does not know, then take Level 00 · Hello, cluster on a real cluster. Open this in the interactive reference.

no matches for kind in version

Error or output

error: resource mapping not found for name: "shop" namespace: "" from "ingress.yaml": no matches for kind "Ingress" in version "extensions/v1beta1"
ensure CRDs are installed first

What it means

kubectl asked the API server how to handle this kind at this apiVersion, and the server serves no such combination. Either the version was removed from Kubernetes, or the kind comes from a CustomResourceDefinition that is not installed.

Causes, most likely first

  1. The apiVersion was removed: Ingress in extensions/v1beta1 (gone in 1.22), CronJob in batch/v1beta1 and PodDisruptionBudget in policy/v1beta1 (gone in 1.25), HorizontalPodAutoscaler in autoscaling/v2beta2 (gone in 1.26).

    Check
    kubectl api-resources | grep -i <kind>   # the group and version this cluster serves
    kubectl version   # Server Version
    Fix

    Use the apiVersion listed, for example networking.k8s.io/v1 for Ingress, and update the fields that changed with it (Ingress v1 needs pathType and service.name with service.port). The kubectl convert plugin rewrites many: kubectl convert -f <file> --output-version <group>/<version>.

  2. A custom resource (ServiceMonitor, Certificate, HTTPRoute) is applied before the CRD that defines it.

    Check
    kubectl get crd | grep <group>   # missing: this cause
    Fix

    Install its CRDs first, wait with kubectl wait --for condition=established crd/<name>, then apply.

  3. A typo in kind or apiVersion. Kinds are case sensitive.

    Check
    kubectl explain <kind> | head -5   # the exact kind and version
    Fix

    Correct the spelling.

Prevent it

  • Before upgrading a cluster, apply your manifests and charts with kubectl apply --dry-run=server against a cluster on the new version.
  • Install CRDs separately from, and before, the resources that use them.

Now learn it: paste the manifest into YAML X-ray, which explains each line and marks the fields it does not know, then take Level 20 · Teaching Kubernetes a new word on a real cluster. Open this in the interactive reference.

field is immutable

Error or output

The Deployment "orders" is invalid: spec.selector: Invalid value: v1.LabelSelector{MatchLabels:map[string]string{"app":"orders-v2"}, MatchExpressions:[]v1.LabelSelectorRequirement(nil)}: field is immutable
The Job "import-menu" is invalid: spec.template: Invalid value: core.PodTemplateSpec{...}: field is immutable

What it means

Some fields cannot change once an object exists, so the API server rejects an update that changes them and leaves the object as it was. The message names the field (spec.selector) and shows the value you sent.

Causes, most likely first

  1. spec.selector of a Deployment, ReplicaSet, StatefulSet or DaemonSet changed, often by renaming a label. The selector decides which Pods it owns, so it is fixed.

    Check
    kubectl get deployment <name> -o jsonpath='{.spec.selector}'
    kubectl diff -f deploy.yaml   # the selector in the diff: this cause
    Fix

    If not meant, put the old value back. If meant, delete and create it again (kubectl delete deployment <name>, then kubectl apply -f deploy.yaml, or kubectl replace --force -f deploy.yaml): the Pods are replaced, so do it when a short outage is acceptable.

  2. A Job's Pod template changed, such as a new image under the same Job name. A Job runs once, so its template is fixed.

    Check
    kubectl diff -f job.yaml   # spec.template differs
    Fix

    Delete the Job and create it again, or give the new one its own name.

  3. A Service's clusterIP changed.

    Check
    kubectl get svc <name> -o jsonpath='{.spec.clusterIP}'   # differs from the manifest
    Fix

    Remove clusterIP from the manifest, or recreate the Service.

  4. For a StatefulSet, anything but its replicas, template, update strategy and a few others changed: updates to statefulset spec for fields other than ... are forbidden, often volumeClaimTemplates.

    Check
    kubectl diff -f sts.yaml   # volumeClaimTemplates or serviceName in the diff
    Fix

    kubectl delete statefulset <name> --cascade=orphan keeps its Pods and PersistentVolumeClaims; then apply the new spec.

Prevent it

  • Choose selectors once, and keep version or release labels out of them: put those only on the Pod template.
  • Give each Job run its own name (generateName, or a CronJob), so a run is always a new object.

Now learn it: break and fix it yourself in Failed Job, then take Level 09 · The cleanup that never ran on a real cluster. Open this in the interactive reference.

Access and security

Unauthorized: You must be logged in to the server

Error or output

error: You must be logged in to the server (Unauthorized)
error: You must be logged in to the server (the server has asked for the client to provide credentials)

What it means

kubectl reached the API server, but the server could not establish who you are from the credentials in your kubeconfig, so it rejected the request (HTTP 401). This is before RBAC: once you are recognised, a missing permission says Forbidden instead.

Causes, most likely first

  1. The client certificate in your kubeconfig has expired. kubeadm's admin.conf lasts a year.

    Check
    kubectl config view --minify --raw -o jsonpath='{.users[0].user.client-certificate-data}' | base64 -d | openssl x509 -noout -enddate   # a past date: this cause
    Fix

    sudo kubeadm certs renew all, then copy the new admin.conf to ~/.kube/config (see x509: certificate has expired).

  2. A token expired or was revoked, such as a cloud login that an exec plugin refreshes.

    Check
    kubectl config view --minify   # user: exec (aws, gke-gcloud-auth-plugin) or token
    Fix

    Log in again with your provider's CLI and fetch fresh credentials, for example aws eks update-kubeconfig --name <cluster> or gcloud container clusters get-credentials <cluster>.

  3. The kubeconfig was written for an older cluster at the same address, since recreated with new credentials.

    Check
    kubectl config view --minify   # the cluster and user entries
    Fix

    Write a fresh kubeconfig, for kind: kind export kubeconfig --name <cluster>.

  4. The token belongs to a ServiceAccount that was deleted.

    Check
    kubectl get serviceaccount <name> -n <ns>   # NotFound, from a working login
    Fix

    Recreate it and issue a new token: kubectl create token <name> -n <ns>.

Prevent it

  • Prefer short-lived credentials from an exec plugin or OIDC over certificates copied by hand, and know how to refresh them.
  • Upgrade kubeadm clusters at least once a year, which renews their certificates.

Now learn it: break and fix it yourself in New user access · Certificates expired, then take Level 10 · Too much access on a real cluster. Open this in the interactive reference.

Forbidden (403) from the API

Error or output

Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:kitchen:reporter" cannot list resource "pods" in API group "" in the namespace "kitchen"

What it means

The API server knows who you are (authentication worked) but RBAC does not allow this action. The message says exactly what was refused: the User is the identity the API server used, then the verb (list), the resource (pods), the API group ("" is the core group) and the namespace.

Causes, most likely first

  1. No Role or ClusterRole bound to this identity grants that verb on that resource. RBAC denies anything not allowed.

    Check
    kubectl auth can-i <verb> <resource> -n <ns> --as=system:serviceaccount:<ns>:<sa>   # no: this cause
    kubectl auth can-i --list -n <ns> --as=system:serviceaccount:<ns>:<sa>
    Fix

    Grant only that verb on that resource in a Role, and bind it with a RoleBinding.

  2. The request runs as another identity than the one you bound. The User in the message is the one the API server used, often the namespace's default ServiceAccount.

    Check
    kubectl get pod <pod> -o jsonpath='{.spec.serviceAccountName}'   # differs from the one in your RoleBinding: this cause
    kubectl auth whoami   # for a person: who the API server thinks you are
    Fix

    Set serviceAccountName on the Pod template to the ServiceAccount you bound, or bind the one it uses.

  3. The RoleBinding names the wrong ServiceAccount or namespace. The grant exists, but for someone else.

    Check
    kubectl describe rolebinding <name> -n <ns>   # Subjects: name and namespace must match the User in the message
    Fix

    Correct the subject's name and namespace in the RoleBinding.

  4. A RoleBinding cannot grant cluster-scoped resources (nodes, namespaces, PersistentVolumes) or other namespaces. The message then has no namespace, or a different one.

    Check
    kubectl api-resources --namespaced=false | grep <resource>   # listed: it needs a ClusterRoleBinding
    Fix

    Use a ClusterRole with a ClusterRoleBinding for cluster-scoped resources, scoped to the verbs needed.

  5. A sub-resource is missing. Logs and exec are separate resources (pods/log, pods/exec), so get on pods is not enough.

    Check
    kubectl auth can-i get pods/log --as=<user>   # no, while get pods says yes
    Fix

    Add the sub-resource to the Role: pods/log, pods/exec.

Prevent it

  • Write Roles per app with the verbs it needs, and test them with kubectl auth can-i in CI.
  • Avoid wildcard verbs and resources; they grant future resources too.

Now learn it: break and fix it yourself in RBAC forbidden · New user access, then take Level 10 · Too much access on a real cluster. Open this in the interactive reference.

kubectl get pods shows nothing

Error or output

No resources found in default namespace.
Error from server (NotFound): deployments.apps "orders" not found

What it means

kubectl asked the cluster in your current context, in its current namespace (default unless set), and found nothing there. The objects usually exist, but somewhere else: another namespace or another cluster.

Causes, most likely first

  1. They are in another namespace. kubectl only looks in the context's namespace unless you pass -n or -A.

    Check
    kubectl get pods -A | grep <name>   # found in another namespace: this cause
    Fix

    Pass -n, or set it once: kubectl config set-context --current --namespace=<ns>.

  2. You are on the wrong cluster. The context points at another cluster that never had them.

    Check
    kubectl config get-contexts   # the * is not the cluster you meant
    Fix

    Switch: kubectl config use-context <name>.

  3. A NotFound for a named object: a typo in its name or kind.

    Check
    kubectl get <kind> -A | grep -i <part-of-name>
    Fix

    Use the exact name, and the namespace it is in.

Prevent it

  • Show the context and namespace in your shell prompt.
  • Set metadata.namespace in manifests, or always apply with -n, so objects land where you expect.

Now learn it: break and fix it yourself in Wrong namespace, then take Level 00 · Hello, cluster on a real cluster. Open this in the interactive reference.

FailedCreate: denied by ValidatingAdmissionPolicy

Error or output

Warning  FailedCreate  replicaset-controller  Error creating: pods "orders-6c9b7f5d4-qx8zm" is forbidden: ValidatingAdmissionPolicy 'allowed-registries' with binding 'allowed-registries-binding' denied request: images must come from registry.kitchen.example

What it means

A ValidatingAdmissionPolicy is a rule written in CEL that the API server checks before storing an object. The ReplicaSet's Pods broke it, so none was created. The message names the policy, its binding, and the policy's own explanation of what it requires.

Causes, most likely first

  1. The image is not from an allowed registry. The policy checks each container's image.

    Check
    kubectl get events --field-selector reason=FailedCreate   # denied request: images must come from ...
    Fix

    Use an image from the allowed registry in the Pod template; change the Pod, not the policy.

  2. A required label, limit or securityContext field is missing. The policy's expression is false for this Pod.

    Check
    kubectl get validatingadmissionpolicy <name> -o yaml   # the validations and their messages
    Fix

    Add the field the message names to the Pod template.

  3. The policy's expression fails on a field your Pod does not set. A CEL error on a missing field counts as a denial unless failurePolicy is Ignore.

    Check
    kubectl describe rs -l app=<app>   # expression error, no such key
    kubectl get validatingadmissionpolicybinding   # validationActions
    Fix

    Fix the expression (has() before reading optional fields) and test it with validationActions: [Warn, Audit] first.

Prevent it

  • Roll out new policies in Warn or Audit mode before Deny.
  • Run kubectl apply --dry-run=server in CI, so policy denials fail the pipeline.

Now learn it: break and fix it yourself in Untrusted registry, then take Level 21 · Final boss: ready for production on a real cluster. Open this in the interactive reference.

failed calling webhook (FailedCreate)

Error or output

Error creating: Internal error occurred: failed calling webhook "validate.policy.kitchen.example": failed to call webhook: Post "https://policy-webhook.policy.svc:443/validate?timeout=10s": no endpoints available for service "policy-webhook"

What it means

Before storing an object, the API server calls each admission webhook that matches it. With failurePolicy Fail, any failure to get an answer rejects the request. The text after failed to call webhook says why the call failed: no endpoints, a timeout, or a certificate error.

Causes, most likely first

  1. The webhook's own Pods are down, so its Service has no endpoints: no endpoints available for service. If the webhook also checks its own Pods, they cannot be recreated.

    Check
    kubectl get validatingwebhookconfiguration <name> -o jsonpath='{.webhooks[*].clientConfig.service}'   # the Service, namespace and port it calls
    kubectl get endpointslices -n <ns> -l kubernetes.io/service-name=<webhook-svc>   # no endpoints: this cause
    Fix

    Bring its Pods back. If the webhook blocks them, set failurePolicy to Ignore only until it is back: kubectl patch validatingwebhookconfiguration <name> -p '{"webhooks":[{"name":"<hook>","failurePolicy":"Ignore"}]}', then exclude its own namespace with a namespaceSelector.

  2. The webhook's certificate: expired, not signed by the caBundle in the webhook configuration, or not naming <svc>.<ns>.svc. The API server will not trust it: x509 in the message.

    Check
    kubectl get events -A | grep 'failed calling webhook'   # x509: certificate has expired / signed by unknown authority
    Fix

    Renew the certificate (or let cert-manager do it) and make the caBundle match its CA.

  3. The API server cannot reach the webhook Pods: a NetworkPolicy, or on a managed cluster a firewall from the control plane to the Pod's port. The call times out: context deadline exceeded.

    Check
    kubectl get events -A | grep 'failed calling webhook'   # context deadline exceeded: this cause
    kubectl get networkpolicy -n <ns>
    Fix

    Allow traffic from the control plane to the webhook's Pod port.

Prevent it

  • Run webhooks with two or more replicas and a PodDisruptionBudget.
  • Never let a webhook with failurePolicy Fail select its own namespace or kube-system.

Now learn it: break and fix it yourself in Admission webhook down, then take Level 21 · Final boss: ready for production on a real cluster. Open this in the interactive reference.

Image has CRITICAL vulnerabilities

Error or output

Total: 12 (HIGH: 9, CRITICAL: 3)

What it means

A scanner such as Trivy compared the packages inside the image with public vulnerability databases and found known CVEs. The summary counts them by severity. Each finding lists the package, its installed version and the Fixed Version that removes it, if one exists.

Causes, most likely first

  1. The image is built on an old base image. Its OS packages have had fixes since, but the image never got them.

    Check
    kubectl get deploy -o wide   # IMAGES: the tag running
    trivy image <image> --severity HIGH,CRITICAL   # findings in OS packages with a Fixed Version
    Fix

    Rebuild on a current base image, scan it, then roll it out: kubectl set image deployment/<name> <container>=<image>:<tag>.

  2. An application dependency is vulnerable. A newer tag of your own image can still ship the same library version.

    Check
    trivy image <image> --severity HIGH,CRITICAL   # findings under the language's packages (jar, npm, pip)
    Fix

    Move the dependency to the Fixed Version the scanner lists, rebuild, and scan again.

  3. No scan runs in CI. Nothing stops a vulnerable image before deploy, so it is only found later.

    Check
    trivy image --exit-code 1 --severity CRITICAL <image>; echo $?   # 1: this would have failed the build
    Fix

    Add that command to CI so critical findings fail the pipeline.

Prevent it

  • Fail CI on critical findings: trivy image --exit-code 1 --severity CRITICAL <image>.
  • Rebuild images on a schedule, so base image fixes reach production.

Now learn it: break and fix it yourself in Vulnerable image, then take Level 21 · Final boss: ready for production on a real cluster. Open this in the interactive reference.

Secrets readable in etcd

Error or output

/registry/secrets/kitchen/payments-api
k8s  v1  Secret  payments-api  kitchen  Opaque

What it means

Secrets are only base64-encoded in the API, and by default they are stored in etcd as plain text. Anyone who can read etcd or its backups can read them. Encryption at rest makes the API server encrypt them before writing; encrypted values start with k8s:enc:.

Causes, most likely first

  1. The API server has no --encryption-provider-config. It writes Secrets as they are.

    Check
    grep encryption-provider-config /etc/kubernetes/manifests/kube-apiserver.yaml   # nothing: this cause
    Fix

    Write an EncryptionConfiguration with aescbc (or a KMS provider) first and identity last, and pass it with --encryption-provider-config.

  2. Encryption is on, but existing Secrets were never rewritten. Only writes after the change are encrypted.

    Check
    etcdctl get /registry/secrets/<ns>/<name> --print-value-only <certificate flags> | head -c 40 | hexdump -C   # first bytes only: k8s then v1 Secret = still plain text
    Fix

    Rewrite every Secret so it is stored encrypted: kubectl get secrets -A -o json | kubectl replace -f -.

  3. identity is the first provider in the config. The first provider is the one used to write, and identity means no encryption.

    Check
    sudo grep -A6 'providers:' <encryption-config-file>   # identity: {} listed first
    Fix

    Put aescbc (or KMS) first and identity last, restart the API server, and rewrite the Secrets. Check: the value now starts with k8s:enc:aescbc:v1:.

Prevent it

  • Turn on encryption at rest when you build the cluster, and keep the key out of etcd backups.
  • Restrict who can read etcd and its backups as tightly as the Secrets themselves.

Now learn it: break and fix it yourself in Encryption at rest, then take Level 21 · Final boss: ready for production on a real cluster. Open this in the interactive reference.

Who deleted it? (audit log)

Error or output

Error from server (NotFound): secrets "payments-api" not found
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","verb":"delete","user":{"username":"ci"},"objectRef":{"resource":"secrets","namespace":"kitchen","name":"payments-api"}}

What it means

The API server can write an audit log entry for every request: who (user.username), what (verb, objectRef) and when. If audit logging is on, the answer is in that file. If it is off, the request was not recorded and cannot be recovered.

Causes, most likely first

  1. A person or automation with broad rights deleted it, for example through a shared edit binding or cluster-admin. The audit entry names that identity.

    Check
    grep '"verb":"delete"' /var/log/kubernetes/audit/audit.log | grep <name>   # the file --audit-log-path names; read user.username and the time
    kubectl auth can-i --list --as=<user>   # what that identity may do
    Fix

    Restore the object from your manifests or backup, and replace the broad binding with a narrow Role.

  2. A controller or the garbage collector deleted it, because its owner was deleted. The audit user is then a system account.

    Check
    grep <name> /var/log/kubernetes/audit/audit.log | grep delete   # user system:serviceaccount:kube-system:generic-garbage-collector
    Fix

    Find why the owner went; restore the owner and its children together.

  3. Audit logging is off, or the policy level is None for that resource. Nothing was recorded.

    Check
    grep audit /etc/kubernetes/manifests/kube-apiserver.yaml   # no --audit-policy-file: this cause
    Fix

    Turn on audit logging now, with at least Metadata for Secrets and RBAC objects, so the next one is recorded.

Prevent it

  • Turn on audit logging with a policy that records at least Metadata for Secrets and RBAC objects.
  • Give automation its own ServiceAccounts with narrow Roles, never shared admin rights.

Now learn it: break and fix it yourself in Audit log, then take Level 21 · Final boss: ready for production on a real cluster. Open this in the interactive reference.

Nodes and control plane

Node NotReady

Error or output

worker-2   NotReady   <none>   40d   v1.33.1
  Ready   Unknown   NodeStatusUnknown   Kubelet stopped posting node status.

What it means

The node's Ready condition is not True. Unknown means the kubelet stopped reporting at all (Kubelet stopped posting node status); False means the kubelet is running and says what is wrong. After about five minutes the node's Pods are evicted and recreated elsewhere.

Causes, most likely first

  1. The kubelet or the container runtime stopped. Nothing reports the node's status, so it goes to Unknown.

    Check
    kubectl get node <node> -o jsonpath='{.status.conditions[?(@.type=="Ready")].message}'   # Kubelet stopped posting node status: this cause
    ssh <node> 'sudo systemctl status kubelet containerd; sudo journalctl -u kubelet -n 50'
    Fix

    Start what stopped (sudo systemctl restart containerd kubelet) after reading why it stopped.

  2. The kubelet runs and reports itself not ready, for example container runtime network not ready (no CNI) or PLEG is not healthy (the runtime is stuck). The condition is False with that reason.

    Check
    kubectl describe node <node>   # Ready False, and the message names the reason
    Fix

    Fix what the message names: the CNI Pod on that node, or restart the runtime.

  3. Disk, memory or PID pressure. The kubelet protects the node, and may stop working well if it cannot write.

    Check
    kubectl describe node <node>   # MemoryPressure, DiskPressure or PIDPressure True
    Fix

    Free the disk or memory the condition names; see Evicted.

  4. The network between the node and the API server is down. The kubelet runs but its reports do not arrive.

    Check
    ssh <node> 'curl -sk https://<api-server>:6443/healthz'   # times out: this cause
    Fix

    Restore the route or firewall between the node and the control plane.

  5. The kubelet's client certificate expired. The API server rejects its reports.

    Check
    ssh <node> 'sudo journalctl -u kubelet -n 50 | grep -iE "x509|Unauthorized"'   # certificate errors: this cause
    Fix

    Renew it (kubeadm certs on the control plane, or rejoin the node), and keep rotateCertificates on.

Prevent it

  • Run every app with at least two replicas spread across nodes, so one node does not take it down.
  • Alert on node conditions (Ready, MemoryPressure, DiskPressure) before the node fails.

Now learn it: break and fix it yourself in Node NotReady, then take Level 14 · Boss level: the Friday night incident on a real cluster. Open this in the interactive reference.

Drain blocked by a PodDisruptionBudget

Error or output

error when evicting pods/"menu-5b8d9c6f7-tw4kp" -n "kitchen" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.

What it means

kubectl drain evicts Pods one by one, and the API server refuses an eviction that would leave fewer Pods than the PodDisruptionBudget allows. drain retries every 5 seconds forever. ALLOWED DISRUPTIONS 0 on the budget is the reason.

Causes, most likely first

  1. minAvailable equals the replica count, or is 1 on a one-replica app. No Pod may ever go.

    Check
    kubectl get pdb -A   # ALLOWED DISRUPTIONS 0
    kubectl describe pdb <name>   # Min available equal to the replicas
    Fix

    Scale up first (kubectl scale deployment/<name> --replicas=<more>) and drain again, or set minAvailable below the replica count, or maxUnavailable: 1.

  2. Some replicas are not Ready. They already use up the budget, so none of the healthy ones may go.

    Check
    kubectl get pods -l <selector>   # some not Ready
    kubectl describe pdb <name>   # Current below Desired
    Fix

    Fix the unready Pods first; the budget frees up when they are Ready.

Prevent it

  • Never set minAvailable equal to the replica count, and never 1 on a single-replica app.
  • Check kubectl get pdb -A for ALLOWED DISRUPTIONS 0 before maintenance.

Now learn it: break and fix it yourself in Drain blocked, then take Level 13 · Ready for the dinner rush on a real cluster. Open this in the interactive reference.

x509: certificate has expired

Error or output

Unable to connect to the server: tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current time 2026-10-30T09:14:02Z is after 2026-10-30T07:42:10Z
error: You must be logged in to the server (Unauthorized)

What it means

TLS checks that every certificate is valid at the current time. current time X is after Y means the certificate ended at Y. kubeadm's API server and client certificates last one year; its CA lasts ten. You must be logged in (Unauthorized) is the same problem for your own client certificate.

Causes, most likely first

  1. kubeadm's certificates expired: the cluster has not been upgraded in a year, and an upgrade is what renews them.

    Check
    sudo kubeadm certs check-expiration   # EXPIRES in the past, RESIDUAL TIME invalid
    sudo openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -enddate
    Fix

    On a control-plane node: sudo kubeadm certs renew all, then restart the control-plane static Pods (move their manifests out of /etc/kubernetes/manifests and back).

  2. The certificates were renewed, but the static Pods still run with the old ones loaded.

    Check
    sudo kubeadm certs check-expiration   # renewed dates
    echo | openssl s_client -connect <host>:6443 2>/dev/null | openssl x509 -noout -enddate   # still the old date: this cause
    Fix

    Restart the control-plane static Pods so they load the new files.

  3. Your kubeconfig embeds an expired client certificate. The API server rejects it: You must be logged in to the server.

    Check
    kubectl config view --minify --raw -o jsonpath='{.users[0].user.client-certificate-data}' | base64 -d | openssl x509 -noout -enddate   # a past date
    Fix

    Copy the renewed admin.conf: sudo cp /etc/kubernetes/admin.conf ~/.kube/config && sudo chown $(id -u):$(id -g) ~/.kube/config.

  4. A clock is wrong on the client or a node. A valid certificate looks expired, or not yet valid (is before).

    Check
    date -u; timedatectl   # far from the real time, or NTP not synchronized
    Fix

    Fix time sync (NTP) rather than renewing certificates that are fine.

Prevent it

  • Upgrade the cluster at least once a year; kubeadm renews certificates on upgrade.
  • Alert 30 days before expiry, for example from kubeadm certs check-expiration in a scheduled job.

Now learn it: break and fix it yourself in Certificates expired, then take Level 14 · Boss level: the Friday night incident on a real cluster. Open this in the interactive reference.

kubeadm upgrade apply fails

Error or output

[upgrade/version] FATAL: the --version argument is invalid due to these errors:

	- Specified version to upgrade to "v1.34.1" is higher than the kubeadm version "v1.33.5". Upgrade kubeadm first using the tool you used to install kubeadm
E: Held packages were changed and -y was used without --allow-change-held-packages.

What it means

kubeadm upgrades the control plane only to a version it knows, so kubeadm itself must be upgraded first, and only one minor version at a time. The apt error is a separate step failing: the packages are held, so apt will not change them.

Causes, most likely first

  1. The kubeadm package is older than the target. kubeadm cannot install a version newer than itself: is higher than the kubeadm version.

    Check
    kubeadm version -o short   # older than the target
    Fix

    Upgrade kubeadm first: sudo apt-mark unhold kubeadm && sudo apt-get install -y kubeadm=<1.x.y>-* && sudo apt-mark hold kubeadm.

  2. The apt source on pkgs.k8s.io is per minor version and still points at the old one. apt cannot see the new packages.

    Check
    cat /etc/apt/sources.list.d/kubernetes.list   # /v1.33/ while upgrading to 1.34
    apt-cache madison kubeadm   # no 1.34 versions
    Fix

    Point it at the new minor version (pkgs.k8s.io/core:/stable:/v1.34/deb/), then sudo apt-get update.

  3. The packages are held. apt refuses to change them: Held packages were changed.

    Check
    apt-mark showhold   # kubeadm, kubelet, kubectl
    Fix

    Unhold, install the exact version, hold again (as above), or pass --allow-change-held-packages for that one install.

  4. The target skips a minor version. kubeadm upgrades one minor at a time.

    Check
    sudo kubeadm upgrade plan   # the versions it will go to
    Fix

    Upgrade through each minor version in turn: sudo kubeadm upgrade apply v<1.x.y>.

Prevent it

  • Follow the fixed order: kubeadm, control plane, then each node's kubelet and kubectl.
  • Keep the packages held, so a routine apt upgrade never changes them.

Now learn it: break and fix it yourself in kubeadm upgrade, then take Level 14 · Boss level: the Friday night incident on a real cluster. Open this in the interactive reference.

Node NotReady after a kubelet upgrade

Error or output

Warning: The unit file, source configuration file or drop-ins of kubelet.service changed on disk. Run 'systemctl daemon-reload' to reload units.
     Active: activating (auto-restart) (Result: exit-code)

What it means

The new kubelet package installed, but the kubelet does not run or does not register with the API server, so the node goes NotReady. activating (auto-restart) means systemd keeps starting it and it keeps exiting; its log says why.

Causes, most likely first

  1. systemctl daemon-reload was skipped after the package changed the unit. systemd still runs the old unit definition.

    Check
    sudo systemctl status kubelet   # The unit file ... changed on disk
    Fix

    sudo systemctl daemon-reload && sudo systemctl restart kubelet.

  2. The kubelet is newer than the API server. A kubelet may not be newer than the API server, so it does not register.

    Check
    kubectl version   # Server Version older than the kubelet's
    kubectl get nodes   # VERSION column
    Fix

    Upgrade the control plane first, then the kubelets.

  3. A flag the new version removed, such as --container-runtime (gone in 1.27). The kubelet exits at start: unknown flag.

    Check
    sudo journalctl -u kubelet -n 50 | grep -i 'unknown flag'   # present: this cause
    cat /var/lib/kubelet/kubeadm-flags.env   # flags kubeadm passes to the kubelet
    Fix

    Remove the flag from /var/lib/kubelet/kubeadm-flags.env (or /etc/default/kubelet), moving the setting into the kubelet config file if it has one, then restart the kubelet.

Prevent it

  • Upgrade the control plane before any kubelet.
  • Drain each node before its kubelet upgrade, and do one node at a time.

Now learn it: break and fix it yourself in kubeadm upgrade, then take Level 14 · Boss level: the Friday night incident on a real cluster. Open this in the interactive reference.

etcd snapshot fails

Error or output

Error: context deadline exceeded
transport: authentication handshake failed: tls: failed to verify certificate: x509: certificate signed by unknown authority

What it means

etcdctl must reach etcd's client port and prove who it is with a client certificate signed by etcd's own CA. context deadline exceeded means it got no usable answer in time; x509: certificate signed by unknown authority means it does not trust the certificate etcd showed.

Causes, most likely first

  1. The wrong endpoint, or no client certificate. etcd ignores or drops the connection, and etcdctl gives up: context deadline exceeded.

    Check
    kubectl -n kube-system describe pod etcd-<node>   # --listen-client-urls and the cert paths
    ls /etc/kubernetes/pki/etcd   # ca.crt, server.crt, server.key
    Fix

    Pass what etcd expects: --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key.

  2. --cacert is not the etcd CA (often the cluster CA, /etc/kubernetes/pki/ca.crt). etcd has its own CA: x509 unknown authority.

    Check
    grep trusted-ca-file /etc/kubernetes/manifests/etcd.yaml   # the CA etcd uses
    Fix

    Use /etc/kubernetes/pki/etcd/ca.crt on kubeadm clusters.

  3. The output directory does not exist. etcdctl cannot create the file.

    Check
    ls -d <dir>   # No such file or directory
    Fix

    Create the directory first, then check the result: etcdutl snapshot status <file> --write-out=table.

Prevent it

  • Schedule snapshots, copy them off the node, and test a restore regularly.
  • Alert when the newest snapshot is older than a day.

Now learn it: break and fix it yourself in etcd backup, then take Level 14 · Boss level: the Friday night incident on a real cluster. Open this in the interactive reference.

Restore etcd from a snapshot

Error or output

Error: data-dir "/var/lib/etcd" not empty or already exists
The connection to the server 172.18.0.2:6443 was refused - did you specify the right host or port?

What it means

Restoring writes the snapshot into a new etcd data directory; etcd then has to be started on that directory. data-dir not empty or already exists means etcdutl refused to overwrite one. A refused connection afterwards is normal for a minute while etcd and the API server restart.

Causes, most likely first

  1. Restoring over /var/lib/etcd. etcdutl refuses a directory that exists, and writing over a running etcd would corrupt it.

    Check
    ls /var/lib/etcd   # not empty: restore elsewhere
    Fix

    Restore into a new, empty directory: etcdutl snapshot restore <file> --data-dir /var/lib/etcd-restore.

  2. Restored into a new directory, but the etcd manifest still points at the old one. etcd starts on the old data, so nothing changes.

    Check
    grep -A2 hostPath /etc/kubernetes/manifests/etcd.yaml   # path still /var/lib/etcd
    Fix

    Point the etcd-data hostPath at it: sudo sed -i 's#path: /var/lib/etcd#path: /var/lib/etcd-restore#' /etc/kubernetes/manifests/etcd.yaml; the kubelet restarts etcd.

  3. --data-dir changed but not the hostPath volume, or the reverse. etcd looks inside the container for a path that is not mounted, and does not start.

    Check
    sudo crictl ps -a | grep etcd   # Exited
    sudo crictl logs <etcd-container>   # the data dir error
    Fix

    Change only the hostPath, and leave --data-dir and the mountPath as they were.

Prevent it

  • Practise the restore before you need it, on a test cluster.
  • Keep snapshots off the node, with their date in the file name.

Now learn it: break and fix it yourself in etcd restore, then take Level 14 · Boss level: the Friday night incident on a real cluster. Open this in the interactive reference.

Namespace stuck Terminating

Error or output

NamespaceFinalizersRemaining   True   SomeFinalizersRemain   Some content in the namespace has finalizers remaining: backup.kitchen.example/protect in 1 resource instances

What it means

Deleting a namespace first deletes everything in it; the namespace goes only when it is empty. Terminating that does not end means something inside cannot be deleted. The namespace's own conditions say what: NamespaceContentRemaining, NamespaceFinalizersRemaining, or NamespaceDeletionDiscoveryFailure.

Causes, most likely first

  1. Custom resources keep finalizers that no controller removes, because their operator was uninstalled first: Some content in the namespace has finalizers remaining.

    Check
    kubectl describe ns <ns>   # NamespaceFinalizersRemaining and the finalizer name
    kubectl api-resources --verbs=list --namespaced -o name | xargs -n 1 kubectl get -n <ns>   # what is left
    Fix

    Bring back the operator if you can. Only if it is gone for good: kubectl patch <kind> <name> -n <ns> --type=merge -p '{"metadata":{"finalizers":null}}'.

  2. An APIService (such as metrics) is unavailable, so the namespace controller cannot list everything to delete: NamespaceDeletionDiscoveryFailure.

    Check
    kubectl get apiservice | grep False   # a broken APIService
    Fix

    Repair or delete that APIService; the namespace then finishes.

  3. A PersistentVolumeClaim or Pod with its own finalizer waits for something else, such as a Pod still using the claim.

    Check
    kubectl get pvc,pods -n <ns>   # Terminating ones
    Fix

    Fix what each one waits for. Do not clear the namespace's own finalizer through its finalize API: objects left in it stay in etcd and come back if it is created again.

Prevent it

  • Delete custom resources before uninstalling the operator that owns them.
  • Remove broken APIServices (kubectl get apiservice shows False) so namespaces can be emptied.

Now learn it: break and fix it yourself in Namespace stuck Terminating, then take Level 00 · Hello, cluster on a real cluster. Open this in the interactive reference.