PlatformEngr® Kubernetes course

Module 07 · Share the cluster · 60 min · You are: Platform and product

Debugging labs: five ways a pod does not run

In this module

  • You learn: One method for any pod that is not running: find the stage it is stuck at (not created, not scheduled, not started, crashing, not ready), then read the one place that explains that stage.
  • You do: Break checkout in five ways and find each cause from what the cluster shows, then fix it through Git. Write one runbook entry for the lab that surprised you most.
  • You end with: checkout is healthy again: 2 pods, the probes and image from module 06, nothing extra. Your repo has docs/runbook.md with one real entry.

Product teams come to the platform team with one sentence: "my pod is not running". There are only a handful of stages a pod can be stuck at, and each has exactly one place where the reason is written. Guessing is slow; walking the stages is fast. Try each lab before you open the hints: the skill is knowing where to look next.

Open this module in the interactive course to track progress.

Concepts

Find the stage first
No pod at all: admission said no, look at the ReplicaSet. Pending: the scheduler, look at the pod's events. CreateContainerConfigError or ErrImagePull: the kubelet or the registry, pod events. CrashLoopBackOff: the process, its previous logs. Restarting while healthy: the probes, events.
Events expire
Events are kept for about an hour. Look while it is failing, or you will find a healthy-looking pod with a restart count and no story.
--previous
After a crash, kubectl logs shows the new container, which has not failed yet. The reason is in the one before: kubectl logs --previous.
CrashLoopBackOff is not a cause
It means "it exited, and I wait longer each time before I try again". The cause is the exit: the code, the last log line, or OOMKilled.
Old pods are your safety net
A rolling update keeps the working pods until new ones are ready. Most of these failures leave the site up, which is why they are found late.

Symptom to cause

What kubectl get showsStageRead this
READY 1/2, no second pod at allNot created (admission)kubectl -n NS get events --field-selector reason=FailedCreate
PendingNot scheduledkubectl -n NS describe pod POD, Events
CreateContainerConfigError, ErrImagePullNot startedkubectl -n NS describe pod POD, Events
CrashLoopBackOff, ErrorCrashingkubectl -n NS logs POD --previous, and Last State in describe pod
Running, RESTARTS climbing, clean logsKilled from outsidekubectl -n NS get events --field-selector reason=Unhealthy

The full table, with usual causes, is docs/debugging.md in the reference repo.

Files to know

Open them in your editor or on GitHub; the bullets say what to look at.

Steps

1. Lab 1: CrashLoopBackOff (Debug lab)

team-checkout wants debug logging and adds a flag from memory.

Add to ~/platformengr/k8s-platform/apps/checkout/deployment.yaml under the container, above ports:

          command: ["./podinfo", "--port=9898", "--log-level=debug"]
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "checkout: debug logging" && git push -q
kubectl apply -f apps/checkout/deployment.yaml

Wait 45 seconds, then look:

sleep 45; kubectl -n checkout-dev get pods

What you see.

Two old pods Running, one new pod in CrashLoopBackOff (or Error) with RESTARTS at 2 or more. The site answers. kubectl -n checkout-dev logs <new pod> may show nothing useful: you are looking at a container that has only just started.

Fix it yourself. Quote the line the process printed before it died, and its exit code. Then fix it through Git.

Hint 1

The container that failed is the previous one: kubectl -n checkout-dev logs <pod> --previous.

Hint 2

Error: unknown flag: --log-level. podinfo's flag is --level. The exit code is under Last State: kubectl -n checkout-dev describe pod <pod> | grep -A3 'Last State'.

One solution

Exit code 2: the program refused its arguments. Changing --log-level to --level would also fix it; a revert is the safe default when you are not sure.

cd ~/platformengr/k8s-platform && git revert --no-edit HEAD && git push -q
kubectl apply -f apps/checkout/deployment.yaml
kubectl -n checkout-dev rollout status deployment/checkout --timeout=120s
The five questions, answered
What failed?
The new container exited at start on an unknown flag.
How was it detected?
CrashLoopBackOff on the new pod; the rollout never completed. The old pods kept serving.
Who responds?
team-checkout.
How is it corrected?
Correct or revert the flag.
How is it prevented?
Run the image with the same arguments in CI before merge; alert on rollouts past their deadline. Other causes of the same status: missing config, OOMKilled, a failing liveness probe.

2. Lab 2: pods that never appear, and a pod that stays Pending (Debug lab)

Two changes by team-checkout, one after the other. Both end with fewer pods than asked for. Notice where each one stops. (a) A background worker, as a second Deployment:

Create ~/platformengr/k8s-platform/apps/checkout/worker.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-worker
  namespace: checkout-dev
spec:
  replicas: 4
  selector:
    matchLabels:
      app.kubernetes.io/name: checkout-worker
  template:
    metadata:
      labels:
        app.kubernetes.io/name: checkout-worker
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 100
        runAsGroup: 101
        seccompProfile:
          type: RuntimeDefault
      serviceAccountName: checkout
      containers:
        - name: worker
          image: ghcr.io/stefanprodan/podinfo:6.17.0
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL
          resources:
            requests:
              cpu: 100m
              memory: 64Mi
            limits:
              memory: 128Mi
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "checkout: background worker" && git push -q
kubectl apply -f apps/checkout/worker.yaml

Look at the Deployments:

kubectl -n checkout-dev get deployment

(b) Once you have found and fixed (a): a node selector copied from the manifest of a cloud cluster.

Add to ~/platformengr/k8s-platform/apps/checkout/deployment.yaml under the pod spec:, above securityContext:

      nodeSelector:
        disktype: ssd
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "checkout: run on SSD nodes" && git push -q
kubectl apply -f apps/checkout/deployment.yaml
kubectl -n checkout-dev get pods

What you see.

(a) checkout-worker shows 3/4. kubectl -n checkout-dev get pods lists three worker pods and no fourth: nothing Pending, nothing failing.

(b) The two old checkout pods, and one new pod in Pending for as long as you wait.

Fix it yourself. For each: say who refused (admission or the scheduler), where you read it, and quote the message. Then get back to two healthy checkout pods and no worker.

Hint 1

(a) A pod that does not exist has no events. Who tried to create it? kubectl -n checkout-dev get events --field-selector reason=FailedCreate. (b) A pod that exists and is not placed: kubectl -n checkout-dev describe pod <pending pod> | tail -n 3.

Hint 2

(a) exceeded quota: team-quota: 2 checkout pods and 3 workers use all 500m. The worker goes: delete the file and the object. (b) didn't match Pod's node affinity/selector: kubectl get nodes --show-labels | grep -c disktype prints 0. Revert.

One solution

(a) Remove the file, then the object, which apply never deletes:

cd ~/platformengr/k8s-platform && git rm -q apps/checkout/worker.yaml && git commit -qm "checkout: no worker for now" && git push -q
kubectl -n checkout-dev delete deployment checkout-worker

(b) Take the node selector out again. Remove it by hand rather than with git revert HEAD: the last commit is now the worker removal, not the selector.

Delete from ~/platformengr/k8s-platform/apps/checkout/deployment.yaml

-       nodeSelector:
-         disktype: ssd
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "checkout: no node selector" && git push -q
kubectl apply -f apps/checkout/deployment.yaml
kubectl -n checkout-dev rollout status deployment/checkout --timeout=120s
The five questions, answered
What failed?
(a) The fourth worker pod was refused by the quota (requested: requests.cpu=100m, used: requests.cpu=500m, limited: requests.cpu=500m). (b) No node has the label disktype: ssd, so the scheduler could not place the pod.
How was it detected?
A replica count that never fills; in (b) a Pending pod with a FailedScheduling event.
Who responds?
team-checkout sees both. (a) is settled between the team and the platform team: ask for more quota with a pull request to tenants/checkout/quota.yaml, or ask for less. (b) is the team's alone.
How is it corrected?
As in the solution.
How is it prevented?
Quota use on a dashboard the team can see; a documented list of node labels teams may select on.

3. Lab 3: a probe that kills a healthy app (Debug lab)

team-checkout wants the liveness probe to "really test the app", and points it at an endpoint that does some work. Here that is podinfo's /delay/3, which answers after three seconds: a stand-in for a health check that calls the database.

Change in ~/platformengr/k8s-platform/apps/checkout/deployment.yaml

-           livenessProbe:
-             httpGet:
-               path: /healthz
-               port: http
-             periodSeconds: 10
-             timeoutSeconds: 2
+           livenessProbe:
+             httpGet:
+               path: /delay/3
+               port: http
+             periodSeconds: 10
+             timeoutSeconds: 1
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "checkout: liveness really tests the app" && git push -q
kubectl apply -f apps/checkout/deployment.yaml
kubectl -n checkout-dev rollout status deployment/checkout --timeout=120s

The rollout succeeded. Wait a minute and a half:

sleep 90; kubectl -n checkout-dev get pods

What you see.

Expected: two pods, RESTARTS 0. You get: both pods restarting, RESTARTS 2 and climbing, later CrashLoopBackOff. kubectl -n checkout-dev logs <pod> --previous shows a normal start and a clean shutdown: no error, no panic. The app says it is fine.

Fix it yourself. Find what is stopping the containers, quote the event, and say why the rollout itself did not catch it. Fix through Git.

Hint 1

The process did not crash; something stopped it. The kubelet writes down what it does: kubectl -n checkout-dev get events --field-selector reason=Unhealthy | tail -n 3, and the same with reason=Killing.

Hint 2

Liveness probe failed: ... context deadline exceeded and Container checkout failed liveness probe, will be restarted. The readiness and startup probes still use fast endpoints, so the new pods became Ready and the rollout finished; liveness only starts counting after that.

One solution
cd ~/platformengr/k8s-platform && git revert --no-edit HEAD && git push -q
kubectl apply -f apps/checkout/deployment.yaml
kubectl -n checkout-dev rollout status deployment/checkout --timeout=120s
The five questions, answered
What failed?
Every checkout container, killed by the kubelet about every 30 seconds for a slow liveness answer.
How was it detected?
Restart counts and Unhealthy events; users saw errors whenever both pods restarted at once.
Who responds?
team-checkout.
How is it corrected?
Liveness back on /healthz with its old timeout.
How is it prevented?
Liveness checks only the process itself, with a timeout well above its worst normal answer; a review rule for probe changes; an alert on restart rate.

4. Lab 4: an image that will not pull (Debug lab)

team-checkout promotes a version that was never built.

Change in ~/platformengr/k8s-platform/apps/checkout/deployment.yaml

-           image: ghcr.io/stefanprodan/podinfo:6.17.0
+           image: ghcr.io/stefanprodan/podinfo:6.17.99
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "checkout: 6.17.99" && git push -q
kubectl apply -f apps/checkout/deployment.yaml
sleep 30; kubectl -n checkout-dev get pods

What you see.

Two old pods Running, one new pod ErrImagePull, then ImagePullBackOff. RESTARTS is 0: the container never started, so there are no logs at all.

Fix it yourself. Quote what the registry answered. Say what else produces the same status. Fix through Git.

Hint 1

No container, so no logs. The kubelet's attempt is in the pod's events: kubectl -n checkout-dev describe pod <pod> | grep -A2 Failed.

Hint 2

The message ends in not found: the tag does not exist. A private image without credentials ends in unauthorized or denied instead, and a typo in the registry host ends in no such host.

One solution
cd ~/platformengr/k8s-platform && git revert --no-edit HEAD && git push -q
kubectl apply -f apps/checkout/deployment.yaml
kubectl -n checkout-dev rollout status deployment/checkout --timeout=120s
The five questions, answered
What failed?
The kubelet could not pull podinfo:6.17.99; the registry has no such tag.
How was it detected?
ImagePullBackOff on the new pod; after ten minutes the Deployment reports ProgressDeadlineExceeded. The old pods kept serving.
Who responds?
team-checkout.
How is it corrected?
Revert to a tag that exists.
How is it prevented?
CI checks that the image exists before the manifest change merges; deploy tags or digests that a build produced, never ones typed by hand.

5. Lab 5: a Deployment with no new pods (Debug lab)

To look around inside the pod, team-checkout adds a helper container with a shell.

Add to ~/platformengr/k8s-platform/apps/checkout/deployment.yaml under containers:, after the whole checkout container (just above volumes:)

        - name: debug
          image: busybox:1.38.0
          command: ["sleep", "86400"]
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "checkout: debug container" && git push -q

Apply it, and read everything it prints:

kubectl apply -f apps/checkout/deployment.yaml
kubectl -n checkout-dev rollout status deployment/checkout --timeout=30s

What you see.

The apply says deployment.apps/checkout configured. The rollout times out at 0 out of 2 new replicas have been updated. kubectl -n checkout-dev get pods shows only the two old pods: nothing new, nothing Pending, nothing crashing.

Fix it yourself. Find who refused the new pods and why. The cluster told you in two places: name both. Fix through Git, then get a shell next to checkout the supported way.

Hint 1

The apply printed a line starting with Warning:. And a pod that was never created leaves its trace one level up: kubectl -n checkout-dev get events --field-selector reason=FailedCreate | tail -n 1.

Hint 2

violates PodSecurity "restricted:v1.37": the debug container has no security context. Revert. For a shell, add a temporary debug container to a running pod with kubectl debug.

One solution
cd ~/platformengr/k8s-platform && git revert --no-edit HEAD && git push -q
kubectl apply -f apps/checkout/deployment.yaml
kubectl -n checkout-dev rollout status deployment/checkout --timeout=120s

The supported way to get a shell: an ephemeral container in a running pod, next to checkout's process, within the restricted rules. Type ps to see podinfo, then exit.

kubectl -n checkout-dev debug -it "$(kubectl -n checkout-dev get pod -l app.kubernetes.io/name=checkout -o name | head -n 1)" --image=busybox:1.38.0 --target=checkout --profile=restricted
The five questions, answered
What failed?
The new ReplicaSet could not create pods: Pod Security rejected them for the debug container.
How was it detected?
A Warning: at apply time that scrolled past, then a rollout that timed out; nothing on any pod.
Who responds?
team-checkout.
How is it corrected?
Remove the container; use an ephemeral debug container.
How is it prevented?
Treat warnings as errors in CI (kubectl apply --dry-run=server and fail on Warning:), and tell teams about kubectl debug before they need it.

6. Write one runbook entry

For the lab that surprised you most, write the entry you would want at 2 a.m. In your own words: what you saw, where the reason was, the cause, the fix, and who owns it.

Done when:

Create ~/platformengr/k8s-platform/docs/runbook.md

# Runbook

## <the symptom, in one line>

- **Symptom:** what `kubectl get` showed.
- **Where the reason was:** the command, and the line that explained it.
- **Cause:**
- **Fix:**
- **Who owns it:**
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "docs: first runbook entry" && git push -q

Simplified for the course

You fixed everything by reverting. Real incidents also need: tell people, restore service first, write it up. The five questions under each lab are the write-up.

Events live about an hour on most clusters. Production platforms ship them, with logs, to a central store, so the story of a 3 a.m. failure is still there at 9.

Quiz

  1. A Deployment shows 2/3 and there is no third pod in any state. Where is the reason?
    • In the logs of the third pod's previous container, with kubectl logs --previous
    • In the events of the ReplicaSet, which tried to create the pod and was refused at admission
    • In the scheduler's events on the third pod, which is waiting in Pending
  2. A pod is in CrashLoopBackOff and kubectl logs shows a clean start. What next?
    • Read kubectl logs --previous and the Last State in kubectl describe pod: the failed container is the one before
    • Delete the pod, because the back-off timer only resets when a new pod is created
    • Raise the memory limit, because CrashLoopBackOff means the container ran out of memory
  3. Both pods restart every half minute, their logs show a clean shutdown, and the rollout had succeeded. What stopped them?
    • The ReplicaSet, which replaces pods that run longer than their progress deadline
    • The scheduler, which moves pods to emptier nodes when the cluster is busy
    • The kubelet, because the liveness probe failed: look at the Unhealthy events
Answers
  1. In the events of the ReplicaSet, which tried to create the pod and was refused at admission No pod means admission refused it: quota, LimitRange or Pod Security.
  2. Read kubectl logs --previous and the Last State in kubectl describe pod: the failed container is the one before The status says it keeps exiting; the cause is in the container that exited.
  3. The kubelet, because the liveness probe failed: look at the Unhealthy events A process that shuts down cleanly was asked to; liveness failures are the usual reason.

Field note

When I am handed a broken pod I say the stage out loud before I touch anything: not created, not scheduled, not started, crashing, not ready. It sounds silly, and it stops me reading the logs of a container that never ran.

Report a problem with this module