Mert Özen Further With Every Line
DevOps Kubernetes — #21

Working Efficiently with kubectl: Output Formats, jsonpath, custom-columns, Selectors, patch, events, k9s and Plugins

Mert Özen Nov 15, 2026 14 min 7 views
Working Efficiently with kubectl: Output Formats, jsonpath, custom-columns, Selectors, patch, events, k9s and Plugins

Using kubectl efficiently means getting exactly the answer you need with custom-columns, jsonpath, selectors and events when reading, and choosing the right patch type and safety catches when changing things. With real output we see a JSON patch overwrite the wrong variable by index and crash a pod, a server-side apply conflict, kubectl edit drift, a confirmed delete via kuberc, and krew, k9s and stern.

Version note: The examples in this post were run with kind 0.33 on Kubernetes 1.37, kubectl 1.37, k9s 0.51.0, krew 0.5.0, stern 1.35.0 and PowerShell 7.7, using a Bookstore API image built with .NET 10.

Guess First

The Bookstore API Deployment's container has a single environment variable: Database__Provider=InMemory. First you use kubectl patch, strategic merge type, to add Logging__LogLevel__Default=Warning, which sets the log level. Right after that you want to change this new variable to Information with a JSON patch, and thinking it is the second element of the list you write the path /spec/template/spec/containers/0/env/1/value. Which variable does the second patch change, and what does that cost in production? The answer is in the middle of the post, with a real CrashLoop.

Reading and Selecting: Output Formats and Selectors

In the second post of the series we saw get, describe, explain and --dry-run=server. This post continues from there: using kubectl quickly and safely, both when reading and when changing things. The lab runs on Bookstore, the online bookstore used as the shared example across this site's series: 1.0.0 runs in the bookstore namespace and 1.1.0 in bookstore-staging. As in the previous post, the API runs in in-memory mode.

When the table is not enough, the first tool is -o custom-columns. You give a column name and a JSONPath expression, and kubectl builds the table for you. The one-line answer to "which image tag runs in which namespace?":

$ kubectl get deploy -A -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,IMAGE:.spec.template.spec.containers[*].image,READY:.status.readyReplicas'
NAMESPACE            NAME                     IMAGE                                                         READY
bookstore-staging    api                      k8sseries-bookstore-api:1.1.0                                 2
bookstore            api                      k8sseries-bookstore-api:1.0.0                                 3
kube-system          coredns                  registry.k8s.io/coredns/coredns:v1.14.6                       2
local-path-storage   local-path-provisioner   docker.io/kindest/local-path-provisioner:v20260820-69b56db7   1

If a script needs tab-separated lines as input, use -o jsonpath with range; a version that counts every image in the cluster is short too:

$ kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.containers[*].image}{"\n"}{end}' | grep bookstore
bookstore-staging	api-d7f5cd5f8-2vcp2	k8sseries-bookstore-api:1.1.0
bookstore-staging	api-d7f5cd5f8-5c9bf	k8sseries-bookstore-api:1.1.0
bookstore	api-7dc5dc5b47-k7d4t	k8sseries-bookstore-api:1.0.0
bookstore	api-7dc5dc5b47-lrpdl	k8sseries-bookstore-api:1.0.0
bookstore	api-7dc5dc5b47-r5xhq	k8sseries-bookstore-api:1.0.0
$ kubectl get pods -A -o jsonpath='{.items[*].spec.containers[*].image}' | tr ' ' '\n' | sort | uniq -c
   3 docker.io/kindest/kindnetd:v20260820-69b56db7
   1 docker.io/kindest/local-path-provisioner:v20260820-69b56db7
   3 k8sseries-bookstore-api:1.0.0
   2 k8sseries-bookstore-api:1.1.0
   2 registry.k8s.io/coredns/coredns:v1.14.6
   1 registry.k8s.io/etcd:3.7.0-0
   1 registry.k8s.io/kube-apiserver:v1.37.0
   1 registry.k8s.io/kube-controller-manager:v1.37.0
   3 registry.k8s.io/kube-proxy:v1.37.0
   1 registry.k8s.io/kube-scheduler:v1.37.0
$ kubectl -n bookstore get pods -l app.kubernetes.io/name=api -o jsonpath='{.items[0].status.conditions[?(@.type=="Ready")].status}'
True

The ?(@.type=="Ready") in the last example is a filter: it selects the condition by type instead of trusting an index. In PowerShell on Windows quoting works differently; pass the JSONPath in single quotes to keep the inner quotes, and use Group-Object instead of sort | uniq -c.

There are three ways to select. -l is a label selector (it supports in, != and !); --field-selector only works with the few fields the API server indexes (status.phase, spec.nodeName, metadata.name…); -A covers all namespaces. Seeing what runs on a node before draining it:

$ kubectl get pods -A --field-selector=spec.nodeName=k8sseries-lab-worker2,status.phase=Running -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name
NS                  NAME
bookstore-staging   api-d7f5cd5f8-5c9bf
bookstore           api-7dc5dc5b47-r5xhq
kube-system         kindnet-2hhjh
kube-system         kube-proxy-lwdq6

-w keeps the list open and prints changes line by line; --output-watch-events adds the event type to the start of each line. When I deleted a pod:

$ kubectl -n bookstore get pods -w --output-watch-events
EVENT      NAME                   READY   STATUS    RESTARTS   AGE
ADDED      api-7dc5dc5b47-2bpm8   1/1     Running   0          63s
ADDED      api-7dc5dc5b47-mwrdh   1/1     Running   0          63s
ADDED      api-7dc5dc5b47-tpvg6   1/1     Running   0          63s
MODIFIED   api-7dc5dc5b47-2bpm8   1/1     Terminating   0          65s
ADDED      api-7dc5dc5b47-fc77w   0/1     Pending       0          0s
...
DELETED    api-7dc5dc5b47-2bpm8   0/1     Completed           0          66s
MODIFIED   api-7dc5dc5b47-fc77w   1/1     Running             0          1s

What Happened? events, explain and get --raw

kubectl events tells the story of a stuck rollout the fastest. Compared with the events at the bottom of describe, you can narrow it to one object with --for or to warnings with --types, and it comes sorted by time. I switched to a tag that does not exist:

$ kubectl -n bookstore set image deploy/api api=k8sseries-bookstore-api:1.2.0
deployment.apps/api image updated
$ kubectl -n bookstore annotate deploy/api kubernetes.io/change-cause='set image api 1.2.0' --overwrite
deployment.apps/api annotated
$ kubectl -n bookstore rollout status deploy/api --timeout=25s
Waiting for deployment "api" rollout to finish: 1 out of 3 new replicas have been updated...
error: timed out waiting for the condition
$ kubectl -n bookstore events --for deploy/api
LAST SEEN   TYPE     REASON              OBJECT           MESSAGE
56s         Normal   ScalingReplicaSet   Deployment/api   Scaled up replica set api-7dc5dc5b47 from 0 to 3
26s         Normal   ScalingReplicaSet   Deployment/api   Scaled up replica set api-544f6fcf45 from 0 to 1
$ kubectl -n bookstore events --for pod/api-544f6fcf45-qx6g6
LAST SEEN           TYPE      REASON      OBJECT                     MESSAGE
26s                 Normal    Scheduled   Pod/api-544f6fcf45-qx6g6   Successfully assigned bookstore/api-544f6fcf45-qx6g6 to k8sseries-lab-worker
24s                 Normal    BackOff     Pod/api-544f6fcf45-qx6g6   Back-off pulling image "k8sseries-bookstore-api:1.2.0"
24s                 Warning   Failed      Pod/api-544f6fcf45-qx6g6   Error: ImagePullBackOff
11s (x2 over 25s)   Normal    Pulling     Pod/api-544f6fcf45-qx6g6   Pulling image "k8sseries-bookstore-api:1.2.0"
10s (x2 over 24s)   Warning   Failed      Pod/api-544f6fcf45-qx6g6   Failed to pull image "k8sseries-bookstore-api:1.2.0": failed to pull and unpack image "docker.io/library/k8sseries-bookstore-api:1.2.0": failed to resolve reference "docker.io/library/k8sseries-bookstore-api:1.2.0": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
10s (x2 over 24s)   Warning   Failed      Pod/api-544f6fcf45-qx6g6   Error: ErrImagePull

The Deployment's events only say "I scaled the new ReplicaSet to 1"; the real reason is in the pod's events. In a stuck rollout the chain is always the same: Deployment → new ReplicaSet → new pod. A note: events are matched by object name and kept for one hour by default; a Deployment that was deleted and recreated under the same name shows the old one's events too. To roll back:

$ kubectl -n bookstore rollout undo deploy/api
Warning: resource deployments/api was previously managed with 'kubectl apply'. Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation, which may cause unexpected behavior on future 'kubectl apply' operations. Consider using 'kubectl apply' with your previous configuration file instead.
deployment.apps/api rolled back
$ kubectl -n bookstore rollout history deploy/api
deployment.apps/api
REVISION  CHANGE-CAUSE
2         set image api 1.2.0
3         <none>

Take the warning seriously: set image and rollout undo change the cluster, not your manifest file. They are the right tools in an emergency, but right after that the same change has to go into the manifest in Git.

Two more reading tools. kubectl explain deployment.spec.strategy --recursive shows the sub-fields as a tree on one screen; since kubectl reads the schema from the cluster, it works for CRDs too. kubectl get --raw sends a request straight to the API server: /readyz?verbose shows the control plane's health checks, /apis/apps/v1 the resources in that group, and /api/v1/namespaces/bookstore/pods?limit=1 paging (the continue token). When you wonder which request a tool sends, -v=8 prints the URLs as well.

Commands That Change Things: create, apply, set, scale, drain

kubectl create creates an object if it does not exist and fails if it does (AlreadyExists); kubectl replace swaps the whole object for the file's version and deletes fields that are not in the file; kubectl apply takes the file as the desired state and applies the difference. Use apply in daily work and create in one-off setup scripts. Use replace only when you know what it will delete.

Most of the small change commands have a safety catch, and using it should be a habit. label and annotate do not change an existing value without --overwrite; scale --current-replicas does nothing if the count is not what you expected; drain tells you up front, with a server dry run, what it would evict:

$ kubectl -n bookstore label deploy/api tier=backend
deployment.apps/api labeled
$ kubectl -n bookstore label deploy/api tier=api
error: 'tier' already has a value (backend), and --overwrite is false
$ kubectl -n bookstore scale deploy/api --current-replicas=2 --replicas=4
error: Expected replicas to be 2, was 3
$ kubectl drain k8sseries-lab-worker2 --ignore-daemonsets --dry-run=server
node/k8sseries-lab-worker2 cordoned (server dry run)
Warning: ignoring DaemonSet-managed Pods: kube-system/kindnet-2hhjh, kube-system/kube-proxy-lwdq6
evicting pod bookstore/api-db78cc5b8-2lnmt (server dry run)
evicting pod bookstore-staging/api-d7f5cd5f8-5c9bf (server dry run)
node/k8sseries-lab-worker2 drained (server dry run)

cordon stops new pods from landing on a node, drain additionally evicts the pods through the eviction API (respecting PDBs), and uncordon opens the node again. We saw in the previous post how drain gets stuck with PDBs and anti-affinity.

kubectl patch: strategic, merge and json

The answer to Guess First. A strategic merge patch is a merge that knows Kubernetes' own types: it matches the container list by name and adds the new element to the env list.

$ kubectl -n bookstore patch deploy api --type=strategic -p '{"spec":{"template":{"spec":{"containers":[{"name":"api","env":[{"name":"Logging__LogLevel__Default","value":"Warning"}]}]}}}}'
deployment.apps/api patched
$ kubectl -n bookstore get deploy api -o jsonpath='{.spec.template.spec.containers[0].env}'
[{"name":"Logging__LogLevel__Default","value":"Warning"},{"name":"Database__Provider","value":"InMemory"}]

The new variable went to the start of the list. Sending the same patch as --type=merge (JSON merge patch, RFC 7386) does not work either, because a merge patch does not merge lists, it replaces them; the container list shrinks to a single element without an image and the API server rejects it:

$ kubectl -n bookstore patch deploy api --type=merge -p '{"spec":{"template":{"spec":{"containers":[{"name":"api","env":[{"name":"Logging__LogLevel__Default","value":"Information"}]}]}}}}'
The Deployment "api" is invalid: spec.template.spec.containers[0].image: Required value
$ kubectl -n bookstore patch deploy api --type=json -p '[{"op":"replace","path":"/spec/template/spec/containers/0/env/1/value","value":"Information"}]'
deployment.apps/api patched
$ kubectl -n bookstore get deploy api -o jsonpath='{.spec.template.spec.containers[0].env}'
[{"name":"Logging__LogLevel__Default","value":"Warning"},{"name":"Database__Provider","value":"Information"}]
$ kubectl -n bookstore logs pod/api-65c947849d-kh28w | tail -3
Unhandled exception. System.InvalidOperationException: Connection string 'BookstoreDb' is missing. Set it with user-secrets or the ConnectionStrings__BookstoreDb environment variable (see README.md).
   at Program.<Main>$(String[] args) in /src/src/Bookstore.Api/Program.cs:line 18
   at Program.<Main>(String[] args)

A JSON patch (RFC 6902) works by index, and index 1 is now Database__Provider. With the value "Information" the app left in-memory mode, looked for a connection string and crashed. Because the new pod never became ready and maxUnavailable rounded down to 0, the three old pods kept carrying the traffic; this time the readiness probe and the rolling update settings saved us. If you use JSON patch, first confirm with a test operation that the path points at what you expect; if it points at the wrong element, the whole patch is rejected:

$ kubectl -n bookstore patch deploy api --type=json -p '[{"op":"test","path":"/spec/template/spec/containers/0/env/1/name","value":"Logging__LogLevel__Default"},{"op":"replace","path":"/spec/template/spec/containers/0/env/1/value","value":"Information"}]'
The request is invalid: the server rejected our request due to an error in our request

The rule: for lists use strategic merge (for Kubernetes' own types) or server-side apply; merge patch only for scalar fields and CRDs; JSON patch together with test. In PowerShell, instead of fighting nested quotes, write the patch to a file and pass it with --patch-file:

spec:
  template:
    spec:
      containers:
        - name: api
          env:
            - name: Logging__LogLevel__Default
              value: Warning
$ kubectl -n bookstore-staging patch deploy api --patch-file log-level-patch.yaml
deployment.apps/api patched

Server-Side Apply, kubectl edit and Drift

With server-side apply the API server does the merge and every field gets an owner (a field manager). That makes it visible when more than one tool writes to the same object:

$ kubectl -n bookstore apply --server-side -f api.yaml
deployment.apps/api serverside-applied
$ kubectl -n bookstore scale deploy/api --replicas=5
deployment.apps/api scaled
$ kubectl -n bookstore get deploy api --show-managed-fields -o jsonpath='{range .metadata.managedFields[*]}{.manager}{" "}{.operation}{" "}{.subresource}{"\n"}{end}'
kubectl Apply
kubectl Update scale
kube-controller-manager Update status
$ kubectl -n bookstore apply --server-side -f api.yaml
error: Apply failed with 1 conflict: conflict with "kubectl" with subresource "scale" using apps/v1: .spec.replicas
...
$ kubectl -n bookstore apply --server-side --force-conflicts -f api.yaml
deployment.apps/api serverside-applied

A conflict is not an error but a question: "someone else manages this field too, shall I overwrite it?" For a Deployment managed by an HPA the answer is usually no; the right fix is to take replicas out of the manifest. --force-conflicts takes ownership of the field and writes the value from the file.

kubectl edit is recorded as a field manager too. I raised a memory limit to 512Mi by hand (with sed as the editor so it runs non-interactively) and compared it with the file in Git:

$ KUBE_EDITOR="sed -i.bak -e 's/memory: 256Mi/memory: 512Mi/'" kubectl -n bookstore edit deploy/api
deployment.apps/api edited
$ kubectl -n bookstore diff --server-side -f api.yaml | grep -E '^[-+] '
W1115 10:35:11.692059   52103 diff.go:723] Object (apps/v1, Kind=Deployment: api) keeps changing, diffing without lock
Error from server (Conflict): Apply failed with 1 conflict: conflict with "kubectl-edit" using apps/v1: .spec.template.spec.containers[name="api"].resources.limits.memory
$ kubectl -n bookstore diff --server-side --force-conflicts -f api.yaml | grep -E '^[-+] |^[-+]{3}'
--- <tmp>/LIVE-685834022/apps.v1.Deployment.bookstore.api	2026-11-15 10:35:17
+++ <tmp>/MERGED-2491651083/apps.v1.Deployment.bookstore.api	2026-11-15 10:35:17
-  generation: 4
+  generation: 5
-            memory: 512Mi
+            memory: 256Mi

The state in the cluster now differs from Git (drift). If self-heal from the third post of the GitOps and Argo CD series is on, Argo CD sees the difference and writes 256Mi back within minutes; if it is off, the app shows as "OutOfSync" and the next sync wipes your manual change. Either way kubectl edit is not a way to make a lasting change in production; if it was an emergency fix, move the same change into the manifest right away.

Efficiency: Completion, kuberc, kubectx and krew

The first step is an alias and completion (the second post of the Terminal and Command Line series). In PowerShell 7 three lines in the profile are enough; I tried them in PowerShell 7.7 with TabExpansion2, and Tab after k get deployment -n bookstore-st completed bookstore-staging:

kubectl completion powershell | Out-String | Invoke-Expression
Set-Alias -Name k -Value kubectl
Register-ArgumentCompleter -CommandName k -ScriptBlock $__kubectlCompleterBlock

The bash equivalent is source <(kubectl completion bash), alias k=kubectl and complete -o default -F __start_kubectl k (I did not try the bash lines separately for this post). kuberc, in beta since kubectl 1.34, is kubectl's own preferences file: default flags per command, and aliases. Two defaults are very useful in production: every delete asks for confirmation first, and apply is server-side.

$ kubectl kuberc set --section defaults --command delete --option interactive=true
Updated /home/user/.kube/kuberc
$ kubectl kuberc set --section defaults --command apply --option server-side=true
Updated /home/user/.kube/kuberc
$ kubectl kuberc set --section aliases --name images --command get --prependarg deployments --option all-namespaces=true --option 'output=custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,IMAGE:.spec.template.spec.containers[*].image'
Updated /home/user/.kube/kuberc
$ kubectl images
NAMESPACE            NAME                     IMAGE
bookstore-staging    api                      k8sseries-bookstore-api:1.1.0
bookstore            api                      k8sseries-bookstore-api:1.0.0
kube-system          coredns                  registry.k8s.io/coredns/coredns:v1.14.6
local-path-storage   local-path-provisioner   docker.io/kindest/local-path-provisioner:v20260820-69b56db7
$ echo n | kubectl -n bookstore-staging delete deploy api
You are about to delete the following 1 resource(s):
deployment.apps/api
Do you want to continue? (y/N): deletion is cancelled

The file lives at ~/.kube/kuberc (%USERPROFILE%\.kube\kuberc on Windows); KUBECTL_KUBERC=false turns it off, which is why scripts in CI keep their behaviour. kubectx and kubens are the common tools for switching context and namespace; they can be installed through krew as the ctx and ns plugins. krew is the package manager for kubectl plugins; it downloads a plugin, checks its checksum and puts the kubectl-<name> binary on the PATH. The warning it prints after installing matters:

$ kubectl krew install ctx ns tree
...
WARNING: You installed plugin "tree" from the krew-index plugin repository.
   These plugins are not audited for security by the Krew maintainers.
   Run them at your own risk.
$ kubectl ctx
kind-k8sseries-lab
staging
$ kubectl ctx staging
Switched to context "staging".
$ kubectl get deploy
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
api    2/2     2            2           3m45s
$ kubectl ctx -
Switched to context "kind-k8sseries-lab".

A plugin is a program that runs with your kubeconfig, which means with your permissions. On a machine with access to a production cluster, install only plugins you know and pin their versions.

k9s and stern

k9s is a full-screen terminal UI for browsing the cluster: you switch between resource types with commands such as :deploy and :pods, l opens logs, d describe and shift-f a port-forward. The screen below was opened with k9s -n bookstore --readonly and captured as text from a pseudo-terminal (I removed the empty table rows and masked the pod IPs):

 Context: kind-k8sseries-lab [R]                   <0> all         <d>       Describe        … ____  __ ________
 Cluster: kind-k8sseries-lab                       <1> bookstore   <?>       Help            <|    |/  /   __   \______
 User:    kind-k8sseries-lab                       <2> default     <shift-j> Jump Owner      <|       /\____    /  ___/
 K9s Rev: v0.51.0                                                  <l>       Logs            <|    \   \  /    /\___  \
 K8s Rev: v1.37.0                                                  <p>       Logs Previous    |____|\__ \/____//____  /
 CPU:     n/a                                                      <shift-f> Port-Forward              \/           \/
 MEM:     n/a
┌───────────────────────────────────────────────── pods(bookstore)[3] ─────────────────────────────────────────────────┐
│ NAME↑                  PF   READY   STATUS         RESTARTS IP              NODE                            AGE      │
│ api-db78cc5b8-2lnmt    ●    1/1     Running               0 <pod-ip>        k8sseries-lab-worker2           49s      │
│ api-db78cc5b8-5nm8p    ●    1/1     Running               0 <pod-ip>        k8sseries-lab-worker            60s      │
│ api-db78cc5b8-khgsf    ●    1/1     Running               0 <pod-ip>        k8sseries-lab-control-plane     48s      │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  <pod>

The [R] in the header shows read-only mode: in this mode k9s turns off shortcuts that change things, such as delete, edit and shell. Opening k9s in this mode on a production context removes the risk of pressing the wrong key. CPU and MEM say "n/a" because this cluster has no metrics-server.

stern shows the logs of several pods in one stream, with the pod name, and picks up new pods by itself. kubectl logs -l misses new pods during a rollout; stern does not. During a rollout restart:

$ stern -n bookstore api --color never --since 1s --template '{{.PodName}} {{.ContainerName}} {{.Message}}{{"\n"}}'
+ api-7dc5dc5b47-4bd4m › api
+ api-7dc5dc5b47-jctrr › api
+ api-7dc5dc5b47-kwqf5 › api
+ api-db78cc5b8-5nm8p › api
api-db78cc5b8-5nm8p api info: Microsoft.EntityFrameworkCore.Update[30100]
api-db78cc5b8-5nm8p api       Saved 62 entities to in-memory store.
api-db78cc5b8-5nm8p api info: Microsoft.Hosting.Lifetime[14]
api-db78cc5b8-5nm8p api       Now listening on: http://[::]:8080
...
api-7dc5dc5b47-kwqf5 api info: Microsoft.Hosting.Lifetime[0]
api-7dc5dc5b47-kwqf5 api       Application is shutting down...
+ api-db78cc5b8-2lnmt › api
...
- api-7dc5dc5b47-kwqf5 › api

The + lines are pods stern started following, the - lines pods it let go. The old pod's "Application is shutting down" line and the new pod's "Now listening" line are in the same stream: the easiest way to watch the graceful shutdown from the tenth post live. For searching and keeping logs over time you need not these tools but the log backends in the Observability series.

Production Notes

Keep production access apart from your development machine. Keep a separate kubeconfig file per cluster and set KUBECONFIG only in that terminal session; twenty contexts in one file is the shortest way to run a command against the wrong cluster. Show the context and namespace in the prompt (kube-ps1, or the Kubernetes segment of tools such as Oh My Posh and Starship) and change the colour for production. Give people read access to production rather than cluster-admin; changes should come through a pipeline or GitOps. The delete --interactive default in kuberc and k9s's --readonly mode reduce the remaining risk. In scripts, do not parse kubectl's table output; columns can change between versions, -o jsonpath and -o json do not.

Common Pitfalls

delete in the wrong context: kubectl delete -f . or delete ns runs without asking; the interactive default in kuberc or a separate kubeconfig catches it. Trusting the index in a JSON patch: the list order can change after any strategic merge. Updating a list with a merge patch and overwriting the whole list. Not moving a change made with kubectl edit or set image into the manifest; GitOps reverts it, and without GitOps the next apply does. Keeping replicas that an HPA manages in the manifest and resetting the replica count on every apply. Assuming field selectors work for every field. And krew plugins installed without checking: the warning text says so plainly.

A Small Experiment

★☆☆ In your own cluster write a -o custom-columns command that lists the container images with their tags across all namespaces, then turn it into a kuberc alias.

★★☆ Add two environment variables to a Deployment with a strategic merge patch, then change one value with a JSON patch. Try the test operation first with the wrong index and then with the right one, and apply the same patch in PowerShell with --patch-file.

★★★ Create a Deployment with server-side apply, change one field with kubectl edit and another with kubectl scale. Work out from --show-managed-fields which field belongs to whom, look at the drift with kubectl diff --server-side, and find a way to resolve the conflict without --force-conflicts.

In this post we installed the kubectl tree plugin but did not use it. In the next and last post of the series we use it to see the Deployment → ReplicaSet → Pod ownership chain; we add a new resource type to Kubernetes with a CRD, write a small controller in .NET, and look at why finalizers lead to namespaces stuck in "Terminating".