Exam Prep · Speed Reference

The Speed Reference — every command you need

The CNPE is performance-based: no multiple choice, just a Linux desktop, a terminal, a few web UIs, and a clock counting down from 120 minutes. Community reports put it at roughly seventeen hands-on tasks — so your average budget is around seven minutes per task, and some tasks are much bigger than others. The documentation is open during the exam, which changes what “studying” means entirely: nobody is testing your memory, they’re testing whether your fingers already know the shape of the work. This page is the muscle memory. Every command here is real, copy-pasteable, and grouped by the job you’ll actually be asked to do. Read it beside The CNPE Exam for logistics, Field Notes for what it feels like, and The Docs Map for where to find things fast when this page runs out.

☺ Explain it like I’m 10

Imagine a cooking contest where the recipe books are open on the counter — you’re allowed to look up anything. The winner isn’t the person who memorised the most recipes. It’s the person who already knows where the salt is, which drawer has the whisk, and how to chop an onion without thinking about it. This page is the kitchen layout: where every tool lives and the exact motion that uses it. Practise the motions until your hands do them while your brain reads the next task.

🦫🐰Your hosts for this topic: Benny the Beaver & Remy the Rabbit — Benny knows every tool in the shed and exactly which one lifts what, and Remy is pure speed: fast recall, no hesitation, never the same keystroke twice.
⚠ Verify the exam facts yourself

Task counts, the pass mark, time limits, domain weightings, and which resources are permitted during the exam change, and much of what circulates online is second-hand. Two figures on this page are not community numbers: the official CNPE FAQ states 2 hours and that “a score of 64% or above must be earned to pass”. The genuinely second-hand ones are the task count (≈17) and the assumption of partial credit on most tasks, which come from published first-hand accounts; the domain percentages used to size each section below (25 / 25 / 20 / 15 / 15) come from the blueprint as it stood when this was written — treat those as indicative, not authoritative. The only authority is the official CNCF / Linux Foundation exam pages and the candidate handbook for your sitting. Check them before you register and again the week you sit. The commands below don’t expire; the exam trivia does.

1 · Exam-day setup — the habits you build before task one

☺ Like you’re 10: Before you cook anything, you check you’re standing at the right stove. Every task, every time — because doing perfect work in the wrong place scores zero.

Almost every catastrophic exam story has the same shape: the candidate solved the task correctly, in the wrong cluster or the wrong namespace, and scored nothing. It is not a knowledge failure — it’s a ritual failure. Build the ritual now so it survives the adrenaline later.

Context and namespace — the single most valuable habit

Every task statement will tell you which cluster and which namespace it lives in. Treat that sentence as a command to run, not information to remember. Do it before you read the rest of the task.

# What clusters do I have, and which am I on? (the * marks current)
kubectl config get-contexts

# Switch cluster — do this FIRST, every single task
kubectl config use-context platform-prod

# Pin the namespace so you never type -n again for this task
kubectl config set-context --current --namespace=team-a

# Confirm both in one line before you touch anything
kubectl config view --minify -o jsonpath='{..namespace}{"\n"}'
kubectl config current-context

Pinning the namespace is not laziness — it removes an entire class of mistake. Once it’s pinned, a bare kubectl get pods is correct, and a forgotten -n can no longer silently point at default. When a task spans two namespaces, pin the primary one and use explicit -n for the other.

The alias block — type it once, save it all exam

Paste this at the very start, into every terminal you open. It costs twenty seconds and pays back many minutes.

alias k=kubectl
export do='--dry-run=client -o yaml'     # "d-o": dry-run output
export now='--force --grace-period=0'    # delete immediately, no waiting
source <(kubectl completion bash)        # tab-completion for kubectl
complete -o default -F __start_kubectl k # ...and for the alias too

# now this works:
k create deploy web --image=nginx $do > web.yaml
k delete pod broken $now

If the shell is zsh, swap the last two lines for source <(kubectl completion zsh). Verify completion actually works by typing k get po and pressing Tab — if nothing happens, don’t waste time debugging it, just move on. Completion is a nicety; the context ritual is not.

ShortcutExpands toWhy it earns its keep
kkubectlSix characters saved on every one of ~200 commands you’ll type.
$do--dry-run=client -o yamlThe gateway to every generated manifest — the highest-leverage export on the list.
$now--force --grace-period=0Deletes a stuck pod instantly instead of watching a 30-second grace period.
kubectl completionTab-completion of verbs, kinds, and live resource namesStops typos in long generated pod names — the silent time thief.
kubectl config set-context --current --namespacePinned namespaceNot a shortcut — an insurance policy. See above.

The first ninety seconds — read everything, bank the easy ones

Do not start task one. Skim all the tasks first, and write a three-column note: task number, rough difficulty, and the weight if one is shown. Then attack easiest-first. Two things follow from partial credit: an unfinished hard task can still be worth points, and a finished easy task is worth all of them. Set a personal rule — if a task has consumed five to seven minutes with no visible progress, flag it, note where you got to, and move on. You can return with whatever time is left, and returning to a task with a fresh brain is often faster than grinding.

1 · Context use-context + namespace 2 · Read whole task, every clause 3 · Generate create … $do then edit 4 · Apply apply -f / -k or sync 5 · Verify get / describe events next task — reset the context again Stuck past 5–7 minutes? Flag it · note where you got to · move on · partial credit still counts The context step is the one that turns a correct answer into a scored answer.

2 · Generating YAML at speed with kubectl

☺ Like you’re 10: Never write a manifest from a blank page. Ask kubectl to print you a nearly-right one, then change the two lines that are wrong.

The imperative create and run commands paired with --dry-run=client -o yaml are the single biggest time saver in a hands-on Kubernetes exam. They give you a valid skeleton with correct apiVersion, correct nesting, and correct indentation — the three things that cost the most time to get right by hand.

Workloads

# Deployment skeleton
kubectl create deployment web --image=nginx:1.25 --replicas=3 $do > web.yaml

# A single pod (fastest way to a valid pod spec)
kubectl run probe --image=busybox:1.36 --restart=Never $do > probe.yaml

# A one-shot pod that runs a command and is cleaned up
kubectl run tmp --image=busybox:1.36 --rm -it --restart=Never -- sh

# Job and CronJob
kubectl create job hello --image=busybox:1.36 $do -- /bin/sh -c 'echo hi' > job.yaml
kubectl create cronjob report --image=busybox:1.36 --schedule='*/5 * * * *' \
  $do -- /bin/sh -c 'date' > cron.yaml

# Trigger a CronJob right now, without waiting for the schedule
kubectl create job manual-1 --from=cronjob/report

Exposure — services and ingress

# Expose an existing deployment (infers the selector for you — prefer this)
kubectl expose deployment web --name=web-svc --port=80 --target-port=8080 \
  --type=ClusterIP $do > svc.yaml

# Build a Service from scratch when there's nothing to expose yet
kubectl create service clusterip web-svc --tcp=80:8080 $do > svc.yaml
kubectl create service nodeport web-np --tcp=80:8080 --node-port=30080 $do > np.yaml

# Ingress in one line — note the rule syntax
kubectl create ingress web --class=nginx \
  --rule='shop.example.com/*=web-svc:80,tls=web-tls' $do > ing.yaml
◆ Key idea

kubectl expose beats kubectl create service almost every time, because it reads the workload’s labels and writes the selector for you. A hand-written selector that doesn’t match the pod labels is the most common “my Service has no endpoints” bug — and it’s invisible until you run kubectl get endpoints.

Config, secrets, and namespaces

kubectl create namespace team-a $do > ns.yaml

kubectl create configmap app-cfg \
  --from-literal=LOG_LEVEL=debug \
  --from-literal=REGION=eu-west-1 \
  --from-file=./app.properties \
  --from-env-file=./.env $do > cm.yaml

kubectl create secret generic db-creds \
  --from-literal=username=app --from-literal=password='s3cr3t' $do > sec.yaml

kubectl create secret tls web-tls --cert=tls.crt --key=tls.key $do > tls.yaml

kubectl create secret docker-registry regcred \
  --docker-server=ghcr.io --docker-username=bot --docker-password="$TOKEN" $do

# Read a secret back (base64 is encoding, NOT encryption)
kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -d; echo

Identity and limits — RBAC, quotas

kubectl create serviceaccount deployer $do > sa.yaml

kubectl create role pod-reader --verb=get,list,watch --resource=pods,pods/log $do > role.yaml
kubectl create rolebinding read-pods --role=pod-reader \
  --serviceaccount=team-a:deployer $do > rb.yaml

kubectl create clusterrole node-viewer --verb=get,list --resource=nodes $do
kubectl create clusterrolebinding ops-view --clusterrole=view --group=platform-ops $do

# Short-lived token for a ServiceAccount (v1.24+)
kubectl create token deployer --duration=1h

kubectl create quota team-a-quota \
  --hard=cpu=4,memory=8Gi,pods=20,services=5 $do > quota.yaml

A quick sanity note on RBAC verbs: --verb and --resource both take comma-separated lists, and sub-resources like pods/log or deployments/scale are separate resources — a role that can read pods cannot read pod logs unless you say so. See Security & Policy Enforcement for the model behind these flags.

Mutating existing objects without opening an editor

kubectl set image deployment/web web=nginx:1.26
kubectl set env deployment/web LOG_LEVEL=info FEATURE_X-        # trailing dash unsets
kubectl set resources deployment/web -c=web --limits=cpu=500m,memory=512Mi
kubectl set serviceaccount deployment/web deployer

kubectl scale deployment/web --replicas=5
kubectl autoscale deployment/web --min=2 --max=10 --cpu-percent=70

kubectl label pod web-abc tier=frontend --overwrite
kubectl annotate ingress web nginx.ingress.kubernetes.io/rewrite-target=/ --overwrite

kubectl rollout status deployment/web --timeout=120s
kubectl rollout history deployment/web
kubectl rollout undo deployment/web --to-revision=2
kubectl rollout restart deployment/web

kubectl patch deployment web --type merge -p '{"spec":{"replicas":4}}'
kubectl patch deployment web --type json \
  -p '[{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"nginx:1.26"}]'

3 · Inspection & debugging — finding the broken thing

☺ Like you’re 10: Three questions in order — what exists, what does it say about itself, and what did it complain about? Almost every broken thing confesses in the events or the logs.

Roughly a third of hands-on tasks are “something is broken, make it work.” The fastest path is nearly always the same triage sequence, and it takes under a minute.

get — shape, filter, extract

kubectl get pods -o wide                      # adds node, IP, nominated node
kubectl get pods -A                           # every namespace
kubectl get deploy,svc,ingress                # several kinds at once
kubectl get pod web-abc -o yaml               # the full truth, including status
kubectl get all -n team-a                     # common kinds only — not literally "all"

# Label selectors
kubectl get pods -l app=web,tier!=canary
kubectl get pods -l 'env in (staging,prod)'
kubectl get pods --show-labels

# Field selectors
kubectl get pods --field-selector status.phase!=Running
kubectl get events --field-selector type=Warning

# Sorting
kubectl get pods --sort-by=.status.containerStatuses[0].restartCount
kubectl get pods --sort-by=.metadata.creationTimestamp
kubectl get nodes --sort-by=.status.capacity.memory
# jsonpath — extract exactly one thing
kubectl get pods -o jsonpath='{.items[*].metadata.name}'
kubectl get pod web-abc -o jsonpath='{.spec.containers[0].image}{"\n"}'
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.capacity.cpu}{"\n"}{end}'
kubectl get svc web-svc -o jsonpath='{.spec.ports[0].nodePort}{"\n"}'

# custom-columns — a readable table, no jq needed
kubectl get pods -o custom-columns=\
'NAME:.metadata.name,NODE:.spec.nodeName,IMAGE:.spec.containers[*].image'

describe and events — where things confess

kubectl describe pod web-abc          # scroll to the Events block at the bottom
kubectl describe node worker-1        # taints, allocatable, pressure conditions

# The single most useful debugging command on this page
kubectl get events --sort-by=.lastTimestamp
kubectl get events -A --sort-by=.lastTimestamp | tail -30
kubectl get events --field-selector involvedObject.name=web-abc
🦆 Dot’s-eye view

“When my pod won’t start, I don’t guess any more. describe tells me why it can’t schedule — insufficient CPU, an unbound PVC, a taint I don’t tolerate. Logs tell me why it started and died — a bad config key, a missing env var. If describe says ImagePullBackOff there’s no point reading logs; the container never ran. Knowing which of the two to open first has saved me more time than any alias.”

logs, exec, cp, port-forward

kubectl logs web-abc                      # single-container pod
kubectl logs web-abc -c sidecar           # pick the container
kubectl logs -f deploy/web                # follow, via a Deployment
kubectl logs web-abc --previous           # -p: the CRASHED container's logs
kubectl logs -l app=web --tail=50 --timestamps --all-containers
kubectl logs web-abc --since=15m
kubectl logs job/hello

kubectl exec -it web-abc -- sh
kubectl exec web-abc -c app -- env | sort
kubectl exec web-abc -- wget -qO- http://web-svc.team-a.svc.cluster.local

kubectl cp ./fix.conf team-a/web-abc:/etc/app/fix.conf
kubectl cp team-a/web-abc:/var/log/app.log ./app.log

kubectl port-forward svc/web-svc 8080:80
kubectl port-forward deploy/web 8080:8080 --address 0.0.0.0

--previous is the flag people forget under pressure. When a pod is in CrashLoopBackOff, the current container hasn’t produced the error yet — the evidence is in the instance that just died.

The rest of the toolkit

kubectl top pod --sort-by=memory          # needs metrics-server
kubectl top pod --containers
kubectl top node

# Ephemeral debug container attached to a running pod (v1.25+)
kubectl debug -it web-abc --image=busybox:1.36 --target=web
# A copy of the pod with a debug image and a shell — safe to poke at
kubectl debug web-abc -it --copy-to=web-debug --container=web --image=busybox:1.36
# A privileged shell onto a node
kubectl debug node/worker-1 -it --image=busybox:1.36

kubectl auth can-i create deployments -n team-a
kubectl auth can-i --list --as=system:serviceaccount:team-a:deployer -n team-a

kubectl api-resources                                   # every kind, short name, group
kubectl api-resources --namespaced=false                # cluster-scoped kinds
kubectl api-versions

kubectl explain deployment.spec.strategy
kubectl explain rollout --recursive | head -40          # works on CRDs too
kubectl explain pod.spec.containers.livenessProbe --recursive
◆ Key idea

kubectl explain --recursive is the open-book exam’s secret weapon for unfamiliar resources. When a task hands you a CRD you’ve never seen — a Crossplane Composition, an Argo Rollout, a Kyverno ClusterPolicyexplain reads the schema straight from the cluster’s own API server. That’s faster than searching docs, and it is guaranteed to match the exact version installed in front of you.

4 · Config & packaging — Kustomize and Helm

☺ Like you’re 10: Two ways to avoid copy-pasting YAML: Kustomize layers patches on top of a base, and Helm fills in a template from a values file.

Both ship in the exam’s world, and both have a “render it and look before you apply” command that you should reach for reflexively. Deeper treatment lives in Configuration Management.

Kustomize

kubectl kustomize overlays/prod                # render to stdout — always look first
kubectl apply -k overlays/prod
kubectl diff -k overlays/prod                  # what WOULD change — huge on the exam
kubectl delete -k overlays/prod

# Standalone binary (identical output, newer features)
kustomize build overlays/prod | kubectl apply -f -

# Edit kustomization.yaml without opening it
kustomize edit set image web=nginx:1.26
kustomize edit set namespace prod
kustomize edit add resource deployment.yaml
kustomize edit add configmap app-cfg --from-literal=LOG_LEVEL=debug
# overlays/prod/kustomization.yaml — the shape to recognise
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
resources:
  - ../../base
images:
  - name: web
    newTag: "1.26"
replicas:
  - name: web
    count: 5
patches:
  - path: resources-patch.yaml
    target: { kind: Deployment, name: web }
configMapGenerator:
  - name: app-cfg
    literals: [ LOG_LEVEL=info ]

Helm

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo nginx --versions | head
helm show values bitnami/nginx | head -60          # discover the knobs

helm install web bitnami/nginx -n web --create-namespace \
  --set replicaCount=3 --version 15.5.2

# The idempotent form — use this by default
helm upgrade --install web bitnami/nginx -n web -f values.yaml --wait --atomic

helm template web bitnami/nginx -f values.yaml > rendered.yaml   # render, don't install
helm list -A
helm get values web -n web            # user-supplied values
helm get values web -n web --all      # merged with chart defaults
helm get manifest web -n web          # what was actually applied
helm history web -n web
helm rollback web 2 -n web
helm uninstall web -n web
helm lint ./mychart

--atomic is worth remembering: if the upgrade fails or times out, Helm rolls the release back automatically instead of leaving you half-deployed. helm template is how you satisfy a task that says “show the manifests that this chart produces” without installing anything.

5 · GitOps — Argo CD and Flux

☺ Like you’re 10: You don’t deploy by hand any more — you tell a robot which folder in Git to copy, and then you ask the robot how it’s getting on.

This is the joint-largest domain at 25%. The concepts are in GitOps Workflows; what follows is the keyboard. Note that both tools have a UI on the exam desktop — if the GUI is faster for you on a given task, use it. Tool choice is often yours.

Argo CD via the CLI

# Get in. The bootstrap admin password lives in a secret:
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath='{.data.password}' | base64 -d; echo
kubectl -n argocd port-forward svc/argocd-server 8080:443 &
argocd login localhost:8080 --username admin --password "$PW" --insecure

argocd app create guestbook \
  --repo https://github.com/argoproj/argocd-example-apps.git \
  --path guestbook --revision HEAD \
  --dest-server https://kubernetes.default.svc --dest-namespace guestbook \
  --sync-policy automated --auto-prune --self-heal --sync-option CreateNamespace=true

argocd app list
argocd app get guestbook                       # health + sync status + resource tree
argocd app diff guestbook                      # live vs desired
argocd app sync guestbook --prune
argocd app sync guestbook --resource apps:Deployment:web    # sync one resource only
argocd app wait guestbook --health --timeout 300
argocd app history guestbook
argocd app rollback guestbook 3
argocd app set guestbook --sync-policy automated --auto-prune --self-heal
argocd app set guestbook -p image.tag=1.4.3    # override a Helm parameter
argocd app manifests guestbook
argocd repo add https://github.com/acme/platform.git --username bot --password "$T"
argocd cluster list
argocd app delete guestbook --cascade

Argo CD without the CLI — plain kubectl

If the argocd binary isn’t logged in and the clock is running, remember that everything is a Kubernetes object. This is often the faster route.

kubectl get applications -n argocd                 # short name: app
kubectl get app guestbook -n argocd -o yaml
kubectl get appprojects -n argocd
kubectl get applicationsets -n argocd

# Turn on auto-sync with prune and self-heal, no CLI login needed
kubectl -n argocd patch app guestbook --type merge -p \
  '{"spec":{"syncPolicy":{"automated":{"prune":true,"selfHeal":true}}}}'

# Force a refresh/sync by annotation
kubectl -n argocd annotate app guestbook argocd.argoproj.io/refresh=hard --overwrite
kubectl -n argocd get app guestbook \
  -o jsonpath='{.status.sync.status}{"\t"}{.status.health.status}{"\n"}'

Flux

flux check --pre                       # before install
flux check                             # after install
flux bootstrap github --owner=acme --repository=platform \
  --branch=main --path=clusters/prod --personal

flux create source git podinfo \
  --url=https://github.com/stefanprodan/podinfo --branch=master --interval=1m
flux create kustomization podinfo \
  --source=GitRepository/podinfo --path='./kustomize' \
  --prune=true --interval=10m --wait --health-check-timeout=2m

flux create source helm bitnami --url=https://charts.bitnami.com/bitnami --interval=1h
flux create helmrelease nginx --source=HelmRepository/bitnami \
  --chart=nginx --chart-version='15.x' --target-namespace=web --interval=10m

# Add --export to any create command to print YAML instead of applying it
flux create kustomization podinfo --source=GitRepository/podinfo \
  --path='./kustomize' --prune=true --interval=10m --export > podinfo-ks.yaml

flux get all -A
flux get sources git
flux get kustomizations --watch
flux get helmreleases -A
flux reconcile source git podinfo             # pull from Git right now
flux reconcile kustomization podinfo --with-source
flux suspend kustomization podinfo            # pause reconciliation
flux resume kustomization podinfo
flux logs --follow --level=error --all-namespaces
flux trace deployment/podinfo -n default      # which Git commit produced this?
flux events --for Kustomization/podinfo
⚠ “Nothing is happening” is usually suspension or interval

If a Flux Kustomization shows no change, check two things before you debug anything else: is it suspended (flux get kustomizations shows Suspended: True), and are you just waiting out the interval? flux reconcile … --with-source answers both. The Argo CD equivalent is an app stuck OutOfSync with auto-sync disabled — check spec.syncPolicy before assuming the manifest is wrong.

6 · Pipelines & progressive delivery

☺ Like you’re 10: First a conveyor belt that builds and tests your app, then a careful usher who lets a few users onto the new version at a time and can send everyone back if it misbehaves.

The background for all of this is in CI/CD & Progressive Delivery and Release Engineering.

Tekton — tkn

tkn task ls
tkn pipeline ls
tkn task start build --param=IMAGE=ghcr.io/acme/app:1.0 \
  --workspace=name=source,claimName=source-pvc --showlog

tkn pipeline start build-and-deploy \
  -p repo-url=https://github.com/acme/app -p revision=main \
  -w name=shared,volumeClaimTemplateFile=pvc.yaml \
  --use-param-defaults --showlog

tkn pipelinerun ls
tkn pipelinerun logs -f build-and-deploy-run-xyz
tkn pr logs --last -f                   # pr = pipelinerun; --last = most recent
tkn pipelinerun describe --last
tkn taskrun logs --last -f
tkn pipelinerun cancel build-and-deploy-run-xyz
tkn pipelinerun delete --all --keep 5
tkn hub install task git-clone          # pull a task from Tekton Hub

Argo Workflows — argo

argo submit -n argo --watch workflow.yaml
argo submit -n argo --from cronwf/nightly-build
argo list -n argo
argo get -n argo @latest                # @latest = the most recent workflow
argo logs -n argo @latest -f
argo watch -n argo @latest
argo retry -n argo @latest
argo resubmit -n argo @latest
argo terminate -n argo my-wf
argo delete -n argo --completed
argo template list -n argo
argo cron list -n argo

Argo Rollouts — the kubectl plugin

kubectl argo rollouts list rollouts
kubectl argo rollouts get rollout web --watch     # live canary progress — use this
kubectl argo rollouts status web
kubectl argo rollouts set image web app=ghcr.io/acme/app:1.4.3
kubectl argo rollouts promote web                 # advance one step
kubectl argo rollouts promote web --full          # skip all remaining steps + analysis
kubectl argo rollouts pause web
kubectl argo rollouts abort web                   # stop and shift traffic back
kubectl argo rollouts undo web --to-revision=2
kubectl argo rollouts retry rollout web
kubectl argo rollouts dashboard                   # local UI on :3100
# The canary strategy shape you must be able to read and edit
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata: { name: web }
spec:
  replicas: 5
  strategy:
    canary:
      steps:
        - setWeight: 20
        - pause: { duration: 60s }
        - setWeight: 50
        - pause: {}            # empty pause = wait for a manual promote
        - setWeight: 100
      analysis:
        templates:
          - templateName: success-rate
  selector:
    matchLabels: { app: web }
# Flagger drives canaries from a Canary CR instead — inspect it the same way
kubectl get canaries -A
kubectl describe canary web -n prod        # the Events show each weight step
kubectl get canary web -n prod -o jsonpath='{.status.phase}{"\n"}'

7 · Platform APIs — CRDs, Crossplane, and the self-service surface

☺ Like you’re 10: Teams add their own new kinds of object to Kubernetes. These commands let you discover what new kinds exist and how to fill one in — even if you’ve never seen it before.

The other 25% domain. When a task drops an unfamiliar API in front of you, discovery beats memory every time. Concepts live in Platform APIs & Operators and Self-Service & Golden Paths.

Discovering an unfamiliar API

kubectl get crds
kubectl get crds | grep -i crossplane
kubectl api-resources --api-group=argoproj.io
kubectl api-resources --api-group=pkg.crossplane.io
kubectl api-resources | grep -i policy

# The schema, straight from this cluster's API server
kubectl explain rollout.spec.strategy.canary --recursive
kubectl explain clusterpolicy.spec.rules --recursive | head -40

# What does an existing instance look like? Copy it and edit.
kubectl get rollout web -o yaml > rollout.yaml
kubectl get crd rollouts.argoproj.io -o jsonpath='{.spec.versions[*].name}{"\n"}'
kubectl get <kind> -A                       # find every instance, all namespaces

Crossplane

kubectl get providers
kubectl get providerconfigs
kubectl get xrds                       # CompositeResourceDefinitions — the API contract
kubectl get compositions               # the implementations behind an XRD
kubectl get crossplane                 # every Crossplane-managed object (category)
kubectl get managed                    # external cloud resources under management
kubectl get composite                  # XRs
kubectl get claim                      # namespaced claims developers create

# Why is my claim not READY? Follow the chain: claim -> composite -> managed
kubectl describe claim my-bucket
crossplane beta trace objectstorage my-bucket -n team-a   # older CLI builds
crossplane trace objectstorage my-bucket -n team-a        # newer CLI builds

kubectl get managed -o custom-columns=\
'NAME:.metadata.name,READY:.status.conditions[?(@.type=="Ready")].status,SYNCED:.status.conditions[?(@.type=="Synced")].status'

The crossplane CLI moved subcommands out of beta over time — if one form errors, run crossplane --help and use whichever the installed build offers. That is exactly the “unfamiliar tooling” trap the exam sets: don’t fight the CLI, fall back to kubectl describe and read the Conditions and Events, which always work.

# The three-layer shape to recognise: XRD (contract) → Composition (impl) → Claim (request)
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata: { name: xpostgresqlinstances.platform.acme.io }
spec:
  group: platform.acme.io
  names: { kind: XPostgreSQLInstance, plural: xpostgresqlinstances }
  claimNames: { kind: PostgreSQLInstance, plural: postgresqlinstances }
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                size: { type: string, enum: [ small, medium, large ] }
              required: [ size ]

8 · Observability — Prometheus, Grafana, Jaeger

☺ Like you’re 10: The platform keeps a diary of numbers. These commands open the diary, ask it questions, and check that the alarm rules you wrote are valid.

Twenty percent of the exam. The theory is in Observability & Operations; here is the keyboard and the port list.

Reaching the UIs — the port cheat table

kubectl -n monitoring port-forward svc/prometheus-operated 9090:9090
kubectl -n monitoring port-forward svc/grafana 3000:80
kubectl -n monitoring port-forward svc/alertmanager-operated 9093:9093
kubectl -n observability port-forward svc/jaeger-query 16686:16686
kubectl -n argocd port-forward svc/argocd-server 8080:443

# Don't guess the port — read it off the Service
kubectl -n monitoring get svc grafana -o jsonpath='{.spec.ports[*].port}{"\n"}'
ToolUsual portWhat you’ll do there
Prometheus9090Run PromQL, check Status → Targets for scrape failures, view active alerts.
Grafana3000Dashboards, panel queries, data-source config. Default login is often admin.
Alertmanager9093Firing alerts, silences, routing tree.
Jaeger Query16686Find a trace, follow a slow span across services.
OTel Collector4317 gRPC / 4318 HTTPOTLP receivers — the endpoints apps ship telemetry to.
Argo CD server8080 → svc 443Sync status, diffs, the resource tree.
Argo Workflows server2746Workflow DAGs and step logs.
Kiali (Istio)20001Mesh topology and traffic graph.
OpenCost9003Cost allocation by namespace, workload, and label.

PromQL you should be able to write cold

# Per-second request rate over 5 minutes, split by status
sum by (status) (rate(http_requests_total{job="api"}[5m]))

# Error ratio — the classic SLO numerator/denominator
sum(rate(http_requests_total{status=~"5.."}[5m]))
  / sum(rate(http_requests_total[5m]))

# p95 latency from a histogram — memorise this shape
histogram_quantile(0.95,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

# Which targets are down?
up == 0

# Pods that keep restarting
increase(kube_pod_container_status_restarts_total[1h]) > 3

# Deployments not fully available
kube_deployment_status_replicas_available
  / kube_deployment_spec_replicas < 1

# Memory actually in use per pod
sum by (pod) (container_memory_working_set_bytes{container!=""})
◆ Key idea

rate() needs a counter and a range vector; histogram_quantile() needs the _bucket series and a by (le) grouping. Getting those two shapes wrong is the most common PromQL error under time pressure. If a query returns “no data,” check the metric name in Prometheus’s own autocomplete before you rewrite the maths — the metric is far more often misnamed than the expression is wrong.

Rules, ServiceMonitors, and Alertmanager

promtool check config prometheus.yml
promtool check rules alerts.yaml           # validate BEFORE you apply — cheap points
promtool test rules alert_tests.yaml
promtool query instant http://localhost:9090 'up{job="kubelet"}'

kubectl get servicemonitors -A
kubectl get podmonitors -A
kubectl get prometheusrules -A
kubectl describe prometheusrule app-alerts -n monitoring

amtool --alertmanager.url=http://localhost:9093 alert query
amtool --alertmanager.url=http://localhost:9093 silence add \
  alertname=HighLatency service=api --duration=2h --comment='deploying fix'
amtool --alertmanager.url=http://localhost:9093 silence query
amtool check-config alertmanager.yml
amtool config routes test --config.file=alertmanager.yml severity=critical
# PrometheusRule — the shape a task will ask you to add a rule to
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: app-alerts
  namespace: monitoring
  labels: { release: kube-prometheus-stack }   # must match the Prometheus ruleSelector
spec:
  groups:
    - name: api.rules
      rules:
        - alert: HighErrorRate
          expr: |
            sum(rate(http_requests_total{status=~"5.."}[5m]))
              / sum(rate(http_requests_total[5m])) > 0.05
          for: 10m
          labels: { severity: critical }
          annotations:
            summary: "API error rate above 5% for 10 minutes"

That labels block is a classic trap: a ServiceMonitor or PrometheusRule that the Prometheus instance doesn’t select is invisible, no matter how correct it is. Check the operator’s ruleSelector and serviceMonitorSelector with kubectl get prometheus -A -o yaml if your rule never appears.

9 · Security & policy — RBAC, admission, mesh, supply chain

☺ Like you’re 10: Four questions: who is allowed to do what, what does the doorman reject, is the traffic encrypted, and can we prove where this container came from?

Fifteen percent, but it touches everything. Background in Security & Policy Enforcement and Secrets Management.

RBAC — proving who can do what

kubectl auth can-i create deployments -n team-a
kubectl auth can-i delete pods --as=system:serviceaccount:team-a:deployer -n team-a
kubectl auth can-i --list --as=system:serviceaccount:team-a:deployer -n team-a
kubectl auth can-i '*' '*' --as=system:serviceaccount:kube-system:default

kubectl get roles,rolebindings -n team-a
kubectl get clusterroles,clusterrolebindings -o wide | grep deployer
kubectl describe clusterrole edit
kubectl get rolebinding read-pods -n team-a -o yaml

--as is the fastest way to verify an RBAC task instead of hoping. A task that says “grant the CI service account permission to restart deployments” is finished when kubectl auth can-i patch deployments --as=system:serviceaccount:ci:runner -n app prints yes.

Admission policy — Kyverno, Gatekeeper, OPA

# Kyverno
kubectl get clusterpolicies            # short name: cpol
kubectl get policies -A                # short name: pol (namespaced)
kubectl get polr -A                    # PolicyReports — per-namespace results
kubectl get cpolr                      # ClusterPolicyReports
kubectl describe cpol require-labels
kubectl get polr -A -o wide | grep -i fail
kyverno apply policy.yaml --resource pod.yaml       # test a policy offline

# Gatekeeper / OPA
kubectl get constrainttemplates
kubectl get constraint                              # the "constraint" CATEGORY, not an API group
kubectl get k8srequiredlabels
kubectl describe k8srequiredlabels ns-must-have-owner   # violations are in .status
kubectl get constrainttemplate k8srequiredlabels -o jsonpath='{.spec.crd.spec.names.kind}{"\n"}'

opa eval -d policy.rego -i input.json 'data.kubernetes.admission.deny'
opa test ./policies -v
conftest test deployment.yaml -p policy/
# Kyverno: audit first, then enforce — the shape you'll be asked to flip
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: require-team-label }
spec:
  validationFailureAction: Enforce      # Audit = report only; Enforce = block
  background: true
  rules:
    - name: check-team-label
      match:
        any:
          - resources: { kinds: [ Pod ] }
      validate:
        message: "Every Pod must carry a 'team' label."
        pattern:
          metadata:
            labels:
              team: "?*"

Service mesh — istioctl and linkerd

istioctl analyze -n prod                  # config lint — run this first, always
istioctl analyze --all-namespaces
istioctl proxy-status                     # are sidecars in sync with the control plane?
istioctl proxy-config routes deploy/web.prod
istioctl proxy-config clusters deploy/web.prod --fqdn web-svc.prod.svc.cluster.local
istioctl proxy-config listeners deploy/web.prod --port 8080
istioctl x describe pod web-abc.prod      # which policies apply to this pod?
istioctl validate -f virtualservice.yaml
kubectl get virtualservices,destinationrules,gateways -A
kubectl get peerauthentication -A         # mTLS mode

linkerd check --pre
linkerd check
linkerd viz stat deploy -n prod           # success rate, RPS, latency per deployment
linkerd viz stat deploy/web -n prod
linkerd viz top deploy/web -n prod
linkerd viz tap deploy/web -n prod        # live request stream
linkerd viz edges deploy -n prod          # who talks to whom, and is it mTLS?
linkerd inject deploy.yaml | kubectl apply -f -

Supply chain — scan, bill of materials, sign

trivy image --severity HIGH,CRITICAL nginx:1.25
trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed ghcr.io/acme/app:1.0
trivy config ./manifests                     # misconfiguration scan of IaC/YAML
trivy fs --scanners vuln,secret,misconfig .
trivy k8s --report summary cluster
trivy image --format cyclonedx --output sbom.json ghcr.io/acme/app:1.0

syft ghcr.io/acme/app:1.0 -o spdx-json=sbom.json
grype sbom:./sbom.json

cosign generate-key-pair
cosign sign --key cosign.key ghcr.io/acme/app@sha256:abc123...
cosign verify --key cosign.pub ghcr.io/acme/app:1.0
cosign attest --predicate sbom.json --type cyclonedx --key cosign.key \
  ghcr.io/acme/app@sha256:abc123...
# Keyless (Fulcio/Rekor) verification needs identity flags:
cosign verify ghcr.io/acme/app:1.0 \
  --certificate-identity-regexp='https://github.com/acme/.*' \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com
⚠ Sign digests, not tags

A tag is a mutable pointer — :1.0 can be re-pushed tomorrow, and a signature over a tag proves nothing about what actually runs. Always sign and verify the immutable @sha256:… digest. The same instinct applies in your manifests: pinning a digest in a Deployment is what makes “what is running right now” an answerable question during an incident.

10 · Editing YAML fast — vim, jq, and yq

☺ Like you’re 10: YAML is a fussy language where one wrong space breaks everything. These settings stop your editor from adding wrong spaces for you.

More candidates lose time to indentation than to concepts. Two minutes of editor setup at the start of the exam removes an entire failure mode.

Vim settings that save YAML

# Type these once inside vim, or put the first line in ~/.vimrc
:set expandtab tabstop=2 shiftwidth=2 softtabstop=2
:set number
:set list                # show tabs as ^I — a tab in YAML is ALWAYS a bug
:set paste               # before a mouse-paste; stops runaway auto-indent
:set nopaste             # turn it back off before typing normally

Why does this matter so much? The YAML spec forbids tab characters for indentation outright. When you paste a block from the docs into an auto-indenting editor, vim helpfully re-indents each line on top of the indentation that was already there, producing a staircase. :set paste tells vim “these keystrokes are literal, don’t be clever.” :set list makes a stray tab visible instead of mysterious.

Moving blocks of YAML

V         # visual-line mode; then j/k to extend the selection
>         # indent the selection one shiftwidth (2 spaces)
<         # outdent one shiftwidth
3>>       # indent 3 lines from the cursor
.         # repeat the last change — press again to indent further
u         # undo     |  Ctrl-r  redo
dd  yy  p # delete / yank / paste a line
:%s/nginx:1.25/nginx:1.26/g       # replace everywhere
/image:    n                      # search, then jump to the next hit
:set ff=unix                      # fix stray carriage returns
:wq
⚠ Never run gg=G on YAML

The reflex to auto-indent a whole file (gg=G) is muscle memory from writing code — and it will mangle a Kubernetes manifest, because vim’s YAML indent rules cannot know your intended structure. Fix indentation with visual-block > and < on the specific lines instead. And if a manifest refuses to apply, validate before you stare: kubectl apply --dry-run=server -f file.yaml gives you a real API-server error with a line-level hint, which is far faster than reading.

jq and yq one-liners

# jq — over kubectl JSON
kubectl get pods -o json | jq -r '.items[].metadata.name'
kubectl get pods -o json | jq -r '.items[] | select(.status.phase!="Running") | .metadata.name'
kubectl get nodes -o json | jq -r '.items[] | "\(.metadata.name) \(.status.capacity.cpu)"'
kubectl get secret db-creds -o json | jq -r '.data | to_entries[] | "\(.key)=\(.value|@base64d)"'
kubectl get deploy -o json | jq -r '.items[] | select(.spec.replicas==0) | .metadata.name'

# yq — edit YAML in place, no editor needed
yq '.spec.replicas' deploy.yaml
yq -i '.spec.replicas = 5' deploy.yaml
yq -i '.spec.template.spec.containers[0].image = "nginx:1.26"' deploy.yaml
yq -i '.metadata.labels.team = "payments"' deploy.yaml
yq 'select(.kind == "Deployment")' all.yaml            # pick one doc from a multi-doc file
yq -o=json '.' values.yaml                             # YAML → JSON
kubectl get deploy web -o yaml | yq '.spec.template.spec.containers[].image'

Note the syntax generation gap: yq v4 (the Go implementation, what you’ll normally meet) uses jq-style expressions as shown. If a command errors oddly, run yq --version. When in doubt, pipe through jq instead — kubectl -o json always works.

11 · The five commands that save you the most time

☺ Like you’re 10: If you only drill five things this week, drill these.

Everything above is a reference. This is the short list — the commands whose absence costs whole minutes each time, several times per exam.

#CommandMinutes it saves — and why
1kubectl config use-context X && kubectl config set-context --current --namespace=YSaves the whole task. This is the difference between “correct” and “scored.” Run it before you read the task body, every time.
2kubectl create … --dry-run=client -o yaml > f.yaml2–4 minutes per manifest. You never hand-write apiVersion, nesting, or indentation again — you edit two lines of a valid file.
3kubectl explain <kind>.<path> --recursive1–3 minutes per unfamiliar CRD. Reads the schema from the cluster in front of you, so it always matches the installed version — faster and safer than searching docs.
4kubectl get events --sort-by=.lastTimestamp2–5 minutes per broken-thing task. The cluster usually tells you exactly what’s wrong; most people just never ask it in the right order.
5kubectl diff -k . / kubectl apply --dry-run=server -f f.yamlPrevents the silent wrong answer. Shows you what would change and catches schema errors before they cost you the task.
🦆 Dot’s-eye view

“The thing nobody told me: I was fast at Kubernetes and still nearly ran out of time, because I burned eleven minutes on one Crossplane task I’d never practised. The fix wasn’t learning more commands — it was accepting at minute five that I didn’t know it, flagging it, and banking three easier tasks instead. I came back with nine minutes left and got partial credit anyway.”

🦫 Benny’s workshop · 30 min

Run a stopwatch drill on a kind or minikube cluster. Round one (10 min): from a cold terminal, set up your aliases and completion, create namespace drill, pin it, then generate and apply — using $do only, never a blank file — a Deployment of 3 nginx replicas, a ClusterIP Service on port 80 → 8080, a ConfigMap with two literals, and a ServiceAccount with a Role granting get,list on pods. Verify the RBAC with kubectl auth can-i --as=…. Round two (10 min): break it deliberately — set the image to nginx:doesnotexist, then diagnose it using only get events --sort-by, describe, and logs --previous, and fix it with kubectl set image. Round three (10 min): install Argo CD, point an Application at a public repo path, enable prune and self-heal via kubectl patch rather than the UI, and confirm with kubectl get app -o jsonpath. Note your times. Repeat next week and beat them. Then take the timed set in Practice Tasks.

🎬 At the Platform Guild
🦊

Foxy: The docs are open during the exam. So… I don’t really need to memorise any of this, right?

🐰

Remy: Open docs don’t make you fast, they make you findable. There’s a difference between “I can look that up in ninety seconds” and “my fingers already did it.” Seventeen tasks. Do that maths.

🦫

Benny: And the lookups you will need are the unfamiliar ones — a Composition field, a Rollout step. That’s what kubectl explain --recursive is for. It reads the schema off the cluster in front of you. No tab-switching.

👺

Gizmo: Or just hand-write the whole manifest from memory. Very impressive. Very fast. Definitely nobody ever mistypes apiVersion under pressure. 🤑

🐢

Timmy: Gizmo, you once shipped a Deployment as apiVersion: v1 and spent four minutes convinced the cluster was broken. create … $do. Always.

🦆

Dot: Honestly the one that saved me was the boring one. Set the context. Set the namespace. Then read the task. I got two questions back that I’d otherwise have answered perfectly into the wrong cluster.

🐰

Remy: That’s the whole page in one line, really. Speed is a ritual, not a talent.

Reference pages are only worth what you’ve rehearsed from them. Take this one to the terminal, not to the sofa: run the Lab Track to build the muscle memory in context, use The Troubleshooting Playbook when a drill goes sideways, drill recall with Flashcards, and tick off readiness in the Exam-Prep Checklist. If a tool here is unfamiliar, that’s a signal, not a footnote — go meet it properly in The Tool Shed or IaC & Control Planes before exam day, because unfamiliar tooling is the biggest single time trap people report.

🐢 Timmy’s checkpoint

1. Write the two commands you run before reading any task body. 2. What exactly does export do='--dry-run=client -o yaml' buy you, and what does $now do? 3. A pod is in CrashLoopBackOff — which log flag shows you the error, and why? 4. Which command shows what a Kustomize overlay would change without applying it? 5. Name the CLI-free way to enable prune and self-heal on an Argo CD Application. 6. Write the p95 latency PromQL shape from memory. 7. How do you prove an RBAC task is complete? 8. Why should you never run gg=G on a manifest?

Check your answers
  1. kubectl config use-context <cluster> then kubectl config set-context --current --namespace=<ns>. Correct work in the wrong place scores zero.
  2. $do generates a valid manifest skeleton you edit instead of writing from scratch — correct apiVersion, nesting, and indentation for free. $now expands to --force --grace-period=0, deleting a stuck resource immediately rather than waiting out the grace period.
  3. kubectl logs <pod> --previous (-p). The current container hasn’t failed yet; the evidence lives in the instance that just crashed.
  4. kubectl diff -k <dir>. (For a plain file: kubectl diff -f f.yaml, or kubectl apply --dry-run=server -f f.yaml to catch schema errors.)
  5. kubectl -n argocd patch app <name> --type merge -p '{"spec":{"syncPolicy":{"automated":{"prune":true,"selfHeal":true}}}}'.
  6. histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) — the _bucket series, a rate() over a range, and grouping by (le).
  7. Impersonate and check: kubectl auth can-i <verb> <resource> --as=system:serviceaccount:<ns>:<sa> -n <ns> — it should print yes. --list shows everything that identity can do.
  8. Vim’s auto-indent rules can’t infer YAML’s intended structure, so it re-indents the whole file wrongly. Use visual-line V with > / < on the specific lines instead, with :set expandtab shiftwidth=2 so you never emit a tab.