Kubernetes Debugging skill for Codex
Root cause from events and exit codes, not restarts. Codex investigates broken pods with read-only kubectl first: describe, events and logs from the previous container, then routes by status and exit code to the likely cause. It attaches ephemeral debug containers to distroless images only with permission, traces connectivity one hop at a time, fixes the source manifest instead of the live object, and reports the evidence behind the root cause.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- kubernetes-debugging Installs to ~/.agents/skills/kubernetes-debugging/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install kubernetes-debugging The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project kubernetes-debugging Installs to .agents/skills/kubernetes-debugging/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/kubernetes-debugging.zipunzip -o kubernetes-debugging.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) kubernetes-debugging PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $kubernetes-debugging, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Diagnoses failing Kubernetes workloads from evidence - CrashLoopBackOff, ImagePullBackOff, CreateContainerConfigError, Pending pods, OOMKilled and exit code 137, failing probes, stuck rollouts, Service with no endpoints, DNS and network policy problems - using kubectl describe, events, logs --previous, kubectl debug ephemeral containers and node inspection. Use when pods crash, restart, never schedule, cannot be reached, or a deployment hangs, and the user wants the root cause and a fix rather than a restart.
Use it when
- Pods crash, restart, get OOMKilled or never schedule
- A rollout is stuck or keeps timing out
- A Service or ingress cannot reach healthy-looking pods
- Image pulls fail or containers cannot start because of missing config
Use something else when
- You are writing new manifests from scratch: use kubernetes-manifests
- The issue is cluster-wide dashboards and alerting rather than one workload: use kubernetes-observability
- Pods are healthy but the system is too slow or too costly at load: use kubernetes-autoscaling
How to check the result
After the fix is deployed, the original symptom is gone, restart counts stay flat for longer than the old crash interval, and no new Warning events appear for the workload.
The mistake it prevents
Deleting the crashing pod and declaring victory when it comes back Running, without reading logs --previous or the exit code that explain the next crash.
The job is a root cause backed by evidence (an event, an exit code, a log line, a failing command) and a fix in the source manifests, not a pod deleted until it looks green. Read before you change; read-only commands first, always.
Ground rules
- Confirm context and namespace before every session:
kubectl config current-contextand-n <ns>on every command. Debugging the wrong cluster is the classic mistake. - Read-only by default:
get,describe,logs,events,top,auth can-i. - Ask the user before anything that mutates a shared cluster: deleting pods,
rollout restart,rollout undo, scaling, editing live objects,kubectl debug(it adds an ephemeral container or creates a pod), cordoning or draining nodes. In production also ask beforeexecinto a running container. - Never
kubectl delete pod --force --grace-period=0on StatefulSet pods without explicit confirmation: it can run two copies of the same identity against one volume. - Fix the cause in Git (manifest, chart values, overlay). A live
kubectl editis lost on the next sync and should only be a confirmed emergency stopgap.
kubectl needs cluster credentials and network. If they are unavailable in the sandbox, give the user the exact commands to run and ask for the output.
First five commands
kubectl get pods -n shop -o wide # STATUS, RESTARTS, NODE
kubectl describe pod <pod> -n shop # Events + Last State block
kubectl events -n shop --for pod/<pod> # kubectl 1.26+; else get events --sort-by=.lastTimestamp
kubectl logs <pod> -n shop -c <container> --previous --tail=100 # the crashed instance
kubectl get deploy,rs -n shop -l app.kubernetes.io/name=orders # which ReplicaSet is failing
The Last State: Terminated block in describe gives Reason and Exit Code; that
pair routes most crash investigations.
Triage by status
| Status / signal | First look | Most likely causes |
|---|---|---|
Pending | describe Events: FailedScheduling message | Requests exceed free capacity, taint without toleration, nodeSelector/affinity matches nothing, PVC unbound, topology spread too strict |
ImagePullBackOff / ErrImagePull | Events: exact pull error | Tag does not exist, private registry without imagePullSecrets, wrong architecture (arm64 vs amd64), registry rate limit |
CreateContainerConfigError | Events | Referenced ConfigMap/Secret or key missing |
CrashLoopBackOff | logs --previous, exit code | App error on boot, missing env/config, bad command, failing liveness probe, OOM |
OOMKilled, exit 137 | describe Last State, kubectl top pod | Memory limit below working set, runtime unaware of the limit (JVM, Node heap), leak |
Running but 0/1 READY | describe readiness probe failures | Wrong probe path/port, dependency down, probe timeout too short |
Terminating forever | get pod -o yaml finalizers, node status | Finalizer controller gone, node unreachable |
| Rollout stuck | rollout status, new ReplicaSet pods | New pods never ready, quota exceeded, PDB plus maxUnavailable 0 with no capacity |
Evicted | describe message | Node memory or ephemeral-storage pressure; pod exceeded emptyDir sizeLimit |
Exit codes: 0 process exited (wrong command for a long-running container), 1 app
error, 126/127 command not executable or not found, 137 SIGKILL (OOM or liveness
kill after grace), 139 segfault, 143 SIGTERM (normal on shutdown; on startup it means
something killed it, often a failing liveness probe).
For the detailed decision trees per status, plus DNS and network-policy checks, read references/failure-playbook.md when the triage table does not settle the cause within a few commands.
Getting inside
Distroless and slim images have no shell, so kubectl exec ... sh fails. Attach an
ephemeral debug container that shares the target’s process namespace (ask first):
kubectl debug -it <pod> -n shop --image=busybox:1.37 --target=<container> -- sh
# inside: ps aux, ls /proc/1/root/app, wget -qO- localhost:8080/healthz
Crashing too fast to attach? Copy the pod with a different command:
kubectl debug <pod> -n shop -it --copy-to=<pod>-dbg --container=<container> -- sh
Delete the -dbg copy afterwards (confirm with the user). For node problems,
kubectl debug node/<node> -it --image=busybox:1.37 gives a shell with the host
filesystem at /host.
Connectivity
Work outward one hop at a time:
kubectl get endpointslices -n shop -l kubernetes.io/service-name=orders # empty => selector or readiness problem
kubectl get svc orders -n shop -o jsonpath='{.spec.selector}'; echo
kubectl get pods -n shop -l app.kubernetes.io/name=orders --show-labels
kubectl port-forward -n shop svc/orders 8080:80 # bypasses ingress and network policy
kubectl run -it --rm nettest -n shop --image=busybox:1.37 --restart=Never -- \
sh -c 'nslookup orders.shop.svc.cluster.local && wget -qO- -T 3 http://orders.shop/healthz'
kubectl get networkpolicy -n shop
- No endpoints: Service selector does not match pod labels, or no pod is Ready, or
targetPortnames a port the container does not declare. - Port-forward works, in-cluster call fails: NetworkPolicy, wrong port, or DNS.
- In-cluster works, ingress fails: Ingress/Gateway backend, host rules, TLS, or the controller’s logs.
Resource evidence
kubectl top pod <pod> -n shop --containers
kubectl describe node <node> | sed -n '/Allocated resources/,/Events/p'
kubectl get pod <pod> -n shop -o jsonpath='{.status.qosClass}'; echo
kubectl top needs metrics-server. Memory near the limit right before restarts confirms
OOM; raise the limit only after checking the runtime’s heap settings respect it.
Workflow
- [ ] 1. Confirm context, namespace, and which workload and since when
- [ ] 2. get / describe / events / logs --previous for one failing pod
- [ ] 3. Route with the triage table; form one hypothesis
- [ ] 4. Prove it with a read-only command (or a confirmed debug container)
- [ ] 5. Fix in source (manifest, values, overlay, app code)
- [ ] 6. After the user deploys the fix: watch rollout, restarts stay at 0 for 10+ min
- [ ] 7. Write the report
If a recent change correlates with the failure, kubectl rollout history deploy/<name>
and kubectl rollout undo are available, but rollback is a production action: propose
it, do not run it without a yes.
Verification
kubectl rollout status deploy/orders -n shop --timeout=5m
kubectl get pods -n shop -l app.kubernetes.io/name=orders -w # RESTARTS stays flat
kubectl events -n shop --types=Warning
The fix is proven when the original symptom is gone, restarts do not increase over a period longer than the original crash interval, and no new Warning events appear.
Report template
**Symptom:** orders pods CrashLoopBackOff in shop-prod since 14:02 UTC (deploy 1.14.2)
**Evidence:** Last State OOMKilled, exit 137; working set 498Mi vs 512Mi limit just before each crash
**Root cause:** 1.14.2 added an in-memory cache sized at 300 MB; memory limit unchanged
**Fix:** limit/request 512Mi -> 1Gi in overlays/production; cache size made configurable (PR #812)
**Verified:** after rollout, 0 restarts in 30 min, working set steady at 610Mi
**Not done:** no restart or rollback performed by me; follow-up alert on working set > 85% of limit What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Kubernetes Debugging"
short_description: "Root cause from events and exit codes, not restarts"
default_prompt: "$kubernetes-debugging My pod is in CrashLoopBackOff, figure out why" references/failure-playbook.md
# Kubernetes failure playbook
Decision trees for when the triage table in SKILL.md does not settle the cause. Every
command here is read-only unless marked (ask first).
## Pending
Read the `FailedScheduling` message in `kubectl describe pod`. It lists, per reason, how
many nodes were rejected, for example `0/6 nodes are available: 3 Insufficient memory,
3 node(s) had untolerated taint {dedicated: gpu}`.
| Message fragment | Check | Fix direction |
|---|---|---|
| `Insufficient cpu` / `Insufficient memory` | `kubectl describe node` Allocated resources; sum of requests, not usage | Lower inflated requests, add nodes, or let the cluster autoscaler or Karpenter scale (check its logs/events if it did not) |
| `untolerated taint` | `kubectl get nodes -o custom-columns=N:.metadata.name,T:.spec.taints` | Add a toleration only if the pod belongs on those nodes |
| `didn't match Pod's node affinity/selector` | `kubectl get nodes --show-labels` vs pod `nodeSelector`/affinity | Fix the label or the selector |
| `pod has unbound immediate PersistentVolumeClaims` | `kubectl get pvc`, `kubectl describe pvc` | StorageClass missing or wrong, provisioner failing, zone mismatch |
| `volume node affinity conflict` | PV `nodeAffinity` zone vs available nodes | Zonal disk in a zone with no capacity; use `volumeBindingMode: WaitForFirstConsumer` |
| `didn't match pod topology spread constraints` | `whenUnsatisfiable: DoNotSchedule` with too few zones | Use `ScheduleAnyway` or add capacity in the missing zone |
| `Too many pods` | node `pods` capacity | Max pods per node reached (CNI IP limits on EKS) |
| No events at all | `kubectl get pod -o jsonpath='{.spec.schedulerName}'` | Custom scheduler not running, or a ResourceQuota/LimitRange rejected the ReplicaSet (check `kubectl describe rs`) |
Quota problems show on the ReplicaSet, not the pod: `kubectl describe rs <rs>` shows
`exceeded quota` and no pod is ever created.
## ImagePullBackOff
1. Read the exact error in Events.
2. `manifest unknown` / `not found`: tag typo or image never pushed. Check the registry.
3. `unauthorized` / `denied`: pod `imagePullSecrets` or the ServiceAccount's secrets;
on cloud registries, node IAM role or workload identity.
4. `no matching manifest for linux/amd64`: image built for one architecture only. Build
multi-arch with `docker buildx build --platform linux/amd64,linux/arm64`.
5. `toomanyrequests`: Docker Hub rate limit. Mirror the image or authenticate pulls.
## CrashLoopBackOff
1. `kubectl logs --previous`. Empty logs with exit 1 often means the process failed
before logging: wrong `command`/`args`, missing binary, or permissions.
2. Exit 127 or `exec format error`: entrypoint not found or wrong architecture.
3. `permission denied` writing a file: `readOnlyRootFilesystem: true` or non-root user
without a writable `emptyDir` at that path.
4. Config errors: compare env with `kubectl exec <pod> -- env` on a healthy sibling (ask
first in production), or render the ConfigMap/Secret keys:
`kubectl get cm <name> -o jsonpath='{.data}'`. Never print Secret values into chat or
logs; check key names only: `kubectl get secret <name> -o jsonpath='{.data}' | jq 'keys'`.
5. Exit 137 or 143 with `Liveness probe failed` events: the probe killed it. The app may
be fine but slow to start; add a startup probe instead of raising liveness delays.
6. Backoff grows to 5 minutes between restarts; after a fix, the next attempt can lag.
## OOMKilled
- Container-level OOM shows `Reason: OOMKilled` in Last State. Node-level pressure shows
`Evicted` with a memory message instead.
- Compare `kubectl top pod --containers` against `resources.limits.memory`.
- Runtime-specific checks:
- JVM: `-XX:MaxRAMPercentage` (default 25 percent); heap plus metaspace plus threads plus direct buffers must fit.
- Node: `--max-old-space-size` defaults are not tied to the cgroup limit on older versions.
- Go: set `GOMEMLIMIT` to about 90 percent of the limit.
- Python: no heap cap; look for unbounded caches and large request bodies.
- Raising the limit is valid when the new working set is understood. If memory grows
monotonically over hours, it is a leak: raising the limit only delays the crash.
## Readiness failing
- `describe` shows `Readiness probe failed: HTTP probe failed with statuscode: 503` or
`connection refused`.
- `connection refused`: app listens on `127.0.0.1` instead of `0.0.0.0`, or on another port.
- Timeouts: probe `timeoutSeconds` defaults to 1 s; a probe that queries dependencies
will time out under load.
- Readiness depending on a shared downstream takes every pod out at once; consider
degrading instead.
## Stuck rollout
```bash
kubectl rollout status deploy/<name> -n <ns>
kubectl get rs -n <ns> -l app.kubernetes.io/name=<name> # new RS desired vs ready
kubectl describe deploy/<name> -n <ns> | sed -n '/Conditions/,/Events/p'
```
- `ProgressDeadlineExceeded`: new pods never became ready within `progressDeadlineSeconds`.
Debug a new pod as above.
- New ReplicaSet at 0 pods: quota or admission webhook rejection; `describe rs`.
- Argo CD or Flux reverting your manual change: the live edit is being self-healed;
fix in Git.
## DNS
```bash
kubectl run -it --rm dnstest -n <ns> --image=busybox:1.37 --restart=Never -- \
sh -c 'cat /etc/resolv.conf; nslookup kubernetes.default; nslookup <svc>.<ns>.svc.cluster.local'
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50
```
(`kubectl run` creates a pod; in shared clusters ask before running it.)
- Short names resolve only within the same namespace; across namespaces use `<svc>.<ns>`.
- `ndots:5` makes external names try every search domain first; slow external lookups
can be fixed with a trailing dot (`api.example.com.`) or `dnsConfig.options`.
- Alpine/musl resolvers handle search domains and TCP fallback differently; intermittent
failures on Alpine images are a known pattern.
## NetworkPolicy
- Once any policy selects a pod for a direction (ingress or egress), everything not
explicitly allowed in that direction is denied.
- Egress policies must allow DNS (UDP and TCP 53 to kube-dns) or every name lookup fails.
- Policies match pod and namespace labels; check `kubectl get ns --show-labels` for the
`kubernetes.io/metadata.name` label used in namespace selectors.
- Enforcement depends on the CNI (Calico, Cilium); some managed defaults do not enforce
policies at all. Cilium users can inspect drops with `hubble observe --verdict DROPPED`.
## Terminating forever
```bash
kubectl get pod <pod> -n <ns> -o jsonpath='{.metadata.finalizers}'; echo
kubectl get node <node> -o wide # NotReady?
```
- Node `NotReady`: pods stay Terminating until the node returns or is removed. For
StatefulSets, force deletion risks two writers; get the user's explicit confirmation
and confirm the node is really down first.
- Finalizer whose controller is gone: removing it by patch is a last resort, needs user
confirmation, and can orphan external resources (load balancers, volumes).
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.