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.

  • namekubernetes-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.

~/.claude/skills/kubernetes-deploy/SKILL.md

--- name: kubernetes-deploy description: Writes and debugs Kubernetes workloads — Deployments, Services, startup/readiness/liveness probes, resource requests and limits, HPA,… license: MIT ---

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-context and namespace. Applying, deleting, scaling, or rolling back in production requires explicit user confirmation.
  • Preview every change: kubectl diff -f <dir> (or kustomize build | kubectl diff -f -).
  • Never use :latest or 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

DecisionRule
Memory requestp95 observed working set + 20–30% headroom
Memory limitEqual to request (predictable, avoids node-pressure eviction surprises)
CPU requestAround p90 steady-state usage; this is what HPA percentages are computed from
CPU limitOmit for latency-sensitive services unless policy requires it; if set, 2–4x request
JVM / Node / GoMake the runtime respect the limit: -XX:MaxRAMPercentage=75, --max-old-space-size, GOMEMLIMIT

4. Probes and shutdown

ProbeAnswersFailure does
startupProbeHas it finished booting?Delays the other probes; restarts after threshold
readinessProbeShould it get traffic now?Removes pod from Service endpoints
livenessProbeIs 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, or kubectl rollout restart.
  • Secrets: External Secrets Operator, Sealed Secrets, or SOPS. Never commit plain Secret manifests.
  • 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

StatusFirst commandUsual cause
Pendingkubectl describe pod (Events)Insufficient CPU/memory, taints, unbound PVC, zone constraints
ImagePullBackOffdescribe podWrong tag, missing imagePullSecrets, private registry auth, arch mismatch
CreateContainerConfigErrordescribe podMissing ConfigMap/Secret or key
CrashLoopBackOffkubectl logs <pod> --previousApp error on boot, bad config, failing liveness probe
OOMKilled (exit 137)describe pod Last StateMemory limit too low or a leak
Exit 143 then restartEventsLiveness probe failing, often too aggressive during load
Running but no trafficget endpointslicesReadiness failing or Service selector mismatch
Rollout stuckrollout status, get rsNew 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.selector on an existing Deployment is rejected; it is immutable.
  • kubectl apply of a manifest without resources silently removes resources set by hand.
  • A liveness probe that calls the database turns a DB blip into a cluster-wide restart storm.
  • maxUnavailable: 0 with a PDB of minAvailable: 100% blocks node drains forever.
  • kubectl edit in 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 97 lines · 3.7 KB
# 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.

esc