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 | See current pricing - includes one free retake |
| Open book | kubernetes.io/docs, kubernetes.io/blog and their subdomains. The exam's Important Instructions page is the authoritative list - read it the week before you sit |
| Questions | ~15-20 tasks across multiple namespaces |
| Curriculum version | Track github.com/cncf/curriculum for the version tied to your exam date. Exam facts on this page verified 2026-09-09 |
Study the Concepts First¶
Exam tactics below assume the concepts are solid. If any domain feels shaky, read the matching deep dive first:
| Exam domain | Weight | Deep-dive companions |
|---|---|---|
| Application Design and Build | 20% | Pods & Deployments · Jobs & CronJobs · Init Containers |
| Application Environment, Configuration and Security | 25% | ConfigMaps & Secrets · Security Context · RBAC · Requests & Limits |
| Application Deployment | 20% | Pods & Deployments · Scaling & HPA · Helm · Kustomize |
| Application Observability and Maintenance | 15% | Health Probes · Troubleshooting |
| Services & Networking | 20% | Services · Ingress · Network Policies |
Weights are from the CNCF CKAD curriculum (github.com/cncf/curriculum) and sum to 100%. Note that Application Environment, Configuration and Security is the single heaviest domain at 25% - budget study time accordingly.
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 and Build (20%)¶
Building Container Images¶
The curriculum says "define, build and modify container images", so know a Dockerfile well enough to write and fix one. A minimal multi-stage build keeps the runtime image small, which is both good practice and a common exam-adjacent ask:
# Build stage - toolchain lives here and is thrown away
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
# Runtime stage - only the binary ships
FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/app"]
docker build -t myregistry.io/myapp:1.0 .
docker run --rm -p 8080:8080 myregistry.io/myapp:1.0
docker push myregistry.io/myapp:1.0
# podman is a drop-in replacement for the same three commands
podman build -t myregistry.io/myapp:1.0 .
podman run --rm -p 8080:8080 myregistry.io/myapp:1.0
podman push myregistry.io/myapp:1.0
Key instruction differences worth knowing cold: ENTRYPOINT maps to a container's command: in a pod spec, CMD maps to args:. Setting command/args in the pod spec overrides both.
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 Environment, Configuration and Security (25%)¶
This is the heaviest domain on the exam. It covers ConfigMaps, Secrets, requests/limits/quotas, ServiceAccounts, authentication and authorization, application security (SecurityContext, capabilities), and discovering resources that extend Kubernetes (CRDs and operators).
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 \
-n default
Use it in a pod:
Projected Volumes¶
Merge multiple config sources into a single volume:
volumes:
- name: projected
projected:
sources:
- configMap:
name: app-config
- secret:
secretName: app-secret
- serviceAccountToken:
path: token
expirationSeconds: 3600
Downward API¶
Expose pod metadata and resource values 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
The same two sources work as a volume (downwardAPI: with items: [{path: ..., fieldRef: ...}]) when the app wants files rather than env vars.
ResourceQuota and LimitRange¶
Namespace-level constraints are explicitly in this domain, and a task may hand you a namespace whose quota is rejecting your pod:
kubectl create quota compute-quota --hard=pods=10,requests.cpu=2,limits.memory=4Gi -n dev
kubectl describe quota -n dev # see used vs hard - this is how you diagnose a rejected pod
kubectl describe limitrange -n dev # defaults injected into pods that omit resources
If a namespace has a ResourceQuota that sets requests.cpu or limits.memory, every container in that namespace must declare the matching requests/limits or the pod is rejected outright.
Discovering Extension Resources (CRDs and Operators)¶
The exam can hand you a cluster with a custom resource installed and ask you to create an instance of it without giving you the schema. Discover it:
kubectl get crd
kubectl api-resources # short names, api group, namespaced or not
kubectl api-resources --api-group=cert-manager.io
kubectl explain certificate --recursive # full field tree, no docs needed
kubectl get certificate -A
kubectl explain reads the schema straight off the API server, so it works for custom resources exactly as it does for built-ins. This is faster than searching the operator's website.
Domain 3: Application Deployment (20%)¶
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.
Helm¶
"Use the Helm package manager to deploy existing packages" is an explicit CKAD objective. You are not asked to author charts - you are asked to install, configure and roll back someone else's.
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo nginx
helm show values bitnami/nginx | less # find the key you need to override
helm install web bitnami/nginx -n web --create-namespace \
--set replicaCount=3 \
-f values.yaml
helm list -A # all releases, all namespaces
helm upgrade web bitnami/nginx --set replicaCount=5 -n web
helm history web -n web
helm rollback web 1 -n web # back to revision 1
helm uninstall web -n web
helm template web bitnami/nginx # render locally without installing
--set beats -f values.yaml when both set the same key, and helm show values is much faster than hunting for the option name in a browser.
Kustomize¶
Kustomize is built into kubectl, so there is nothing to install on the exam.
base/
kustomization.yaml # resources: [deployment.yaml, service.yaml]
deployment.yaml
service.yaml
overlays/prod/
kustomization.yaml
replica-patch.yaml
# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
resources:
- ../../base
namePrefix: prod-
commonLabels:
env: prod
images:
- name: myapp
newTag: "2.0"
patches:
- path: replica-patch.yaml
target:
kind: Deployment
name: myapp
kubectl kustomize overlays/prod # render to stdout - always check before applying
kubectl apply -k overlays/prod
kubectl delete -k overlays/prod
Domain 4: Application Observability and Maintenance (15%)¶
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
API Deprecations¶
"Understand API deprecations" is an objective in this domain, and a task may hand you a manifest that no longer applies. The tell is an error naming a version that "no longer served".
kubectl api-versions # every group/version this server serves
kubectl explain ingress # prints the version kubectl will use
kubectl apply -f old.yaml --dry-run=server # surfaces deprecation warnings without writing
kubectl convert -f old.yaml --output-version networking.k8s.io/v1 # if the plugin is present
Versions to know by heart, because stale manifests still float around:
| Object | Current API version |
|---|---|
| Deployment, DaemonSet, StatefulSet, ReplicaSet | apps/v1 |
| Job, CronJob | batch/v1 |
| Ingress, NetworkPolicy, IngressClass | networking.k8s.io/v1 |
| HorizontalPodAutoscaler | autoscaling/v2 |
| Role, RoleBinding, ClusterRole, ClusterRoleBinding | rbac.authorization.k8s.io/v1 |
| PodDisruptionBudget | policy/v1 |
Deprecation policy in one line: a GA (v1) API is supported for at least 12 months or 3 releases after deprecation, whichever is longer; beta APIs get 9 months or 3 releases.
Domain 5: Services and Networking (20%)¶
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
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¶
No affiliate relationship: the tools and courses recommended on this page are linked plainly and earn this site nothing.
- Killer.sh CKAD Simulator - the best prep tool, included with exam purchase
- KodeKloud CKAD Course - good hands-on labs
- K8s Guide mobile app - adaptive CKAD practice questions and mastery tracking (iOS now, Android in closed beta). The questions are multiple-choice recall practice: a complement to hands-on cluster time, not a substitute for it, since the exam itself is 100% performance-based.
- Kubernetes Official Docs - your only reference during the exam
- kubectl Cheat Sheet - this site's version, plus the official one
- Official CKAD Curriculum - the authoritative topic list
Related Concepts¶
The Learn pages that map to each CKAD domain:
- Application Design and Build - Pods & Deployments · Jobs & CronJobs · Init Containers · Storage
- Application Environment, Configuration and Security - ConfigMaps & Secrets · Requests & Limits · Security Context · RBAC
- Application Deployment - Scaling & HPA · Helm · Kustomize
- Application Observability and Maintenance - Health Probes · Troubleshooting
- Services and Networking - Services · Ingress · Network Policies
Also: Learning Paths - the track that leads here · Certification Preparation · kubectl Cheat Sheet