Break a local kind cluster five ways — CrashLoop, OOM, bad readiness probe, image pull failure, quota starvation — then diagnose each with K8sGPT and HolmesGPT (both CNCF projects) and score their answers against kubectl ground truth you establish yourself.
The deliverable is a comparison matrix: per failure class, does each tool find the cause, half-find it, or miss it — and would you let it near your production cluster.
~2 hours hands-on · Advanced tier (maps to module M3). $0 with a local-model or free-tier LLM backend; K8sGPT's rule-based mode needs no LLM at all.
Requirements: Docker, kind, kubectl, k8sgpt (brew or GitHub releases), HolmesGPT (Homebrew tap or pipx per the repo README; the CLI is holmes). Create the cluster: kind create cluster --name lab07.
Save the five break manifests. Scenarios 1–4 go in namespace lab07; scenario 5 gets its own namespace — a requests.cpu quota would otherwise reject every request-less pod in the namespace and contaminate the other scenarios.
# break-1-crashloop.yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: worker, namespace: lab07 }
spec:
replicas: 1
selector: { matchLabels: { app: worker } }
template:
metadata: { labels: { app: worker } }
spec:
containers:
- name: worker
image: busybox:1.36
command: ["sh", "-c", "echo starting worker; sleep 2; exit 1"]
# break-2-oom.yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: cache, namespace: lab07 }
spec:
replicas: 1
selector: { matchLabels: { app: cache } }
template:
metadata: { labels: { app: cache } }
spec:
containers:
- name: cache
image: busybox:1.36
command: ["sh", "-c", "tail /dev/zero"]
resources:
requests: { memory: 16Mi, cpu: 50m }
limits: { memory: 32Mi, cpu: 100m }
# break-3-probe.yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: web, namespace: lab07 }
spec:
replicas: 1
selector: { matchLabels: { app: web } }
template:
metadata: { labels: { app: web } }
spec:
containers:
- name: web
image: nginx:1.27
ports: [{ containerPort: 80 }]
readinessProbe:
httpGet: { path: /, port: 8081 }
periodSeconds: 5
# break-4-imagepull.yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: gateway, namespace: lab07 }
spec:
replicas: 1
selector: { matchLabels: { app: gateway } }
template:
metadata: { labels: { app: gateway } }
spec:
containers:
- name: gateway
image: nginx:1.99.9-typo
# break-5-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata: { name: tight, namespace: lab07-q }
spec:
hard: { requests.cpu: 500m }
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: api, namespace: lab07-q }
spec:
replicas: 2
selector: { matchLabels: { app: api } }
template:
metadata: { labels: { app: api } }
spec:
containers:
- name: api
image: nginx:1.27
resources:
requests: { cpu: "1", memory: 64Mi }
limits: { cpu: "1", memory: 128Mi }
The five break commands:
kubectl create ns lab07 && kubectl create ns lab07-q
kubectl apply -f break-1-crashloop.yaml # 1: CrashLoopBackOff
kubectl apply -f break-2-oom.yaml # 2: OOMKilled
kubectl apply -f break-3-probe.yaml # 3: never Ready
kubectl apply -f break-4-imagepull.yaml # 4: ImagePullBackOff
kubectl apply -f break-5-quota.yaml # 5: pods never created
kubectl -n lab07 get pods + kubectl -n lab07 logs deploy/worker --previous; (2) kubectl -n lab07 describe pod -l app=cache — look for Last State: Terminated, Reason: OOMKilled; (3) describe pod -l app=web — readiness probe failing, connection refused on 8081, pod Running but 0/1 Ready; (4) describe pod -l app=gateway — ErrImagePull, manifest not found; (5) the trap: kubectl -n lab07-q get pods shows nothing at all — the evidence lives in kubectl -n lab07-q describe rs events: exceeded quota: tight, requested: requests.cpu=1, limited: requests.cpu=500m.k8sgpt analyze --namespace lab07 and k8sgpt analyze --namespace lab07-q. Record per scenario what its analyzers surface with no AI attached.k8sgpt auth add openai (any OpenAI-compatible key), or for the free path a local model via the LocalAI/Ollama backend with your local base URL — then k8sgpt analyze --namespace lab07 --explain. Grade each explanation: cause correct? fix correct? anything invented?holmes ask "why is the worker pod in namespace lab07 crash looping?", then one per scenario, ending with the hard one: holmes ask "why does the api deployment in namespace lab07-q have zero pods?". HolmesGPT is agentic — it chooses which kubectl/log queries to run. Note which evidence it gathered versus what you gathered in step 1.kind delete cluster --name lab07.kubectl remains the source of truth. The tools are explanation layers; the moment an explanation conflicts with describe output, the explanation loses.
Diagnose scenario 2 before its pod restarts too many times — --previous logs and Last State reasons are the OOM fingerprint, and models often mislabel OOMKilled as a generic crash if you only paste current state.
comparison-matrix.md (the scored matrix + per-tool trust conclusions + cluster-output excerpts). Self-grading rubric (pass = 7/10):
k8sgpt auth list shows what's configured; the rule-based run (step 2) needs nothing.describe output — the comparison matrix still works with that as column three.kind on your laptop is already client-safe — it holds no client data. The decision point is the LLM backend: on client engagements, point K8sGPT/HolmesGPT at the client-approved tenancy or a local Ollama model, never a personal key, because describe output can leak client naming conventions and internal registries. A services-team pattern worth stealing: rehearse these five failure classes on kind exactly like this lab, so the tooling is proven before anyone asks to run it inside a client cluster.
tail /dev/zero allocates unboundedly into a 32Mi limit → OOMKilled (Last State reason), then restart loop. The right fix is "this workload's memory is wrong for its limit," not "raise the limit" reflexively.1.99.9-typo doesn't exist → ErrImagePull/ImagePullBackOff.exceeded quota events hang on the ReplicaSet. This is the scenario that most cleanly separates tools (and engineers) that reason about desired-vs-actual state from those that only inspect existing objects.