CKAD Mock Exam · Set 2
Your second full sitting — same shape as Set 1, entirely new scenarios. Twelve performance-based tasks in one unbroken 120-minute block, weighted to the CKAD curriculum's own 20/20/15/25/20 domain split so the time you spend here maps honestly onto the real exam, not onto whichever domain happens to be easiest to write questions for. Application Environment, Configuration and Security still carries the single largest share — 25 of the 100 points — because it does on the real exam too, not because this paper leans on it deliberately the way a troubleshooting-heavy sitting might. Every task builds or fixes something that actually runs: a multi-container Pod, a blue/green cutover on a live Service, an immutable ConfigMap you can't just edit in place, a Deployment quietly starved by its own namespace's ResourceQuota, a cross-namespace NetworkPolicy. Each has an objective done when check and a worked solution folded away until you've genuinely attempted it. Sit it cold, total the sheet against the published pass mark, and let the domain breakdown decide what you study next — not the raw number.
Set 1 was your first full dress rehearsal in a costume you'd already tried on once. This one is a second dress rehearsal — same play, same running time, same five acts in the same order — but every single prop and line is different, so you can't coast on having memorized last time's answers. That's the whole point of a second paper: if you do noticeably better here than on Set 1, it tells you something real got easier, not that you just got lucky remembering one specific fix. And if the same domain trips you up both times, that's not bad luck either — that's the paper doing exactly its job, pointing at the one place your practice time is actually worth spending.
Before you start — exam conditions
☺ Like you're 10: A dress rehearsal only teaches you something if you actually run the full play — one clock, no script in your pocket, nobody feeding you lines.
Build a disposable cluster the night before: a multi-node kind or minikube setup covers everything here. Install ingress-nginx and a NetworkPolicy-enforcing CNI — Calico or Cilium, N3 needs it, the default bridge silently ignores NetworkPolicy objects — and confirm both helm and kustomize are on your PATH ahead of time; D1 and D2 need them ready, not something you're installing mid-clock. Start one 120-minute timer and don't pause it. Keep only kubernetes.io/docs, kubernetes.io/blog, and — since this is CKAD — the Helm documentation open, matching the real exam's permitted-resource list; no search engine, no AI assistant, no notes from Set 1. Read all twelve tasks first: five minutes spent doing that is the only way to know which of the twelve is going to eat the most time before you've already committed to one.
When a task passes its budgeted minutes below with no passing done-when check, stop, write one line about where you stalled, leave your partial work in place exactly as it sits, and move to the next task. A blank task scores zero every time; a half-finished one graded by a script often doesn't. Benny's version: you are not paid to finish elegantly, you are paid to bank points. Timmy's addition: a fix you believe worked but never verified against the done-when check is worth exactly as much as a blank task — nothing.
Your time budget and domain weights
☺ Like you're 10: The clock gets sliced up ahead of time in the same proportions the real test uses — nothing here is stacked to make one topic feel scarier than it actually is.
These minutes and points track the same five domains and the same 20/20/15/25/20 weights laid out on the CKAD blueprint and budgeted week-by-week on the study plan — unlike a paper deliberately skewed toward one domain, this one is a faithful proportional sample of the real thing.
| Domain | Tasks | Points | Share of paper | Minutes |
|---|---|---|---|---|
| 🦫 Application Design and Build | B1–B2 | 20 | 20% | 20 |
| 🤖 Application Deployment | D1–D2 | 20 | 20% | 20 |
| 🐘 Application Observability and Maintenance | O1–O2 | 15 | 15% | 15 |
| 🐢 Application Environment, Configuration and Security | E1–E3 | 25 | 25% | 25 |
| 🐦 Services and Networking | N1–N3 | 20 | 20% | 20 |
| Total | 12 tasks | 100 | 100% | 100 + 5 read + 15 verify |
Application Design and Build — B1 to B2 (20 points)
☺ Like you're 10: This block is about picking the right shape of container for the job, and correctly stacking more than one of them inside the same Pod when the job needs it.
Background: The Object Model, Kubernetes Architecture, and the multi-container patterns drilled in Capstone Part 2.
B1 · A Pod with an init container and a log-shipping sidecar (12 pts)
Your task: build a Pod named report-generator in namespace reports where an init container fetches a template file into a shared volume before the main container starts, and a second, permanently-running sidecar container tails the main container's log file from a second shared volume and streams it to its own stdout.
Done when: kubectl get pod report-generator -n reports shows 2/2 Ready containers, the init container shows Completed in kubectl describe, and kubectl logs report-generator -n reports -c log-shipper prints lines written by the main container.
Show the worked solution
apiVersion: v1
kind: Pod
metadata:
name: report-generator
namespace: reports
labels: { app: report-generator }
spec:
initContainers:
- name: fetch-template
image: busybox:1.36
command: ["sh", "-c", "wget -qO /templates/report.tmpl http://config-svc.reports.svc/report.tmpl"]
volumeMounts:
- { name: templates, mountPath: /templates }
containers:
- name: generator
image: registry.example.com/report-generator:1.4.0
volumeMounts:
- { name: templates, mountPath: /templates, readOnly: true }
- { name: logs, mountPath: /var/log/app }
- name: log-shipper
image: busybox:1.36
command: ["sh", "-c", "touch /var/log/app/report.log; tail -F /var/log/app/report.log"]
volumeMounts:
- { name: logs, mountPath: /var/log/app, readOnly: true }
volumes:
- { name: templates, emptyDir: {} }
- { name: logs, emptyDir: {} }Why: an init container runs to completion, in order, before any regular container in the Pod starts — that ordering guarantee is exactly what sequences "fetch the template" ahead of "generate the report from it," with no polling or retry loop needed in the main container itself. The sidecar shares the Pod's network namespace and, here, one specific volume with the main container — not the templates volume, which it never needs — a reminder that "shared" in a multi-container Pod means whatever you explicitly mount twice, nothing more. This pairing, init-then-main plus a permanent sidecar, is the two patterns the domain names by name: init and sidecar.
B2 · Replace a sleep-loop anti-pattern with a real CronJob (8 pts)
An existing nightly-cleanup Pod in reports has restartPolicy: Always and a shell script that sleeps until 2am, runs a cleanup, then loops forever — it has been "Running" for months and nobody can tell you whether last night's cleanup actually succeeded.
Done when: the old Pod is deleted, kubectl get cronjob nightly-cleanup -n reports shows schedule 0 2 * * *, and kubectl create job --from=cronjob/nightly-cleanup test-run -n reports produces a Job that reaches Complete.
Show the worked solution
apiVersion: batch/v1
kind: CronJob
metadata: { name: nightly-cleanup, namespace: reports }
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
jobTemplate:
spec:
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: OnFailure
containers:
- name: cleanup
image: registry.example.com/report-cleanup:1.0
command: ["/bin/sh", "-c", "/scripts/cleanup.sh"]kubectl delete pod nightly-cleanup -n reports kubectl apply -f cronjob.yaml kubectl create job --from=cronjob/nightly-cleanup test-run -n reports kubectl get job test-run -n reports -w
Why: a Pod that loops internally never reports success or failure back to the API — kubectl get pods shows Running whether or not last night's cleanup actually worked, and a node failure mid-loop just silently loses the schedule with nothing to reconcile it. A CronJob turns every run into its own Job, an object the API can grade objectively as Complete or Failed — the exact state a grading script, or a 3am on-call engineer, can check without reading a log. concurrencyPolicy: Forbid stops overlap if one run overruns into the next scheduled time; activeDeadlineSeconds is a hard ceiling so a stuck run doesn't hang the namespace forever.
Application Deployment — D1 to D2 (20 points)
☺ Like you're 10: One task teaches you to swap in a new version of a running app without anyone noticing the switch; the other teaches you to keep one set of files and only ever change the small parts that differ per environment.
Background: Workloads & Scheduling for rollout mechanics, and the Helm and Kustomize tool guides.
D1 · Cut over a Service from blue to green without dropping traffic (10 pts)
Namespace catalog runs catalog-blue (Deployment, 3 replicas, label version: blue) behind Service catalog-svc, currently selecting version: blue. Ship catalog-green — same app, image tag 2.4.0, label version: green — verify it's healthy, then cut the Service over to it with zero dropped requests.
Done when: kubectl get endpoints catalog-svc -n catalog lists only green Pod IPs after the cutover, and at no point during the switch did the Service route to a Pod that hadn't yet passed its readiness probe.
Show the worked solution
apiVersion: apps/v1
kind: Deployment
metadata: { name: catalog-green, namespace: catalog }
spec:
replicas: 3
selector: { matchLabels: { app: catalog, version: green } }
template:
metadata: { labels: { app: catalog, version: green } }
spec:
containers:
- name: api
image: registry.example.com/catalog:2.4.0
ports: [{ containerPort: 8080 }]
readinessProbe: { httpGet: { path: /readyz, port: 8080 }, periodSeconds: 5 }kubectl apply -f catalog-green.yaml
kubectl wait --for=condition=Ready pod -l app=catalog,version=green -n catalog --timeout=60s
kubectl patch svc catalog-svc -n catalog -p '{"spec":{"selector":{"app":"catalog","version":"green"}}}'
kubectl get endpoints catalog-svc -n catalog # green IPs only, immediately
kubectl scale deploy/catalog-blue -n catalog --replicas=0 # once you're confidentWhy: plain blue/green with no mesh or rollout controller is really just two independent Deployments and one Service selector — the cutover is a single, atomic Service patch, instantly reversible by patching the selector back to blue. The trap is sequencing: kubectl patch does not wait for anything, so flipping the selector before kubectl wait confirms green's readiness probe has actually passed routes live traffic straight at containers that may not be able to serve it yet. Endpoints, not the Deployment's own status, is the object that reflects what a client actually gets routed to right now.
D2 · A Kustomize overlay that never touches base (10 pts)
base/ holds a working Deployment named catalog, image registry.example.com/catalog:2.4.0, replicas: 1. Build overlays/staging/ that produces staging-catalog with 3 replicas and image tag 2.4.0-staging, without editing a single file under base/.
Done when: kubectl kustomize overlays/staging shows replicas: 3 and the staging image tag, kubectl apply -k overlays/staging/ creates staging-catalog, and base/deployment.yaml is byte-identical to before you started.
Show the worked solution
# overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namePrefix: staging-
patches:
- target: { kind: Deployment, name: catalog }
patch: |-
- op: replace
path: /spec/replicas
value: 3
images:
- name: registry.example.com/catalog
newTag: 2.4.0-stagingkubectl kustomize overlays/staging | grep -E "replicas|image:" kubectl apply -k overlays/staging/ git status --short base/ # empty output — confirms base/ was never touched
Why: Kustomize's entire value proposition is that base/ stays pristine and reusable across every environment — a difference like replica count or image tag lives entirely in the overlay's kustomization.yaml as a patch, never as a forked copy of the manifest. A JSON6902 replace op is more reliable on the exam than a strategic-merge patch for a single scalar field, because it changes exactly the one path named and nothing else. The images: block rewrites the tag without you hand-editing the container spec at all — base/ is not, and should never be, aware staging exists.
Application Observability and Maintenance — O1 to O2 (15 points)
☺ Like you're 10: This block is about noticing when something is actually broken — a manifest the cluster refuses to run at all, or a container that dies before it's finished waking up.
Background: Observability on Kubernetes and A Troubleshooting Methodology; drill the debugging loop itself in Drill — Debug a Stuck Pod.
O1 · A manifest the cluster won't even accept, then a crash that logs hide (8 pts)
Applying invoice-worker.yaml in namespace billing fails outright with no matches for kind "Deployment" in version "apps/v1beta1". Once it applies, the resulting Pods immediately go CrashLoopBackOff, and kubectl logs against the current container shows nothing useful.
Done when: kubectl apply -f invoice-worker.yaml succeeds with no API error, and the Pods reach Running with a stable restart count across two check intervals.
Show the worked solution
# before — two separate bugs stacked on top of each other
apiVersion: apps/v1beta1 # removed entirely in Kubernetes 1.16, not just deprecated
kind: Deployment
metadata: { name: invoice-worker, namespace: billing }
spec:
replicas: 2
template: { ... } # v1beta1 defaulted the selector for you — v1 requires it explicit
# after
apiVersion: apps/v1
kind: Deployment
metadata: { name: invoice-worker, namespace: billing }
spec:
replicas: 2
selector: { matchLabels: { app: invoice-worker } } # now mandatory, must match template labels
template:
metadata: { labels: { app: invoice-worker } }
spec:
containers:
- name: worker
image: registry.example.com/invoice-worker:3.1.0
env:
- { name: FEATURE_FLAG_KEY, value: "billing-v2" } # the actual crash causekubectl logs -n billing <pod> # empty or unhelpful — this container just started kubectl logs -n billing <pod> --previous # panic: missing required env FEATURE_FLAG_KEY kubectl apply -f invoice-worker.yaml kubectl rollout status deploy/invoice-worker -n billing
Why: apps/v1beta1 (and extensions/v1beta1 for Deployments) were removed in Kubernetes 1.16, not merely deprecated — the API server genuinely doesn't recognize the group/version any more, which is why the error names a "kind" the cluster claims doesn't exist rather than issuing a validation warning. This is exactly the failure the "API deprecations" competency exists for: a manifest that worked fine two cluster upgrades ago silently stops applying. The second bug is unrelated and requires kubectl logs --previous specifically — a fresh CrashLoopBackOff container has already replaced the one that actually panicked, so its own current logs are empty by the time you look.
O2 · A slow-starting app killed by its own liveness probe (7 pts)
model-loader in namespace inference takes roughly 50 seconds to load a model file before it can serve traffic. Its livenessProbe — periodSeconds: 10, failureThreshold: 3 — kills the container at around the 30-second mark, every time, before it ever finishes loading.
Done when: the Pod shows zero restarts a full two minutes after creation, and kubectl get endpoints model-loader -n inference gains the Pod's IP only once /readyz genuinely returns 200.
Show the worked solution
startupProbe:
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30
periodSeconds: 2
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet: { path: /readyz, port: 8080 }
periodSeconds: 5Why: startupProbe exists specifically to give a slow-booting container its own generous grace period without loosening livenessProbe's own thresholds — which would otherwise leave a genuinely hung container running far too long once it's actually up. While the startup probe is still failing, kubelet does not evaluate liveness or readiness at all; the moment it succeeds once, kubelet switches over to the regular cadence for the rest of the container's life. Splitting readiness onto its own /readyz endpoint, distinct from /healthz, lets the app report "alive but not yet serving" during a slow load or a heavy pause without kubelet concluding it should be killed.
Application Environment, Configuration and Security — E1 to E3 (25 points)
☺ Like you're 10: This is the biggest block on the whole paper — it's about giving an app exactly the settings, secrets and permissions it needs, and not one thing more.
Background: Scheduling & Resource Management for requests, limits and quotas; RBAC & Admission Control and Security: Defense in Depth for the rest. For ConfigMaps and Secrets specifically, the sibling Platform Engineering course's Configuration & Packaging and Secrets & Workload Identity pages go further than this exam requires; DevSecOps's Kubernetes Security Deep Dive covers E3's territory from the attacker's side.
E1 · Roll out a config change to an immutable ConfigMap (8 pts)
The now-stable catalog Deployment (from D1) reads FEATURE_X from ConfigMap catalog-flags-v1, created with immutable: true — deliberate platform policy, not a bug. Turn FEATURE_X from off to on across every replica, with zero failed Pods and without removing immutability from anything.
Done when: kubectl exec into every catalog Pod shows FEATURE_X=on, and kubectl get cm -n catalog still shows catalog-flags-v1 present, untouched, alongside a new ConfigMap.
Show the worked solution
kubectl create configmap catalog-flags-v2 -n catalog --from-literal=FEATURE_X=on
kubectl patch deploy catalog -n catalog --type=json -p='[
{"op":"replace","path":"/spec/template/spec/containers/0/envFrom/0/configMapRef/name","value":"catalog-flags-v2"}
]'
kubectl rollout status deploy/catalog -n catalog
kubectl exec -n catalog deploy/catalog -- env | grep FEATURE_X # on, on every replicaWhy: immutable: true is deliberately one-way — the API server rejects any change to data, binaryData, or the flag itself once set, precisely so a running Pod's mounted ConfigMap can never change silently underneath it. (Mutable ConfigMaps have a real footgun here: kubelet's own sync interval means different Pods can observe the old and new value for up to a minute after an in-place edit.) The correct pattern is versioning by name — create -v2, repoint the Deployment at it, let a normal rolling update carry the change out safely — the same reasoning Helm's own config-hash-in-name convention encodes automatically. Deleting and recreating catalog-flags-v1 in place would, for a moment, leave in-flight Pods with no ConfigMap to read from at all.
E2 · A ResourceQuota silently starving a Deployment's rollout (9 pts)
Namespace batch has a ResourceQuota capping requests.cpu at 2 cores and a LimitRange that injects a default 500m CPU request onto any container that doesn't set its own. A batch-worker Deployment with 5 replicas and no resources block is stuck at 4 Pods forever — kubectl get pods alone looks unremarkable, just short a replica.
Done when: kubectl get pods -n batch -l app=batch-worker shows all 5 replicas Running, and kubectl describe resourcequota batch-quota -n batch shows requests.cpu used at or under the 2-core hard limit.
Show the worked solution
# kubectl get events -n batch --field-selector involvedObject.kind=ReplicaSet shows:
# Error creating: pods "batch-worker-xxxxx" is forbidden: exceeded quota: batch-quota,
# requested: requests.cpu=500m, used: requests.cpu=2, limited: requests.cpu=2
spec:
template:
spec:
containers:
- name: worker
image: registry.example.com/batch-worker:1.2.0
resources:
requests: { cpu: 300m, memory: 200Mi } # 5 × 300m = 1500m, fits under the 2-core cap
limits: { cpu: 600m, memory: 400Mi }Why: a LimitRange default request only fires when a container declares none at all — set your own and the default is skipped, though the LimitRange's own min/max bounds still apply to whatever you set. ResourceQuota is enforced synchronously, per Pod, at admission — a ReplicaSet that can't fit its next replica within the namespace's remaining budget gets no error surfaced on the Deployment or the missing Pods themselves, just a FailedCreate warning event against the ReplicaSet. That's exactly why kubectl get pods alone can look unremarkable — one replica short reads as "still starting," not "permanently blocked," unless you check describe or events.
E3 · Harden the catalog Deployment's SecurityContext and ServiceAccount (8 pts)
The catalog Deployment (from D1/E1) still runs as root under the namespace's default ServiceAccount, which auto-mounts an API token the app never uses, with no capability restrictions at all. Lock it down.
Done when: kubectl exec into a catalog Pod shows a non-root UID, ls /var/run/secrets/kubernetes.io/serviceaccount from inside the container fails (no token mounted), and the container still starts successfully despite a read-only root filesystem.
Show the worked solution
apiVersion: v1
kind: ServiceAccount
metadata: { name: catalog-sa, namespace: catalog }
automountServiceAccountToken: false
---
spec:
template:
spec:
serviceAccountName: catalog-sa
securityContext: { runAsNonRoot: true, runAsUser: 10001, fsGroup: 2000 }
containers:
- name: api
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- { name: scratch, mountPath: /tmp } # rootfs is read-only — writable scratch space needed
volumes:
- { name: scratch, emptyDir: {} }Why: automountServiceAccountToken: false removes a token the app has no legitimate reason to hold — if a dependency CVE or SSRF ever lets an attacker read files out of the container, there's simply no Kubernetes API credential sitting there to steal. Combined with readOnlyRootFilesystem, allowPrivilegeEscalation: false and dropping every Linux capability, the container's blast radius if compromised shrinks to "can write to one scratch volume, can't escalate, can't touch the API" — which is the point of the domain's Application Security competency, not a box-ticking exercise. The one thing easy to forget: a read-only root filesystem breaks any app that writes anywhere outside an explicit volume, including /tmp by default, which is why the scratch emptyDir above exists.
"Every one of these three shows up in my world as something that just doesn't work, never as a security finding with a name. My feature flag change 'doesn't take effect.' My deploy is 'stuck at 4 out of 5 for some reason.' My container 'won't start' once someone turns on read-only root. I never once think 'immutability,' 'quota,' or 'capabilities' — I think 'why is this broken,' and the actual answer is always two layers below the YAML I personally touched. That's exactly why this is the biggest domain on the exam: almost everything an app developer's day-to-day confusion turns out to be is filed under Config & Security once you trace it down.”
Services and Networking — N1 to N3 (20 points)
☺ Like you're 10: These three teach the cluster to actually deliver traffic where it belongs — to the right container, to the right host, and only to visitors who are allowed in.
Background: Networking & the CNI, ingress-nginx, and Cilium or Calico for policy enforcement; drilled hands-on in Drill — Diagnose a Networking Failure.
N1 · A Service left pointing at a port the app no longer listens on (6 pts)
In namespace shop, Service cart-svc has targetPort: 8080. A recent image rebuild moved the app to listen on 3000 instead, and nobody updated the Service. kubectl get endpoints cart-svc currently shows Pod IPs with no working port behind them.
Done when: a curl from another Pod in shop to http://cart-svc/health returns a real response, not a connection refused or timeout.
Show the worked solution
kubectl describe pod -n shop -l app=cart | grep -i port # confirm: container actually listens on 3000
kubectl patch svc cart-svc -n shop -p '{"spec":{"ports":[{"port":80,"targetPort":3000}]}}'
kubectl run probe -n shop --image=busybox:1.36 --restart=Never --rm -it -- wget -qO- cart-svc/healthWhy: a Service's selector finding the right Pods says nothing about whether the targetPort it forwards to is one the container actually listens on — kubectl get endpoints will happily list real Pod IPs even when every connection to them fails, because endpoint membership is decided by label match alone, not by anything answering on the port. This exact mismatch — a Service's targetPort drifting out of sync after an image or Dockerfile change — is one of the most common self-inflicted "why can't anything reach my app" bugs on the real exam.
N2 · Host-based Ingress routing for two Services (7 pts)
Still in shop: web-svc and api-svc both listen on port 80 and both need to be reachable through one Ingress — shop.example.com to web-svc, api.shop.example.com to api-svc.
Done when: curl -H "Host: shop.example.com" http://<ingress-ip>/ reaches web-svc, and curl -H "Host: api.shop.example.com" http://<ingress-ip>/ reaches api-svc.
Show the worked solution
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: shop, namespace: shop }
spec:
ingressClassName: nginx
rules:
- host: shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: web-svc, port: { number: 80 } } }
- host: api.shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: api-svc, port: { number: 80 } } }Why: host-based routing needs no path juggling or rewrite annotation at all — each rules entry names its own host, and ingress-nginx dispatches on the incoming request's Host header before it ever looks at the path, so two entirely separate rule blocks, each with a plain / prefix, is the whole solution. This is genuinely simpler than Set 1's or the CKA side's path-based routing tasks — worth knowing as the first option to reach for whenever the two backends can live on different subdomains rather than one shared host.
N3 · Allow one cross-namespace caller through a default-deny NetworkPolicy (7 pts)
Namespace payments already enforces default-deny ingress. Monitoring Pods labeled role: monitoring, running in a separate namespace ops, need to scrape /metrics on tcp/9090 from every Pod in payments — and only those specific Pods, not the whole ops namespace.
Done when: a request from a role: monitoring Pod in ops to any payments Pod on 9090 succeeds, and the same request from any other Pod in ops — unlabeled — still times out.
Show the worked solution
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-monitoring-scrape, namespace: payments }
spec:
podSelector: {}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: ops } }
podSelector: { matchLabels: { role: monitoring } }
ports: [{ protocol: TCP, port: 9090 }]Why: putting namespaceSelector and podSelector inside the same from list entry is an AND — the source must be a pod matching role: monitoring and be running inside a namespace matching the selector, both at once. Written as two separate list entries instead, each with only one selector, it becomes an OR: every Pod in ops regardless of label, or every role: monitoring Pod in any namespace — a far wider hole than intended. kubernetes.io/metadata.name is a label every namespace has carried automatically since Kubernetes 1.21, which is why it's safe to select on without depending on someone remembering to label the namespace by hand. Requires a CNI that enforces NetworkPolicy, same as N1 and N2's Ingress controller assume a real ingress-nginx is installed.
Score yourself
☺ Like you're 10: Add up the points, but pay closer attention to whether your misses cluster in one domain — that pattern is worth more than the total number.
Mark only after attempting all twelve. Full credit only when the done-when check actually passes on your cluster — YAML that looks right but was never applied, or a fix you believe worked but never verified, scores nothing, exactly as the real exam grades it.
| Task | Domain | Points | Your score |
|---|---|---|---|
| B1 · init container + log-shipping sidecar | Design and Build | 12 | |
| B2 · CronJob replacing a sleep-loop Pod | Design and Build | 8 | |
| D1 · blue/green Service cutover | Deployment | 10 | |
| D2 · Kustomize staging overlay | Deployment | 10 | |
| O1 · removed API version + hidden crash | Observability and Maintenance | 8 | |
| O2 · startupProbe for a slow app | Observability and Maintenance | 7 | |
| E1 · immutable ConfigMap rollout | Environment, Configuration and Security | 8 | |
| E2 · ResourceQuota starving a rollout | Environment, Configuration and Security | 9 | |
| E3 · SecurityContext + ServiceAccount hardening | Environment, Configuration and Security | 8 | |
| N1 · Service targetPort mismatch | Services and Networking | 6 | |
| N2 · host-based Ingress routing | Services and Networking | 7 | |
| N3 · cross-namespace NetworkPolicy | Services and Networking | 7 | |
| Total | All five domains | 100 |
CKAD's commonly published pass mark is 66% — this paper uses the same figure as a study reference. Below that here, don't just re-read the explanations above; that teaches these twelve exact answers, not the underlying skill. Go re-run the matching drill instead: B1/B2 point back to Capstone Part 2, D1/D2 to the Helm and Kustomize tool guides, O1/O2 to debug a stuck pod, E1/E3 to harden an RBAC configuration, E2 to Scheduling & Resource Management, and N1/N2/N3 to diagnose a networking failure.
This is an independent, unofficial study resource — not affiliated with the CNCF or the Linux Foundation. The 120-minute duration, 66% pass mark, task count and permitted-documentation allowlist referenced on this page all change over time. Confirm current details on the official Linux Foundation CKAD page and the CNCF certification page before you pay for anything. See Platform Engineering's CKAD page for the same exam from an operator's-eye angle, and the Golden Astronaut course if CKAD is one stop on your way to the full Kubestronaut or Golden Kubestronaut ladder.
Benny: E1 got me the first time through. I just tried to kubectl edit the ConfigMap like always, and the API server flatly refused. Took me a second to remember I'd set immutable: true myself, on purpose, months ago.
Timmy: That's not the API server being difficult — that's it protecting you from yourself. Immutability exists so you can't forget you changed something. Versioning the name is the fix, not fighting the flag.
Gizmo: Or just delete the whole namespace and recreate everything from scratch whenever config gets annoying. Clean slate! 🤑
Timmy: That's not a fix, that's an outage you scheduled yourself. E1's actual fix was one new ConfigMap and one field patch — nothing needed to go down at all.
Recon: E2 was the interesting one for me. The ReplicaSet controller kept quietly retrying the fifth Pod, forever, logging a warning nobody was watching. Reconciliation doesn't mean "eventually correct" if the thing blocking it never changes.
Pip: And N1 is the one I keep making other people check twice — Endpoints listing a Pod's IP is not the same promise as "something is actually listening on that port." I deliver the message; I don't grade whether anyone picks up.
Benny: Different domain, same lesson every time, though — the object existing and the object working are two separate questions, and this exam only ever grades the second one.
1. In B1, why does the sidecar only mount the logs volume and not the templates volume the init container wrote to? 2. Why does B2's CronJob give Kubernetes something to grade that the original looping Pod never could? 3. In O1, why does kubectl logs against the current container show nothing useful, and what command actually reveals the crash? 4. In E1, why is creating a new, differently-named ConfigMap the correct fix instead of editing the existing one? 5. In E2, why can a Deployment be short a replica with no error visible from kubectl get pods alone? 6. In N3, what changes if namespaceSelector and podSelector are written as two separate list entries instead of one combined entry?
Check your answers
- Because the templates volume was never mounted into the sidecar's spec at all — sharing in a multi-container Pod means whatever volume you explicitly mount into more than one container, nothing is shared by default just for being in the same Pod.
- A CronJob turns every run into its own Job object, which the API can grade objectively as
CompleteorFailed. The original Pod, looping internally withrestartPolicy: Always, never reported success or failure anywhere Kubernetes could see. - A fresh
CrashLoopBackOffcontainer has already replaced the one that actually panicked, so its current logs are empty.kubectl logs --previousretrieves the terminated container's own output instead. - The ConfigMap was created with
immutable: true, and the API server rejects any change to its data once that flag is set — by design, so a running Pod can never observe its mounted config change silently underneath it. Versioning by name and repointing the Deployment is the supported way to roll out a change. - ResourceQuota is enforced synchronously at Pod admission, and a rejected create shows up as a
FailedCreateevent on the ReplicaSet, not as any visible state on the Deployment or on the Pods that were never created —kubectl get podsalone just looks one replica short. - Combined in one entry, the two selectors are ANDed — the source must match both at once. Split into two entries, each becomes its own independent OR condition, opening access to every Pod in the matching namespace or every Pod with the matching label anywhere, a much wider hole than intended.
That's Set 2. Whichever domain your misses clustered in, loop back to the matching week on the study plan and the drills linked above before your next sitting rather than re-reading this page's answer key on its own. For shorter, single-topic reps instead of a full timed paper, see CKAD Practice Tasks; for the full official domain breakdown this paper is weighted against, see the CKAD blueprint.