Kubernetes Deploy skill for Claude Code
Rollouts that finish, pods that stay up. Claude checks the kube context before touching anything, writes Deployments with startup, readiness and liveness probes that mean different things, sizes requests from observed usage, and adds PDBs, spread constraints and a preStop drain. When pods fail it walks a status-to-cause table using describe, logs --previous and ephemeral debug containers instead of guessing.
- name
kubernetes-deploy - size225 lines · ≈2,150 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Writes and debugs Kubernetes workloads — Deployments, Services, startup/readiness/liveness probes, resource requests and limits, HPA, PodDisruptionBudgets, ConfigMaps and Secrets — and diagnoses CrashLoopBackOff, Pending, OOMKilled, and ImagePullBackOff with kubectl. Use when writing k8s manifests or kustomize overlays, when a rollout hangs or pods restart, when sizing resources or autoscaling, or when a Service returns no endpoints. Try asking
- Our pods are in CrashLoopBackOff after the last deploy. Figure out why
- Write a production Deployment, Service and HPA for this API
- Rollouts drop requests for a few seconds every time. Make them zero-downtime
Use it when
- You are writing or reviewing Kubernetes manifests or kustomize overlays
- Pods are Pending, OOMKilled, restarting, or never become Ready
- You need to size CPU and memory requests or set up autoscaling
- A Service has no endpoints or traffic drops during rollouts
Skip it when
- You are packaging manifests as a reusable chart — use helm-charts instead
- You need canary or blue/green traffic shifting — use blue-green-deploys instead
How to check the result
kubectl apply --dry-run=server and kubectl diff are clean, kubectl rollout status completes, and EndpointSlices list every new pod as ready with zero restarts.
The common mistake it prevents
Pointing the liveness probe at a health endpoint that checks the database, so a brief database outage restarts every pod at once.
A deployment is done when the rollout completes with zero failed requests, pods carry honest resource requests, probes reflect real readiness, the workload survives a node drain, and you can roll back in one command.
Ground rules
- Confirm the target before any mutating command:
kubectl config current-contextand namespace. Applying, deleting, scaling, or rolling back in production requires explicit user confirmation. - Preview every change:
kubectl diff -f <dir>(orkustomize build | kubectl diff -f -). - Never use
:latestor mutable tags in manifests; use an immutable tag or digest. - Liveness probes check only the process itself, never databases or downstream services.
- Every container gets CPU and memory requests. Set a memory limit; be deliberate about CPU limits (they cause throttling).
- Secrets objects are base64, not encryption. Source them from a secret manager.
Workflow
- [ ] 1. Inspect current state (context, namespace, existing objects, events)
- [ ] 2. Write/adjust manifests: Deployment, Service, config, PDB, HPA
- [ ] 3. Size resources from observed usage
- [ ] 4. Configure probes and graceful shutdown
- [ ] 5. Validate: server-side dry run + diff
- [ ] 6. Apply (with confirmation), watch rollout, verify traffic
- [ ] 7. Record rollback command and outcome
1. Inspect
kubectl config current-context; kubectl config view --minify -o jsonpath='{..namespace}'
kubectl get deploy,rs,pods,svc,ingress,hpa,pdb -n <ns> -o wide
kubectl get events -n <ns> --sort-by=.lastTimestamp | tail -30
kubectl top pods -n <ns> --containers # needs metrics-server
2. Reference Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
labels: { app.kubernetes.io/name: api }
spec:
replicas: 3
revisionHistoryLimit: 5
selector:
matchLabels: { app.kubernetes.io/name: api }
strategy:
type: RollingUpdate
rollingUpdate: { maxSurge: 25%, maxUnavailable: 0 }
template:
metadata:
labels: { app.kubernetes.io/name: api }
spec:
terminationGracePeriodSeconds: 30
securityContext:
runAsNonRoot: true
seccompProfile: { type: RuntimeDefault }
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: { app.kubernetes.io/name: api }
containers:
- name: api
image: registry.example.com/api:1.14.2
ports:
- { name: http, containerPort: 8080 }
envFrom:
- configMapRef: { name: api-config }
- secretRef: { name: api-secrets }
resources:
requests: { cpu: 250m, memory: 384Mi }
limits: { memory: 384Mi }
startupProbe:
httpGet: { path: /healthz, port: http }
periodSeconds: 5
failureThreshold: 30 # up to 150 s to boot
readinessProbe:
httpGet: { path: /ready, port: http }
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet: { path: /healthz, port: http }
periodSeconds: 10
failureThreshold: 3
lifecycle:
preStop:
sleep: { seconds: 5 } # k8s 1.30+; older: exec ["sleep","5"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
---
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector: { app.kubernetes.io/name: api }
ports:
- { name: http, port: 80, targetPort: http }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
maxUnavailable: 1
selector:
matchLabels: { app.kubernetes.io/name: api }
readOnlyRootFilesystem: true needs an emptyDir mounted wherever the app writes
(/tmp, caches).
3. Resources
| Decision | Rule |
|---|---|
| Memory request | p95 observed working set + 20–30% headroom |
| Memory limit | Equal to request (predictable, avoids node-pressure eviction surprises) |
| CPU request | Around p90 steady-state usage; this is what HPA percentages are computed from |
| CPU limit | Omit for latency-sensitive services unless policy requires it; if set, 2–4x request |
| JVM / Node / Go | Make the runtime respect the limit: -XX:MaxRAMPercentage=75, --max-old-space-size, GOMEMLIMIT |
4. Probes and shutdown
| Probe | Answers | Failure does |
|---|---|---|
startupProbe | Has it finished booting? | Delays the other probes; restarts after threshold |
readinessProbe | Should it get traffic now? | Removes pod from Service endpoints |
livenessProbe | Is the process wedged? | Restarts the container |
Graceful shutdown sequence: pod marked Terminating -> removed from endpoints (async)
-> preStop runs -> SIGTERM -> grace period -> SIGKILL. The preStop sleep covers the
gap while load balancers stop routing. The app must stop accepting, drain in-flight
requests, then exit within terminationGracePeriodSeconds.
5. Config, secrets, autoscaling
- ConfigMap/Secret changes do not restart pods. Use kustomize
configMapGenerator(hash-suffixed names), a checksum annotation, orkubectl rollout restart. - Secrets: External Secrets Operator, Sealed Secrets, or SOPS. Never commit plain
Secretmanifests. - HPA (requires metrics-server and CPU requests):
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: api }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: api }
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
behavior:
scaleDown: { stabilizationWindowSeconds: 300 }
When an HPA manages replicas, remove spec.replicas from the Deployment manifest so
each apply does not reset the count.
6. Validate, apply, watch
kubectl apply --dry-run=server -f k8s/ # schema + admission checks
kubectl diff -f k8s/
kubectl apply -f k8s/ # after user confirmation for prod
kubectl rollout status deploy/api -n <ns> --timeout=5m
kubectl get endpointslices -n <ns> -l kubernetes.io/service-name=api
Rollback: kubectl rollout undo deploy/api -n <ns> (confirm first). Prefer reverting
the manifest in git when GitOps (Argo CD/Flux) owns the cluster, or the controller will
re-apply the bad version.
Failure triage
| Status | First command | Usual cause |
|---|---|---|
Pending | kubectl describe pod (Events) | Insufficient CPU/memory, taints, unbound PVC, zone constraints |
ImagePullBackOff | describe pod | Wrong tag, missing imagePullSecrets, private registry auth, arch mismatch |
CreateContainerConfigError | describe pod | Missing ConfigMap/Secret or key |
CrashLoopBackOff | kubectl logs <pod> --previous | App error on boot, bad config, failing liveness probe |
OOMKilled (exit 137) | describe pod Last State | Memory limit too low or a leak |
| Exit 143 then restart | Events | Liveness probe failing, often too aggressive during load |
| Running but no traffic | get endpointslices | Readiness failing or Service selector mismatch |
| Rollout stuck | rollout status, get rs | New pods never Ready; maxUnavailable: 0 keeps old ones serving |
For deeper diagnosis (ephemeral debug containers, DNS, network policies, node pressure), read references/kubectl-debugging.md.
Gotchas
- Changing
spec.selectoron an existing Deployment is rejected; it is immutable. kubectl applyof a manifest withoutresourcessilently removes resources set by hand.- A liveness probe that calls the database turns a DB blip into a cluster-wide restart storm.
maxUnavailable: 0with a PDB ofminAvailable: 100%blocks node drains forever.kubectl editin production creates drift from git; change the manifest instead.- Probes default to
timeoutSeconds: 1; slow health endpoints under load fail spuriously.
Output
**Change:** api 1.14.1 -> 1.14.2, memory 256Mi -> 384Mi, startupProbe added
**Context/namespace:** prod-eu / shop
**Validation:** dry-run=server OK, diff reviewed (3 objects changed)
**Rollout:** completed in 2m10s, 0 restarts, endpoints 3/3 ready
**Rollback:** kubectl rollout undo deploy/api -n shop (revision 41) Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
references/kubectl-debugging.md
# kubectl debugging deep dive
Read this when the triage table in SKILL.md did not explain the failure.
## Get the full picture of one pod
```bash
kubectl describe pod <pod> -n <ns> # Events at the bottom matter most
kubectl get pod <pod> -n <ns> -o yaml | less # status.containerStatuses[].lastState
kubectl logs <pod> -n <ns> -c <container> --previous # logs of the crashed instance
kubectl logs deploy/api -n <ns> --all-containers --since=15m
kubectl get pod <pod> -n <ns> -o jsonpath='{.status.containerStatuses[*].lastState.terminated}'
```
Exit codes: 0 completed, 1 app error, 126/127 command not executable/not found
(wrong entrypoint, distroless without shell), 137 SIGKILL (OOM or grace period
exceeded), 139 segfault, 143 SIGTERM.
## Shell into containers that have no shell
```bash
# Ephemeral debug container sharing the target's process namespace
kubectl debug -it <pod> -n <ns> --image=busybox:1.37 --target=<container>
# Inside: ls /proc/1/root/app ; cat /proc/1/environ | tr '\0' '\n'
# Copy of the pod with a different command (for CrashLoopBackOff)
kubectl debug <pod> -n <ns> -it --copy-to=api-debug --container=api -- sh
# Node-level debugging
kubectl debug node/<node> -it --image=busybox:1.37 # host fs at /host
```
Delete `--copy-to` debug pods afterwards.
## Networking
```bash
# Is the Service selecting anything?
kubectl get svc api -n <ns> -o jsonpath='{.spec.selector}'; echo
kubectl get pods -n <ns> -l app.kubernetes.io/name=api --show-labels
kubectl get endpointslices -n <ns> -l kubernetes.io/service-name=api -o wide
# DNS and connectivity from inside the cluster
kubectl run -it --rm nettest -n <ns> --image=busybox:1.37 --restart=Never -- sh
# nslookup api.<ns>.svc.cluster.local
# wget -qO- -T 3 http://api.<ns>.svc.cluster.local/healthz
# Bypass the Service to test one pod
kubectl port-forward pod/<pod> -n <ns> 8080:8080
```
Check order for "connection refused / timeout": Service selector -> endpoints present ->
pod readiness -> container listening on 0.0.0.0 (not 127.0.0.1) and the right port ->
NetworkPolicy (`kubectl get networkpolicy -n <ns>`) -> Ingress/Gateway rules and TLS.
## Scheduling and nodes
```bash
kubectl describe node <node> | sed -n '/Allocated resources/,/Events/p'
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints
kubectl get pvc -n <ns> # Pending PVC blocks the pod
kubectl get events -A --field-selector reason=FailedScheduling
```
`0/6 nodes are available: 3 Insufficient memory, 3 node(s) had untolerated taint` reads
as: requests too big for free capacity on 3 nodes, the other 3 reserved. Fix requests,
add tolerations deliberately, or scale the node pool.
## Evictions and pressure
- `Evicted` pods with `The node was low on resource: memory` mean node pressure, not your
limit. Pods using more than their request are evicted first; set request = limit for
critical workloads (Guaranteed QoS).
- `kubectl get pods -A --field-selector=status.phase=Failed` finds evicted leftovers.
## Rollout history
```bash
kubectl rollout history deploy/api -n <ns>
kubectl rollout history deploy/api -n <ns> --revision=41
kubectl get rs -n <ns> -l app.kubernetes.io/name=api # which ReplicaSet is live
```
Set `kubernetes.io/change-cause` annotation in CI so history shows what each revision was.
## Resource usage over time
`kubectl top` is a point-in-time sample. For sizing, query Prometheus:
```promql
quantile_over_time(0.95, container_memory_working_set_bytes{namespace="shop",container="api"}[7d])
rate(container_cpu_cfs_throttled_periods_total{container="api"}[5m])
/ rate(container_cpu_cfs_periods_total{container="api"}[5m])
```
Throttling above ~10–20% of periods on a latency-sensitive service is a sign the CPU
limit is too tight.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.