CKAD Exam: 17 Questions, Fastest Approaches and How to Validate
Distilled from recent CKAD exam reports. Each question below has the task, the fastest approach, how to validate it, and the pitfalls that cost people points. Most of the exam is debugging and fixing real workloads, not building operators.
What showed up (and what didn’t)
Section titled “What showed up (and what didn’t)”Appeared: Secrets/env vars, Ingress (fix + create), NetworkPolicy via labels, resource requests/limits and quotas, Docker/Podman OCI image build, canary Deployment, Service selector, CronJob, securityContext, RBAC (two questions), rollout/rollback, deprecated APIs, pod labels.
Mostly absent in reports: CRDs, Helm, Kustomize, PV/PVC, ConfigMaps, volume mounts, init/sidecar containers. They can appear, but don’t spend 30-40% of prep there.
Setup in the first 30 seconds
Section titled “Setup in the first 30 seconds”alias k=kubectlexport do="--dry-run=client -o yaml"source <(kubectl completion bash); complete -o default -F __start_kubectl kUse k create … $do > f.yaml for anything with a generator (deploy, job, cronjob, ingress, role, secret) and k explain <path> --recursive when unsure of a field.
Workflow for every question
Section titled “Workflow for every question”- Read the task fully; note the namespace and exact names.
- Inspect before changing:
get,describe,logs,get ep. - Make the smallest change: prefer imperative (
create,set,label), thenedit. - Validate with the commands below. Never skip this.
- Over ~8 minutes? Flag it and move on.
Cheat sheet
Section titled “Cheat sheet”| # | Task | Fastest path |
|---|---|---|
| 1 | Env vars → Secret | k create secret generic db-creds --from-literal=… → k edit deploy api (valueFrom.secretKeyRef) |
| 2 | Fix a broken Ingress | k get svc → k edit ing (name, port, pathType) |
| 3 | Create an Ingress | k create ingress shop-ing --class=nginx --rule="host/app*=svc:8080" |
| 4 | NetworkPolicy via labels | k get netpol -o yaml | grep -A3 matchLabels → k label pod … |
| 5 | Resources vs ResourceQuota | k describe rs → k set resources deploy app --requests=… --limits=… |
| 6 | Limit = half of namespace max | k describe ns dev → k set resources … --limits=memory=<max/2> |
| 7 | Build image, save as OCI | podman build -t my-app:1.2 . → podman save --format oci-archive -o f.tar my-app:1.2 |
| 8 | Canary deployment | k create deploy … --dry-run=client -o yaml → fix labels/selector → apply |
| 9 | Fix Service selector | k get ep → k set selector svc shop-svc app=shop |
| 10 | Fix a CronJob | k edit cronjob (schedule, history limits, jobTemplate.spec.activeDeadlineSeconds, exiting command) |
| 11 | SecurityContext merge | k edit deploy → add runAsUser: 10000 inside the existing block |
| 12 | RBAC: bind existing Role | k logs → k create rolebinding … --role --serviceaccount=ns:sa → k set serviceaccount |
| 13 | RBAC: build from logs | k create sa/role/rolebinding → k set serviceaccount → k auth can-i |
| 14 | Rollback a broken update | k rollout undo deploy x → k rollout status |
| 15 | Rollout from a file | edit file → k apply -f (never replace --force) |
| 16 | Deprecated API fix | k create ingress … --dry-run=client -o yaml or fix apiVersion/service.name/pathType |
| 17 | Change Pod labels | k label pod p k=v [--overwrite] · k label pod p k- |
Q1: Env vars → Secret
Section titled “Q1: Env vars → Secret”Namespace q1 · target ~7 min
Task. Create Secret db-creds with DB_USER, DB_PASS, DB_HOST; switch the Deployment api env vars to valueFrom.secretKeyRef.
Fastest approach
Section titled “Fastest approach”- Read the current values:
k -n q1 get deploy api -o yaml | grep -A8 env: - Create the Secret (keys = env var names):
Terminal window k -n q1 create secret generic db-creds \--from-literal=DB_USER=admin --from-literal=DB_PASS=s3cret --from-literal=DB_HOST=db.internal - Edit the Deployment:
k -n q1 edit deploy api. Replace eachvalue: xwith:Repeat for DB_PASS and DB_HOST. Delete the old- name: DB_USERvalueFrom:secretKeyRef: { name: db-creds, key: DB_USER }value:lines (an env var can’t have both).
Validate
Section titled “Validate”k -n q1 get secret db-creds -o jsonpath='{.data.DB_PASS}' | base64 -d→s3cretk -n q1 get deploy api -o yaml | grep -B1 -A3 secretKeyRef→ 3 entriesk -n q1 rollout status deploy apithenk -n q1 exec deploy/api -- env | grep DB_shows the values
Q2: Fix a broken Ingress
Section titled “Q2: Fix a broken Ingress”Namespace q2 · target ~4 min
Task. Ingress web-ing has the wrong Service name, wrong port and pathType: Exact. Don’t change the Service.
Fastest approach
Section titled “Fastest approach”- Inspect the Service first:
k -n q2 get svc(note name and PORT, here 8081). - Look at the Ingress:
k -n q2 get ing web-ing -o yaml - Edit:
k -n q2 edit ing web-ingand setpathType: Prefix,service.name: web-svc,port.number: 8081.
Validate
Section titled “Validate”k -n q2 describe ing web-ing→ Backend showsweb-svc:8081 (10.x.x.x:80,...)with endpoints, not<error: service not found>k -n q2 get ep web-svchas addresses
Q3: Create an Ingress
Section titled “Q3: Create an Ingress”Namespace q3 · target ~3 min
Task. Ingress shop-ing, class nginx, host shop.example.com, path /app (Prefix) → shop-svc:8080.
Fastest approach
Section titled “Fastest approach”- Check the Service port:
k -n q3 get svc shop-svc - Generate it in one line (trailing
*means pathType Prefix):Terminal window k -n q3 create ingress shop-ing --class=nginx --rule="shop.example.com/app*=shop-svc:8080"
Validate
Section titled “Validate”k -n q3 get ing shop-ing -o yaml | grep -E 'host|path|pathType|name:|number|ingressClassName'k -n q3 describe ing shop-ingshows the backend with endpoints
Q4: NetworkPolicy via labels
Section titled “Q4: NetworkPolicy via labels”Namespace q4 · target ~4 min
Task. 4 existing policies; label pods frontend, backend, database so traffic flows. Don’t touch the policies.
Fastest approach
Section titled “Fastest approach”- Read selectors only:
k -n q4 get netpol -o yaml | grep -B1 -A3 matchLabels(ork -n q4 describe netpol). - Work out the chain:
web-egressselectstier=weband allows egress totier=api;api-trafficselectstier=api, ingress from web, egress totier=data;data-ingressselectstier=data. - Label:
Terminal window k -n q4 label pod frontend tier=webk -n q4 label pod backend tier=apik -n q4 label pod database tier=data
Validate
Section titled “Validate”k -n q4 get pods --show-labelsk -n q4 get netpolstill lists 4, untouched
Q5: Resources vs ResourceQuota
Section titled “Q5: Resources vs ResourceQuota”Namespace q5 · target ~4 min
Task. Pods aren’t created. Set requests cpu=100m mem=128Mi and limits double those.
Fastest approach
Section titled “Fastest approach”- Find why:
k -n q5 get rs(DESIRED 2, READY 0) thenk -n q5 describe rs→ “must specify limits…” (quota requires them). - Set both at once:
Terminal window k -n q5 set resources deploy app --requests=cpu=100m,memory=128Mi --limits=cpu=200m,memory=256Mi
Validate
Section titled “Validate”k -n q5 rollout status deploy appk -n q5 get deploy app -o jsonpath='{.spec.template.spec.containers[0].resources}'k -n q5 describe quotashows usage
Q6: Limit = half of namespace max
Section titled “Q6: Limit = half of namespace max”Namespace dev · target ~4 min
Task. Request memory 256Mi; limit = half of the namespace maximum.
Fastest approach
Section titled “Fastest approach”- Find the max:
k describe ns dev(LimitRange →Max memory 1Gi) ork -n dev get limitrange -o yaml. - Half of 1Gi = 512Mi:
Terminal window k -n dev set resources deploy cache --requests=memory=256Mi --limits=memory=512Mi
Validate
Section titled “Validate”k -n dev rollout status deploy cachek -n dev get deploy cache -o jsonpath='{.spec.template.spec.containers[0].resources}'
Q7: Build image, save as OCI
Section titled “Q7: Build image, save as OCI”Namespace — · target ~3 min
Task. Build my-app:1.2 and save in OCI format to ~/ckad-sim/q7/my-app-1.2.tar.
Fastest approach
Section titled “Fastest approach”-
Terminal window cd ~/ckad-sim/q7podman build -t my-app:1.2 .podman save --format oci-archive -o ~/ckad-sim/q7/my-app-1.2.tar my-app:1.2
2. On the real exam use the exact path from the question (absolute).
### Validate
- `podman images | grep my-app` shows `1.2`- `tar -tf ~/ckad-sim/q7/my-app-1.2.tar` lists `oci-layout`, `index.json`, `blobs/`- `ls -lh` the tar (non-empty)
:::caution[Pitfalls]- `podman save` needs `--format oci-archive` for OCI; plain `docker-archive` has `manifest.json` instead of `oci-layout`.- Tag must be `name:version` exactly.:::
## Q8: Canary deployment
*Namespace `q8` · target ~6 min*
**Task.** `web-canary`: 1 replica, nginx:1.26-alpine, labels app=web + version=v2; the Service must select it too.
### Fastest approach
1. Look at the Service selector: `k -n q8 get svc web-svc -o wide` (selector `app=web`) and the stable labels: `k -n q8 get deploy web-stable -o yaml`.2. Generate and edit: ```bash k -n q8 create deploy web-canary --image=nginx:1.26-alpine --replicas=1 --dry-run=client -o yaml > c.yaml vim c.yamlChange both selector.matchLabels and template.metadata.labels to app: web + version: v2 (also metadata.labels if present). 3. k apply -f c.yaml (don’t touch the Service)
Validate
Section titled “Validate”k -n q8 get pods --show-labels→ 4× v1, 1× v2k -n q8 get ep web-svc→ 5 addressesk -n q8 get deploy→ stable 4/4, canary 1/1
Q9: Fix Service selector
Section titled “Q9: Fix Service selector”Namespace q9 · target ~2 min
Task. Service shop-svc has no endpoints. Fix it without changing Pod labels.
Fastest approach
Section titled “Fastest approach”k -n q9 get ep shop-svc→<none>- Compare:
k -n q9 get svc shop-svc -o wide(selector) vsk -n q9 get pods --show-labels. - Fix:
k -n q9 set selector svc shop-svc app=shop(ork -n q9 edit svc shop-svc).
Validate
Section titled “Validate”k -n q9 get ep shop-svc→ 3 addresses
Q10: Fix a CronJob
Section titled “Q10: Fix a CronJob”Namespace q10 · target ~6 min
Task. Every 30 min, history 2 successful / 2 failed, activeDeadlineSeconds: 60, container must exit.
Fastest approach
Section titled “Fastest approach”k -n q10 edit cronjob backupand set:spec:schedule: "*/30 * * * *"successfulJobsHistoryLimit: 2failedJobsHistoryLimit: 2jobTemplate:spec:activeDeadlineSeconds: 60template:spec:containers:- name: backupimage: busybox:1.36command: ["sh", "-c", "echo done"]activeDeadlineSecondsgoes underjobTemplate.spec, not the pod spec.
Validate
Section titled “Validate”k -n q10 get cronjob backup -o yaml | grep -E 'schedule|HistoryLimit|activeDeadline|command' -A1- Run it now:
k -n q10 create job t1 --from=cronjob/backup && k -n q10 wait --for=condition=complete job/t1 --timeout=60s k -n q10 logs job/t1→done
Q11: SecurityContext merge
Section titled “Q11: SecurityContext merge”Namespace q11 · target ~3 min
Task. Add runAsUser: 10000 to the container without losing existing settings.
Fastest approach
Section titled “Fastest approach”k -n q11 edit deploy secure-app- Under
containers[0].securityContextadd one line; keep what’s there:securityContext:runAsUser: 10000allowPrivilegeEscalation: falsecapabilities: { drop: ["ALL"] } - Leave the pod-level
securityContext.fsGroupalone.
Validate
Section titled “Validate”k -n q11 get deploy secure-app -o jsonpath='{.spec.template.spec.containers[0].securityContext}'k -n q11 exec deploy/secure-app -- id→uid=10000
Q12: RBAC: bind existing Role
Section titled “Q12: RBAC: bind existing Role”Namespace q12 · target ~4 min
Task. SA pod-reader-sa and Role pod-reader exist. Create the RoleBinding and use the SA in the Deployment.
Fastest approach
Section titled “Fastest approach”- Read the error:
k -n q12 logs deploy/pod-lister→cannot list resource "pods" -
Terminal window k -n q12 create rolebinding pod-reader-rb --role=pod-reader --serviceaccount=q12:pod-reader-sak -n q12 set serviceaccount deploy pod-lister pod-reader-sa
### Validate
- `k -n q12 auth can-i list pods --as=system:serviceaccount:q12:pod-reader-sa` → `yes`- `k -n q12 rollout status deploy pod-lister`; `k -n q12 logs deploy/pod-lister` now shows a PodList
:::caution[Pitfalls]- `--serviceaccount` format is `namespace:name`.- Check the Role verbs match the error (`k -n q12 describe role pod-reader`).:::
## Q13: RBAC: build from logs
*Namespace `q13` · target ~6 min*
**Task.** Create SA, Role, RoleBinding granting only what the logs ask for, and assign it to the Deployment.
### Fastest approach
1. `k -n q13 logs deploy/svc-lister` → forbidden to list **services**.2. ```bashk -n q13 create sa svc-reader-sak -n q13 create role svc-reader --verb=get,list --resource=servicesk -n q13 create rolebinding svc-reader-rb --role=svc-reader --serviceaccount=q13:svc-reader-sak -n q13 set serviceaccount deploy svc-lister svc-reader-saValidate
Section titled “Validate”k -n q13 auth can-i list services --as=system:serviceaccount:q13:svc-reader-sa→ yesk -n q13 auth can-i list pods --as=system:serviceaccount:q13:svc-reader-sa→ nok -n q13 logs deploy/svc-listershows a ServiceList
Q14: Rollback a broken update
Section titled “Q14: Rollback a broken update”Namespace q14 · target ~2 min
Task. rollme is broken after an update; roll back and confirm.
Fastest approach
Section titled “Fastest approach”-
Terminal window k -n q14 rollout history deploy rollmek -n q14 rollout undo deploy rollmek -n q14 rollout status deploy rollme
2. Specific revision: `k -n q14 rollout undo deploy rollme --to-revision=1`
### Validate
- `k -n q14 get deploy rollme -o jsonpath='{.spec.template.spec.containers[0].image}'` → `nginx:1.25-alpine`- `k -n q14 get pods` 3/3 Running
:::caution[Pitfalls]- If `rollout status` hangs but pods are fine, try `k rollout resume deploy rollme`.- `rollout undo` only works if history exists, which is why you apply changes with `kubectl apply`, not `replace --force`.:::
## Q15: Rollout from a file
*Namespace `q15` · target ~4 min*
**Task.** Update `~/ckad-sim/q15/deploy.yaml` to image nginx:1.26-alpine and maxSurge 5%; apply it.
### Fastest approach
1. `cd ~/ckad-sim/q15 && cp deploy.yaml /tmp/orig.yaml` (remember the original image!)2. Edit the file: `image: nginx:1.26-alpine`, `maxSurge: 5%`.3. `k apply -f deploy.yaml` and `k -n q15 rollout status deploy rollapp`
### Validate
- `k -n q15 rollout history deploy rollapp` → 2 revisions- `k -n q15 get deploy rollapp -o jsonpath='{.spec.strategy.rollingUpdate.maxSurge}'` → `5%`- `k -n q15 get deploy rollapp -o jsonpath='{.spec.template.spec.containers[0].image}'`
:::caution[Pitfalls]- **Never** `kubectl replace --force` here: it recreates the Deployment, leaving 1 revision, so `rollout undo` is useless.- Status stuck on "Waiting…"? `k rollout resume deploy rollapp`.:::
## Q16: Deprecated API fix
*Namespace `q16` · target ~4 min*
**Task.** `~/ckad-sim/q16/ingress.yaml` uses `v1beta1` and `serviceName/servicePort`; fix and apply.
### Fastest approach
1. See the failure: `k apply -f ~/ckad-sim/q16/ingress.yaml` → no matches for kind in `v1beta1`.2. Fix by hand: ```yaml apiVersion: networking.k8s.io/v1 ... - path: / pathType: Prefix backend: service: name: legacy-svc port: {number: 80}- Or regenerate:
k -n q16 create ingress legacy-ing --rule="legacy.example.com/*=legacy-svc:80" --dry-run=client -o yaml > ~/ckad-sim/q16/ingress.yaml k apply -f ~/ckad-sim/q16/ingress.yaml
Validate
Section titled “Validate”k -n q16 get ing legacy-ingk -n q16 get ing legacy-ing -o yaml | grep -E 'pathType|service' -A3k explain ingress.spec.rules.http.paths --recursiveif unsure of field names
Q17: Change Pod labels
Section titled “Q17: Change Pod labels”Namespace q17 · target ~2 min
Task. Add tier=backend to api-pod, set env=prod on db-pod, remove debug from cache-pod.
Fastest approach
Section titled “Fastest approach”-
Terminal window k -n q17 label pod api-pod tier=backendk -n q17 label pod db-pod env=prod --overwritek -n q17 label pod cache-pod debug-
### Validate
- `k -n q17 get pods --show-labels`- Filter test: `k -n q17 get pods -l tier=backend`
:::caution[Pitfalls]- `kubectl label`, not `labels`.- Trailing `-` removes a label; `--overwrite` changes an existing one.:::
## Rollout tips that cost people time
- Use `kubectl apply`, **not** `kubectl replace --force`, for rollout questions. Force-replace recreates the Deployment, leaving a single revision, so `rollout undo` has nothing to go back to.- Write down the original image before editing.- If `kubectl rollout status` keeps saying "Waiting…" and you are sure your change is right, try `kubectl rollout resume deployment <name>`.- `kubectl label pod …` works; `kubectl labels` does not exist.
## Practice
Practice these tasks against a real kind cluster with a local simulator that sets up each scenario and grades your fix with kubectl: `npm run sim -- start`, `check <n>` after each question, `grade` at the end.