skills/kubernetes-debugging-runbook/SKILL.md
Use when triaging CrashLoopBackOff, OOMKilled, ImagePullBackOff, Pending pods, networking failures, ingress 502/504, PVC stuck, HPA not scaling, init-container loops, sidecar startup ordering, or "kubectl describe says nothing useful". Triggers: pod restart counts climbing, "no nodes available to schedule", "back-off restarting failed container", DNS resolution failures inside the cluster, NetworkPolicy denials, livenessProbe killing healthy pods. NOT for cluster setup (kubeadm, kops), cloud-provider-specific managed K8s admin (EKS/GKE/AKS), helm chart authoring, or service mesh internals.
npx skillsauth add curiositech/windags-skills kubernetes-debugging-runbookInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
kubectl describe is the single most useful command in Kubernetes. The events at the bottom answer 80% of "why isn't this working" questions if you read them. This runbook is the playbook for the other 20%.
Pending, CrashLoopBackOff, ImagePullBackOff, Init:Error.kubectl get pods -n NAMESPACE # status overview
kubectl describe pod POD -n NAMESPACE # events at the bottom
kubectl logs POD -n NAMESPACE --previous # logs from the crashed instance
kubectl logs POD -n NAMESPACE -c CONTAINER # specific container in a multi-container pod
kubectl get events -n NAMESPACE --sort-by='.lastTimestamp' # cluster-wide context
--previous is critical for CrashLoopBackOff — the current logs are from the not-yet-crashed instance.
| Status | Likely cause |
|--------|-------------|
| Pending + no events | Scheduler hasn't tried yet; usually transient. |
| Pending + "no nodes available" | Resource requests too high, taint mismatch, PV affinity. |
| ContainerCreating (long) | ImagePull, volume attach, or sidecar init delay. |
| ImagePullBackOff | Image name typo, registry auth, image deleted. |
| CrashLoopBackOff | App exits non-zero on startup. Check --previous logs. |
| Init:Error | Init container failed. kubectl logs POD -c INIT_NAME. |
| OOMKilled (in describe) | Memory limit exceeded. |
| Error exit code 137 | SIGKILL — usually OOM or liveness probe failure. |
| Completed | Job ran. For Deployment, this means the container exited 0 immediately — usually a misconfigured command. |
# 1. Last logs before the crash
kubectl logs POD --previous
# 2. Exit code
kubectl describe pod POD | grep -A3 "Last State"
# 3. Resource limits — was it OOMKilled?
kubectl describe pod POD | grep -A2 "Limits"
# 4. Liveness probe killed it?
kubectl describe pod POD | grep -A5 "Liveness"
# 5. Configmap/secret references resolved?
kubectl describe pod POD | grep -A1 "Mounts"
Common roots:
timeoutSeconds to respond.kubectl describe pod POD | grep -E "OOMKilled|Last State|Limits"
Set both requests and limits:
resources:
requests: { memory: 256Mi, cpu: 100m }
limits: { memory: 512Mi, cpu: 500m }
Limit too low → OOMKilled under load. No limit → noisy neighbor evicts other pods. Use kubectl top pod to see actual usage and size accordingly.
kubectl describe pod POD | grep -A3 "Events:"
# "Failed to pull image": typo, deleted tag, or auth.
# "no such host": private registry not reachable from nodes.
Verify the image exists from the cluster:
kubectl run -i --rm test --image=YOUR_IMAGE --restart=Never --command -- sh -c "echo OK"
For private registries, imagePullSecrets must reference a Docker-config secret in the pod's namespace:
kubectl create secret docker-registry regcred \
--docker-server=ghcr.io \
--docker-username=... \
--docker-password=... \
-n YOUR_NAMESPACE
kubectl describe pod POD | grep -A5 "Events"
# "0/N nodes are available: N Insufficient cpu, N Insufficient memory."
# "0/N nodes are available: N node(s) had untolerated taint."
# "0/N nodes are available: N node(s) didn't find available persistent volumes to bind."
For taint mismatches, add a toleration:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
For PV binding, check the PVC's storage class and the PV's claimRef:
kubectl get pvc -n NS
kubectl describe pvc PVC -n NS
kubectl get pv | grep PVC_NAME
kubectl get endpoints SVC -n NS # empty?
kubectl describe svc SVC -n NS | grep "Selector" # selector
kubectl get pods -n NS --selector=KEY=VALUE -l # what does the selector match?
Endpoints empty = the selector matches no pods, OR matched pods aren't ready. Pods need both Running AND passing readinessProbe.
# Run a transient debug pod with curl + dig.
kubectl run -i --rm dbg --image=nicolaka/netshoot --restart=Never -- sh
# From dbg:
nslookup my-svc.my-ns.svc.cluster.local
curl -v http://my-svc.my-ns.svc.cluster.local:8080/health
If DNS fails, check CoreDNS pods (kubectl get pods -n kube-system | grep coredns).
If DNS works but curl times out, suspect a NetworkPolicy:
kubectl get netpol -n NS
kubectl describe netpol POLICY -n NS
NetworkPolicies are deny-by-default once any policy targets a pod. A default-deny ingress policy blocks all traffic until you explicitly allow it.
kubectl describe hpa HPA -n NS | grep -A5 "Conditions"
# "FailedGetResourceMetric" → metrics-server not running or pod has no requests.
HPA needs resources.requests on the pod containers to compute utilization. Without requests.cpu, the CPU-percentage HPA is undefined.
kubectl get apiservices | grep metrics
kubectl top pods -n NS # quick "is metrics-server alive" check
kubectl describe node NODE | grep -A5 "Conditions"
kubectl get events --field-selector reason=Evicted -A
Common roots: unbounded log files (set up logrotate or use a log driver), emptyDir volumes filling local disk, container image cache full.
Symptom: kubectl logs POD shows nothing useful; pod is restarting.
Diagnosis: Reading the in-flight container's logs, which haven't started writing yet.
Fix: kubectl logs POD --previous to read the last instance's logs.
Symptom: Healthy pods get killed mid-flight. Diagnosis: Liveness asserts the app is wedged; readiness asserts it can serve traffic. Conflating them causes a slow GC pause to trigger a kill. Fix: Liveness should check process aliveness only (a TCP socket open, /healthz that returns 200 from a goroutine independent of the request path). Readiness checks dependencies.
limit without requestsSymptom: Pod scheduled on a fully-utilized node and OOMKilled at random.
Diagnosis: Without requests, scheduler treats the pod as 0-memory; node accepts it past capacity.
Fix: Always set requests ≤ typical usage and limits 1.5-2x requests.
kubectl exec to "fix" a misbehaving podSymptom: Engineer manually kubectl execs in and edits config.
Diagnosis: Pod is ephemeral; restart wipes the change. Worse, the pod is now an undocumented snowflake.
Fix: Reproduce in a debug pod (kubectl run --rm), commit the fix to the manifest.
Symptom: Adding a deny policy in one namespace breaks unrelated services. Diagnosis: Cluster-wide controllers (Prometheus, cert-manager) traffic blocked. Fix: Always pair a default-deny with explicit allows for monitoring, ingress controllers, and any cross-namespace traffic the policy needs to permit.
imagePullSecrets in the wrong namespaceSymptom: ImagePullBackOff after migrating a Deployment to a new namespace.
Diagnosis: Secret existed in default only; deployment's namespace doesn't have it.
Fix: Recreate the secret in the new namespace, or use serviceAccount.imagePullSecrets referenced everywhere.
requests and limits for memory and CPU.latest tag; pin to immutable tags or digests.kube_pod_container_status_restarts_total).requests defined.ImagePullBackOff from a missing layer, → dockerfile-build-cache-mastery.opentelemetry-instrumentation.grafana-dashboard-builder.structured-logging-design.data-ai
license: Apache-2.0 NOT for unrelated tasks outside this domain.
development
Use when designing caching strategies (cache-aside, write-through, write-behind), implementing distributed locks, building rate limiters, leaderboards, real-time streams (XADD/consumer groups), pub/sub, or tuning eviction policies. Triggers: thundering-herd on cache miss, dogpile on key expiry, Redlock vs SET-NX-PX choice, sliding-window rate limiter, hot-key on a single cluster slot, big-key blowup, MULTI/EXEC across slots, KEYS in production. NOT for Redis Cluster operations/admin (different domain), embedded KV (SQLite, leveldb), in-process LRU caches, or Memcached.
tools
Drawing the `'use client'` boundary correctly in React Server Components apps (Next.js App Router, RSC frameworks) — leaf-pushing, slot composition, serialization rules, and environment poisoning prevention. Grounded in react.dev and Next.js 16 docs.
development
Use when designing rate limiting for an API, choosing between token bucket / sliding window / leaky bucket / fixed window, implementing it in Redis, deciding edge (Cloudflare/Upstash) vs origin enforcement, sizing per-user vs per-IP vs per-endpoint quotas, returning the right 429 response with Retry-After, or fixing the boundary-burst bug in fixed-window limiters. Triggers: 429 too many requests, INCR + EXPIRE, ZADD + ZREMRANGEBYSCORE + ZCARD, X-RateLimit-Remaining header, Cloudflare WAF rate limiting rules, Upstash @upstash/ratelimit, leaky bucket shaping vs policing, distributed rate limiter consistency. NOT for DDoS mitigation specifically (different scale), CAPTCHA / bot management, full WAF design, or per-user quota billing.