CKAD — Kubernetes Application Developer
CKAD Mock Exam A
This is a practice exam
In the real CKAD, you solve 15–20 tasks in 120 minutes and pass if you score above 66%. This set is also matched to 17 tasks, 120 minutes, and a 66% passing score. Because scoring is partial, you do not need to get everything right. Passing 12 of the 17 counts as complete.
Do not look at the hints; solve everything first. The real exam has no hints. For a task you are stuck on, it is better for your score to skip it and come back if time remains. Use the hints and answers for review after you have finished the exam once.
Things that cost you time if you first learn them at the real exam
- The exam is a remote desktop, and for each task you work by running
sshto the designated host. Nested ssh is not supported. When the work is done, be sure to return to where you started withexit. - The
kalias and bash completion are already set up. The advice to create an alias as soon as you start does not fit the current environment. This lab Pod is set up the same way. yq,curl,wget, andmanare also already installed.- Terminal copy is
Ctrl+Shift+C, and paste isCtrl+Shift+V. Ctrl+Wcloses the browser tab. To delete a word, useCtrl+Alt+W.- The INSERT key is disabled, so in vim you have to enter insert mode with
i. - Each question has a different weight, and several paths to the correct answer are accepted.
Domain distribution
| Domain | Actual weight | Tasks in this set |
|---|---|---|
| Application Design and Build | 20% | tasks 1–3 |
| Application Deployment | 20% | tasks 4–7 |
| Application Observability and Maintenance | 15% | tasks 8–10 |
| Application Environment, Configuration and Security | 25% | tasks 11–14 |
| Services and Networking | 20% | tasks 15–17 |
About time allocation
Task 3 (CronJob) takes up to 1 minute until the controller actually runs it once. Create it first,
solve other tasks, and check afterward. Tasks 4 and 5 have to wait until the rollout finishes,
so it is faster to use kubectl rollout status.
What is different in this environment
The cluster inside the Pod is a single-user cluster started by kwokctl. The control plane is real, so
manifests, RBAC, scheduling, quotas, CRDs, PV binding, and drain all actually work.
However, workload containers do not run, so kubectl exec, logs, and port-forward cannot be
used. The tasks do not require them either.
Before you start, check with kubectl get nodes that all 3 nodes are Ready. If they are not yet,
the cluster is still coming up (it takes about 2 minutes).
Grading
When you press the check button on each task, the grader recomputes the values from the live cluster and compares them. It looks at what state the cluster is in, not at what you wrote in a file. Several paths to the correct answer are accepted.
Init container and sidecar
In the namespace app, create a Pod logger.
- Put in an emptyDir volume
work, and have every container mount it at/work. - The init container
seedcreates/work/seed.txtwithbusybox:1.36. - The container
appisnginx:1.27. - The container
tailertails/work/seed.txtwithbusybox:1.36.
Write the init container separately under spec.initContainers. Declare the volume once on the Pod, and each of the three containers uses it through its own volumeMounts.
A Job with a specified completion count
In the namespace app, create a Job migrate. The image is busybox:1.36 and it runs echo done.
The completions are 4, parallelism 2, backoffLimit 2, activeDeadlineSeconds 120, and restartPolicy is Never. The Job must end as Complete.
A Job's completions cannot be changed once it is created. If you wrote the wrong value, delete it and create it again. Write restartPolicy on the Pod template side.
CronJob
In the namespace app, create a CronJob report. The image is busybox:1.36, running date, and restartPolicy is OnFailure.
The schedule is */1 * * * *, concurrencyPolicy is Forbid, startingDeadlineSeconds is 30, and keep only 2 successful histories and 1 failed history. It must actually run at least once.
The period is 1 minute, so right after creating it there is no run record yet. Wait until a time appears in the LAST SCHEDULE column of kubectl get cronjob report -n app, then grade.
Updating the image with a rolling update
In the namespace deploy, create a Deployment api with nginx:1.27 and 3 replicas. The strategy is RollingUpdate with maxSurge 2 and maxUnavailable 0.
Then update the image to nginx:1.28. You must update it rather than deleting and recreating.
If you update with kubectl set image deploy/api <컨테이너>=nginx:1.28 (the placeholder is the container name), the previous ReplicaSet remains as history. Wait until it finishes with kubectl rollout status.
Rollback
In the namespace deploy, create a Deployment web with nginx:1.27 and 2 replicas.
Then push the image to nginx:1.99-broken, confirm it is a bad deployment, and roll back to the first revision. Even after the rollback, the problem revision must remain in the history.
After checking the revision number with kubectl rollout history deploy/web, use kubectl rollout undo deploy/web --to-revision=1. If you delete and recreate it, the history is lost.
A canary split by labels
In the namespace deploy, create Deployments shop-stable (4 replicas, Pod labels app=shop and version=stable, image nginx:1.27) and shop-canary (1 replica, Pod labels app=shop and version=canary, image nginx:1.28). The container port of both Pods is 8080.
The Service shop takes port 80 and forwards it to 8080, and must point to the Pods of both Deployments.
If you put version in the Service selector, only one side is caught. Use only one label that both Deployments share as the selector. You only need to check that there are 5 endpoints.
A kustomize overlay
In /root/exam/kustomize/base, put a Deployment cart (1 replica, image nginx:1.27, Pod label app=cart) and a kustomization.
In /root/exam/kustomize/overlay, create an overlay that adds prod- in front of the name, changes the namespace to deploy-prod, changes the replicas to 3, changes the image tag to 1.28, and adds the label env=prod (do not put it in the selector).
Apply that overlay to the cluster.
First check the result by eye with kubectl kustomize <디렉터리> (the placeholder is the directory), then apply with kubectl apply -k. A selector cannot be changed once created, so if you put the common label in the selector, the next apply is rejected.
The three probes
In the namespace obs, create a Deployment checkout. The image is nginx:1.27, 2 replicas, and the container port is 8080 (named http).
- startupProbe: httpGet
/startup:8080, failureThreshold 30, periodSeconds 5 - readinessProbe: httpGet
/ready:8080, initialDelaySeconds 5, periodSeconds 10 - livenessProbe: httpGet
/health:8080, periodSeconds 15, failureThreshold 3, timeoutSeconds 2
Write the three probes side by side inside the container spec. If you leave out even one value, the default gets in, but the grader expects the values written in the question exactly.
Extracting a Pod list into a file
In the namespace obs, sort the Pods with the label app=checkout by name, and save them to /root/exam/09-pods.txt in the format <파드이름> <노드이름> <상태> on each line (the placeholders are the Pod name, the node name, and the status). Do not include a header.
The grader compares this file against the cluster at grading time. If you recreated the Pods later, recreate the file as well.
Extract only the fields you need with the range of kubectl get pods -o jsonpath and sort with sort. The node name is .spec.nodeName, and the status is .status.phase.
Moving off a vanished API version
The manifest below is written with API versions the cluster does not serve now. Move it to the current versions and apply it in the namespace obs. If required fields have increased, you must fill them in.
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata: {name: obs-pdb}
spec:
minAvailable: 1
---
apiVersion: batch/v1beta1
kind: CronJob
metadata: {name: obs-cron}
spec:
schedule: "0 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers: [{name: obs, image: busybox:1.36, command: ["sh","-c","date"]}]
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata: {name: obs-ing}
spec:
rules:
- host: obs.example.com
http:
paths:
- path: /
backend:
serviceName: checkout
servicePort: 80
Also create the Service checkout that the Ingress will point to (port 80 → container port http) and an IngressClass nginx (controller k8s.io/ingress-nginx), and specify that class on the Ingress. The PDB's selector is app=checkout.
You can check the field names of the current version with kubectl explain <리소스> --recursive (the placeholder is the resource). The PDB in policy/v1 requires a selector, and the Ingress in networking.k8s.io/v1 requires pathType and the backend.service structure.
Injecting configuration and secret values
In the namespace cfg, create a ConfigMap app-config. The keys are APP_MODE=production, LOG_LEVEL=info, and a file key app.properties (the content is up to you).
In a Secret app-secret (Opaque), put the key API_TOKEN. Choose the value yourself.
The Deployment svc (image nginx:1.27, 1 replica) uses all three methods.
- Receive the whole ConfigMap as environment variables with
envFrom. - The environment variable
API_TOKENreferences the same-named key of the Secret (do not write it in plaintext). - Mount the ConfigMap as a volume, but with
subPathplace onlyapp.propertiesat/etc/app/app.properties.
With subPath, only a single file, not the directory, is placed at that path. Write the file path in mountPath and the key name in subPath. If you create the value with --from-literal, kubectl does the base64 encoding for you.
Tightening with securityContext
In the namespace cfg, create a Deployment hardened. The image is nginx:1.27 and it has 1 replica.
Pod level: runAsUser 10001, runAsGroup 10001, fsGroup 10001, runAsNonRoot true, seccompProfile is RuntimeDefault.
Container level: allowPrivilegeEscalation false, readOnlyRootFilesystem true, drop all capabilities and add only NET_BIND_SERVICE.
The Pod level and the container level are in different places. capabilities, allowPrivilegeEscalation, and readOnlyRootFilesystem exist only on the container side.
An application-specific service account
In the namespace cfg, create a service account app-sa so that its token is not mounted automatically.
The Role cm-reader allows only get and list on configmaps in the core group. Bind it with a RoleBinding cm-reader.
Make the Deployment reader (image nginx:1.27, 1 replica) run with that service account.
automountServiceAccountToken is a top-level field of the service account object (not inside spec). In a Deployment, specify it with serviceAccountName in the Pod spec.
A workload that runs within the limits
In the namespace cfg-quota, put a LimitRange cfg-limits. Per container, max is cpu 500m and memory 512Mi, min is cpu 50m and memory 64Mi, default is cpu 250m and memory 256Mi, and defaultRequest is cpu 100m and memory 128Mi.
The limits of the ResourceQuota cfg-quota are pods 10, requests.cpu 1, requests.memory 1Gi, limits.cpu 2, and limits.memory 2Gi.
The Deployment sized (image nginx:1.27, 2 replicas) has requests of cpu 100m and memory 128Mi and limits of cpu 250m and memory 256Mi, and both Pods must come up.
Values above the LimitRange's max are rejected at admission. Check whether the quota is actually counting with the Used column of kubectl describe quota cfg-quota -n cfg-quota.
ClusterIP and NodePort
In the namespace net, create a Deployment web. The image is nginx:1.27, 3 replicas, the container port is 8080, and its name is http.
The Service web is ClusterIP, takes port 80, and forwards it to the port name http. The Service web-nodeport is NodePort, points to the same Pods, takes port 80, and its node port is 30080.
For both Services, you must write the name, not a number, in targetPort. You can specify nodePort directly only within the range 30000–32767.
An Ingress that splits two paths
In the namespace net, create a Deployment admin (image nginx:1.27, 1 replica, container port 8080 named http) and a Service admin (port 80 → http).
Create an IngressClass nginx (controller k8s.io/ingress-nginx), and with an Ingress net-ing, send / of the host app.example.com to web:80 and /admin to admin:80. The pathType of both paths is Prefix.
When paths overlap, the more specific one is matched first, but grading only checks that the two rules are written exactly. Also check that the backend Services actually exist.
Three policies: block and allow
In the namespace net, create three NetworkPolicies.
deny-all: for all Pods in the namespace, block both incoming and outgoing traffic.allow-dns: allow all Pods in the namespace to send UDP 53 and TCP 53 out to thekube-systemnamespace.allow-web: for theapp=webPods, allow TCP 8080 coming from theapp=clientPods.
If you block all egress, DNS is blocked too and Pods cannot resolve any name. DNS does not use only UDP; when a response is large it falls over to TCP, so you have to open both. You can pick the namespace selector with the kubernetes.io/metadata.name label.