First: which cluster, which namespace

Most “it isn’t there” moments are one of these two.

kubectl config current-context        # what am I about to change?
kubectl config get-contexts
kubectl config use-context prod
 
kubectl config set-context --current --namespace=my-ns   # stop typing -n

Make current-context a reflex before anything destructive. The command that wrecks prod is usually the one you meant for staging. -A / --all-namespaces works on any get.

Looking around

kubectl get pods -o wide              # adds node and pod IP
kubectl get pods -A
kubectl get pods --watch
kubectl get pods --field-selector status.phase=Failed
kubectl get pods --sort-by='.status.containerStatuses[0].restartCount'
 
kubectl api-resources                 # what kinds exist here, short names, namespaced?
kubectl explain pod.spec.containers.resources    # schema docs, offline

kubectl get all is a trap: it does not include ConfigMaps, Secrets, Ingresses or PVCs.

kubectl explain is the most underused command on this page — it beats searching the docs for field names.

Describe and events

kubectl describe pod POD              # spec + recent events for this object
kubectl get events --sort-by=.lastTimestamp
kubectl get events --field-selector involvedObject.name=POD

Two traps. get events is not chronological by default, so always sort. And events are garbage-collected after about an hour, so this morning’s crash is already gone — describe is usually faster anyway because it filters to the object for you.

Logs

kubectl logs POD
kubectl logs POD -f --tail=100
kubectl logs POD -c sidecar           # pick a container
kubectl logs POD --all-containers
kubectl logs POD --previous           # the container that died
kubectl logs POD --since=15m
 
kubectl logs -l app=my-app --prefix --max-log-requests=20   # every pod of a deployment

--previous is the only way to see why a crashed container died, and it disappears once the pod is rescheduled onto another node. Grab it before anything else.

Getting inside

kubectl exec -it POD -- sh
kubectl exec POD -- env
 
# distroless or no shell: attach a debug container sharing its process namespace
kubectl debug -it POD --image=busybox --target=app
 
# node level: kernel log, disk, the OOM killer's report
kubectl debug node/NODE -it --image=busybox
 
# a throwaway pod inside the cluster network
kubectl run tmp --rm -it --image=nicolaka/netshoot --restart=Never -- sh

Copying a file out — kubectl cp needs tar inside the container, which distroless images do not have:

kubectl cp POD:/path/file ./file
kubectl exec POD -- cat /path/file > ./file      # fallback that always works

Resources

kubectl top pod
kubectl top node
kubectl describe node NODE | grep -A5 Allocated
kubectl get pod POD -o jsonpath='{.spec.containers[*].resources}'

kubectl top is a sampled gauge over roughly 15–60 seconds. Fine for “what is big right now”, useless as a post-mortem — it will not catch the spike that killed something. Use the metrics stack for that.

Rollouts

kubectl rollout status deploy/NAME
kubectl rollout restart deploy/NAME       # clean restart of every pod
kubectl rollout history deploy/NAME
kubectl rollout undo deploy/NAME
kubectl rollout undo deploy/NAME --to-revision=3
 
kubectl scale deploy/NAME --replicas=0    # actually stop it
kubectl set image deploy/NAME app=repo/image:tag

Deleting a pod owned by a Deployment stops nothing — the ReplicaSet recreates it immediately. Scale to 0 instead. And rollout restart beats deleting pods in a loop, because it respects maxUnavailable.

Applying changes safely

kubectl diff -f manifest.yaml                            # what would actually change
kubectl apply -f manifest.yaml
kubectl apply -f manifest.yaml --dry-run=server -o yaml  # validated by the API server
kubectl wait --for=condition=Ready pod/NAME --timeout=120s

kubectl diff before apply is the cheapest good habit here. --dry-run=server catches what client-side validation misses, because admission controllers and defaulting actually run.

Output worth having

kubectl get pod POD -o yaml
kubectl get pod POD -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'
kubectl get pods -o custom-columns=\
NAME:.metadata.name,NODE:.spec.nodeName,RESTARTS:.status.containerStatuses[0].restartCount
kubectl get pods -o json | jq '.items[] | {name: .metadata.name, phase: .status.phase}'

Config and secrets

kubectl get configmap NAME -o yaml
kubectl get secret NAME -o jsonpath='{.data.password}' | base64 -d
kubectl create secret generic NAME --from-literal=key=value --dry-run=client -o yaml

Secrets are base64-encoded, not encrypted. Anyone who can get them can read them.

Networking

kubectl port-forward POD 8080:8080
kubectl port-forward svc/NAME 8080:80
kubectl get endpoints NAME            # is the Service selecting any pods?
kubectl get svc,ingress

Empty endpoints means the Service’s selector matches nothing. That is the answer to most “the service returns 503”.

To pull one pod out of a Service without killing it, so you can debug it in peace:

kubectl label pod POD app-            # drop the label the selector uses

Permissions

kubectl auth can-i delete pods
kubectl auth can-i '*' '*' --all-namespaces
kubectl auth can-i get secrets --as=system:serviceaccount:my-ns:my-sa

The --as form is how you check what a workload’s ServiceAccount can do, without deploying anything.

Symptom to first command

StatusUsuallyCheck
Pendingno node fitsdescribe pod
ContainerCreatingimage or volumedescribe pod
ImagePullBackOffbad tag / no secretdescribe pod
CrashLoopBackOffexits on startuplogs --previous
OOMKilled, 137memorybelow
Error, 137SIGTERM ignoredlogs --previous
Evictednode pressuredescribe node
Completednormal for a Job—

For 137 and memory in general: OOMKilled without Java OOM.

The danger zone

kubectl delete pod POD --grace-period=0 --force
kubectl drain NODE --ignore-daemonsets --delete-emptydir-data
kubectl cordon NODE
kubectl uncordon NODE

--force --grace-period=0 drops the object from the API without waiting for the kubelet to confirm the process is dead. For a StatefulSet that can mean two pods with the same identity writing to the same volume. Use it when you know the node is gone — not to make things go faster.

Worth installing

ToolWhat for
k9sterminal UI: browse, logs, exec, delete
kubectx / kubensswitch context and namespace quickly
sterntail logs across many pods, coloured
kubecolorcolourised kubectl output
alias k=kubectl
source <(kubectl completion zsh)      # or bash
compdef __start_kubectl k             # bash: complete -o default -F __start_kubectl k