Certified Kubernetes Application Developer (CKAD) #21 Full-Length Practice Exam — 18 Tasks with Solutions

From #1 the exam environment through #20 exam tips, we have covered every domain once. The final post of this series is not one you read but one you solve. Just like the real CKAD, it gathers 18 tasks that integrate every domain in one place. These are not multiple choice — they are hands-on scenarios where you build resources directly in an empty terminal, and each task carries a point value.

The recommended time limit is 2 hours, the same as the real exam. The pass line is 66%, scored by summing the point values of all 18 tasks. If you get stuck on a task, mark it, move on, and bank points from the easier tasks first — that is the way over the pass line.

For each task, solve it fully on your own first, then unfold the solution. If you read the solution first, your hands never learn it. Each solution includes the kubectl commands, the YAML you need, and an explanation that points out why you solve it that way and the traps people commonly fall into.

How to take it #

  1. Spin up a local cluster with minikube or kind. To grade the NetworkPolicy task accurately, you want an environment with a CNI such as Calico installed (minikube start --cni=calico).
  2. For each task, switch to the specified context and namespace first. As this series has repeated, a misconfigured context or namespace scores 0 even if your answer is correct.
k config use-context <the context the question specifies>
k config set-context --current --namespace=<the question's namespace>
  1. Create the namespaces the tasks need ahead of time, before you start.
for ns in web build batch data sec net rbac probe cfg deploy; do
  kubectl create namespace $ns 2>/dev/null
done
  1. Solve all 18 to the end, then unfold the solutions and grade them in one pass. Peeking at solutions mid-exam dulls your sense of the real thing. Applying the alias k=kubectl and export do="--dry-run=client -o yaml" setup from #1 first will save you time.

Domain distribution #

The 18 tasks are arranged to match the domain weights of the real CKAD.

#DomainTasksTask numbers
1Application Design and Build41, 2, 3, 4
2Application Deployment45, 6, 7, 8
3Observability and Maintenance39, 10, 11
4Environment,Configuration,Security412, 13, 14, 15
5Services and Networking316, 17, 18

The points reflect the domain weights and task difficulty, totaling 100. The scoring criteria are laid out at the end of the post.


Task 1 (5 points): Application Design and Build #

In namespace web, create a Deployment front with the nginx image and replicas set to 3. Then create a ClusterIP Service front-svc that receives traffic on port 80 and forwards it to the container’s port 80.

Solution
k -n web create deploy front --image=nginx --replicas=3
k -n web expose deploy front --name=front-svc --port=80 --target-port=80

Explanation: For simple creation tasks, the fastest path is to finish with imperative generators rather than hand-writing the manifest. expose automatically pulls the Deployment’s selector, so you don’t need to write the Service’s selector yourself. If you omit --target-port, it is set to the same value as --port, so you can drop it when the two are equal.

Task 2 (6 points): Application Design and Build #

In namespace web, create a Pod cache-loader. The Pod runs the redis image as its main container, but before that, an init container wait-dns using the busybox image runs until nslookup front-svc.web.svc.cluster.local; do sleep 2; done to wait until the Service DNS is ready, and only then does the main container start.

Solution
k -n web run cache-loader --image=redis $do > cache.yaml

Add the init container to the generated skeleton.

apiVersion: v1
kind: Pod
metadata:
  name: cache-loader
  namespace: web
spec:
  initContainers:
    - name: wait-dns
      image: busybox
      command: ["sh", "-c", "until nslookup front-svc.web.svc.cluster.local; do sleep 2; done"]
  containers:
    - name: cache-loader
      image: redis
k apply -f cache.yaml

Explanation: Init containers run in order before the main container and must succeed before the next step proceeds. The trap is that when you give command in shell form, shell syntax like until won’t work unless you wrap it in ["sh", "-c", "..."]. The init container only finishes once the Service from Task 1 exists, so the two tasks can be verified together.

Task 3 (6 points): Application Design and Build #

In namespace web, create a Pod printer. Use the busybox image, but when the container starts it must run echo CKAD-2026 && sleep 3600. Override the image’s default entrypoint with this command.

Solution
k -n web run printer --image=busybox --command -- sh -c "echo CKAD-2026 && sleep 3600"

The generated manifest looks like this.

spec:
  containers:
    - name: printer
      image: busybox
      command: ["sh", "-c", "echo CKAD-2026 && sleep 3600"]

Explanation: When you add --command to kubectl run, the tokens after -- go into the container’s command (Docker’s entrypoint). Drop --command and the same tokens go into args, acting as arguments to the default entrypoint, which is not what you intended. Because the command itself contains &&, you must wrap it in sh -c for the shell to interpret it.

Task 4 (5 points): Application Design and Build #

In namespace build, create a Pod web-static using the container image httpd:2.4, and attach the labels tier=frontend and env=staging. Then write the command that lists only the Pods with the label env=staging.

Solution
k -n build run web-static --image=httpd:2.4 --labels="tier=frontend,env=staging"
k -n build get pods -l env=staging

Explanation: --labels attaches multiple labels at once, separated by commas. A label selector query takes the form -l key=value; omitting the value as in -l env selects every Pod that has the key, and -l '!env' selects Pods that lack the key. Labels are the criteria the grading scripts use to find resources throughout CKAD, so there must be no typos.


Task 5 (6 points): Application Deployment #

In namespace deploy, create a Deployment api with the nginx:1.25 image and replicas 4. Then roll it forward to nginx:1.26, and once the rollout finishes, roll it back to the previous version. Finally, write the command that confirms the current image is nginx:1.25.

Solution
k -n deploy create deploy api --image=nginx:1.25 --replicas=4
k -n deploy set image deploy/api nginx=nginx:1.26
k -n deploy rollout status deploy/api
k -n deploy rollout undo deploy/api
k -n deploy rollout status deploy/api
k -n deploy get deploy api -o jsonpath='{.spec.template.spec.containers[0].image}'

Explanation: The container name in set image must match the default container name created by create deploy. Here you need to check the container name the generator assigned — not the name nginx that happens to match the Deployment name. The container name for kubectl create deploy api --image=nginx:1.25 is nginx, the first token of the image. rollout undo reverts to the immediately preceding revision; to target a specific revision, use --to-revision=N.

Task 6 (6 points): Application Deployment #

In namespace batch, create a Job pi-calc. Run perl -Mbignum=bpi -wle "print bpi(200)" with the perl image, set it to require 4 completions (completions) and run up to 2 at a time (parallelism). Limit retries to a maximum of 3.

Solution
k -n batch create job pi-calc --image=perl $do \
  -- perl -Mbignum=bpi -wle "print bpi(200)" > pi.yaml

Add completions, parallelism, and backoffLimit to the generated skeleton.

apiVersion: batch/v1
kind: Job
metadata:
  name: pi-calc
  namespace: batch
spec:
  completions: 4
  parallelism: 2
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: pi-calc
          image: perl
          command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(200)"]
k apply -f pi.yaml

Explanation: completions is the number of Pods that must succeed, parallelism is how many run at once, and backoffLimit is the retry cap on failure. The generator does not create these three fields, so add them directly to the dry-run skeleton. A Job’s restartPolicy allows only Never or OnFailure; Always is rejected.

Task 7 (5 points): Application Deployment #

In namespace batch, create a CronJob cleanup. Run date; echo cleanup done every 5 minutes with the busybox image, and keep only 3 successful Job records and 1 failed Job record.

Solution
k -n batch create cronjob cleanup --image=busybox \
  --schedule="*/5 * * * *" $do -- sh -c "date; echo cleanup done" > cj.yaml

Add the history limits to the generated skeleton.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: cleanup
  namespace: batch
spec:
  schedule: "*/5 * * * *"
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: cleanup
              image: busybox
              command: ["sh", "-c", "date; echo cleanup done"]
k apply -f cj.yaml

Explanation: successfulJobsHistoryLimit and failedJobsHistoryLimit go directly under CronJob.spec, not inside jobTemplate. The schedule expression has 5 fields — minute, hour, day, month, weekday — and */5 means every 5 minutes. Because the command contains a semicolon, it is wrapped in sh -c.

Task 8 (5 points): Application Deployment #

In namespace deploy, build a kustomize overlay with the following two files. base/deployment.yaml is a Deployment site with the nginx image, and the overlay must apply the common label app=site and the image tag nginx:1.27, then deploy with kubectl apply -k.

Solution

base/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: site
spec:
  replicas: 1
  selector:
    matchLabels:
      app: site
  template:
    metadata:
      labels:
        app: site
    spec:
      containers:
        - name: nginx
          image: nginx

base/kustomization.yaml:

resources:
  - deployment.yaml

overlay/kustomization.yaml:

resources:
  - ../base
labels:
  - pairs:
      app: site
    includeSelectors: false
images:
  - name: nginx
    newTag: "1.27"
k -n deploy apply -k overlay

Explanation: The kustomize images transformer does not edit the manifest directly — it only changes the tag of images whose names match. For label transforms, recent kustomize recommends the labels entry over commonLabels, and you set includeSelectors: false when you don’t want to change selectors too. apply -k finds the kustomization.yaml in the directory and applies the build result.


Task 9 (6 points): Observability and Maintenance #

In namespace probe, create a Pod health. Use the nginx image, and attach a liveness probe that sends an HTTP GET / to the container’s port 80 every 5 seconds, and a readiness probe that checks the same path every 3 seconds starting 5 seconds after startup.

Solution
k -n probe run health --image=nginx $do > health.yaml

Add the two probes to the generated skeleton.

spec:
  containers:
    - name: health
      image: nginx
      ports:
        - containerPort: 80
      livenessProbe:
        httpGet:
          path: /
          port: 80
        periodSeconds: 5
      readinessProbe:
        httpGet:
          path: /
          port: 80
        initialDelaySeconds: 5
        periodSeconds: 3
k apply -f health.yaml

Explanation: A liveness probe restarts the container on failure, while a readiness probe removes it from the Service endpoints on failure. initialDelaySeconds is the wait before the first check, and periodSeconds is the interval between checks. A probe is one of three types — httpGet, exec, or tcpSocket — and when the field paths confuse you, check them with k explain pod.spec.containers.livenessProbe.

Task 10 (5 points): Observability and Maintenance #

In namespace probe, a Pod broken is already running in CrashLoopBackOff state. Diagnose the cause and save the previous logs of the most recently terminated container to the file /tmp/broken.log.

Solution
k -n probe describe pod broken
k -n probe logs broken --previous > /tmp/broken.log

Explanation: CrashLoopBackOff is a state where the container repeatedly terminates, so the currently running log may be empty. The --previous (-p) flag is the key to pulling the logs of the container that terminated just before. The Events in describe and the Reason and Exit Code under Last State reveal the cause — you read OOMKilled, exit code 1, and the like right here.

Task 11 (5 points): Observability and Maintenance #

You want to find the Pod with the highest CPU usage across the whole cluster. Write the command that prints the resource usage of Pods in all namespaces, sorted in descending order by CPU. Assume metrics-server is installed.

Solution
k top pod -A --sort-by=cpu

Explanation: kubectl top reads metrics-server data to show real-time usage. -A is the abbreviation for --all-namespaces, and --sort-by=cpu or --sort-by=memory sets the sort criterion. Without metrics-server you get error: Metrics API not available, so on a local cluster you need to install it ahead of time with something like minikube addons enable metrics-server.


Task 12 (7 points): Environment,Configuration,Security #

In namespace cfg, create a ConfigMap app-cfg with the keys APP_MODE=production and LOG_LEVEL=info. Then create a Pod reader (busybox, sleep 3600), injecting APP_MODE as an environment variable while mounting the entire ConfigMap as a volume at the path /etc/config.

Solution
k -n cfg create configmap app-cfg \
  --from-literal=APP_MODE=production --from-literal=LOG_LEVEL=info
k -n cfg run reader --image=busybox $do --command -- sleep 3600 > reader.yaml

Add env and volume to the generated skeleton.

spec:
  containers:
    - name: reader
      image: busybox
      command: ["sleep", "3600"]
      env:
        - name: APP_MODE
          valueFrom:
            configMapKeyRef:
              name: app-cfg
              key: APP_MODE
      volumeMounts:
        - name: cfg-vol
          mountPath: /etc/config
  volumes:
    - name: cfg-vol
      configMap:
        name: app-cfg
k apply -f reader.yaml

Explanation: To give a single key as env, use configMapKeyRef; to give the whole thing as env, use envFrom.configMapRef; to mount it as files, use volumes.configMap. When you mount it as a volume, each key becomes a file like /etc/config/APP_MODE, and unlike env, a ConfigMap update is reflected automatically. This difference is the core of #13.

Task 13 (6 points): Environment,Configuration,Security #

In namespace sec, create a Secret db-cred with the keys username=admin and password=s3cr3t. Then, in a Pod db-client (busybox, sleep 3600), inject password as the environment variable DB_PASSWORD.

Solution
k -n sec create secret generic db-cred \
  --from-literal=username=admin --from-literal=password=s3cr3t
k -n sec run db-client --image=busybox $do --command -- sleep 3600 > db.yaml

Add env to the generated skeleton.

spec:
  containers:
    - name: db-client
      image: busybox
      command: ["sleep", "3600"]
      env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-cred
              key: password
k apply -f db.yaml

Explanation: A single Secret key is pulled with secretKeyRef, which has the same structure as ConfigMap’s configMapKeyRef and differs only in the kind of reference. kubectl create secret automatically base64-encodes the value, so you don’t encode it yourself. To verify the value, decode it with k -n sec get secret db-cred -o jsonpath='{.data.password}' | base64 -d.

Task 14 (6 points): Environment,Configuration,Security #

In namespace sec, create a Pod hardened. Use the nginx image but run the container as UID 1000 and GID 3000, disallow privilege escalation (allowPrivilegeEscalation: false), and make the root filesystem read-only.

Solution
k -n sec run hardened --image=nginx $do > hardened.yaml

Add securityContext to the generated skeleton.

spec:
  containers:
    - name: hardened
      image: nginx
      securityContext:
        runAsUser: 1000
        runAsGroup: 3000
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
k apply -f hardened.yaml

Explanation: runAsUser and runAsGroup can be set at both the Pod level and the container level, with the container level taking precedence. By contrast, allowPrivilegeEscalation and readOnlyRootFilesystem exist only in the container-level securityContext. nginx may fail to start because writing to temporary paths is blocked on a read-only root, so in real operation you mount a separate writable path with emptyDir. Grading often only checks whether the fields are set.

Task 15 (6 points): Environment,Configuration,Security #

In namespace data, create a Pod sized. Use the nginx image and set CPU request 100m / limit 200m, and memory request 128Mi / limit 256Mi. Also write the command that checks this Pod’s QoS class.

Solution
k -n data run sized --image=nginx $do > sized.yaml

Add resources to the generated skeleton.

spec:
  containers:
    - name: sized
      image: nginx
      resources:
        requests:
          cpu: "100m"
          memory: "128Mi"
        limits:
          cpu: "200m"
          memory: "256Mi"
k apply -f sized.yaml
k -n data get pod sized -o jsonpath='{.status.qosClass}'

Explanation: Both requests and limits are set but their values differ, so this Pod’s QoS class is Burstable. When requests and limits are exactly equal for every resource it becomes Guaranteed, and with no values at all it becomes BestEffort. CPU’s m is millicores, and memory’s Mi is the mebibyte unit.


Task 16 (6 points): Services and Networking #

In namespace net, create a Deployment shop with the nginx image and replicas 2, and expose it through a NodePort Service shop-np accessible from outside the cluster on the node’s port 30080. The Service port is 80 and the target port is 80.

Solution
k -n net create deploy shop --image=nginx --replicas=2
k -n net expose deploy shop --name=shop-np --type=NodePort --port=80 --target-port=80

Pin the nodePort value to 30080.

k -n net patch svc shop-np --type='json' \
  -p='[{"op":"replace","path":"/spec/ports/0/nodePort","value":30080}]'

Explanation: When you create a NodePort with expose, nodePort is auto-assigned from the 30000〜32767 range. To pin it to a specific value, either set spec.ports[0].nodePort directly in the manifest or patch it as above. Pulling the YAML with dry-run and writing nodePort: 30080 directly before apply can be simpler.

Task 17 (6 points): Services and Networking #

In namespace net, create an Ingress shop-ing. Route traffic arriving at host shop.example.com path / to port 80 of the Service shop-np created in Task 16. Use Prefix for pathType.

Solution
k -n net create ingress shop-ing \
  --rule="shop.example.com/*=shop-np:80" $do > ing.yaml

The generated manifest looks like this.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-ing
  namespace: net
spec:
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: shop-np
                port:
                  number: 80
k apply -f ing.yaml

Explanation: In the --rule of create ingress you use the form host/path=service:port, and the trailing /* of the path converts to pathType: Prefix. The backend port is specified by number or name. For routing to actually work, an Ingress controller such as ingress-nginx must be installed in the cluster, but grading checks whether the resource definition is correct.

Task 18 (4 points): Services and Networking #

In namespace net, create a NetworkPolicy db-allow. Allow ingress traffic to Pods with the label app=db only when a Pod with the label role=api in the same namespace accesses TCP port 5432. Block all other ingress.

Solution
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-allow
  namespace: net
spec:
  podSelector:
    matchLabels:
      app: db
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              role: api
      ports:
        - protocol: TCP
          port: 5432
k apply -f db-allow.yaml

Explanation: podSelector selects the target Pods the policy applies to (app=db), and ingress.from selects the allowed sources (role=api). Once even one NetworkPolicy applies to a Pod, all traffic not explicitly listed is blocked, so other ingress is shut off without a separate deny-all policy. Putting from and ports in the same rule makes it an AND of both conditions. NetworkPolicy is enforced only on a CNI that supports it.


Scoring criteria #

Grade by summing each task’s points. The total is 100, and 66 or higher is the passing zone.

DomainTasks , pointsSubtotal
Application Design and Build1(5) , 2(6) , 3(6) , 4(5)22
Application Deployment5(6) , 6(6) , 7(5) , 8(5)22
Observability and Maintenance9(6) , 10(5) , 11(5)16
Environment,Configuration,Security12(7) , 13(6) , 14(6) , 15(6)25
Services and Networking16(6) , 17(6) , 18(4)16
Total(total)100

Grading is result-based, just like the real exam. It looks not at how you typed the commands but at whether the resources you created match the requirements. Even within a single task, partial credit is split by item — labels, ports, requested amounts — so even when you’re stuck on one item, filling in the parts you can is better for your score.

Reviewing weak domains #

After grading, go back to the corresponding post in the table below for any low-scoring domain and review it.

DomainRelated tasksPosts to review
Application Design and Build1, 2, 3, 4#2 , #3 , #4
Application Deployment5, 6, 7, 8#5 , #7 , #9 , #10
Observability and Maintenance9, 10, 11#11 , #12
Environment,Configuration,Security12, 13, 14, 15#13 , #14 , #15 , #16
Services and Networking16, 17, 18#18 , #19

If you ran short on time on a particular task, it may be a matter of hand speed rather than domain knowledge. In that case, re-read #1 kubectl setup and #20 time management, and solve the same 18 tasks once more against the clock. With CKAD, repeating the same task two or three times noticeably reduces the time per task.

Closing the series #

Starting from the kubectl setup in #1, we passed through every CKAD domain across 21 posts — Pods, multi-container, images, workloads, deployment strategies, Helm, kustomize, probes, observability, ConfigMap, Secret, RBAC, SecurityContext, resources, volumes, Service, Ingress, and NetworkPolicy. If you cleared 66 points on this mock, you have built the hands-on skills to clear the pass line in the real exam room as well. Congratulations.

If CKAD was your first hands-on exam as an app developer, the next step is the CKA, which operates the cluster itself. The kubectl speed, the dry-run habit, and the manifest sense you sharpened here become a direct stepping stone, so carry the momentum of passing into the next hands-on exam.

X