Skip to content

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.

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.

Terminal window
alias k=kubectl
export do="--dry-run=client -o yaml"
source <(kubectl completion bash); complete -o default -F __start_kubectl k

Use 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.

  1. Read the task fully; note the namespace and exact names.
  2. Inspect before changing: get, describe, logs, get ep.
  3. Make the smallest change: prefer imperative (create, set, label), then edit.
  4. Validate with the commands below. Never skip this.
  5. Over ~8 minutes? Flag it and move on.
# 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-

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.

  1. Read the current values: k -n q1 get deploy api -o yaml | grep -A8 env:
  2. 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
  3. Edit the Deployment: k -n q1 edit deploy api. Replace each value: x with:
    - name: DB_USER
    valueFrom:
    secretKeyRef: { name: db-creds, key: DB_USER }
    Repeat for DB_PASS and DB_HOST. Delete the old value: lines (an env var can’t have both).
  • k -n q1 get secret db-creds -o jsonpath='{.data.DB_PASS}' | base64 -d → s3cret
  • k -n q1 get deploy api -o yaml | grep -B1 -A3 secretKeyRef → 3 entries
  • k -n q1 rollout status deploy api then k -n q1 exec deploy/api -- env | grep DB_ shows the values

Namespace q2 · target ~4 min

Task. Ingress web-ing has the wrong Service name, wrong port and pathType: Exact. Don’t change the Service.

  1. Inspect the Service first: k -n q2 get svc (note name and PORT, here 8081).
  2. Look at the Ingress: k -n q2 get ing web-ing -o yaml
  3. Edit: k -n q2 edit ing web-ing and set pathType: Prefix, service.name: web-svc, port.number: 8081.
  • k -n q2 describe ing web-ing → Backend shows web-svc:8081 (10.x.x.x:80,...) with endpoints, not <error: service not found>
  • k -n q2 get ep web-svc has addresses

Namespace q3 · target ~3 min

Task. Ingress shop-ing, class nginx, host shop.example.com, path /app (Prefix) → shop-svc:8080.

  1. Check the Service port: k -n q3 get svc shop-svc
  2. 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"
  • k -n q3 get ing shop-ing -o yaml | grep -E 'host|path|pathType|name:|number|ingressClassName'
  • k -n q3 describe ing shop-ing shows the backend with endpoints

Namespace q4 · target ~4 min

Task. 4 existing policies; label pods frontend, backend, database so traffic flows. Don’t touch the policies.

  1. Read selectors only: k -n q4 get netpol -o yaml | grep -B1 -A3 matchLabels (or k -n q4 describe netpol).
  2. Work out the chain: web-egress selects tier=web and allows egress to tier=api; api-traffic selects tier=api, ingress from web, egress to tier=data; data-ingress selects tier=data.
  3. Label:
    Terminal window
    k -n q4 label pod frontend tier=web
    k -n q4 label pod backend tier=api
    k -n q4 label pod database tier=data
  • k -n q4 get pods --show-labels
  • k -n q4 get netpol still lists 4, untouched

Namespace q5 · target ~4 min

Task. Pods aren’t created. Set requests cpu=100m mem=128Mi and limits double those.

  1. Find why: k -n q5 get rs (DESIRED 2, READY 0) then k -n q5 describe rs → “must specify limits…” (quota requires them).
  2. Set both at once:
    Terminal window
    k -n q5 set resources deploy app --requests=cpu=100m,memory=128Mi --limits=cpu=200m,memory=256Mi
  • k -n q5 rollout status deploy app
  • k -n q5 get deploy app -o jsonpath='{.spec.template.spec.containers[0].resources}'
  • k -n q5 describe quota shows usage

Namespace dev · target ~4 min

Task. Request memory 256Mi; limit = half of the namespace maximum.

  1. Find the max: k describe ns dev (LimitRange → Max memory 1Gi) or k -n dev get limitrange -o yaml.
  2. Half of 1Gi = 512Mi:
    Terminal window
    k -n dev set resources deploy cache --requests=memory=256Mi --limits=memory=512Mi
  • k -n dev rollout status deploy cache
  • k -n dev get deploy cache -o jsonpath='{.spec.template.spec.containers[0].resources}'

Namespace — · target ~3 min

Task. Build my-app:1.2 and save in OCI format to ~/ckad-sim/q7/my-app-1.2.tar.

  1. Terminal window
    cd ~/ckad-sim/q7
    podman 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.yaml

Change 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)

  • k -n q8 get pods --show-labels → 4× v1, 1× v2
  • k -n q8 get ep web-svc → 5 addresses
  • k -n q8 get deploy → stable 4/4, canary 1/1

Namespace q9 · target ~2 min

Task. Service shop-svc has no endpoints. Fix it without changing Pod labels.

  1. k -n q9 get ep shop-svc → <none>
  2. Compare: k -n q9 get svc shop-svc -o wide (selector) vs k -n q9 get pods --show-labels.
  3. Fix: k -n q9 set selector svc shop-svc app=shop (or k -n q9 edit svc shop-svc).
  • k -n q9 get ep shop-svc → 3 addresses

Namespace q10 · target ~6 min

Task. Every 30 min, history 2 successful / 2 failed, activeDeadlineSeconds: 60, container must exit.

  1. k -n q10 edit cronjob backup and set:
    spec:
    schedule: "*/30 * * * *"
    successfulJobsHistoryLimit: 2
    failedJobsHistoryLimit: 2
    jobTemplate:
    spec:
    activeDeadlineSeconds: 60
    template:
    spec:
    containers:
    - name: backup
    image: busybox:1.36
    command: ["sh", "-c", "echo done"]
  2. activeDeadlineSeconds goes under jobTemplate.spec, not the pod spec.
  • 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

Namespace q11 · target ~3 min

Task. Add runAsUser: 10000 to the container without losing existing settings.

  1. k -n q11 edit deploy secure-app
  2. Under containers[0].securityContext add one line; keep what’s there:
    securityContext:
    runAsUser: 10000
    allowPrivilegeEscalation: false
    capabilities: { drop: ["ALL"] }
  3. Leave the pod-level securityContext.fsGroup alone.
  • 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

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.

  1. Read the error: k -n q12 logs deploy/pod-lister → cannot list resource "pods"
  2. Terminal window
    k -n q12 create rolebinding pod-reader-rb --role=pod-reader --serviceaccount=q12:pod-reader-sa
    k -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. ```bash
k -n q13 create sa svc-reader-sa
k -n q13 create role svc-reader --verb=get,list --resource=services
k -n q13 create rolebinding svc-reader-rb --role=svc-reader --serviceaccount=q13:svc-reader-sa
k -n q13 set serviceaccount deploy svc-lister svc-reader-sa
  • k -n q13 auth can-i list services --as=system:serviceaccount:q13:svc-reader-sa → yes
  • k -n q13 auth can-i list pods --as=system:serviceaccount:q13:svc-reader-sa → no
  • k -n q13 logs deploy/svc-lister shows a ServiceList

Namespace q14 · target ~2 min

Task. rollme is broken after an update; roll back and confirm.

  1. Terminal window
    k -n q14 rollout history deploy rollme
    k -n q14 rollout undo deploy rollme
    k -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}
  1. 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
  2. k apply -f ~/ckad-sim/q16/ingress.yaml
  • k -n q16 get ing legacy-ing
  • k -n q16 get ing legacy-ing -o yaml | grep -E 'pathType|service' -A3
  • k explain ingress.spec.rules.http.paths --recursive if unsure of field names

Namespace q17 · target ~2 min

Task. Add tier=backend to api-pod, set env=prod on db-pod, remove debug from cache-pod.

  1. Terminal window
    k -n q17 label pod api-pod tier=backend
    k -n q17 label pod db-pod env=prod --overwrite
    k -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.