OnCallReady

Lesson 15.38 · Kubernetes: Architecture & Workloads · 23 min read

Reading the API fast: -o yaml, jsonpath, custom-columns, sort and filter

In plain words

The table kubectl shows you is like the summary card on a library book: title, author, shelf. The book itself has every page. When you need one specific fact, you don't read the whole book; you jump to the right page number.

kubectl get -o yaml shows the whole object: metadata (identity, labels), spec (what you asked for) and status (what the system observed). jsonpath jumps straight to a page: -o jsonpath='{.spec.nodeName}', {.items[*].metadata.name} on lists, {range .items[*]}...{end} for one line each, [?(@.type=="Ready")] filters. custom-columns builds your own table, --sort-by sorts, --field-selector filters on the server, and -o json | jq computes what jsonpath can't.

Why read the API, not the table

At 3am somebody asks "which node is the broken pod on, and how many times has it restarted?". kubectl get pods prints a table, you squint at it, you copy the wrong line. Worse: a runbook (a written step-by-step procedure, 0.21) that says "look at the output" cannot be run by a script.

Every question like that has an exact answer stored in the object. This lesson teaches you to ask kubectl for just that field, printed exactly the way you want - one line, a file, your own table - so you never read a table by eye again when it matters.

What you need to know already: JSON and jq (7.11, 7.13), pipes and xargs (1.7, 7.16), quoting in bash (6.6), pods and their status (15.14), Deployments and ReplicaSets (15.16), labels and -l selectors, annotations and namespaces (15.26), the speed kit k alias (15.3).

Every object is one document

The tables kubectl prints are a view: a few columns picked from a much bigger record. The record itself - every field any component wrote - is what -o yaml (print as YAML) or -o json (print as JSON) shows. They are the same data in two notations.

$ k create deployment web --image=nginx:1.27 --replicas=2     # AlreadyExists is fine
deployment.apps/web created
$ k get $(k get pods -l app=web -o name | head -1) -o yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2026-09-23T10:31:07Z"
  labels:
    app: web
    pod-template-hash: vf2s9shm74
  name: web-5jth9
  ownerReferences:
  - apiVersion: apps/v1
    kind: ReplicaSet
    name: web-vf2s9shm74
  ...
spec:
  containers:
  - image: nginx:1.27
    name: nginx
  nodeName: worker-2
  ...
status:
  containerStatuses:
  - name: nginx
    ready: true
    restartCount: 0
    state:
      running:
        startedAt: "2026-09-23T10:31:10Z"
  phase: Running
  podIP: 10.244.2.118

The second command nests one kubectl inside another: $(...) (command substitution, 2.11) runs k get pods -l app=web -o name | head -1 first, which prints one pod as pod/<name>, and that becomes the argument of the outer k get ... -o yaml.

Read the document from the top. It always has the same three parts:

The rule of thumb: you write spec, controllers write status. "What image did we ask for" is a spec question; "is it running, how often did it restart" is a status question.

Every question you will be asked is a path into that document: node = spec.nodeName, restarts = status.containerStatuses[0].restartCount. Look at an object once with -o yaml to find the path, then extract just that.

jsonpath: ask for one field

-o jsonpath='TEMPLATE' prints only what the template picks. The template is text with expressions in curly braces {...}; each expression is a path that starts with . (the object itself):

k get pod web-5jth9 -o jsonpath='{.spec.nodeName}'
worker-2
k get pod web-5jth9 -o jsonpath='{.status.containerStatuses[0].restartCount}'
0

A list has one more level. Without a name, get pods returns one document of kind List whose items field holds the objects: {kind: List, items: [pod, pod, ...]}. So every path on a list starts with .items:

$ k get pods -l app=web -o jsonpath='{.items[*].metadata.name}'
web-vf2s9shm74-5jth9 web-vf2s9shm74-fp4q6
$ k get pods -l app=web -o jsonpath='{.items[0].status.podIP}'
10.244.2.118

[*] means "every entry"; kubectl prints the results on one line, separated by spaces. [0] is just the first pod. The most common jsonpath mistake is forgetting .items on a list (you get empty output) or adding it on a single object (also empty).

range: one line per item

For one line per pod, loop with {range LIST}...{end}. Everything between range and end is printed once per entry, and paths inside it start from that entry:

$ k get pods -l app=web -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.phase}{"\t"}{.spec.nodeName}{"\n"}{end}'
web-vf2s9shm74-5jth9	Running	worker-2
web-vf2s9shm74-fp4q6	Running	worker-1

{"\t"} and {"\n"} are literal strings inside the template: a tab and a newline. The output is tab-separated (TSV), ready for cut, sort or awk. Without a final {"\n"} there is no newline at the end and your next prompt ends up glued to the output.

Filters: pick list entries by a condition

[?(CONDITION)] keeps only the list entries where the condition is true. Inside it, @ means "the entry being tested":

$ k get nodes -o jsonpath='{.items[*].status.conditions[?(@.type=="Ready")].status}'
True True True
$ k get pods -o jsonpath='{.items[?(@.status.phase=="Running")].metadata.name}'

Read the first one slowly: for every node (.items[*]), go into its list of conditions (15.7: Ready, MemoryPressure, DiskPressure...), keep the one whose type is "Ready", print its status. Three True = three healthy nodes. It is the standard way to script "are all nodes ready". The second prints the names of the pods whose phase is Running.

Keys that contain dots

jsonpath uses . to separate path steps. But many label and annotation keys contain dots, like deployment.kubernetes.io/revision (the revision number a Deployment keeps, 15.16). Each literal dot in such a key must be escaped with a backslash, \.:

k get deploy web -o jsonpath='{.metadata.annotations.deployment\.kubernetes\.io/revision}'

The mistake to make once: without single quotes, bash removes the backslashes (6.6) before kubectl sees them, kubectl then reads deployment, kubernetes, io/revision as three steps, finds nothing and prints nothing - no error.

Recursive descent: find a key anywhere

.. searches every depth for a key, like .. in jq (7.11):

$ k get pods -l app=web -o jsonpath='{..image}'
nginx:1.27 docker.io/library/nginx:1.27 nginx:1.27 docker.io/library/nginx:1.27

It found every image key - so twice per pod: the one you asked for in spec.containers, and the full name the container runtime actually pulled, in status.containerStatuses (nginx:1.27 is short for docker.io/library/nginx:1.27, 10.5). Handy for exploring; for a precise answer say where: {.items[*].spec.containers[*].image}.

custom-columns: your own table

-o custom-columns=HEADER:PATH,HEADER:PATH,... builds a table with the columns you name. Same paths, without the braces, and each path is applied to every item (so no .items):

$ k get pods -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName,IP:.status.podIP,RESTARTS:.status.containerStatuses[0].restartCount
NAME                   NODE       IP             RESTARTS
web-vf2s9shm74-5jth9   worker-2   10.244.2.118   0
web-vf2s9shm74-fp4q6   worker-1   10.244.1.155   0

Sorting and filtering

k get pods --sort-by=.metadata.creationTimestamp          # oldest first
k get pods --sort-by='.status.containerStatuses[0].restartCount'
k get events --sort-by=.lastTimestamp

--sort-by=PATH sorts the items by one field (ascending: smallest, oldest first). Quote it when the path has [ or ]. Sorting events by .lastTimestamp (when the event last happened) gives you a timeline, newest at the bottom.

Field selectors filter by a field value, like -l filters by label:

k get pods --field-selector=status.phase!=Running -A      # everything not Running
k get pods -A --field-selector=spec.nodeName=worker-1     # what runs on a node
k get events --field-selector=type=Warning

-o name, for pipelines

$ k get pods -l app=web -o name
pod/web-vf2s9shm74-5jth9
pod/web-vf2s9shm74-fp4q6
$ k get pods -l app=web -o name | head -1 | xargs -I{} kubectl logs {}

-o name prints only type/name, one per line. Every kubectl verb accepts type/name, so the output feeds straight back into kubectl: here xargs -I{} (7.16) runs kubectl logs with the first pod in place of {}.

jq, when jsonpath runs out

jsonpath can only select fields: it cannot count, add, group or compare. -o json | jq can, with everything from Ch 7:

k get pods -A -o json | jq -r '.items[] | select(.status.phase!="Running") | "\(.metadata.namespace)/\(.metadata.name)"'
k get pods -o json | jq '[.items[].status.containerStatuses[0].restartCount] | add'
k get pods -A -o json | jq -r '.items[].spec.containers[].image' | sort | uniq -c | sort -rn
  1. Every pod not Running, as namespace/name (-r = raw strings, no quotes).
  2. The total number of restarts across the namespace (add sums the list).
  3. Every image in the cluster, counted and sorted by how often it is used - a quick image inventory (the sort | uniq -c | sort -rn pattern from 7.4).

Rule of thumb: jsonpath when you need a field (it is short to type and needs nothing installed); jq when you need to compute.

Later (Ch 19): in the Kubernetes admin exam many tasks say "write the result to a file" - this lesson is exactly that skill.

What you can now do:

Why it helps

Answering "which node is this on", "which image is running", "how many restarts" should take seconds, and in the exam many tasks ask you to write exactly such output to a file. At work these become scripts: checking all nodes are Ready with the Ready-condition filter, listing every image in the cluster for a vulnerability review with jq ... | sort | uniq -c, finding pods not Running across all namespaces with a field selector. The classic mistakes are worth knowing now: forgetting .items on lists, unquoted \. escapes eaten by bash so the annotation lookup silently returns nothing, and no final {"\n"} gluing your prompt to the output.

FAQ

Why does my jsonpath return nothing?

Usually one of three things. On a list (get pods without a name) paths must start with .items. Keys with dots, like annotations deployment.kubernetes.io/revision, need escaping as \. inside single quotes, or bash removes the backslash. Or the field doesn't exist at that path; check with -o yaml first. jsonpath fails silently rather than with an error.

What is the difference between spec and status?

spec is the desired state, what you or a controller asked for: containers, replicas, nodeName once bound. status is the observed state, written by controllers and the kubelet: phase, podIP, containerStatuses, conditions. You write spec; the system writes status. Most "what is actually happening" questions are answered in status.

When should I use jq instead of jsonpath?

When you need computation: counting, sums, grouping, conditions on multiple fields, string formatting, or sorting and deduplicating. jsonpath only selects and prints. kubectl get pods -A -o json | jq -r '.items[].spec.containers[].image' | sort | uniq -c lists images by frequency, which jsonpath can't do. In the exam jsonpath is faster to type; at work jq is usually the tool.

What can --field-selector filter on?

Only a few indexed fields, evaluated server-side: metadata.name, metadata.namespace, spec.nodeName and status.phase for pods, and type or reason for events, among a few others. So --field-selector=status.phase!=Running -A and --field-selector=spec.nodeName=worker-1 work, but arbitrary fields don't. Labels, by contrast, work on everything.

What is -o name good for?

It prints type/name, like pod/web-vf2s9shm74-5jth9, which every kubectl verb accepts. So it feeds pipelines: kubectl get pods -l app=web -o name | xargs -I{} kubectl logs {}, or deleting a filtered set after checking it. It's the safest way to pass objects between commands.

In an interview Junior

How do you extract specific fields from Kubernetes objects with kubectl?

Find the path once with -o yaml (metadata / spec / status), then ask for just that:

Also asked: How would you list every container image running in the cluster, with how many pods use it? · How would you script a check that all nodes are Ready? · What is the difference between jsonpath and custom-columns output?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.