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.
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.
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-contextPinning 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.
| Shortcut | Expands to | Why it earns its keep |
|---|---|---|
k | kubectl | Six characters saved on every one of ~200 commands you’ll type. |
$do | --dry-run=client -o yaml | The gateway to every generated manifest — the highest-leverage export on the list. |
$now | --force --grace-period=0 | Deletes a stuck pod instantly instead of watching a 30-second grace period. |
kubectl completion | Tab-completion of verbs, kinds, and live resource names | Stops typos in long generated pod names — the silent time thief. |
kubectl config set-context --current --namespace | Pinned namespace | Not 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.
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
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; echoIdentity 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
“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
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 ClusterPolicy — explain 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 --cascadeArgo 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
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 namespacesCrossplane
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"}'| Tool | Usual port | What you’ll do there |
|---|---|---|
| Prometheus | 9090 | Run PromQL, check Status → Targets for scrape failures, view active alerts. |
| Grafana | 3000 | Dashboards, panel queries, data-source config. Default login is often admin. |
| Alertmanager | 9093 | Firing alerts, silences, routing tree. |
| Jaeger Query | 16686 | Find a trace, follow a slow span across services. |
| OTel Collector | 4317 gRPC / 4318 HTTP | OTLP receivers — the endpoints apps ship telemetry to. |
| Argo CD server | 8080 → svc 443 | Sync status, diffs, the resource tree. |
| Argo Workflows server | 2746 | Workflow DAGs and step logs. |
| Kiali (Istio) | 20001 | Mesh topology and traffic graph. |
| OpenCost | 9003 | Cost 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!=""})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
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
gg=G on YAMLThe 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.
| # | Command | Minutes it saves — and why |
|---|---|---|
| 1 | kubectl config use-context X && kubectl config set-context --current --namespace=Y | Saves the whole task. This is the difference between “correct” and “scored.” Run it before you read the task body, every time. |
| 2 | kubectl create … --dry-run=client -o yaml > f.yaml | 2–4 minutes per manifest. You never hand-write apiVersion, nesting, or indentation again — you edit two lines of a valid file. |
| 3 | kubectl explain <kind>.<path> --recursive | 1–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. |
| 4 | kubectl get events --sort-by=.lastTimestamp | 2–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. |
| 5 | kubectl diff -k . / kubectl apply --dry-run=server -f f.yaml | Prevents the silent wrong answer. Shows you what would change and catches schema errors before they cost you the task. |
“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.”
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.
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.
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
kubectl config use-context <cluster>thenkubectl config set-context --current --namespace=<ns>. Correct work in the wrong place scores zero.$dogenerates a valid manifest skeleton you edit instead of writing from scratch — correctapiVersion, nesting, and indentation for free.$nowexpands to--force --grace-period=0, deleting a stuck resource immediately rather than waiting out the grace period.kubectl logs <pod> --previous(-p). The current container hasn’t failed yet; the evidence lives in the instance that just crashed.kubectl diff -k <dir>. (For a plain file:kubectl diff -f f.yaml, orkubectl apply --dry-run=server -f f.yamlto catch schema errors.)kubectl -n argocd patch app <name> --type merge -p '{"spec":{"syncPolicy":{"automated":{"prune":true,"selfHeal":true}}}}'.histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))— the_bucketseries, arate()over a range, and groupingby (le).- Impersonate and check:
kubectl auth can-i <verb> <resource> --as=system:serviceaccount:<ns>:<sa> -n <ns>— it should printyes.--listshows everything that identity can do. - Vim’s auto-indent rules can’t infer YAML’s intended structure, so it re-indents the whole file wrongly. Use visual-line
Vwith>/<on the specific lines instead, with:set expandtab shiftwidth=2so you never emit a tab.