#CKAD#Kubernetes#Certified Kubernetes Application Developer#kubectl#Certification
Info: Exam Reality
The real CKAD gives you 15–20 performance-based tasks over 2 hours in a live cluster, open-book (kubernetes.io and kubectl --help are both allowed), with a 66% pass mark. Every lab below mirrors that format: a requirement to build from scratch, not a concept quiz.
Quick Reference — 6 Builds at a Glance
- ●1. Multi-container Pod with a sidecar — Application Design and Build (20%)
- ●2. Zero-downtime rolling update with a readiness gate — Application Deployment (20%)
- ●3. ConfigMap, Secret, and a non-root SecurityContext — Environment, Config & Security (25%)
- ●4. NetworkPolicy lockdown with a ResourceQuota — Environment, Config & Security (25%)
- ●5. Liveness, readiness, and startup probes — Application Observability (15%)
- ●6. ClusterIP Service behind an Ingress with path-based routing — Services and Networking (20%)
1. Design & Build — Multi-Container Pod with a Sidecar
Requirement: deploy a Pod named log-shipper in namespace apps with two containers sharing a volume — an app container that writes to /var/log/app.log, and a sidecar that tails that file. This pattern (main container + sidecar sharing an emptyDir) is one of the most exam-relevant multi-container designs.
apiVersion: v1
kind: Pod
metadata:
name: log-shipper
namespace: apps
labels:
app: log-shipper
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "while true; do echo \"$(date) - request served\" >> /var/log/app.log; sleep 5; done"]
volumeMounts:
- name: log-volume
mountPath: /var/log
- name: log-sidecar
image: busybox:1.36
command: ["sh", "-c", "tail -f /var/log/app.log"]
volumeMounts:
- name: log-volume
mountPath: /var/log
volumes:
- name: log-volume
emptyDir: {}
$ kubectl apply -f log-shipper.yaml
pod/log-shipper created
$ kubectl get pod log-shipper -n apps
NAME READY STATUS RESTARTS AGE
log-shipper 2/2 Running 0 12s
$ kubectl logs log-shipper -n apps -c log-sidecar --tail=3
Mon Aug 10 09:14:32 UTC 2026 - request served
Mon Aug 10 09:14:37 UTC 2026 - request served
Mon Aug 10 09:14:42 UTC 2026 - request served
2. Deployment — Zero-Downtime Rolling Update with a Readiness Gate
Requirement: deploy checkout-api as a Deployment with 4 replicas, a rolling update strategy that never drops below 3 available Pods, and a readiness probe so traffic only reaches Pods that are actually ready. Then perform a rolling update to a new image tag and verify it completes without dropping availability.
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-api
namespace: apps
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: checkout-api
template:
metadata:
labels:
app: checkout-api
spec:
containers:
- name: checkout-api
image: myregistry.io/checkout-api:1.4.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
$ kubectl apply -f checkout-api.yaml
deployment.apps/checkout-api created
$ kubectl set image deployment/checkout-api checkout-api=myregistry.io/checkout-api:1.5.0 -n apps
deployment.apps/checkout-api image updated
$ kubectl rollout status deployment/checkout-api -n apps
Waiting for deployment "checkout-api" rollout to finish: 2 out of 4 new replicas have been updated...
deployment "checkout-api" successfully rolled out
$ kubectl rollout undo deployment/checkout-api -n apps
deployment.apps/checkout-api rolled back
Tip: Know the Rollback Command Cold
kubectl rollout undo is easy to forget under time pressure because you rarely need it outside an incident. Practice it explicitly — CKAD scenarios sometimes require rolling back a bad deploy as part of the task.
3. Environment, Config & Security — ConfigMap, Secret, and a Non-Root SecurityContext
Requirement: create a ConfigMap with a database host setting, a Secret with a database password, and a Pod that consumes both as environment variables — running as a non-root user with no privilege escalation allowed.
$ kubectl create configmap db-config -n apps --from-literal=DB_HOST=postgres.internal
configmap/db-config created
$ kubectl create secret generic db-secret -n apps --from-literal=DB_PASSWORD='S3cure!Pass'
secret/db-secret created
apiVersion: v1
kind: Pod
metadata:
name: billing-worker
namespace: apps
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
containers:
- name: billing-worker
image: myregistry.io/billing-worker:2.1.0
securityContext:
allowPrivilegeEscalation: false
envFrom:
- configMapRef:
name: db-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: DB_PASSWORD
$ kubectl apply -f billing-worker.yaml
pod/billing-worker created
$ kubectl exec billing-worker -n apps -- whoami
1000
$ kubectl exec billing-worker -n apps -- printenv DB_HOST
postgres.internal
4. Environment, Config & Security — ResourceQuota and a NetworkPolicy Lockdown
Requirement: in namespace restricted-ns, cap total CPU and memory requests with a ResourceQuota, and lock down network traffic so only Pods labeled role=frontend can reach Pods labeled role=backend on port 8080 — everything else denied by default.
apiVersion: v1
kind: ResourceQuota
metadata:
name: restricted-quota
namespace: restricted-ns
spec:
hard:
requests.cpu: "2"
requests.memory: 2Gi
limits.cpu: "4"
limits.memory: 4Gi
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
namespace: restricted-ns
spec:
podSelector:
matchLabels:
role: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 8080
$ kubectl apply -f restricted-quota.yaml -f backend-allow-frontend.yaml
resourcequota/restricted-quota created
networkpolicy.networking.k8s.io/backend-allow-frontend created
$ kubectl describe quota restricted-quota -n restricted-ns
Name: restricted-quota
Namespace: restricted-ns
Resource Used Hard
-------- ---- ----
limits.cpu 0 4
limits.memory 0 4Gi
requests.cpu 0 2
requests.memory 0 2Gi
Tip: NetworkPolicy Only Works if the CNI Supports It
On the real exam the cluster's CNI plugin (Calico, Cilium, etc.) already enforces NetworkPolicy — you just write the manifest. When practicing locally with kind or minikube, confirm your CNI actually enforces NetworkPolicy, or your policy will silently do nothing.
5. Observability — Liveness, Readiness, and Startup Probes
Requirement: deploy a Pod for a slow-starting legacy app that needs up to 60 seconds before it is ready, but should be restarted if it ever stops responding after that. Use all three probe types together — this exact combination (startup + liveness + readiness) is a common exam pattern for apps with a long boot time.
apiVersion: v1
kind: Pod
metadata:
name: legacy-reporting
namespace: apps
spec:
containers:
- name: legacy-reporting
image: myregistry.io/legacy-reporting:3.0.0
ports:
- containerPort: 9000
startupProbe:
httpGet:
path: /startupz
port: 9000
failureThreshold: 12
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 9000
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 9000
periodSeconds: 5
$ kubectl apply -f legacy-reporting.yaml
pod/legacy-reporting created
$ kubectl describe pod legacy-reporting -n apps | grep -A2 "Startup\|Liveness\|Readiness"
Startup: http-get http://:9000/startupz delay=0s timeout=1s period=5s #success=1 #failure=12
Liveness: http-get http://:9000/healthz delay=0s timeout=1s period=10s #success=1 #failure=3
Readiness: http-get http://:9000/readyz delay=0s timeout=1s period=5s #success=1 #failure=3
6. Services & Networking — ClusterIP Service Behind an Ingress
Requirement: expose Deployment catalog-api internally via a ClusterIP Service on port 80, then route external traffic to it at path /catalog using an existing Ingress controller — with a second backend at /orders routing to a different Service, on the same Ingress resource.
apiVersion: v1
kind: Service
metadata:
name: catalog-api-svc
namespace: apps
spec:
type: ClusterIP
selector:
app: catalog-api
ports:
- protocol: TCP
port: 80
targetPort: 8080
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: storefront-ingress
namespace: apps
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /catalog
pathType: Prefix
backend:
service:
name: catalog-api-svc
port:
number: 80
- path: /orders
pathType: Prefix
backend:
service:
name: orders-api-svc
port:
number: 80
$ kubectl apply -f catalog-api-svc.yaml -f storefront-ingress.yaml
service/catalog-api-svc created
ingress.networking.k8s.io/storefront-ingress created
$ kubectl get ingress storefront-ingress -n apps
NAME CLASS HOSTS ADDRESS PORTS AGE
storefront-ingress nginx * 10.96.201.44 80 8s
$ kubectl get endpoints catalog-api-svc -n apps
NAME ENDPOINTS AGE
catalog-api-svc 10.244.1.7:8080,10.244.2.9:8080 14s
Tip: Practice Until It's Muscle Memory
The real exam rewards speed as much as correctness. Rebuild each of these six tasks yourself in a local cluster (kind or minikube both work) using imperative kubectl commands first, then only fall back to hand-writing YAML for fields imperative commands can't set — that's the actual time-saving skill CKAD is testing.
Put These Builds to the Test