kubectl get pods answers the easy questions. The moment you need something it has no column for, which containers have no memory limit, which images the cluster actually runs, which pods restarted most, you reach for -o json and a filter.

These recipes ran against a live two-node k3s cluster (Kubernetes 1.35) with jq 1.8.2, including a deliberately broken deployment so the failure output is real. Swap the namespaces for yours.

The Shape You’re Filtering

Every kubectl get <resource> -o json returns one object with an items array, so most recipes start the same way:

kubectl get pods -A -o json | jq -r '.items[] | ...'

-A means all namespaces, -r prints raw strings instead of quoted JSON, and @tsv lines things up for column -t. The jq cookbook covers the filter syntax itself; this guide is about what to ask a cluster.

Find What’s Broken

Pods that aren’t running

kubectl get pods -A -o json \
  | jq -r '.items[] | select(.status.phase != "Running")
           | [.metadata.namespace, .metadata.name, .status.phase] | @tsv'
default	broken-7b58495bf7-n24xh	Pending
default	oneshot	Failed
kube-system	helm-install-traefik-crd-8pr9q	Succeeded

Read that output carefully, because it shows the trap: the pod stuck on a bad image reports phase Pending, not Failed. Phase describes the pod’s lifecycle, not whether the container is healthy. Completed jobs show as Succeeded and are usually noise here.

Why a container isn’t ready

The useful detail lives in containerStatuses, not in the phase:

kubectl get pods -A -o json \
  | jq -r '.items[] | select(.status.containerStatuses != null)
           | .status.containerStatuses[] | select(.ready == false)
           | [.name,
              (.state.waiting.reason // .state.terminated.reason // "-"),
              (.state.waiting.message // "-")] | @tsv'
nginx	ImagePullBackOff	Back-off pulling image "nginx:doesnotexist": ErrImagePull
oneshot	Error	-
traefik	ContainerCreating	-

// supplies a fallback when a key is absent, which matters here because a waiting container has state.waiting and a crashed one has state.terminated.

Recent warnings, without scrolling

kubectl get events -A -o json \
  | jq -r '.items[] | select(.type == "Warning")
           | [.involvedObject.name, .reason, .message] | @tsv'
broken-7b58495bf7-n24xh	Failed	Failed to pull image "nginx:doesnotexist": rpc error
broken-7b58495bf7-n24xh	Failed	Error: ImagePullBackOff
k3d-jqdemo-agent-0	InvalidDiskCapacity	invalid capacity 0 on image filesystem

Restart counts, worst first

kubectl get pods -A -o json \
  | jq -r '[.items[] | select(.status.containerStatuses != null)
            | {ns: .metadata.namespace, pod: .metadata.name,
               restarts: ([.status.containerStatuses[].restartCount] | add)}]
           | sort_by(-.restarts)[] | [.ns, .pod, .restarts] | @tsv'
kube-system	helm-install-traefik-nh92k	2
default	broken-7b58495bf7-n24xh	0
default	web-6cc9857b4b-hbxwz	0

A pod with several containers needs the add: restart counts are per container.

Audit the Cluster

Every image actually running

kubectl get pods -A -o json | jq -r '[.items[].spec.containers[].image] | unique | .[]'
busybox:1.37
nginx:1.29-alpine
nginx:doesnotexist
rancher/mirrored-coredns-coredns:1.14.3

This is the fastest answer to “are we still running that old tag anywhere?”. Add .spec.initContainers[]? when init containers matter; the ? keeps jq quiet for pods that have none.

Containers with no resource limits

kubectl get pods -A -o json \
  | jq -r '.items[] | . as $p | .spec.containers[]
           | select(.resources.limits == null)
           | [$p.metadata.namespace, $p.metadata.name, .name] | @tsv'
default	broken-7b58495bf7-n24xh	nginx
default	oneshot	oneshot
kube-system	metrics-server-786d997795-bj7fd	metrics-server

. as $p saves the pod before descending into its containers, so each row can still name the pod it came from. That pattern shows up in most cluster-wide audits.

Requests, per container

kubectl get pods -n default -o json \
  | jq -r '.items[] | . as $p | .spec.containers[]
           | [$p.metadata.name, .name,
              (.resources.requests.cpu // "-"),
              (.resources.requests.memory // "-")] | @tsv'
broken-7b58495bf7-n24xh	nginx	-	-
web-6cc9857b4b-hbxwz	nginx	50m	64Mi
web-6cc9857b4b-n28qb	nginx	50m	64Mi

Where pods are scheduled

kubectl get pods -A -o json \
  | jq -r '.items | group_by(.spec.nodeName)[]
           | "\(.[0].spec.nodeName // "unscheduled"): \(length) pods"'
k3d-jqdemo-agent-0: 6 pods
k3d-jqdemo-server-0: 8 pods

A count of pods by phase

kubectl get pods -A -o json \
  | jq -r '[.items[].status.phase] | group_by(.)
           | map({phase: .[0], count: length}) | sort_by(-.count)[]
           | "\(.count)\t\(.phase)"'
5	Pending
5	Running
2	Succeeded
1	Failed

Node capacity

kubectl get nodes -o json \
  | jq -r '.items[] | [.metadata.name, .status.capacity.cpu,
                       .status.capacity.memory, .status.allocatable.pods] | @tsv'
k3d-jqdemo-agent-0	16	13519688Ki	110
k3d-jqdemo-server-0	16	13519688Ki	110

ConfigMap keys without dumping values

kubectl get configmaps -A -o json \
  | jq -r '.items[] | select(.data != null)
           | [.metadata.namespace, .metadata.name, (.data | keys | join(","))] | @tsv'
kube-system	cluster-dns	clusterDNS,clusterDomain
kube-system	coredns	Corefile,NodeHosts

Listing keys instead of values keeps secrets and certificates out of your scrollback. The same filter works on secrets, where it matters more.

Build a label selector from a deployment

kubectl get deployment web -o json \
  | jq -r '.spec.selector.matchLabels | to_entries
           | map("\(.key)=\(.value)") | join(",")'
app=web

Feed that straight back in: kubectl get pods -l "$(…)".

jq or jsonpath?

kubectl has a built-in filter, and for simple field extraction it saves a pipe:

kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\n"}{end}'
default	broken-7b58495bf7-n24xh
default	oneshot
default	web-6cc9857b4b-hbxwz

Use jsonpath when you want a couple of fields and no dependency. Use jq when you need anything it can’t do: filtering on a comparison, grouping, sorting, counting, defaults for missing keys, or reshaping into CSV. --sort-by and -o custom-columns cover a little more ground, but the moment the question contains “which ones”, jq is shorter and clearer.

One habit worth keeping: kubectl get ... -o json sends the full object over the wire. On a large cluster, narrow it first with -n, -l, or --field-selector rather than filtering everything client-side.

  • jq Cookbook covers the filter syntax these recipes use: select, //, group_by, @tsv, and the rest.
  • Learn Kubernetes covers the objects being queried here.
  • Learn systemd is the equivalent for the machines underneath the cluster.

References