Certified Kubernetes Application Developer (CKAD)¶
The CKAD is a two-hour, hands-on lab exam focused entirely on running applications in Kubernetes - not managing the cluster. You'll be creating and modifying workloads, wiring up config and secrets, writing probes, exposing services, and debugging broken apps. Speed is the biggest constraint: you need to produce correct YAML fast and know the right kubectl flags without looking them up.
Exam Facts¶
| Format | Browser-based terminal, multiple live clusters |
| Duration | 2 hours |
| Passing score | 66% |
| Price | $395 USD (one free retake included) |
| Open book | kubernetes.io/docs only |
| Questions | ~15–20 tasks across multiple namespaces |
First 3 Minutes: Terminal Setup¶
# kubectl autocomplete
source <(kubectl completion bash)
echo "source <(kubectl completion bash)" >> ~/.bashrc
# Essential alias
alias k=kubectl
complete -F __start_kubectl k
# Editor (pick one and stick with it)
export KUBE_EDITOR=vim # or nano
Vim settings (~/.vimrc):
The Core Skill: Generate YAML, Don't Write It¶
Writing manifests from scratch is slow and error-prone. Use --dry-run=client -o yaml to generate a scaffold, then edit.
# Pod
kubectl run mypod --image=nginx --dry-run=client -o yaml > pod.yaml
# Pod with specific port
kubectl run mypod --image=nginx --port=8080 --dry-run=client -o yaml > pod.yaml
# Pod with env var
kubectl run mypod --image=nginx --env="DB_HOST=localhost" --dry-run=client -o yaml > pod.yaml
# Pod with labels
kubectl run mypod --image=nginx --labels="app=frontend,tier=web" --dry-run=client -o yaml > pod.yaml
# Pod with command override
kubectl run mypod --image=busybox --dry-run=client -o yaml -- /bin/sh -c "sleep 3600" > pod.yaml
# Deployment
kubectl create deployment myapp --image=nginx --replicas=3 --dry-run=client -o yaml > deploy.yaml
# Job
kubectl create job myjob --image=busybox --dry-run=client -o yaml -- /bin/sh -c "echo hello" > job.yaml
# CronJob
kubectl create cronjob mycron --image=busybox --schedule="*/5 * * * *" --dry-run=client -o yaml -- date > cron.yaml
# ConfigMap
kubectl create configmap app-config \
--from-literal=DB_HOST=localhost \
--from-literal=DB_PORT=5432 \
--dry-run=client -o yaml > cm.yaml
# ConfigMap from file
kubectl create configmap app-config --from-file=config.properties --dry-run=client -o yaml
# Secret
kubectl create secret generic app-secret \
--from-literal=password=mysecret \
--dry-run=client -o yaml > secret.yaml
# ServiceAccount
kubectl create serviceaccount my-sa --dry-run=client -o yaml
# Service
kubectl create service clusterip mysvc --tcp=80:8080 --dry-run=client -o yaml
kubectl expose deployment myapp --port=80 --target-port=8080 --dry-run=client -o yaml
Domain 1: Application Design & Build¶
Multi-Container Pod Patterns¶
These appear regularly. Know the YAML for each.
Sidecar - enhances the main container (log shipper, proxy):
apiVersion: v1
kind: Pod
metadata:
name: app-with-sidecar
spec:
volumes:
- name: shared-logs
emptyDir: {}
containers:
- name: app
image: nginx
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
- name: log-shipper
image: busybox
command: ["/bin/sh", "-c", "tail -f /logs/access.log"]
volumeMounts:
- name: shared-logs
mountPath: /logs
Init Container - runs to completion before main containers start:
spec:
initContainers:
- name: init-db
image: busybox
command: ['sh', '-c', 'until nslookup mydb; do echo waiting; sleep 2; done']
containers:
- name: app
image: myapp:1.0
Use init containers for: waiting for a dependency, seeding a volume, running migrations.
Ephemeral container (debug a running pod):
Jobs and CronJobs¶
# Job - runs once
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
completions: 3 # run this many times total
parallelism: 2 # run this many in parallel
backoffLimit: 4 # retry up to 4 times before marking failed
template:
spec:
restartPolicy: Never # Never or OnFailure - not Always
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
# CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: cleanup
spec:
schedule: "0 2 * * *" # 2am daily
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: cleanup
image: busybox
command: ["/bin/sh", "-c", "echo cleanup done"]
Cron schedule cheat sheet:
* * * * *
│ │ │ │ └── day of week (0-7, 0 and 7 = Sunday)
│ │ │ └──── month (1-12)
│ │ └────── day of month (1-31)
│ └──────── hour (0-23)
└────────── minute (0-59)
Domain 2: Application Configuration¶
ConfigMaps - Four Ways to Use Them¶
1. Environment variable (single key):
2. All keys as env vars:
3. Mounted as a file:
volumes:
- name: config-vol
configMap:
name: app-config
containers:
- volumeMounts:
- name: config-vol
mountPath: /etc/config
# each key becomes a file: /etc/config/DB_HOST, etc.
4. Mounted as a specific file:
volumes:
- name: config-vol
configMap:
name: app-config
items:
- key: config.properties
path: app.properties # mounted as /etc/config/app.properties
Secrets - Same Patterns, Different Object¶
# Single key env var
env:
- name: PASSWORD
valueFrom:
secretKeyRef:
name: app-secret
key: password
# All keys as env vars
envFrom:
- secretRef:
name: app-secret
# Volume mount (files)
volumes:
- name: secret-vol
secret:
secretName: app-secret
Gotcha: secret values in YAML must be base64 encoded. kubectl create secret handles this automatically. If you're writing the YAML by hand:
Resource Requests and Limits¶
containers:
- name: app
image: nginx
resources:
requests:
memory: "64Mi"
cpu: "250m" # 250 millicores = 0.25 CPU
limits:
memory: "128Mi"
cpu: "500m"
- Requests: what the scheduler uses to find a node; guaranteed to the container
- Limits: hard ceiling; CPU is throttled at limit, memory causes OOM kill
100m= 0.1 CPU,1000m= 1 CPU,1= 1 CPU
SecurityContext¶
# Pod-level (applies to all containers)
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
# Container-level (overrides pod-level)
containers:
- name: app
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
ServiceAccounts¶
# Create a ServiceAccount
kubectl create serviceaccount my-sa -n default
# Bind a Role to it
kubectl create rolebinding my-sa-binding \
--role=pod-reader \
--serviceaccount=default:my-sa
# Use it in a pod
spec:
serviceAccountName: my-sa
automountServiceAccountToken: false
Domain 3: Application Deployment¶
Deployment Strategies¶
RollingUpdate (default):
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # max pods over desired count during update
maxUnavailable: 0 # max pods that can be unavailable
Recreate (takes downtime, useful for incompatible migrations):
Rollout commands:
kubectl rollout status deployment/myapp
kubectl rollout history deployment/myapp
kubectl rollout undo deployment/myapp
kubectl rollout undo deployment/myapp --to-revision=2
kubectl set image deployment/myapp nginx=nginx:1.25
Canary deployment (manual approach - two deployments, adjust replicas):
# stable: 9 replicas with label version=stable
# canary: 1 replica with label version=canary
# Both have label app=myapp so same Service selects them
# Traffic split ≈ 90/10 by replica ratio
Horizontal Pod Autoscaler¶
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
Note: HPA requires metrics-server to be running in the cluster.
Domain 4: Application Observability and Maintenance¶
Probes - Get These Right¶
Probes are a major exam topic. There are three types, and you need to know when to use each.
Liveness probe - restarts the container if it fails:
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15 # wait before first check
periodSeconds: 10 # check every 10s
failureThreshold: 3 # restart after 3 consecutive failures
Readiness probe - removes pod from service endpoints if it fails (doesn't restart):
Startup probe - for slow-starting apps; disables liveness/readiness until it passes:
startupProbe:
exec:
command: ["/bin/sh", "-c", "cat /tmp/healthy"]
failureThreshold: 30 # 30 * 10s = 5 minutes to start
periodSeconds: 10
Probe types:
- httpGet - HTTP GET, success if 200–399
- tcpSocket - TCP connection succeeds
- exec - command exits with 0
Common mistake: setting initialDelaySeconds too low for a slow app. The liveness probe fires before the app is up, triggers a restart, and you get a CrashLoopBackOff even though the app itself is fine.
Debugging¶
# Pod not starting
kubectl describe pod <name> # look at Events section
kubectl logs <name>
kubectl logs <name> --previous # logs from crashed container
# Running pod - inspect without exec
kubectl describe pod <name>
kubectl top pod <name> # resource usage (needs metrics-server)
# Get a shell
kubectl exec -it <pod> -- /bin/sh
kubectl exec -it <pod> -c <container> -- /bin/sh # specific container
# Debug without exec available
kubectl debug -it <pod> --image=busybox --target=<container>
# Copy logs out
kubectl cp <pod>:/var/log/app.log ./app.log
Domain 5: Services and Networking¶
Service Types¶
# ClusterIP (default) - internal only
apiVersion: v1
kind: Service
metadata:
name: my-svc
spec:
selector:
app: myapp
ports:
- port: 80 # port the service listens on
targetPort: 8080 # port on the pod
type: ClusterIP
# NodePort - exposed on every node
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # optional; 30000-32767 if not set
Headless Service - no cluster IP; returns pod IPs directly via DNS:
DNS format for headless pods: <pod-name>.<service>.<namespace>.svc.cluster.local
Ingress¶
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
spec:
ingressClassName: nginx
rules:
- host: myapp.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: frontend-svc
port:
number: 80
NetworkPolicy¶
# Deny all ingress to a set of pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
# Allow only from specific pod
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- port: 8080
Domain 6: Environment, Configuration and Security (Exam Remix)¶
This catches exam-takers who know the concepts but fumble the syntax. Quick reference:
Projected volume - merge multiple sources into one volume:
volumes:
- name: projected
projected:
sources:
- configMap:
name: app-config
- secret:
secretName: app-secret
Downward API - expose pod metadata to the container:
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: CPU_LIMIT
valueFrom:
resourceFieldRef:
containerName: app
resource: limits.cpu
Speed Techniques That Matter¶
Edit live resources:
Patch a single field without opening an editor:
kubectl patch deployment myapp -p '{"spec":{"replicas":5}}'
kubectl set image deployment/myapp nginx=nginx:1.25
kubectl set resources deployment myapp -c=nginx --limits=cpu=200m,memory=512Mi
Check all resources in a namespace quickly:
Get YAML of existing resource (to understand what's there):
kubectl explain - use instead of searching docs for field names:
kubectl explain pod.spec.containers.livenessProbe
kubectl explain deployment.spec.strategy
kubectl explain cronjob.spec.jobTemplate.spec
What CKAD Exam Questions Actually Look Like¶
Understanding the format helps you read questions efficiently:
- "Create a Pod named
webin namespaceappusing imagenginx:1.25that uses ConfigMapweb-configto set env varAPP_MODEfrom keymode." - "A deployment
frontendis failing. Find and fix the issue." (Usually: wrong image, bad probe, wrong resource limits) - "Update the deployment
apito use a rolling update strategy withmaxSurge=2andmaxUnavailable=1." - "Create a Job
data-exportthat runs 5 completions with max 2 parallel." - "The pod
appshould not be able to use more than 200m CPU. Set appropriate limits."
Pattern for every question: read carefully, identify what resource/namespace, generate or edit YAML, apply, verify.
Common CKAD Mistakes¶
- Wrong namespace - most questions specify one. Always use
-n <ns>or--namespace. restartPolicy: Alwayson a Job - Jobs needNeverorOnFailure. This is the most common Job mistake.- Forgetting
selectorin a Service - the selector must match the pod labels. - Missing
containerPort- doesn't break functionality (it's informational only) but some exam graders check for it. - ConfigMap/Secret key case mismatch -
DB_HOSTvsdb_host- exact match required. - Liveness probe too aggressive -
failureThreshold: 1withinitialDelaySeconds: 0will restart a healthy pod before it finishes starting. - Not verifying - spend 15 seconds confirming the resource exists and has the right spec.
Practice Approach¶
- Use Killer.sh - the free simulator included with your exam purchase. It's harder than the real exam and gives you a realistic time constraint.
- Practice generating YAMLs from scratch without the docs - the exam has the docs available, but looking things up takes time. Know the core manifest patterns cold.
- Time constraint drills - give yourself 2 minutes per task. If you can't do it in 2 minutes, practice that scenario until you can.
- Deliberately practice the annoying parts: multi-container pods, projection volumes, complex probe configurations, CronJob schedules.
Recommended Resources¶
- Killer.sh CKAD Simulator - the best prep tool, included with exam purchase
- KodeKloud CKAD Course - good hands-on labs
- Kubernetes Official Docs - your only reference during the exam
- kubectl Cheat Sheet - bookmark this
- Official CKAD Curriculum - the authoritative topic list