CKA Mock Exam · Set 2
This is the second of this course's full-length CKA practice papers, and every scenario on it is new — none of the 17 tasks below repeats a task from Set 1, so sitting this one is a genuine second measurement, not a replay of something you already half-remember. Same rules as the real thing: a 120-minute block on a live cluster, tasks weighted exactly like the official curriculum's five domains (30/25/20/15/10%), and every task graded the way the real exam grades it — on the state you leave the cluster in, checked by an objective done when command you can run yourself. Worked solutions sit folded under each task so you can sit the paper honestly and mark it afterward. Total the 100 points, compare against the 66% pass mark, and let your domain breakdown — not the raw number — tell you what to study next.
You already sat one full practice driving test — parallel parking, hill starts, the works. This is the second one, at a different set of streets with a different examiner, so you can't just remember "turn left here" from last time. It still has the same five sections weighted the same way, the same clock, and the same rule: the examiner only cares whether the car ends up parked correctly, not whether you talked yourself through it nicely on the way. Drive the whole route before you look at the answer sheet, then go back and see exactly which section cost you the most points — that's the only part of this that actually makes you better before the real test.
Before you start — sitting conditions
☺ Like you're 10: This only works as practice if you play it by the real rules — one clock, no notes, no help, and you don't get to peek at the answer before you've committed to one.
Build a throwaway multi-node lab first — kubeadm on disposable VMs is closest to the real environment, or a multi-node kind cluster if VMs aren't available; a handful of tasks below assume you can reach a node's shell directly (SSH or docker exec into a kind node), so a single-node cluster with no separate worker won't let you sit every task honestly. Start one timer for the full 120 minutes and don't pause it — not for a stuck terminal, not for a coffee. Keep only kubernetes.io/docs open, since that's the allowlisted documentation you're permitted during the real exam — no notes, no this course, no AI assistant. Read all 17 tasks first; five minutes spent scanning the whole paper is what lets you bank the two-minute tasks before you burn twelve on one stubborn manifest.
This is an independent, unofficial study resource, not affiliated with the CNCF or the Linux Foundation. The task count, 120-minute duration, 66% pass mark, and domain weights below reflect the CKA exam's published shape at a point in time, and all of them are subject to change without much notice. Confirm current specifics on the official Linux Foundation CKA page and the CNCF certification page before paying for anything — see the exam logistics table on the CKA study plan for the fuller picture, and this course's Certifications page for the whole ladder.
Your 120-minute budget
☺ Like you're 10: Give each section a slice of the clock sized to how much of your grade it's worth — and if a task blows way past its slice, flag it and move on, you can always come back.
Seventeen tasks in 120 minutes isn't an even split — it's sized to the same weights the real curriculum uses, so a slow task in Troubleshooting costs you more attention than a slow task in Storage, and the budget below reflects that on purpose.
There's no folded worked solution to sneak a look at mid-paper the way this write-up presents them — each task's solution sits directly beneath its done-when check, so the only discipline required is not opening it before you've committed to your own attempt.
🦉 Cluster Architecture, Installation & Configuration — T1–T4 (25 points)
☺ Like you're 10: This section is about building and maintaining the cluster itself — adding a machine to it, upgrading it, backing it up, and deciding who's allowed to touch what.
Background reading, after the sitting, not during it: the D1 blueprint page, the kubeadm tool guide, and control-plane internals.
T1 · Join a worker node and dedicate it to ingress (7 points · 8 min)
Platform has provisioned a fresh VM, worker-04, that must join the existing kubeadm cluster and then be reserved exclusively for ingress-controller workloads — nothing else should ever land there.
Your task:
- On the control-plane node, generate a fresh bootstrap token good for one hour and print the full
kubeadm joincommand it produces. - Run that join command on
worker-04. - Once the node is
Ready, label itrole=ingressand taint itdedicated=ingress:NoScheduleso nothing else schedules there by accident.
Done when: kubectl get node worker-04 shows Ready, carries the role=ingress label, and kubectl describe node worker-04 lists the dedicated=ingress:NoSchedule taint. (7 points)
Show the worked solution
# on the control-plane node kubeadm token create --ttl 1h --print-join-command
# on worker-04, using the exact output from the command above sudo kubeadm join 10.0.4.10:6443 --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a1
kubectl label node worker-04 role=ingress kubectl taint node worker-04 dedicated=ingress:NoSchedule kubectl get node worker-04 -o wide kubectl describe node worker-04 | grep -A2 Taints
Why: --print-join-command bundles the two things a joining node actually needs to prove — the token, which authenticates it to the control plane, and the discovery CA cert hash, which lets the node verify it's really talking to your API server and not an impostor. Tokens expire by design (24h default, here shortened to 1h) so a leaked join command can't be replayed forever. Note that the taint alone only repels other workloads — it doesn't pull ingress pods toward this node by itself; that half needs a matching toleration plus a nodeSelector or nodeAffinity on the ingress controller's own spec.
T2 · Upgrade a control-plane node with kubeadm (6 points · 7 min)
The cluster's single control-plane node, cp-01, is running v1.30.4 and needs to move to v1.30.6 before a security fix lands.
Your task:
- Drain
cp-01safely, ignoring DaemonSets and allowing localemptyDirdata to be deleted. - Upgrade the
kubeadmpackage to1.30.6, runkubeadm upgrade plan, then apply the upgrade. - Upgrade the
kubeletandkubectlpackages to match, restartkubelet, and uncordon the node.
Done when: kubectl get nodes shows cp-01 at v1.30.6 with status Ready (not SchedulingDisabled). (6 points)
Show the worked solution
kubectl drain cp-01 --ignore-daemonsets --delete-emptydir-data apt-mark unhold kubeadm apt-get update && apt-get install -y kubeadm=1.30.6-1.1 apt-mark hold kubeadm kubeadm upgrade plan kubeadm upgrade apply v1.30.6 apt-mark unhold kubelet kubectl apt-get install -y kubelet=1.30.6-1.1 kubectl=1.30.6-1.1 apt-mark hold kubelet kubectl systemctl daemon-reload systemctl restart kubelet kubectl uncordon cp-01 kubectl get nodes
Why: kubeadm upgrade apply only touches the control-plane components it directly manages — the static-pod manifests for the API server, controller-manager, scheduler, and its own bookkeeping. kubelet and kubectl are ordinary packages upgraded by hand afterward, deliberately, since kubeadm never auto-upgrades the node agent it's running under. Drain-before, uncordon-after is what keeps workloads from being force-killed mid-upgrade rather than rescheduled cleanly first — and only ever cross one minor version at a time.
T3 · Take and verify an etcd snapshot (6 points · 7 min)
Before a risky config change, the team wants a fresh, verified etcd snapshot on disk — not a snapshot nobody has actually confirmed is readable.
Your task:
- Using the correct etcd peer certificates and the local etcd endpoint, save a snapshot to
/var/backups/etcd-snapshot.db. - Verify the snapshot's integrity with
etcdutl, without touching the live cluster. - Confirm the reported revision and hash are present with no errors.
Done when: the snapshot file exists at that path and etcdutl snapshot status /var/backups/etcd-snapshot.db --write-out=table prints a table with a hash and nonzero revision, with no error. (6 points)
Show the worked solution
ETCDCTL_API=3 etcdctl snapshot save /var/backups/etcd-snapshot.db \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key etcdutl snapshot status /var/backups/etcd-snapshot.db --write-out=table
Why: etcd is the cluster's only source of truth, so this isn't ceremony — it's the actual backup for everything else on the cluster. The three TLS flags point etcdctl at etcd's own peer-authenticated port, not the API server; reaching for the wrong certificate set (like apiserver-etcd-client) is the single most common way candidates fail this exact task. snapshot status is a read-only integrity check — it never touches the live cluster, which is why it's safe to run before you've ever needed to trust the file for a real restore.
T4 · Scope a ServiceAccount with least-privilege RBAC (6 points · 7 min)
A CI pipeline needs a ServiceAccount, ci-bot, in namespace ci that can watch Pods and create Events — and nothing more. It must not be able to delete anything.
Your task:
- Create the namespace
ciand a ServiceAccountci-botinside it. - Create a Role (not a ClusterRole) granting
get/list/watchon Pods andcreateon Events, in that namespace only. - Bind it to
ci-botwith a RoleBinding, and prove both what it can and can't do.
Done when: kubectl auth can-i list pods --as=system:serviceaccount:ci:ci-bot -n ci prints yes, and kubectl auth can-i delete pods --as=system:serviceaccount:ci:ci-bot -n ci prints no. (6 points)
Show the worked solution
kubectl create ns ci kubectl create sa ci-bot -n ci
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-watcher
namespace: ci
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-bot-pod-watcher
namespace: ci
subjects:
- kind: ServiceAccount
name: ci-bot
namespace: ci
roleRef:
kind: Role
name: pod-watcher
apiGroup: rbac.authorization.k8s.iokubectl apply -f pod-watcher-rbac.yaml kubectl auth can-i list pods --as=system:serviceaccount:ci:ci-bot -n ci kubectl auth can-i delete pods --as=system:serviceaccount:ci:ci-bot -n ci
Why: least privilege means writing the narrowest rule the workload's actual verbs need, not reaching for a broad ClusterRole because it's on hand. A namespaced Role, not a ClusterRole, matters too — nothing here needs cluster-wide reach, and granting it anyway is exactly the over-broad grant this domain is built to catch. --as=system:serviceaccount:<ns>:<name> impersonates the SA's real identity for verification, rather than trusting your own — almost certainly cluster-admin — permissions. More in RBAC & admission control.
🦫 Workloads & Scheduling — T5–T7 (15 points)
☺ Like you're 10: This section is about how your actual applications run — rolling out a new version safely, undoing a bad one, putting the right work on the right machine, and running scheduled jobs without them tripping over each other.
Background: the D2 blueprint page and scheduling & resource management.
T5 · Roll out, break, and roll back to a specific revision (5 points · 6 min)
The catalog Deployment needs a zero-downtime rollout strategy, then a deliberately broken update, then a rollback to a specific known-good revision — not just "one step back."
Your task:
- Create Deployment
catalog, imagenginx:1.25, 4 replicas, withmaxSurge: 1andmaxUnavailable: 0, and record the change cause. - Update the image to
nginx:1.26(revision 2), then to a nonexistent tagnginx:broken-tag-9999(revision 3), recording a change cause each time. - Roll back directly to revision 2, not to the immediately previous revision.
Done when: kubectl rollout status deploy/catalog reports success and kubectl get deploy catalog -o jsonpath='{.spec.template.spec.containers[0].image}' prints nginx:1.26. (5 points)
Show the worked solution
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog
spec:
replicas: 4
selector:
matchLabels: { app: catalog }
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels: { app: catalog }
spec:
containers:
- name: nginx
image: nginx:1.25kubectl apply -f catalog.yaml
kubectl annotate deploy catalog kubernetes.io/change-cause="initial release"
kubectl set image deploy/catalog nginx=nginx:1.26
kubectl annotate deploy catalog kubernetes.io/change-cause="routine bump" --overwrite
kubectl rollout status deploy/catalog
kubectl set image deploy/catalog nginx=nginx:broken-tag-9999
kubectl annotate deploy catalog kubernetes.io/change-cause="bad release" --overwrite
kubectl rollout history deploy/catalog
kubectl rollout undo deploy/catalog --to-revision=2
kubectl rollout status deploy/catalog
kubectl get deploy catalog -o jsonpath='{.spec.template.spec.containers[0].image}'Why: maxUnavailable: 0 paired with maxSurge: 1 is what actually delivers zero downtime — a new pod comes up before an old one goes down — and it's a one-line spec choice most people skip until it's asked for directly. rollout undo with no flag only ever steps back one revision; if you'd already rolled forward past the good one, a plain undo lands you back on the broken tag, not the working one, which is why --to-revision=2 matters here. The change-cause annotation is what turns rollout history into something readable instead of a bare list of numbers.
T6 · Dedicate a node to a workload with affinity and a taint (5 points · 6 min)
A memory-hungry batch Pod, cruncher, must run only on highmem-01 — a node tainted and labeled specifically for this class of work — and must request 2 CPU / 2Gi with a 4Gi memory limit.
Your task:
- Label
highmem-01withhardware=highmemand taint itworkload=highmem:NoSchedule. - Write Pod
cruncherwith a matching toleration, arequiredDuringSchedulingIgnoredDuringExecutionnode affinity forhardware=highmem, and the resource requests/limits above. - Confirm it lands specifically on
highmem-01, not just any untainted node.
Done when: kubectl get pod cruncher -o wide shows NODE=highmem-01 and Running. (5 points)
Show the worked solution
kubectl label node highmem-01 hardware=highmem kubectl taint node highmem-01 workload=highmem:NoSchedule
apiVersion: v1
kind: Pod
metadata:
name: cruncher
spec:
tolerations:
- key: workload
operator: Equal
value: highmem
effect: NoSchedule
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: hardware
operator: In
values: ["highmem"]
containers:
- name: cruncher
image: busybox:1.36
command: ["sleep", "3600"]
resources:
requests: { cpu: "2", memory: 2Gi }
limits: { cpu: "2", memory: 4Gi }kubectl apply -f cruncher.yaml kubectl get pod cruncher -o wide
Why: this task is built around the trap that a taint and a matching toleration only cancel out repulsion — they never attract a pod toward that node. Without the node affinity, cruncher's toleration would let it land on highmem-01, but any ordinary untainted node is equally eligible, so it could just as easily schedule anywhere else. Pairing taint/toleration (repel everyone else) with node affinity (pull this one workload here) is the standard two-part pattern for genuinely dedicating a node.
T7 · Prevent overlapping CronJob runs (5 points · 6 min)
A reconciliation job sometimes overlaps with the previous run still finishing, causing double writes. The team wants overlapping runs skipped outright, a hard cap on run time, and limited retries.
Your task:
- Create CronJob
reconcile, schedule* * * * *, withconcurrencyPolicy: Forbid. - Its job template should sleep 90 seconds (to guarantee overlap with the next minute's tick), with
backoffLimit: 2andactiveDeadlineSeconds: 120. - Watch two consecutive ticks and confirm the second is skipped while the first is still running.
Done when: across two consecutive scheduled ticks, kubectl get pods -l job-name --watch shows no more than one reconcile pod running at once, and kubectl describe cronjob reconcile shows an event noting a skipped run. (5 points)
Show the worked solution
apiVersion: batch/v1
kind: CronJob
metadata:
name: reconcile
spec:
schedule: "* * * * *"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 30
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 120
template:
spec:
restartPolicy: Never
containers:
- name: reconcile
image: busybox:1.36
command: ["sh", "-c", "sleep 90 && echo done"]kubectl apply -f reconcile-cronjob.yaml kubectl get pods -w # second tick produces no new pod while the first is still sleeping kubectl describe cronjob reconcile # Events: skipped run because of ConcurrencyPolicy Forbid
Why: concurrencyPolicy only governs runs the CronJob controller itself starts on schedule — Forbid means if the previous scheduled Job's pod isn't finished when the next tick fires, that tick is skipped outright rather than queued or run in parallel. It has no opinion about a Job someone creates by hand with kubectl create job --from=cronjob/reconcile, which is an independent object the controller never tracks — a trap worth knowing before assuming a manual run is automatically "safe" under Forbid. activeDeadlineSeconds caps total run time regardless of retries; backoffLimit caps how many times a failing pod is recreated before the Job reports Failed.
🐦 Services & Networking — T8–T10 (20 points)
☺ Like you're 10: This section is about how traffic actually finds its way to your pods — locking most of it out by default, opening one exact door, and routing outside requests to the right service by name.
Background: the D3 blueprint page, networking & the CNI, and the ingress-nginx tool guide.
T8 · Default-deny a namespace, then open one exact hole (7 points · 8 min)
Namespace billing needs to default-deny all ingress and egress, then allow exactly two things: inbound traffic from pods labeled app=api-gateway on port 8443, and outbound traffic to the billing-db pods on port 5432 plus DNS to kube-system.
Your task:
- Write a default-deny-all NetworkPolicy for
billingcovering both directions. - Write a policy allowing ingress from
app=api-gatewayon TCP8443only. - Write a policy allowing egress to
app=billing-dbon TCP5432, and tokube-systemon UDP/TCP53for DNS.
Done when: a test pod labeled app=api-gateway can curl a billing pod on 8443, an unlabeled test pod in the same namespace times out on the same request, and DNS resolution still works from inside billing. (7 points)
Show the worked solution
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: billing
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-gateway
namespace: billing
spec:
podSelector: {}
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels: { app: api-gateway }
ports:
- protocol: TCP
port: 8443
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-db-and-dns
namespace: billing
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- podSelector:
matchLabels: { app: billing-db }
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53kubectl apply -f netpols.yaml kubectl run allowed --image=curlimages/curl -n billing -l app=api-gateway --command -- sleep 3600 kubectl run denied --image=curlimages/curl -n billing --command -- sleep 3600 kubectl exec -n billing allowed -- curl -sm3 -k https://billing-svc:8443 kubectl exec -n billing denied -- curl -sm3 -k https://billing-svc:8443 # should time out
Why: NetworkPolicies are additive and namespace-scoped — a default-deny podSelector: {} with no rules blocks everything to and from every pod in the namespace, and every later policy only opens a narrow hole, never closes one. The DNS rule is the one candidates forget under time pressure: block egress without explicitly re-opening port 53 to kube-system, and every other allow rule in the namespace silently breaks too, because names stop resolving. Selecting kube-system by its immutable kubernetes.io/metadata.name label (auto-applied to every namespace since 1.21) is more reliable than trusting a custom label to still be there.
T9 · Split internal and external access to the same workload (7 points · 8 min)
The reports API needs ordinary cluster-internal access on its own stable DNS name, plus one specific, pinned NodePort — 30080 — for a legacy on-prem probe that can't reach an Ingress.
Your task:
- Create Deployment
reports(2 replicas) listening on 8080. - Create ClusterIP Service
reports-internal, port 80 → targetPort 8080. - Create a second Service,
reports-external, typeNodePort, same selector, withnodePort: 30080pinned explicitly.
Done when: kubectl exec test -- curl -s reports-internal.default.svc.cluster.local succeeds from inside the cluster, and curl -s http://<any-node-ip>:30080/ succeeds from a node's own shell. (7 points)
Show the worked solution
kubectl create deployment reports --image=hashicorp/http-echo --replicas=2 -- -text="reports-ok"
apiVersion: v1
kind: Service
metadata:
name: reports-internal
spec:
selector: { app: reports }
ports:
- port: 80
targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: reports-external
spec:
type: NodePort
selector: { app: reports }
ports:
- port: 80
targetPort: 8080
nodePort: 30080kubectl apply -f reports-services.yaml
kubectl run test --image=curlimages/curl --command -- sleep 3600
kubectl exec test -- curl -s reports-internal.default.svc.cluster.local
NODE_IP=$(kubectl get node worker-01 -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}')
curl -s http://$NODE_IP:30080/Why: the NodePort is pinned rather than auto-allocated because that's exactly what the external dependency needs — an auto-assigned port would change every time the Service was recreated, breaking the legacy probe's config. A NodePort Service is also reachable in-cluster the normal way, but this task deliberately keeps two Service objects with the same selector so the internal DNS name stays stable and untouched by whatever happens externally. Every node — not only the one running a reports pod — listens on 30080 and forwards traffic via kube-proxy, which is the part candidates most often assume incorrectly.
T10 · Route two hostnames through one Ingress (6 points · 7 min)
A single ingress-nginx controller is already installed cluster-wide. It needs to front two backends: shop.example.internal at /, and admin.example.internal at /console, with anything unmatched falling through to the default 404.
Your task:
- Write Ingress
storefrontwithingressClassName: nginx, routingshop.example.internal→shop-svc:80andadmin.example.internal/console→admin-console-svc:8080. - Use
curl --resolveto test both hostnames against the controller's IP without editing DNS. - Confirm an unmatched hostname returns the default backend's 404.
Done when: both curl --resolve requests above return their respective backend's response, and a third, unmatched hostname returns 404. (6 points)
Show the worked solution
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: storefront
spec:
ingressClassName: nginx
rules:
- host: shop.example.internal
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: shop-svc, port: { number: 80 } }
- host: admin.example.internal
http:
paths:
- path: /console
pathType: Prefix
backend:
service: { name: admin-console-svc, port: { number: 8080 } }kubectl apply -f storefront-ingress.yaml
INGRESS_IP=$(kubectl -n ingress-nginx get svc ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl --resolve shop.example.internal:80:$INGRESS_IP http://shop.example.internal/
curl --resolve admin.example.internal:80:$INGRESS_IP http://admin.example.internal/console/
curl --resolve nowhere.example.internal:80:$INGRESS_IP http://nowhere.example.internal/ # default backend 404Why: pathType: Prefix versus Exact is what this task is really checking — Prefix matches /console and everything beneath it, while Exact would only match that literal string, silently 404-ing every sub-path the console app actually serves. curl --resolve fakes DNS for one request without touching /etc/hosts, which is the fast way to test host-based routing against an IP with no real DNS record yet. This is Kubernetes's original routing API — teams increasingly reach for the newer Gateway API for the same job with a cleaner split of responsibilities; see networking & the CNI for both.
🐘 Storage — T11–T12 (10 points)
☺ Like you're 10: This section is about disks that outlive the pod using them — making one grow safely while it's in use, and fixing a claim that's stuck refusing to match an existing disk.
Background: the D4 blueprint page and storage & the CSI.
T11 · A retained, resizable volume for compliance logs (5 points · 6 min)
An audit-log volume must survive Pod and PVC deletion — compliance requires the disk isn't auto-reclaimed — and must grow without downtime as log volume increases.
Your task:
- Create StorageClass
audit-retainwithreclaimPolicy: RetainandallowVolumeExpansion: true. - Create PVC
audit-logs(5Gi) using it, mounted into Podauditorat/var/log/audit. - Expand the PVC to 8Gi and confirm the filesystem inside the pod actually grew — not just the PVC object's requested size.
Done when: kubectl get pvc audit-logs -o jsonpath='{.status.capacity.storage}' prints 8Gi, and kubectl exec auditor -- df -h /var/log/audit shows roughly 8G available. (5 points)
Show the worked solution
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: audit-retain
provisioner: rancher.io/local-path # swap for your cluster's actual CSI driver
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: audit-logs
spec:
storageClassName: audit-retain
accessModes: ["ReadWriteOnce"]
resources:
requests: { storage: 5Gi }
---
apiVersion: v1
kind: Pod
metadata:
name: auditor
spec:
containers:
- name: auditor
image: busybox:1.36
command: ["sleep", "3600"]
volumeMounts:
- name: logs
mountPath: /var/log/audit
volumes:
- name: logs
persistentVolumeClaim: { claimName: audit-logs }kubectl apply -f audit-storage.yaml
kubectl patch pvc audit-logs -p '{"spec":{"resources":{"requests":{"storage":"8Gi"}}}}'
kubectl get pvc audit-logs -w
kubectl exec auditor -- df -h /var/log/audit
kubectl delete pvc audit-logs
kubectl get pv # the PV is Released, not gone — Retain honoredWhy: allowVolumeExpansion on the StorageClass is the actual gate — without it, the patch above is accepted by the API server but silently never actualized on disk. With volumeBindingMode: WaitForFirstConsumer, the volume isn't provisioned until a Pod claiming it is scheduled, letting the provisioner pick a location matching where the Pod lands. reclaimPolicy: Retain is the real compliance control: deleting the PVC leaves the PersistentVolume — and the disk behind it — in Released state instead of deleted, forcing a human decision about what happens to that data.
T12 · Fix a PVC stuck Pending against a static PV (5 points · 6 min)
A legacy dataset already has a pre-provisioned static PV, archive-pv (5Gi, ReadWriteOnce, storageClassName: manual). A PVC written against it, archive-claim, has been Pending for an hour, and Pod archive-reader is stuck waiting on it.
Your task:
- Diagnose why
archive-claimwon't bind toarchive-pv. - Correct the mismatch — the PVC's spec is largely immutable once created, so delete and recreate it with matching values.
- Confirm the Pod becomes
Running.
Done when: kubectl get pvc archive-claim -o jsonpath='{.status.phase}' prints Bound and kubectl get pod archive-reader -o jsonpath='{.status.phase}' prints Running. (5 points)
Show the worked solution
kubectl describe pvc archive-claim # Events: no persistent volumes available for this claim kubectl get pv archive-pv -o yaml | grep -E "capacity|storageClassName|accessModes" # archive-pv: 5Gi, manual, ReadWriteOnce — but archive-claim requested 10Gi, too large to bind statically
kubectl delete pvc archive-claim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: archive-claim
spec:
storageClassName: manual
accessModes: ["ReadWriteOnce"]
resources:
requests: { storage: 5Gi }kubectl apply -f archive-claim.yaml kubectl get pvc archive-claim kubectl get pod archive-reader
Why: static binding — no dynamic provisioner in play — is stricter than dynamic provisioning: the PVC's storageClassName, accessModes, and requested size must all be satisfiable by an existing PV's fields, and requested size must be at most the PV's capacity, not merely close. Because most PVC spec fields are immutable once created (unlike the resize path in T11, which only ever grows an already-bound claim), the fix is delete-and-recreate with corrected values, not kubectl edit. kubectl describe pvc is the fastest first move — its Events line states the rejection reason directly instead of leaving you to guess.
🦊 Troubleshooting — T13–T17 (30 points)
☺ Like you're 10: This is the biggest section on purpose — something is broken, and your job is to find out why using only what the cluster tells you, fix the actual cause, and prove it's actually fixed.
The largest domain, and it's a method more than a new set of facts: node before app, describe before logs, logs before exec. Background: the D5 blueprint page and a troubleshooting methodology.
T13 · Recover a NotReady node (6 points · 5 min)
worker-03 shows NotReady, and every pod scheduled there is stuck. Recently someone changed the container runtime's cgroup driver without updating the kubelet to match.
Your task:
- Confirm the node condition from the control plane, then check
kubelet's status onworker-03itself. - Read the kubelet's journal to find the actual failure reason.
- Correct the mismatch and confirm the node returns to
Ready.
Done when: kubectl get node worker-03 shows Ready, and previously-Pending pods on that node move to Running. (6 points)
Show the worked solution
kubectl describe node worker-03 | grep -A5 Conditions
# on worker-03 systemctl status kubelet journalctl -u kubelet -n 50 --no-pager # ... misconfiguration: kubelet cgroup driver "cgroupfs" is different from the container runtime cgroup driver "systemd"
sudo sed -i 's/cgroupDriver: cgroupfs/cgroupDriver: systemd/' /var/lib/kubelet/config.yaml sudo systemctl daemon-reload sudo systemctl restart kubelet systemctl status kubelet
# back on the control plane kubectl get node worker-03 -w kubectl get pods -A -o wide --field-selector spec.nodeName=worker-03
Why: kubectl describe node shows the symptom — a NotReady condition and a short reason string — but the actual cause almost always lives in that node's own systemd journal, a layer kubectl can't see into at all. A cgroup-driver mismatch between the kubelet and the runtime is one of the most common real causes of a kubelet that starts, errors immediately, and gets marked NotReady — it's a config-file fix, not a restart-and-hope fix, since restarting with the same bad config just repeats the crash.
T14 · Fix an ImagePullBackOff behind a private registry (6 points · 5 min)
Every pod in the payments Deployment is stuck ImagePullBackOff, pulling from a private registry.
Your task:
- Read the pod events to identify the exact failure — a bad tag, a missing pull secret, or an unreachable registry are three different fixes.
- If it's an authentication failure, create the missing registry credentials and attach them so every future pod in the namespace inherits them.
- Confirm the pods recover without editing the Deployment's own template.
Done when: kubectl get pods -n payments -l app=payments shows all pods Running, with no further pull errors in their events. (6 points)
Show the worked solution
kubectl describe pod -n payments payments-7d8f9-abcde | tail -20 # Events: Failed to pull image "registry.acme.internal/payments:4.2.0": ... 401 Unauthorized
kubectl create secret docker-registry acme-registry \
--docker-server=registry.acme.internal \
--docker-username=ci-bot \
--docker-password="$REGISTRY_TOKEN" \
-n payments
kubectl patch sa default -n payments \
-p '{"imagePullSecrets":[{"name":"acme-registry"}]}'
kubectl rollout restart deploy/payments -n payments
kubectl get pods -n payments -l app=payments -wWhy: ImagePullBackOff is a bucket for several distinct root causes, and the events string names which one — 401/403 means an auth problem, "manifest unknown" means a bad name or tag, a bare timeout usually means the registry is unreachable. Patching the ServiceAccount, not just one Pod's imagePullSecrets, fixes it for every pod that Deployment creates going forward, without editing the Deployment template — worth knowing, since re-applying the original manifest later would otherwise silently drop a pod-level-only fix.
T15 · Fix a Service with no endpoints (6 points · 5 min)
The orders Service in namespace orders times out on every request, even though its backing pods are all Running.
Your task:
- Check whether the Service actually has any endpoints.
- Compare the Service's selector against the real Pod labels to find the mismatch.
- Fix the Service — not the Pods, which the Deployment already owns and would just re-diverge.
Done when: kubectl get endpoints orders -n orders lists non-empty pod IPs, and kubectl exec test -n orders -- curl -s orders.orders.svc.cluster.local succeeds. (6 points)
Show the worked solution
kubectl get endpoints orders -n orders # <none>
kubectl get svc orders -n orders -o jsonpath='{.spec.selector}'
kubectl get pods -n orders --show-labels
# svc selector: app=orders-api — actual pod label: app=orderskubectl patch svc orders -n orders -p '{"spec":{"selector":{"app":"orders"}}}'
kubectl get endpoints orders -n orders
kubectl exec -n orders test -- curl -s orders.orders.svc.cluster.localWhy: a Service never inspects a Deployment's own spec.selector — it only ever matches live Pod labels against its own, so a Service and the Deployment fronting it can drift out of sync from a single typo with neither object ever erroring. An empty kubectl get endpoints is the fastest possible signal that this is a label-matching problem rather than a network problem — genuine connectivity issues still produce a populated endpoint list, they just fail to reach it.
T16 · Fix a CrashLoopBackOff caused by the probe, not the app (6 points · 5 min)
The inventory Deployment keeps restarting. Its logs look completely healthy right up until each kill.
Your task:
- Read the pod's events for the liveness probe's failure reason, and its previous logs to see what the app was actually doing before it was killed.
- Compare the probe's target port against the port the app is really listening on.
- Correct the Deployment's probe and confirm restarts stop.
Done when: kubectl get pods -n inventory -l app=inventory shows RESTARTS not incrementing over at least two minutes, with no new Liveness failures in kubectl describe pod. (6 points)
Show the worked solution
kubectl describe pod -n inventory inventory-6c9d-xyz | grep -A5 Liveness # Liveness probe failed: Get "http://10.244.1.9:8080/healthz": dial tcp ... connection refused kubectl logs -n inventory inventory-6c9d-xyz --previous # inventory listening on :9090
kubectl patch deploy inventory -n inventory --type=json \
-p='[{"op":"replace","path":"/spec/template/spec/containers/0/livenessProbe/httpGet/port","value":9090}]'
kubectl rollout status deploy/inventory -n inventory
kubectl get pods -n inventory -l app=inventory -wWhy: a CrashLoopBackOff caused by the probe itself, not the app, is diagnosable in one place — kubectl logs --previous shows the container's own last words before kubelet killed it, and if those logs look completely healthy, the probe is the actual defect. kubelet has no way to know 9090 was the real intended port; it only executes what the probe spec says, restarting a perfectly fine container forever until the spec itself is corrected.
T17 · Fix a broken static-pod control-plane component (6 points · 5 min)
After a manual edit, kube-scheduler is CrashLoopBackOff in kube-system. New pods across the cluster stay Pending — nothing is deciding placement.
Your task:
- Check the scheduler's previous logs for its actual startup error.
- Find and correct the bad flag in its static-pod manifest on the control-plane node's disk.
- Confirm the kubelet picks up the fix on its own and scheduling resumes.
Done when: kubectl get pods -n kube-system -l component=kube-scheduler is Running with no new restarts, and a freshly created Pod with no nodeName set leaves Pending promptly. (6 points)
Show the worked solution
kubectl get pods -n kube-system -l component=kube-scheduler kubectl logs -n kube-system kube-scheduler-cp-01 --previous # invalid value "999.999.999.999" for flag --bind-address
# on the control-plane node sudo grep bind-address /etc/kubernetes/manifests/kube-scheduler.yaml sudo sed -i 's/--bind-address=999.999.999.999/--bind-address=127.0.0.1/' /etc/kubernetes/manifests/kube-scheduler.yaml
kubectl get pods -n kube-system -w -l component=kube-scheduler kubectl run probe --image=busybox:1.36 --command -- sleep 60 kubectl get pod probe -o wide # should leave Pending promptly once the scheduler is healthy again
Why: kube-scheduler, kube-apiserver, kube-controller-manager, and etcd on a kubeadm cluster are all static pods — the kubelet on that node reads their manifests directly from /etc/kubernetes/manifests/, not from the API server, which is exactly why the fix is editing a file on disk rather than kubectl edit. The kubelet watches that directory and restarts the static pod automatically the moment the file changes — no kubectl apply, no rollout, sometimes not even an explicit restart. It's also why a broken kube-apiserver manifest doesn't lock you out entirely: the kubelet still reads the local file even while the API server itself is down.
"None of these seventeen tasks are glamorous. Nobody notices the RBAC scope that was actually narrow, the taint that actually kept the batch job off the wrong node, or the static pod that came back on its own because the manifest was fixed instead of guessed at. What I notice is what stopped happening — the on-call page that never fired, the disk that was still there after someone fat-fingered a delete. That's the whole job, most days."
Score yourself
☺ Like you're 10: Add up your points, then do the more useful thing: look at which section lost the most points and go study that section, not the whole syllabus again.
Mark after a short break — grading your own work while still adrenalized from the clock produces generous nonsense. Award full points only when the done-when check actually passed on your cluster, half credit when the resource exists and is broadly right but the check didn't pass, and zero for anything unattempted.
| Task | Domain | Points | Your score |
|---|---|---|---|
| T1 · Join a worker, dedicate it to ingress | Cluster Arch | 7 | |
| T2 · kubeadm upgrade of a control-plane node | Cluster Arch | 6 | |
| T3 · etcd snapshot, taken and verified | Cluster Arch | 6 | |
| T4 · Least-privilege RBAC for a ServiceAccount | Cluster Arch | 6 | |
| T5 · Rollout, break, and roll back a revision | Workloads | 5 | |
| T6 · Dedicate a node with affinity and a taint | Workloads | 5 | |
| T7 · Prevent overlapping CronJob runs | Workloads | 5 | |
| T8 · Default-deny, then one exact hole | Networking | 7 | |
| T9 · Split internal/external Service access | Networking | 7 | |
| T10 · Route two hosts through one Ingress | Networking | 6 | |
| T11 · Retained, resizable compliance volume | Storage | 5 | |
| T12 · Fix a PVC stuck against a static PV | Storage | 5 | |
| T13 · Recover a NotReady node | Troubleshooting | 6 | |
| T14 · Fix a private-registry ImagePullBackOff | Troubleshooting | 6 | |
| T15 · Fix a Service with no endpoints | Troubleshooting | 6 | |
| T16 · Fix a probe-caused CrashLoopBackOff | Troubleshooting | 6 | |
| T17 · Fix a broken static-pod manifest | Troubleshooting | 6 | |
| Total | All five domains | 100 |
Computing your result. The points total 100, so your raw score is your percentage, and the pass mark is 66%. That leaves 34 points spendable — but the more useful arithmetic is per domain, not total: a 70 built from five roughly even domains and a 70 built from four strong domains plus a zeroed-out Troubleshooting are completely different results, and the second one fails the real exam the day the task draw is unkind.
| Domain | Available | Yours | If you scored under two-thirds, go here |
|---|---|---|---|
| Cluster Architecture, Installation & Configuration | 25 | D1 blueprint, kubeadm, control-plane internals. | |
| Workloads & Scheduling | 15 | D2 blueprint, scheduling & resource management. | |
| Services & Networking | 20 | D3 blueprint, networking & the CNI, ingress-nginx. | |
| Storage | 10 | D4 blueprint, storage & the CSI. | |
| Troubleshooting | 30 | D5 blueprint, a troubleshooting methodology, then the Hands-On drills. |
What your score means
☺ Like you're 10: One number doesn't predict a pass — but the shape of it tells you exactly what to practice next, which is more useful anyway.
| Score | Read it as | Next move |
|---|---|---|
| 80–100 | Comfortably ready, with margin for a bad task draw. | Stop grinding tasks. Work the exam-prep checklist, skim exam day — proctoring & environment, and book it. |
| 66–79 | A pass on this paper — but no margin if two tasks go sideways. | Re-drill only your weakest domain, then sit Set 3. Speed work, not new topics. |
| 50–65 | Close. Usually a speed problem plus one weak domain, not a knowledge gap. | A week of timed drills from CKA Practice Tasks, plus the kubectl fluency baseline for anything you kept looking up. |
| Under 50 | Genuine gaps across domains. Booking now wastes the sitting. | Back to the CKA study plan for your two lowest-scoring domains, then the lab track before another mock. |
One pattern worth checking independent of the total: how many tasks did you leave completely unattempted? More than two, and your problem is pacing rather than knowledge — the cheapest thing on this list to fix. Sit a paper again in a week with a hard rule that you touch every task once before returning to any of them.
Remy: 74 out of 100! That clears the bar. I'm booking it tonight.
Timmy: Show me the domain breakdown before you touch that booking page.
Remy: ...Cluster Arch 25, Workloads 15, Networking 19, Storage 10, Troubleshooting 5.
Foxy: You scored ninety percent on four domains and barely touched the one worth thirty. That's not luck, that's the clock running out before you reached it.
Gizmo: Or just skip the review sweep next time — that's eight whole minutes back for more tasks! 🤑
Timmy: The sweep is where Remy would've caught two half-finished troubleshooting tasks, not where he loses points. What he actually needs is the flag-and-move rule — abandon at the budget, not after it.
Professor Owl: Sit it again in a week, Remy — Troubleshooting first this time. If the number jumps without you learning a single new command, you'll never underrate pacing again.
1. What are the five CKA domains and their weights, and why does Troubleshooting alone get 30 minutes of the 120-minute budget on this paper? 2. In T8, what does a default-deny NetworkPolicy block by default, and which specific traffic type do candidates most often forget to re-open? 3. Why does T5's rollback use --to-revision=2 instead of a plain kubectl rollout undo? 4. In T6, why doesn't a matching toleration alone guarantee cruncher lands on highmem-01? 5. Why is the fix for T17 an edit to a file on the node's disk rather than kubectl edit? 6. What's the actual difference between full and half credit when you mark your own paper? 7. Two candidates both score 70 — one evenly across domains, one with a zeroed domain. Why is only one of them exam-ready?
Check your answers
- Troubleshooting 30%, Cluster Architecture 25%, Services & Networking 20%, Workloads & Scheduling 15%, Storage 10% — summing to 100%. Troubleshooting gets the largest time block here because it's worth the most points of any single domain on the real exam, and this paper's budget mirrors that.
- It blocks all ingress and egress traffic for every pod it selects, with no exceptions until a later policy opens one. The type candidates most often forget to re-open is DNS (UDP/TCP 53 to
kube-system) — without it, every other allow rule silently breaks too because names stop resolving. - Because a plain
rollout undoonly ever steps back exactly one revision. Having already rolled forward through a broken revision, an undo with no flag would land back on that broken revision, not the last known-good one —--to-revision=2targets it explicitly. - Because a taint and a matching toleration only cancel out repulsion — they let
cruncherland onhighmem-01, but they don't stop it from landing anywhere else untainted too. Only the added node affinity actually pulls it toward that specific node. - Because
kube-scheduleris a static pod — the kubelet on that node reads its manifest directly from/etc/kubernetes/manifests/on disk, not from the API server, so there's no API object forkubectl editto change in the first place. - Full credit only when the done-when command actually passed on a real cluster; half credit when the resource exists and looks broadly correct but the check itself didn't pass. There's no credit for "I knew how to do that one" without running it.
- Because raw score hides risk. The evenly-scored candidate has margin no matter which domain the real exam happens to weight hardest for them that day; the one with a zeroed domain is one unlucky task draw away from failing, even at the identical total.
That's the whole paper. Score it, write your domain breakdown somewhere you'll actually see it again, and let the two weakest numbers pick your next week of practice. When the numbers stop moving, the remaining work is logistical, not technical: exam day — proctoring & environment covers the sitting itself, Field Notes collects what people actually reported afterward, and the exam-prep checklist is the last thing worth reading before you sit down. See the same domains explained from a different angle on Platform Engineering's CKA page, SRE's CKA page, and DevSecOps's CKA page — and if the CKA is one stop on the way to the full ladder, see the Golden Astronaut course for the other nine CNCF certs plus the LFCS.