My CKAD study notes, practice questions, and kubectl cheat sheet. Kubernetes v1.35. I scored 91% — this is everything I used to prepare.
⭐ If this repo helps you pass the CKAD, a star helps other candidates find it.
I took the CKAD in March 2026 and scored 91%. Writing this while it's fresh — partly because I was frustrated with how many outdated CKAD study guides still floating around (v1.31 references in 2026, really?) and partly because organizing my notes helped me retain what I learned.
The CKAD (Certified Kubernetes Application Developer) is a hands-on, terminal-based exam. 2 hours, roughly 15–20 tasks, no multiple choice. I prepped for about 4 weeks. Below are my study notes, the kubectl commands I actually used, YAML I wrote from memory, practice questions, and the mistakes I made along the way.
Blog version of these notes: How to Pass the CKAD Exam
If this helped you prepare for the CKAD, a star helps other candidates find it.
Short on time? Here's the fast track:
- Run the setup script —
bash scripts/exam-setup.shto get aliases, vim config, and tab completion - Memorize the skeletons — skim
skeletons/once, then practice writing each from memory - Do exercises 1, 3, 5, 6, 10, 11 — they cover pods, ConfigMaps, NetworkPolicy, rolling updates, security, and StatefulSets (the highest-weighted domains)
- Run the interactive quiz —
bash scripts/quiz.shfor timed practice with auto-verification - Take the mock exam below — 17 questions, 70% weight coverage. Time yourself (5–8 min per question)
- Run killer.sh — do both sessions. Review every wrong answer. Repeat the weak topics
- Read Exam Day Strategy and Common Mistakes the night before
If you score 80%+ on the mock exam and 70%+ on killer.sh, you're ready. Book the exam.
Every exercise includes a
verify.shscript — run it after you attempt the exercise to auto-check your work:bash exercises/01-pod-basics/verify.sh
README.md # This guide
exercises/ # 14 hands-on labs (one per folder, each with verify.sh)
01-pod-basics/ # Easy
02-multi-container-pod/ # Medium
03-configmap-secret/ # Medium
04-rbac/ # Medium
05-networkpolicy/ # Hard
06-rolling-update/ # Medium
07-helm/ # Medium
08-probes/ # Medium
09-ingress-gateway/ # Hard
10-security-pvc/ # Hard
11-statefulset/ # Hard ← NEW
12-daemonset/ # Medium ← NEW
13-init-containers/ # Medium ← NEW
14-in-place-scaling/ # Hard ← NEW (v1.35 GA feature)
skeletons/ # 14 bare YAML templates for quick reference
scripts/
exam-setup.sh # Aliases + vim config (run first thing)
quiz.sh # Interactive terminal quiz with auto-verification
CONTRIBUTING.md
LICENSE
- Quick Start (< 4 Weeks to Exam)
- CKAD Exam Details & Quick Facts
- CKAD Syllabus Breakdown (v1.35)
- CKAD Domain Weight Distribution
- Practice Scenarios with Full Solutions
- Practice Questions — Full Mock Exam
- Study Progress Tracker
- What Changed in v1.35 for CKAD
- Before You Book the CKAD Exam
- The Exam Environment (PSI Remote Desktop)
- First 60 Seconds — Aliases, vim, bash
- Imperative Commands Quick Reference
- kubectl Cheat Sheet for CKAD
- Exam Day Strategy — Time Allocation
- Mistakes That Will Fail You on the CKAD
- Troubleshooting Decision Flowchart
- CKAD Exam Day Checklist
- Docs Pages I Actually Used During the Exam
- Study Resources for CKAD 2026
- CKAD Study Plan (4-5 Weeks)
- killer.sh vs the Real CKAD Exam
- CKAD Exam Details — Cost, Duration, Passing Score, Format
- How Much Does the CKAD Exam Cost?
- CKAD vs CKA vs CKS — Which Certification First?
- CKAD vs CKA vs CKS Scope Architecture Diagram
- CKAD FAQ — Common Questions
| Detail | Description |
|---|---|
| Exam Type | Performance-based (live terminal — NOT multiple choice) |
| Exam Duration | 2 hours |
| Passing Score | 66% (one free retake included) |
| Kubernetes Version on Exam | Kubernetes v1.35 |
| Certification Validity | 2 years |
| Exam Cost | $445 USD — check Linux Foundation site for coupons (30% off codes appear regularly; up to 50% during Black Friday / Cyber Monday) |
| Exam Delivery | PSI Bridge — remote proctored, browser-based |
| Allowed Resources During Exam | One browser tab open to kubernetes.io/docs, kubernetes.io/blog, and GitHub kubernetes repos only |
| Number of Questions | ~15–20 performance tasks |
| Cluster Contexts | Multiple clusters — you must switch context before each task |
Important: The Linux Foundation occasionally updates the exam Kubernetes version. Always verify the current version at training.linuxfoundation.org before you register.
The CKAD exam costs $445 USD (as of March 2026). That includes:
- One free retake if you fail (valid 12 months)
- Two killer.sh simulator sessions (each valid 36 hours)
I waited for a Linux Foundation 30% off promotion and saved about $130. They run these a few times a year — check the training site for current deals. Black Friday / Cyber Monday discounts can go up to 50%. Bundle pricing (CKA + CKAD or CKAD + CKS) also knocks 20–30% off.
| CKAD | CKA | CKS | |
|---|---|---|---|
| Focus | Application development, pod design | Cluster management, troubleshooting | Security hardening, vulnerability scanning |
| Difficulty | Medium | Medium-Hard | Hard (requires CKA first) |
| Duration | 2 hours | 2 hours | 2 hours |
| Cost | $445 | $445 | $445 |
| Prerequisite | None | None | Active CKA certification |
| Best for | Developers deploying on K8s | DevOps engineers, SREs, platform teams | Security engineers, compliance roles |
| Typical order | 1st or 2nd (if you're a dev) | Start here (if you're ops) | 3rd (requires CKA) |
Recommendation: If you build and deploy applications on Kubernetes, CKAD first. If you manage clusters, start with CKA. The ~40% shared content means studying for one makes the other easier. CKS requires an active CKA, so you cannot skip it.
graph TB
subgraph CKAD ["CKAD — Application Developer"]
A1[Pods & Workloads]
A2[Deployments & Rollouts]
A3[ConfigMaps & Secrets]
A4[Services & Networking]
A5[Helm & Kustomize]
A6[Probes & Logging]
A7[RBAC for Apps]
A8[SecurityContext]
A9[Gateway API]
end
subgraph CKA ["CKA — Cluster Admin"]
B1[Cluster Install & Upgrade]
B2[etcd Backup & Restore]
B3[Node Management]
B4[Cluster DNS & Networking]
B5[Scheduler & Taints]
B6[Storage Classes & Provisioners]
B7[Cluster RBAC]
B8[Troubleshoot Control Plane]
end
subgraph CKS ["CKS — Security Specialist"]
C1[Pod Security Standards]
C2[Network Policies Advanced]
C3[Container Image Scanning]
C4[Audit Logging]
C5[Falco & Runtime Security]
C6[Supply Chain Security]
C7[OPA Gatekeeper]
end
CKAD -->|"~40% overlap"| CKA
CKA -->|"required"| CKS
style CKAD fill:#326CE5,color:#fff
style CKA fill:#1a73e8,color:#fff
style CKS fill:#d32f2f,color:#fff
The CKAD exam runs on Kubernetes v1.35 as of March 2026. Here are the changes relevant to the exam:
| Feature | Status | What It Means for CKAD |
|---|---|---|
| Sidecar Containers | GA (Stable) | Sidecars use restartPolicy: Always inside initContainers. They start before the main container and persist for the Pod lifetime. Multi-container Pod questions reference this. |
| In-Place Pod Vertical Scaling | GA (Stable) | Resize CPU/memory of a running Pod without restarting it. Expect resource management tasks using this. |
| OCI Artifact Volumes | Beta (default) | Mount OCI images as read-only volumes. Enabled by default in v1.35. Useful for tooling and init workflows. |
| Gateway API (v1.4+) | Standard | HTTPRoute is the standard. Ingress NGINX is retired (March 2026). Exam questions now favor Gateway API over Ingress. |
| Ingress NGINX Retired | Removed | Ingress NGINX is archived after March 2026. Migrate to Gateway API — know both for the exam but expect Gateway API questions. |
| Fine-Grained Container Restart Rules | Beta | Per-container restartPolicyRules — restart individual containers based on exit codes without recreating the Pod. |
| cgroup v1 Removed | Removed | Kubelet requires cgroup v2. Nodes on old distros without cgroup v2 will fail. |
| KYAML | Beta (default) | Safer YAML subset for Kubernetes — enabled by default. Less ambiguous than standard YAML. |
| Configurable HPA Tolerance | Beta | Set per-resource tolerance for HPA scaling sensitivity instead of the fixed 10% global default. |
| Deployment terminatingReplicas | Beta | New status field showing Pods being terminated — helps with rollout observability. |
When I took the exam in March 2026, I saw Helm, multi-container pod (sidecar), probes, and NetworkPolicy questions. The Ingress NGINX retirement means Gateway API knowledge is no longer optional.
Full changelog: Kubernetes v1.35 CHANGELOG
I booked my exam two weeks before I was ready because Linux Foundation had a 30% off sale. Here's what I'd suggest before registering:
-
Get comfortable with vim first. The exam terminal doesn't support Ctrl+C/Ctrl+V the way you expect. I fumbled with paste on my first question and lost a few minutes. Practice writing YAML by hand.
-
Do killer.sh at least twice. Two free sessions come with the exam purchase (each valid 36 hours). I scored 52% the first time and 81% a week later. killer.sh is harder than the real exam on purpose.
-
Use Killercoda for free daily practice. killercoda.com — browser-based Kubernetes playground, no local setup required.
-
Read the Candidate Handbook. The Linux Foundation publishes a Candidate Handbook. Understand what IDs are accepted, what you can have in the room, and how the PSI check-in process works before exam day.
-
Schedule your exam at your best time of day. I booked mine at 9 AM on a Saturday. If you're a night owl, don't book early morning.
-
Clean your room the night before. The proctor asked me to unplug my second monitor and remove a sticky note from my desk before starting. Clear your desk the night before to avoid delays.
-
Learn where things are in the docs, not just what they say. You get one tab open to kubernetes.io/docs during the exam. If you've never navigated those pages under pressure, you'll waste time. The Tasks section has the most copy-paste-friendly YAML.
The exam runs inside a remote desktop hosted by PSI — a Linux desktop in your browser. Here's the setup:
- You open the PSI Secure Browser on your machine.
- The proctor checks your ID and room via webcam.
- You get dropped into a remote Linux desktop with a terminal and a Firefox browser pointed at kubernetes.io/docs.
- The terminal has access to multiple Kubernetes clusters (usually 4–6 clusters across the questions).
- Every question includes a context switch command like
kubectl config use-context k8s— run this before you do anything else in that question.
Things that tripped me up:
- Copy/paste uses
Ctrl+Shift+C/Ctrl+Shift+V— NOTCtrl+C/V. I hit Ctrl+C thinking I was copying and killed a running process. - The connection will lag at some point. Don't retype the same command — just wait.
- You get one terminal tab by default — try
Ctrl+Alt+Tto open a second one. Having two terminals open saved me on a multi-container pod question. mousepadis available as a text editor if you hate vim, but honestly just learn vim. Under pressure, fewer tools = fewer mistakes.- Don't use
tmuxunless you literally use it every day at work. I watched a YouTuber recommend it and wasted time in practice fighting with tmux instead of solving questions.
Allowed documentation during the exam:
- kubernetes.io/docs
- kubernetes.io/blog
- GitHub repos under the
kubernetesorganization
That is it. No Stack Overflow, no Medium, no your own notes.
Pro tip: search on kubernetes.io/docs and click the Tasks result, not Concepts. Tasks pages have ready-to-use YAML.
Before you touch Question 1, run this setup. It takes about 30 seconds and you'll use these aliases constantly.
# 1. Set aliases (your best friend for 2 hours)
alias k=kubectl
export do='--dry-run=client -o yaml'
export now='--force --grace-period=0'
# 2. Enable tab completion
source <(kubectl completion bash)
complete -F __start_kubectl k
# 3. Fix vim for YAML editing
cat << 'EOF' >> ~/.vimrc
set expandtab
set tabstop=2
set shiftwidth=2
set number
EOF
# 4. Verify everything works
k get nodesThat's it. Don't overthink it. Don't try to set up tmux. Don't customize your bash prompt. Just these 4 steps, then start solving.
This is also available as a standalone script: scripts/exam-setup.sh
I practiced this sequence every morning during my study weeks so I could type it without thinking on exam day.
Why imperative? Under exam pressure you need speed. Most CKAD questions can be solved faster with imperative commands than writing YAML from scratch. Use --dry-run=client -o yaml to generate scaffolds, then edit if needed.
# Basic pod
k run nginx --image=nginx
# Pod with port
k run nginx --image=nginx --port=80
# Pod with labels
k run nginx --image=nginx --labels=app=web,tier=frontend
# Pod with environment variables
k run nginx --image=nginx --env=LOG_LEVEL=debug --env=APP_ENV=prod
# Pod with command override
k run nginx --image=nginx -- sh -c "echo 'Hello' && sleep 3600"
# Generate YAML without running
k run nginx --image=nginx $do > pod.yaml# Basic deployment
k create deployment webapp --image=nginx
# Deployment with replicas
k create deployment webapp --image=nginx --replicas=3
# Scale deployment
k scale deployment webapp --replicas=5
# Update image (rolling update)
k set image deployment/webapp nginx=nginx:1.27 --record
# Rollout commands
k rollout status deployment/webapp
k rollout history deployment/webapp
k rollout undo deployment/webapp
k rollout undo deployment/webapp --to-revision=2# Expose as ClusterIP (internal only)
k expose deployment webapp --port=80 --target-port=8080
# Expose as NodePort
k expose deployment webapp --port=80 --target-port=8080 --type=NodePort
# Expose as LoadBalancer
k expose deployment webapp --port=80 --target-port=8080 --type=LoadBalancer
# Generate service YAML for editing
k expose deployment webapp --port=80 $do > svc.yaml# Create ConfigMap from literals
k create configmap app-config --from-literal=LOG_LEVEL=debug --from-literal=DB_HOST=postgres
# Create ConfigMap from file
k create configmap app-config --from-file=config.properties
# Create Secret from literals
k create secret generic db-secret --from-literal=username=admin --from-literal=password=secret123
# Create Secret from file
k create secret generic tls-secret --from-file=tls.crt=cert.pem --from-file=tls.key=key.pem
# Generate as YAML
k create configmap app-config --from-literal=key=value $do > cm.yaml# Create ServiceAccount
k create sa my-app -n prod
# Create Role
k create role pod-reader --verb=get,list,watch --resource=pods -n prod
# Create RoleBinding
k create rolebinding read-pods --role=pod-reader --serviceaccount=prod:my-app -n prod
# Create ClusterRole
k create clusterrole node-reader --verb=get,list --resource=nodes
# Create ClusterRoleBinding
k create clusterrolebinding read-nodes --clusterrole=node-reader --serviceaccount=prod:my-app
# Check permissions
k auth can-i list pods -n prod --as=system:serviceaccount:prod:my-appExam tip: Use
$doto generate YAML, review it, then apply. Never type full manifests from memory under time pressure.
You can't bookmark in the exam browser, but you can memorize where things are. These are the kubernetes.io pages I actually used during the exam:
| Topic | How to Find It | Why It's Useful |
|---|---|---|
| Pod YAML | Search: pod → Tasks > Configure Pods |
Basic pod spec with volumes, env, etc. |
| Multi-container Pod / Sidecar | Search: sidecar → first result |
The restartPolicy: Always initContainer syntax |
| Deployment + Rolling Update | Search: deployment → Concepts > Deployments |
Strategy, maxSurge, maxUnavailable examples |
| CronJob YAML | Search: cronjob → Tasks result |
Complete CronJob spec with schedule |
| ConfigMap | Search: configmap → Tasks > Configure Pods to use ConfigMaps |
env, envFrom, and volume mount examples |
| Secret | Search: secret → Tasks > Secrets |
Creating and consuming secrets |
| Probes | Search: liveness → Tasks > Configure Liveness, Readiness Probes |
httpGet, exec, tcpSocket probe examples |
| NetworkPolicy YAML | Search: network policy → Tasks result |
Ingress + egress + DNS egress ready to copy |
| Service Types | Search: service → Concepts > Service |
ClusterIP, NodePort, LoadBalancer |
| Ingress | Search: ingress → Concepts > Ingress |
TLS + path-based routing examples |
| Gateway API HTTPRoute | Search: gateway api → or go to gateway-api.sigs.k8s.io |
GatewayClass + Gateway + HTTPRoute examples |
| RBAC | Search: rbac → Using RBAC Authorization |
Role, ClusterRole, bindings |
| PV / PVC | Search: persistent volume → Tasks > Configure a Pod to Use a PersistentVolume |
Complete PV → PVC → Pod chain |
| SecurityContext | Search: security context → Tasks result |
runAsUser, capabilities, drop ALL |
| Resource Quotas | Search: resource quota → first result |
LimitRange + ResourceQuota YAML |
| Helm | Search: helm or go directly to helm.sh/docs |
install, upgrade, rollback, show values |
| kubectl Cheat Sheet | Search: cheat sheet → first result |
All the imperative commands |
The Tasks section usually has better copy-paste YAML than Concepts.
These aliases save a lot of typing. Set them up in the first 30 seconds of the exam.
alias k=kubectl
export do='--dry-run=client -o yaml'
export now='--force --grace-period=0'
source <(kubectl completion bash)
complete -F __start_kubectl kNow k get pods -A works, and k delete pod mypod $now kills pods instantly without the 30-second wait.
cat << 'EOF' >> ~/.vimrc
set expandtab
set tabstop=2
set shiftwidth=2
set number
EOFWithout this, a single Tab key in vim breaks YAML indentation. This is one of the most common silent failures — your YAML looks right but fails with an indentation error.
# Pod
k run nginx --image=nginx $do > pod.yaml
# Deployment
k create deployment nginx --image=nginx --replicas=3 $do > deploy.yaml
# Service (expose existing deployment)
k expose deployment nginx --port=80 $do > svc.yaml
# ConfigMap
k create configmap app-cfg --from-literal=ENV=prod $do > cm.yaml
# Secret
k create secret generic app-sec --from-literal=password=1234 $do > sec.yaml
# ServiceAccount
k create serviceaccount app-sa $do > sa.yaml
# Role
k create role pod-reader --verb=get,list,watch --resource=pods $do > role.yaml
# RoleBinding
k create rolebinding bind-reader --role=pod-reader --serviceaccount=default:app-sa $do > rb.yaml
# Job
k create job hello --image=busybox -- echo hello $do > job.yaml
# CronJob
k create cronjob hello --image=busybox --schedule="*/1 * * * *" -- echo hello $do > cj.yamlYou generate the scaffold, edit the 2–3 fields you need, then k apply -f. This is 10x faster than writing YAML from scratch.
# Context management — run BEFORE every question
kubectl config use-context <context-name>
kubectl config current-context
# Explore what a field means
kubectl explain pod.spec.containers.resources
kubectl explain pod.spec --recursive | grep -A 3 tolerations
# Watch pods in real-time
kubectl get pods -w
# Get all resources in all namespaces
kubectl get all -A
# Get events sorted by time (best for debugging)
kubectl get events --sort-by='.lastTimestamp' -n <namespace>
# Force delete a stuck pod immediately
kubectl delete pod stuck-pod --force --grace-period=0
# Port forward to test a service
kubectl port-forward svc/my-service 8080:80
# Resource usage (requires metrics-server)
kubectl top pods --sort-by=cpu
kubectl top nodes
# Label operations
kubectl label pod mypod app=web
kubectl get pods -l app=webThis follows the official CKAD Curriculum v1.35. Application Environment, Configuration & Security (25%) and the two 20% domains make up the bulk of the exam. I spent most of my study time on those.
pie title CKAD Exam Weight Distribution (v1.35)
"App Environment & Security (25%)" : 25
"Application Design & Build (20%)" : 20
"Application Deployment (20%)" : 20
"Services & Networking (20%)" : 20
"Observability & Maintenance (15%)" : 15
| Domain | Key Concepts | Weight |
|---|---|---|
| Application Design and Build | Container images, workload resources, multi-container pods, volumes | 20% |
| Application Deployment | Blue/green, canary, rolling updates, Helm, Kustomize | 20% |
| Application Observability and Maintenance | API deprecations, probes, monitoring, logs, debugging | 15% |
| Application Environment, Configuration, and Security | CRDs, RBAC, requests/limits, ConfigMaps, Secrets, ServiceAccounts, SecurityContexts | 25% |
| Services & Networking | NetworkPolicies, Services, Ingress, Gateway API | 20% |
App Environment & Security + either Design or Networking = 45% of the exam. If you're short on time, focus on those.
20% of the exam. Covers container images, workload types, multi-container pods, and volumes. If you've used Docker before, most of this will feel familiar.
You need to know how to package an app into a container image. The exam may ask you to write or edit a Dockerfile.
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.htmldocker build -t your-dockerhub-username/custom-nginx:latest .
docker push your-dockerhub-username/custom-nginx:latestDeploy it:
apiVersion: apps/v1
kind: Deployment
metadata:
name: custom-nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: your-dockerhub-username/custom-nginx:latestkubectl apply -f deployment.yamlDifferent workloads solve different problems:
- Deployment: scalable, stateless apps
- DaemonSet: one pod per node (logging agents, monitoring)
- Job: run-to-completion tasks
- CronJob: scheduled Jobs
- StatefulSet: stateful apps needing stable identity and persistent storage
CronJob example — run a backup every night at 2 AM:
apiVersion: batch/v1
kind: CronJob
metadata:
name: backup-job
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: busybox
args:
- "/bin/sh"
- "-c"
- "echo Backup complete"
restartPolicy: OnFailureJob example:
k create job hello --image=busybox -- echo hello $do > job.yaml
k apply -f job.yaml
k get jobs
k logs job/helloKubernetes: Workloads Overview
Sometimes your Pod needs more than one container. The three patterns to know:
Init container (runs before main container starts):
spec:
initContainers:
- name: init-db
image: busybox
command: ['sh', '-c', 'until nslookup mysql; do echo waiting; sleep 2; done']
containers:
- name: app
image: myapp:v1Sidecar container (GA since v1.33 — uses restartPolicy: Always inside initContainers):
spec:
initContainers:
- name: log-sidecar
image: fluentd:v1.16
restartPolicy: Always # This makes it a sidecar — starts before main, runs for Pod lifetime
containers:
- name: app
image: myapp:v1Classic multi-container Pod (sidecar logging with shared volume):
apiVersion: v1
kind: Pod
metadata:
name: sidecar-example
spec:
containers:
- name: main-app
image: busybox
command: ["sh", "-c", "while true; do echo $(date) >> /var/log/app.log; sleep 5; done"]
volumeMounts:
- name: log-volume
mountPath: /var/log
- name: sidecar
image: busybox
command: ["sh", "-c", "tail -f /var/log/app.log"]
volumeMounts:
- name: log-volume
mountPath: /var/log
volumes:
- name: log-volume
emptyDir: {}Ephemeral containers (for debugging — stable since v1.33):
# Attach a debug container to a running pod
kubectl debug -it pod/myapp --image=busybox --target=app
# Create a copy with a debug container
kubectl debug pod/myapp -it --copy-to=debug-pod --image=ubuntu --share-processesKnow when to use emptyDir (temporary, dies with the Pod) vs PersistentVolumeClaim (data survives Pod restarts).
PVC example:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-example
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1GiPod mounting the PVC:
apiVersion: v1
kind: Pod
metadata:
name: pod-with-pvc
spec:
containers:
- name: app-container
image: nginx
volumeMounts:
- mountPath: /usr/share/nginx/html
name: storage
volumes:
- name: storage
persistentVolumeClaim:
claimName: pvc-exampleCommon volume types you should know:
# emptyDir — shared temp storage between containers (lost when pod dies)
volumes:
- name: cache
emptyDir: {}
# hostPath — mounts a path from the node (use carefully)
volumes:
- name: host-logs
hostPath:
path: /var/log
type: Directory
# configMap as volume
volumes:
- name: config
configMap:
name: app-config
# secret as volume
volumes:
- name: certs
secret:
secretName: tls-secretkubectl apply -f pvc.yaml
kubectl apply -f pod.yaml
kubectl get pvcPersistent Volumes | Ephemeral Volumes
20% of the exam. Covers deployment strategies, rolling updates, Helm, and Kustomize. The Helm and rolling update questions were the quickest points for me.
Kubernetes doesn't have built-in blue/green or canary strategies, but you can implement them with labels, selectors, and Services.
Blue/Green — create separate deployments and toggle traffic with the Service selector:
# blue-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: blue-deployment
spec:
replicas: 2
selector:
matchLabels:
app: my-app
version: blue
template:
metadata:
labels:
app: my-app
version: blue
spec:
containers:
- name: app
image: my-app:blue# green-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: green-deployment
spec:
replicas: 2
selector:
matchLabels:
app: my-app
version: green
template:
metadata:
labels:
app: my-app
version: green
spec:
containers:
- name: app
image: my-app:green# switch Service to green
apiVersion: v1
kind: Service
metadata:
name: my-app-svc
spec:
selector:
app: my-app
version: green # switch this to toggle traffic
ports:
- port: 80
targetPort: 8080Canary — roll out a small number of replicas with the new version:
apiVersion: apps/v1
kind: Deployment
metadata:
name: canary
spec:
replicas: 1
selector:
matchLabels:
app: my-app
version: canary
template:
metadata:
labels:
app: my-app
version: canary
spec:
containers:
- name: app
image: my-app:canarykubectl apply -f canary-deployment.yaml
kubectl scale deployment canary --replicas=3Zero-downtime updates and quick rollbacks — know these commands cold:
# Create a deployment
k create deployment webapp --image=nginx:1.24 --replicas=3
# Update the image (triggers rolling update)
k set image deployment/webapp nginx=nginx:1.25
# Check rollout status
k rollout status deployment/webapp
# View rollout history
k rollout history deployment/webapp
# Rollback to previous version
k rollout undo deployment/webapp
# Rollback to a specific revision
k rollout undo deployment/webapp --to-revision=2
# Scale
k scale deployment/webapp --replicas=5Deployment strategy in YAML:
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: nginx:1.25
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256MiHelm lets you install, upgrade, and manage apps from packaged charts.
# Add a repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# Search for a chart
helm search repo bitnami/nginx
# Install a release
helm install my-release bitnami/nginx
# List installed releases
helm list
helm list -A # all namespaces
# Upgrade a release with new values
helm upgrade my-release bitnami/nginx --set replicaCount=3
# Rollback to previous revision
helm rollback my-release 1
# Uninstall
helm uninstall my-release
# Show chart values (useful during exam)
helm show values bitnami/nginxKustomize lets you layer manifest configurations without templates. It's built into kubectl.
Base Deployment:
# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: kustom-demo
spec:
replicas: 2
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
containers:
- name: app
image: my-app:v1Overlay Patch:
# overlays/prod/kustomization.yaml
resources:
- ../../base
patchesStrategicMerge:
- patch.yaml# overlays/prod/patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: kustom-demo
spec:
replicas: 5# Apply with Kustomize
kubectl apply -k overlays/prod/
# Preview what Kustomize generates
kubectl kustomize ./base/15% of the exam. Covers probes, logging, monitoring, API deprecations, and debugging. I found these questions relatively quick if you know your kubectl commands. Check the common mistakes section — forgetting to set the right namespace is a classic way to lose points here.
Kubernetes APIs get deprecated across versions. Know how to detect and fix deprecated API versions in manifests.
kubectl convert -f deployment-v1beta1.yaml --output-version=apps/v1
kubectl get events --all-namespaces | grep -i deprecatedProbes tell Kubernetes whether your app is healthy and ready to receive traffic.
- Liveness: is the container alive? If it fails, kubelet restarts the container.
- Readiness: is the container ready to serve traffic? If it fails, the pod is removed from Service endpoints.
- Startup: for slow-starting containers — prevents liveness probe from killing the app before it starts.
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10Other probe types:
# exec probe — runs a command inside the container
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
# tcpSocket probe — checks if a port is open
readinessProbe:
tcpSocket:
port: 3306kubectl describe pod <pod-name> # check probe status in Eventskubectl top nodes
kubectl top pods
kubectl top pods --sort-by=cpu
kubectl top pods -A --sort-by=memory # all namespaces
kubectl describe pod <pod-name>
kubectl get events --sort-by='.lastTimestamp' --all-namespacesLogs are your first stop when something breaks.
kubectl logs <pod-name>
kubectl logs -f <pod-name> # follow/stream logs
kubectl logs <pod-name> -c <container-name> # specific container in multi-container pod
kubectl logs <pod-name> --previous # previous crash logs (critical for CrashLoopBackOff)
kubectl logs -l app=webapp --all-containers # all pods with labelSometimes you need to exec into a Pod or attach a debug container.
kubectl exec -it <pod-name> -- /bin/sh
kubectl get pod <pod-name> -o yaml
kubectl debug -it pod/myapp --image=busybox --target=app
kubectl debug pod/myapp -it --copy-to=debug-pod --image=ubuntu --share-processesSystematic debugging approach (use this on every troubleshooting question):
# Step 1: Check the pod status
k get pods -n <namespace> -o wide
# Step 2: Describe the pod (events section reveals the cause 90% of the time)
k describe pod <pod-name> -n <namespace>
# Step 3: Check logs
k logs <pod-name> -n <namespace>
k logs <pod-name> -n <namespace> --previous # if CrashLoopBackOff
# Step 4: Check events
k get events --sort-by='.lastTimestamp' -n <namespace>
# Step 5: Exec into the pod if it's running
k exec -it <pod-name> -n <namespace> -- /bin/shCommon pod failure states and what to check:
| Status | Likely Cause | What to Check |
|---|---|---|
| Pending | Insufficient resources, no matching PV | Check describe for events; check node capacity |
| ImagePullBackOff | Wrong image name, private registry | Fix image name; create imagePullSecret |
| CrashLoopBackOff | App crashing at startup, wrong command/args | Check logs --previous; fix command/config |
| CreateContainerError | Missing ConfigMap/Secret, volume mount issue | Verify referenced resources exist |
| OOMKilled | Container exceeded memory limit | Increase memory limit |
25% — the heaviest section. I got questions on RBAC, ConfigMaps, Secrets, and SecurityContexts. I spent more time studying this domain than any other. See also the exam tips section for how I prioritized questions by weight.
Custom Resource Definitions (CRDs) and Operators let you add new API types to Kubernetes or automate app management.
# List existing CRDs in the cluster
kubectl get crds
# Describe a CRD
kubectl describe crd <crd-name>
# Get custom resources
kubectl get <custom-resource-kind>Create a CRD:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: widgets.example.com
spec:
group: example.com
names:
kind: Widget
plural: widgets
singular: widget
shortNames:
- wg
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
size:
type: string
color:
type: stringCreate a custom resource instance:
apiVersion: example.com/v1
kind: Widget
metadata:
name: my-widget
spec:
size: large
color: blueOn the exam you may need to:
- Create custom resources from an existing CRD
- Inspect CRDs with
kubectl get crdsandkubectl describe - Install an operator via
kubectl apply -for Helm
RBAC controls who can do what. On killer.sh I used ClusterRole in a roleRef but forgot you can bind a ClusterRole with a RoleBinding to scope it to a namespace.
Core concepts:
- Role: grants permissions within a single namespace
- ClusterRole: grants permissions cluster-wide (or can be bound per-namespace with RoleBinding)
- RoleBinding: binds a Role or ClusterRole to a user/group/serviceaccount in a namespace
- ClusterRoleBinding: binds a ClusterRole cluster-wide
# Role — allow read access to pods in "dev" namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: dev
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
# RoleBinding — bind the Role to a ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods-binding
namespace: dev
subjects:
- kind: ServiceAccount
name: app-sa
namespace: dev
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.ioQuick imperative commands:
# Create a Role
k create role pod-reader --verb=get,list,watch --resource=pods -n dev
# Create a RoleBinding
k create rolebinding read-pods --role=pod-reader --serviceaccount=dev:app-sa -n dev
# Check if a ServiceAccount can do something
k auth can-i list pods --as=system:serviceaccount:dev:app-sa -n dev
# Check which roles exist
k get roles,rolebindings -n devResource constraints matter. Set them per container and per namespace.
resources:
requests: # scheduler uses this to find a node
cpu: "250m"
memory: "64Mi"
limits: # kubelet enforces this ceiling
cpu: "500m"
memory: "128Mi"- requests: minimum guaranteed resources — scheduler uses requests to decide where to place the pod
- limits: maximum allowed — exceeding memory limit → OOMKilled, exceeding CPU → throttled
- A pod stays Pending if no node has enough allocatable resources to satisfy its requests
LimitRange (set defaults per namespace):
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: dev
spec:
limits:
- default:
cpu: 500m
memory: 256Mi
defaultRequest:
cpu: 100m
memory: 128Mi
type: ContainerResourceQuota (cap total usage per namespace):
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-quota
spec:
hard:
pods: "10"
requests.cpu: "4"
requests.memory: "2Gi"
limits.cpu: "8"
limits.memory: "4Gi"ConfigMap:
# From literal
k create configmap app-config --from-literal=DB_HOST=mysql --from-literal=DB_PORT=3306
# From file
k create configmap nginx-conf --from-file=nginx.conf# Use ConfigMap as env variables
spec:
containers:
- name: app
image: myapp:v1
envFrom:
- configMapRef:
name: app-config
# Or individual keys:
env:
- name: DATABASE_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOSTSecret:
k create secret generic db-creds --from-literal=username=admin --from-literal=password=s3cret# Mount Secret as a volume
spec:
containers:
- name: app
image: myapp:v1
volumeMounts:
- name: secret-vol
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-vol
secret:
secretName: db-creds# Or as env:
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-creds
key: usernameConfigMaps Guide | Secrets Overview
ServiceAccounts bind Pods to identities that control what they can access in the Kubernetes API.
kubectl create serviceaccount app-botspec:
serviceAccountName: app-botUse SecurityContexts to run containers as non-root, drop privileges, and control file permissions.
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
containers:
- name: app
image: nginx
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]Networking was my weak spot. I got every NetworkPolicy question wrong on my first killer.sh attempt — usually because of a single indent breaking the policy. I ended up writing NetworkPolicies by hand repeatedly until the YAML stuck. For practice, see the study resources section.
NetworkPolicies control traffic flow between pods. By default, all pods accept all traffic. A NetworkPolicy acts as a firewall.
# Allow traffic to "api" pods only from "frontend" pods on port 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow-frontend
namespace: production
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
- to: # Allow DNS
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53Default deny all ingress in a namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {}
policyTypes:
- IngressCritical: Always allow DNS egress (port 53 UDP) in your NetworkPolicies, otherwise pods cannot resolve service names. This is the #1 NetworkPolicy gotcha on the exam.
# ClusterIP (default — internal only)
apiVersion: v1
kind: Service
metadata:
name: backend-svc
spec:
type: ClusterIP
selector:
app: backend
ports:
- port: 80
targetPort: 8080
---
# NodePort (exposes on every node's IP at a static port)
apiVersion: v1
kind: Service
metadata:
name: webapp-nodeport
spec:
type: NodePort
selector:
app: webapp
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # Range: 30000-32767Imperative:
k expose deployment webapp --type=NodePort --port=80 --target-port=8080 --name=webapp-npVerify and troubleshoot:
k get svc
k get endpoints <service-name> # Empty → selector doesn't match pod labels
k describe svc <service-name>
k exec -it <pod-name> -- curl http://my-app-svcIngress provides HTTP(S) routing to services. Note that Ingress NGINX was retired in March 2026 — Gateway API is now preferred, but Ingress is still in the curriculum.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: frontend-svc
port:
number: 80
tls:
- hosts:
- app.example.com
secretName: tls-secretGateway API is the successor to Ingress. On v1.35, HTTPRoute is standard channel (Gateway API v1.4+). The exam now tests this.
# GatewayClass — defines the controller
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: example-gc
spec:
controllerName: example.com/gateway-controller
---
# Gateway — the actual listener
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: example-gc
listeners:
- name: http
protocol: HTTP
port: 80
---
# HTTPRoute — routing rules
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-route
namespace: default
spec:
parentRefs:
- name: my-gateway
hostnames:
- "app.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: api-svc
port: 80
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: frontend-svc
port: 80CoreDNS runs as a Deployment in kube-system namespace. Its config is in a ConfigMap named coredns.
# Check CoreDNS pods
k get pods -n kube-system -l k8s-app=kube-dns
# View CoreDNS configuration
k get configmap coredns -n kube-system -o yaml
# Test DNS from inside a pod
k run dns-test --image=busybox --rm -it --restart=Never -- nslookup kubernetes.defaultDNS formats to remember:
- Service:
<service-name>.<namespace>.svc.cluster.local - Pod:
<pod-ip-dashed>.<namespace>.pod.cluster.local
If DNS is broken, check:
- CoreDNS pods running?
- CoreDNS service (
kube-dns) exists? - Endpoints populated?
k get endpoints kube-dns -n kube-system - Pod's
/etc/resolv.confpointing to the right nameserver?
You get 2 hours for ~15–20 questions, so roughly 6–8 minutes each. Here's the approach I used:
Pass 1 (first 90 minutes): Go through every question in order. If a question takes more than 5 minutes and you're stuck, flag it and move on. Do the quick wins first — a 2-point question you answer in 2 minutes is better than spending 15 minutes on a 4-point question you might get wrong.
Pass 2 (last 30 minutes): Return to flagged questions. You now have context from other questions, and the pressure of "I haven't started yet" is gone.
| Question Type | Target Time | Tips |
|---|---|---|
| Create a Pod/Deployment/Service | 2–3 min | Use imperative commands + $do for YAML generation |
| ConfigMap/Secret + inject into Pod | 3–4 min | Imperative create, then edit pod YAML |
| RBAC (Role + RoleBinding) | 3–4 min | Imperative commands are fastest |
| Multi-container Pod (sidecar/init) | 4–6 min | Know the sidecar restartPolicy: Always syntax |
| NetworkPolicy | 5–7 min | Have the skeleton memorized; adjust selectors |
| Helm install/upgrade | 2–3 min | Fastest points on the exam |
| Probes | 3–4 min | Add to existing pod spec — know the three types |
| Rolling update + rollback | 2–3 min | Imperative commands |
| CronJob/Job | 3–4 min | Generate with $do, adjust schedule |
| Troubleshooting | 5–8 min | Systematic: describe → logs → events → fix |
| Ingress / Gateway API | 5–7 min | Reference kubernetes.io/docs if needed |
- Always switch context first. Every question has a context command — run it, or you work on the wrong cluster and get 0 points.
- Validate your work. After creating a resource, verify it works:
k get,k describe, check logs. - Do not over-engineer. The exam wants the minimum working solution. Don't add resource limits if not asked.
- Use the notepad. The PSI environment has a notepad — use it to paste complex commands you want to reuse.
- If a pod is stuck, delete and recreate. Don't spend 5 minutes debugging your YAML. Delete the resource, regenerate, fix, apply.
These are the recurring mistakes I've seen discussed on Reddit and Slack, plus the ones I made myself. For how to handle time pressure, see exam day strategy.
Every question requires you to be on a specific cluster context. If you skip this, you deploy to the wrong cluster and get 0 for that question. Get in the habit:
kubectl config use-context <context-name>Run this at the start of every single question, even if you think you're already on the right context.
One wrong space and your resource fails silently. Use set expandtab tabstop=2 shiftwidth=2 in vim. Always validate:
k apply -f resource.yaml --dry-run=server--dry-run=server catches more errors than --dry-run=client because it validates against the API server.
Many questions specify a namespace. If you create a resource without -n <namespace>, it goes to default and you get 0.
# Check what namespace the question asks for
# Then either use -n flag:
k apply -f pod.yaml -n production
# Or set namespace in the YAML metadata:
metadata:
name: my-pod
namespace: productionWriting YAML from scratch under time pressure is slow and error-prone. Use imperative commands to generate scaffolds:
k run nginx --image=nginx $do > pod.yaml
# Edit the 2-3 fields you need, then applyYou created a Deployment? Check it:
k get deploy
k get pods
k describe pod <pod-name>If the pod is not Running, you lost the points. Catch it now, not after the exam.
If you're stuck after 7 minutes, flag it and move on. You can return to it later. A 3-point question is not worth missing two 2-point questions.
When you write a NetworkPolicy with egress restrictions, always include DNS:
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53kubernetes.io is open during the exam. Use the search bar, go straight to the page, and copy examples. The Tasks pages have the most copy-paste-friendly YAML.
If the question says "create a Pod", don't create a Deployment. If it says "in namespace staging", you must create in staging. Read every word.
I used this mental model every time something wasn't working. Saved me from going down rabbit holes on the exam:
flowchart TD
START[Something is broken] --> Q1{What's broken?}
Q1 -->|Pod| POD[kubectl describe pod]
Q1 -->|Service| SVC[kubectl get endpoints]
Q1 -->|DNS| DNS_CHECK[nslookup from inside a pod]
Q1 -->|NetworkPolicy| NP_CHECK[kubectl get netpol -n namespace]
POD --> POD_STATUS{Pod Status?}
POD_STATUS -->|Pending| PENDING{Events say what?}
PENDING -->|Insufficient CPU/Memory| FIX_RES[Reduce resource requests<br/>or check node capacity]
PENDING -->|No matching PV| FIX_PV[Create PV or<br/>fix storageClassName]
PENDING -->|Unschedulable| FIX_TAINT[Check taints/tolerations<br/>and node affinity]
POD_STATUS -->|ImagePullBackOff| FIX_IMG[Check image name<br/>Check imagePullSecret<br/>Check registry access]
POD_STATUS -->|CrashLoopBackOff| CRASH[kubectl logs --previous]
CRASH --> FIX_CRASH[Fix command/args<br/>Fix ConfigMap/Secret refs<br/>Fix permissions]
POD_STATUS -->|Running but wrong| RUNNING_BAD[kubectl logs + exec into pod]
SVC --> SVC_EP{Endpoints empty?}
SVC_EP -->|Yes| FIX_SEL[Selector doesn't match<br/>pod labels — fix them]
SVC_EP -->|No| SVC_PORT{Port mismatch?}
SVC_PORT -->|Yes| FIX_PORT[Fix targetPort<br/>to match container port]
SVC_PORT -->|No| CHECK_NP[Check NetworkPolicy]
DNS_CHECK --> DNS_STATUS{DNS working?}
DNS_STATUS -->|No| CHECK_DNS[Check CoreDNS pods<br/>Check kube-dns service<br/>Check /etc/resolv.conf]
DNS_STATUS -->|Yes| CHECK_SVC_NAME[Service name wrong?<br/>Namespace missing?]
NP_CHECK --> NP_FIX[Missing DNS egress?<br/>Wrong podSelector?<br/>Wrong port number?]
style START fill:#ff6b6b,color:#fff
style FIX_RES fill:#51cf66,color:#fff
style FIX_PV fill:#51cf66,color:#fff
style FIX_TAINT fill:#51cf66,color:#fff
style FIX_IMG fill:#51cf66,color:#fff
style FIX_CRASH fill:#51cf66,color:#fff
style FIX_SEL fill:#51cf66,color:#fff
style FIX_PORT fill:#51cf66,color:#fff
style NP_FIX fill:#51cf66,color:#fff
On the exam, when something isn't working, start at the top of this flow and work down.
These are based on what I actually practiced. Some are close to what I saw on the exam, some are from killer.sh patterns, some are from killercoda labs. Do them in a real cluster — reading the solution without trying first teaches you nothing. For more questions, see the practice questions section.
For hands-on labs you can clone and run locally, see the exercises/ folder — 10 structured exercises covering every CKAD domain.
Task: Create a pod web-logger with two containers: (1) nginx serving on port 80, writing access logs to a shared volume at /var/log/nginx, and (2) a sidecar log-reader using busybox that tails the access log file.
Solution
apiVersion: v1
kind: Pod
metadata:
name: web-logger
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: logs
mountPath: /var/log/nginx
- name: log-reader
image: busybox
command: ["sh", "-c", "tail -f /var/log/nginx/access.log"]
volumeMounts:
- name: logs
mountPath: /var/log/nginx
volumes:
- name: logs
emptyDir: {}k apply -f web-logger.yaml
k get pod web-logger
k logs web-logger -c log-readerTask: Create a ConfigMap app-settings with ENV=production and LOG_LEVEL=info. Create a Secret db-creds with username=admin and password=p@ss123. Create a pod config-pod using image nginx that uses the ConfigMap as environment variables and mounts the Secret at /etc/db-creds.
Solution
k create configmap app-settings --from-literal=ENV=production --from-literal=LOG_LEVEL=info
k create secret generic db-creds --from-literal=username=admin --from-literal=password=p@ss123apiVersion: v1
kind: Pod
metadata:
name: config-pod
spec:
containers:
- name: nginx
image: nginx
envFrom:
- configMapRef:
name: app-settings
volumeMounts:
- name: secret-vol
mountPath: /etc/db-creds
readOnly: true
volumes:
- name: secret-vol
secret:
secretName: db-credsk apply -f config-pod.yaml
k exec config-pod -- env | grep -E "ENV|LOG_LEVEL"
k exec config-pod -- ls /etc/db-credsTask: In namespace apps, create a ServiceAccount deploy-sa that can only get, list, and create Deployments.
Solution
k create namespace apps
k create serviceaccount deploy-sa -n apps
k create role deploy-manager --verb=get,list,create --resource=deployments -n apps
k create rolebinding deploy-sa-binding --role=deploy-manager --serviceaccount=apps:deploy-sa -n apps
# Verify
k auth can-i create deployments --as=system:serviceaccount:apps:deploy-sa -n apps
# yes
k auth can-i delete deployments --as=system:serviceaccount:apps:deploy-sa -n apps
# noTask: In namespace secure, create a NetworkPolicy that allows ingress traffic to pods labeled app=db only from pods labeled app=api on port 3306. Allow DNS egress.
Solution
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-allow-api
namespace: secure
spec:
podSelector:
matchLabels:
app: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 3306
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53k apply -f netpol.yaml
k describe networkpolicy db-allow-api -n secureTask: Create a CronJob log-cleaner in namespace maintenance that runs every hour and executes find /var/log -name '*.log' -mtime +7 -delete using image busybox.
Solution
apiVersion: batch/v1
kind: CronJob
metadata:
name: log-cleaner
namespace: maintenance
spec:
schedule: "0 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: cleaner
image: busybox
command: ["sh", "-c", "find /var/log -name '*.log' -mtime +7 -delete"]
restartPolicy: OnFailureTask: Create a PersistentVolume pv-log of 100Mi using hostPath /pv/log. Create a PVC pvc-log requesting 50Mi. Create a pod logger using image busybox with command sleep 3600 that mounts this volume at /var/log/app.
Solution
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-log
spec:
capacity:
storage: 100Mi
accessModes:
- ReadWriteOnce
hostPath:
path: /pv/log
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-log
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Mi
---
apiVersion: v1
kind: Pod
metadata:
name: logger
spec:
containers:
- name: logger
image: busybox
command: ["sleep", "3600"]
volumeMounts:
- name: log-vol
mountPath: /var/log/app
volumes:
- name: log-vol
persistentVolumeClaim:
claimName: pvc-logk apply -f pv-pvc-pod.yaml
k get pv,pvc
k exec logger -- ls /var/log/appTask: Add the Bitnami Helm repo. Install a release named web-app using chart bitnami/nginx in namespace web. Then upgrade the release to set replicaCount=3.
Solution
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
kubectl create namespace web
helm install web-app bitnami/nginx -n web
helm upgrade web-app bitnami/nginx -n web --set replicaCount=3
helm list -n webTask: Create a pod health-check with image nginx that has: a liveness probe checking HTTP GET / on port 80 every 10 seconds, and a readiness probe checking TCP socket on port 80 every 5 seconds with an initial delay of 3 seconds.
Solution
apiVersion: v1
kind: Pod
metadata:
name: health-check
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /
port: 80
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 3
periodSeconds: 5k apply -f health-check.yaml
k describe pod health-check | grep -A 5 "Liveness\|Readiness"Disclaimer: These are original practice questions based on publicly available curriculum objectives and common themes discussed by candidates on Reddit, Slack, and YouTube. They are NOT actual exam questions. Sharing real exam content violates the CNCF certification agreement.
Set a timer (5–8 minutes per question), use only kubernetes.io/docs as reference, and work in a real cluster. Try each one before opening the solution.
Context: kubectl config use-context k8s-cluster1
Task: Create a pod named api-server in namespace backend with image nginx:1.25. Set CPU request to 100m, memory request to 128Mi, CPU limit to 250m, memory limit to 256Mi. Add labels app=api and tier=backend.
Solution
k run api-server --image=nginx:1.25 -n backend --labels="app=api,tier=backend" $do > q1.yaml
# Edit q1.yaml to add resources, then apply
k apply -f q1.yamlapiVersion: v1
kind: Pod
metadata:
name: api-server
namespace: backend
labels:
app: api
tier: backend
spec:
containers:
- name: api-server
image: nginx:1.25
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256MiContext: kubectl config use-context k8s-cluster1
Task: Create a CronJob named report-gen in namespace jobs that runs every 30 minutes using image busybox. The command should be echo "Report generated at $(date)". Set successfulJobsHistoryLimit to 3 and failedJobsHistoryLimit to 1.
Solution
k create cronjob report-gen --image=busybox --schedule="*/30 * * * *" -n jobs -- sh -c 'echo "Report generated at $(date)"' $do > q2.yaml
# Edit to add history limits, then applyapiVersion: batch/v1
kind: CronJob
metadata:
name: report-gen
namespace: jobs
spec:
schedule: "*/30 * * * *"
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
containers:
- name: report-gen
image: busybox
command: ["sh", "-c", "echo Report generated at $(date)"]
restartPolicy: OnFailureContext: kubectl config use-context k8s-cluster2
Task: Create a pod named web-app in namespace frontend with:
- An init container named
init-configusing imagebusyboxthat creates a file/work-dir/index.htmlwith content<h1>Hello CKAD</h1> - A main container named
nginxusing imagenginxthat mounts the same volume at/usr/share/nginx/html - Use an emptyDir volume named
html
Solution
apiVersion: v1
kind: Pod
metadata:
name: web-app
namespace: frontend
spec:
initContainers:
- name: init-config
image: busybox
command: ["sh", "-c", "echo '<h1>Hello CKAD</h1>' > /work-dir/index.html"]
volumeMounts:
- name: html
mountPath: /work-dir
containers:
- name: nginx
image: nginx
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
volumes:
- name: html
emptyDir: {}k apply -f q3.yaml
k exec web-app -n frontend -- curl -s localhost
# Should output: <h1>Hello CKAD</h1>Context: kubectl config use-context k8s-cluster1
Task: Create a Deployment named frontend in namespace production with image nginx:1.24 and 4 replicas. Configure the rolling update strategy with maxSurge=2 and maxUnavailable=1. Then update the image to nginx:1.25. After the update completes, rollback to the previous revision.
Solution
k create deployment frontend --image=nginx:1.24 --replicas=4 -n production $do > q4.yaml
# Edit to add strategy, then apply
k apply -f q4.yaml
k set image deployment/frontend nginx=nginx:1.25 -n production
k rollout status deployment/frontend -n production
k rollout undo deployment/frontend -n productionContext: kubectl config use-context k8s-cluster1
Task: Using Helm:
- Add the Bitnami repository:
https://charts.bitnami.com/bitnami - Install a release named
cacheusing chartbitnami/redisin namespacedata(create the namespace if it doesn't exist) - Upgrade the release to set
replica.replicaCount=3
Solution
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
kubectl create namespace data
helm install cache bitnami/redis -n data
helm upgrade cache bitnami/redis -n data --set replica.replicaCount=3
helm list -n dataContext: kubectl config use-context k8s-cluster2
Task: A pod web-server in namespace monitoring is running with image nginx but has no probes configured. Add:
- A liveness probe: HTTP GET on path
/healthzport 80, initial delay 10s, period 15s - A readiness probe: HTTP GET on path
/readyport 80, initial delay 5s, period 10s
Solution
k get pod web-server -n monitoring -o yaml > q6.yaml
# Edit to add probes under spec.containers[0]: livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /ready
port: 80
initialDelaySeconds: 5
periodSeconds: 10k delete pod web-server -n monitoring
k apply -f q6.yaml
k describe pod web-server -n monitoring | grep -A 5 "Liveness\|Readiness"Context: kubectl config use-context k8s-cluster1
Task: Pod data-processor in namespace batch is in CrashLoopBackOff state. Investigate the issue and fix it so the pod runs successfully.
Solution
# Step 1: Check status
k get pod data-processor -n batch
# Step 2: Check events
k describe pod data-processor -n batch
# Step 3: Check logs (including previous crash)
k logs data-processor -n batch --previous
# Common fixes:
# - Wrong command/args → fix the command
# - Missing ConfigMap/Secret → create the missing resource
# - Wrong image → fix the image name
# - OOMKilled → increase memory limits
# After identifying the issue, get the YAML, fix it, and recreate:
k get pod data-processor -n batch -o yaml > fix.yaml
# Edit fix.yaml
k delete pod data-processor -n batch
k apply -f fix.yamlContext: kubectl config use-context k8s-cluster1
Task: Create a ServiceAccount named monitoring-sa in namespace monitoring. Create a Role named pod-metrics-reader that grants get, list, and watch permissions on pods and pods/log resources. Bind it using a RoleBinding named monitoring-binding.
Solution
k create serviceaccount monitoring-sa -n monitoring
k create role pod-metrics-reader --verb=get,list,watch --resource=pods,pods/log -n monitoring
k create rolebinding monitoring-binding --role=pod-metrics-reader --serviceaccount=monitoring:monitoring-sa -n monitoring
# Verify
k auth can-i list pods --as=system:serviceaccount:monitoring:monitoring-sa -n monitoring
k auth can-i delete pods --as=system:serviceaccount:monitoring:monitoring-sa -n monitoringContext: kubectl config use-context k8s-cluster2
Task: Create a pod named secure-app in namespace restricted with image nginx. Configure it to:
- Run as user 1000
- Run as group 3000
- Set fsGroup to 2000
- Drop ALL capabilities
- Disallow privilege escalation
Solution
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: restricted
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
containers:
- name: nginx
image: nginx
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]Context: kubectl config use-context k8s-cluster2
Task: In namespace restricted, create a NetworkPolicy named api-netpol that applies to pods labeled role=api and:
- Allows ingress only from pods labeled
role=frontendin the same namespace, on TCP port 443 - Allows egress only to pods labeled
role=databasein the same namespace on TCP port 5432 - Allows egress to any destination on UDP port 53 (DNS)
- Denies all other ingress and egress traffic
Solution
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-netpol
namespace: restricted
spec:
podSelector:
matchLabels:
role: api
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 443
egress:
- to:
- podSelector:
matchLabels:
role: database
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53Context: kubectl config use-context k8s-cluster1
Task: A deployment payment-api with 3 replicas exists in namespace finance. Pods are running with label app=payment but the existing service payment-svc has no endpoints. Investigate and fix the service so it correctly routes traffic to the pods on port 8080.
Solution
# Check current service selector
k get svc payment-svc -n finance -o yaml | grep -A5 selector
# Check pod labels
k get pods -n finance --show-labels
# The selector in the service likely doesn't match the pod labels
# Fix the service selector to match app=payment
k edit svc payment-svc -n finance
# Update the selector to: app: payment
# Or recreate:
k delete svc payment-svc -n finance
k expose deployment payment-api --name=payment-svc --port=80 --target-port=8080 -n finance
# Verify endpoints
k get endpoints payment-svc -n financeContext: kubectl config use-context k8s-cluster1
Task: A GatewayClass external-gc and a Gateway main-gateway already exist in namespace default. Create an HTTPRoute named app-routing that:
- Attaches to
main-gateway - Routes requests for host
shop.example.comwith path prefix/apito serviceapi-svcon port 8080 - Routes requests for host
shop.example.comwith path prefix/to serviceweb-svcon port 80
Solution
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-routing
namespace: default
spec:
parentRefs:
- name: main-gateway
hostnames:
- "shop.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: api-svc
port: 8080
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: web-svc
port: 80Context: kubectl config use-context k8s-cluster1
Task: A base deployment exists at ./base/deployment.yaml. Create a production overlay at ./overlays/prod/ that:
- References the base
- Changes replicas to 5
- Adds a label
env: productionto the deployment
Solution
# overlays/prod/kustomization.yaml
resources:
- ../../base
patchesStrategicMerge:
- patch.yaml# overlays/prod/patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: kustom-demo
labels:
env: production
spec:
replicas: 5kubectl kustomize ./overlays/prod/ # preview
kubectl apply -k overlays/prod/ # applyContext: kubectl config use-context k8s-cluster2
Task: Create a PersistentVolumeClaim named data-pvc in namespace storage requesting 500Mi with access mode ReadWriteOnce. Create a pod named data-writer in the same namespace using image busybox with command sleep 3600 that mounts the PVC at /app/data.
Solution
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
namespace: storage
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
---
apiVersion: v1
kind: Pod
metadata:
name: data-writer
namespace: storage
spec:
containers:
- name: writer
image: busybox
command: ["sleep", "3600"]
volumeMounts:
- name: data
mountPath: /app/data
volumes:
- name: data
persistentVolumeClaim:
claimName: data-pvcContext: kubectl config use-context k8s-cluster1
Task: Create an Ingress resource named app-ingress in namespace web that routes traffic for myapp.example.com/shop to service shop-svc on port 80, and /cart to service cart-svc on port 80.
Solution
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: web
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /shop
pathType: Prefix
backend:
service:
name: shop-svc
port:
number: 80
- path: /cart
pathType: Prefix
backend:
service:
name: cart-svc
port:
number: 80Context: kubectl config use-context k8s-cluster2
Task: Create a CronJob named db-cleanup in namespace maintenance that:
- Runs every day at 2:00 AM (
0 2 * * *) - Uses image
postgres:16-alpine - Runs the command:
psql -h db-svc -U admin -c "DELETE FROM sessions WHERE expires_at < NOW()" - Keeps only the 3 most recent successful job histories
- Keeps only 1 failed job history
- Sets
restartPolicy: OnFailure - Has an
activeDeadlineSecondsof 120 on the job
Solution
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-cleanup
namespace: maintenance
spec:
schedule: "0 2 * * *"
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
activeDeadlineSeconds: 120
template:
spec:
containers:
- name: cleanup
image: postgres:16-alpine
command:
- psql
- -h
- db-svc
- -U
- admin
- -c
- "DELETE FROM sessions WHERE expires_at < NOW()"
restartPolicy: OnFailurek apply -f cronjob.yaml
k get cronjob -n maintenance
k describe cronjob db-cleanup -n maintenanceContext: kubectl config use-context k8s-cluster1
Task:
- Create a ServiceAccount named
restricted-sain namespacesecure - Create a pod named
hardened-appin namespacesecureusing imagenginx:alpinewith the following security requirements:- Runs as user ID 1000 and group ID 3000
- Uses the
restricted-saServiceAccount - Drops ALL Linux capabilities
- Sets the root filesystem to read-only
- Does not allow privilege escalation
- Mounts an
emptyDirvolume at/tmpso nginx can write temp files
Solution
k create namespace secure --dry-run=client -o yaml | k apply -f -
k create serviceaccount restricted-sa -n secureapiVersion: v1
kind: Pod
metadata:
name: hardened-app
namespace: secure
spec:
serviceAccountName: restricted-sa
securityContext:
runAsUser: 1000
runAsGroup: 3000
containers:
- name: nginx
image: nginx:alpine
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}k apply -f hardened-app.yaml
k get pod hardened-app -n secure
k exec hardened-app -n secure -- id
# uid=1000 gid=3000
k exec hardened-app -n secure -- touch /test 2>&1
# touch: /test: Read-only file system| Domain | Questions | Weight |
|---|---|---|
| 1. Application Design and Build | Q1–Q3, Q14, Q16 | 20% |
| 2. Application Deployment | Q4–Q5, Q13 | 10% |
| 3. Application Observability and Maintenance | Q6–Q7 | 9% |
| 4. Application Environment, Configuration, and Security | Q8–Q9, Q17 | 12% |
| 5. Services and Networking | Q10–Q12, Q15 | 19% |
| Total | 17 questions | 70% |
Target: Complete all 17 questions in under 2 hours using only kubernetes.io/docs. If you finish in time with 70%+ correct, you're ready. Add the practice scenarios for the remaining coverage.
| Resource | Description |
|---|---|
| Killercoda CKAD Scenarios | Free browser-based CKAD practice labs — no setup required |
| Kubernetes Official Docs | The only reference allowed during the exam — know it well |
| Kubernetes Tasks | Step-by-step guides for every common operation — great for practice |
| kubectl Cheat Sheet | Official kubectl reference |
| CKAD Exam Curriculum | Official exam objectives from CNCF — always check the latest version |
| Resource | Description |
|---|---|
| killer.sh CKAD Simulator | 2 free sessions included with exam purchase — harder than the real exam |
| KodeKloud CKAD Course | Hands-on course with integrated labs |
| Udemy — CKAD by Mumshad Mannambeth | Popular CKAD course with practice tests |
# kind (Kubernetes in Docker) — recommended for CKAD practice
kind create cluster --name ckad-practice
# minikube
minikube start --kubernetes-version=v1.35.0
# kubeadm on VMs (closest to exam experience)
# Use Vagrant or cloud VMs to practice| Week | Focus | Weight | What to Do |
|---|---|---|---|
| Week 1 | Design & Build + Deployment | 40% | Container images, workloads, pods, Helm, Kustomize, rolling updates |
| Week 2 | Environment, Config & Security | 25% | ConfigMaps, Secrets, RBAC, SecurityContexts, CRDs, resource limits |
| Week 3 | Networking + Observability | 35% | Services, Ingress, Gateway API, NetworkPolicy, probes, debugging, logs |
| Week 4 | killer.sh Simulator #1 | — | Take the first simulator, review every wrong answer |
| Week 5 | Weak areas + killer.sh #2 | — | Revisit weak domains, take second simulator, then book the exam |
After doing killer.sh twice and then taking the real exam, here's how they compare.
| Aspect | killer.sh | Real CKAD Exam |
|---|---|---|
| Difficulty | Harder — intentionally brutal | Moderate — challenging but fair |
| Number of Questions | ~20–25 questions | ~15–20 questions |
| Time Pressure | Almost impossible to finish in time | Tight but doable with practice |
| Question Clarity | Some questions are vaguely worded | Clearer instructions, less ambiguity |
| Topics Covered | Everything including edge cases | Focused on curriculum — no surprises |
| Environment Lag | None (runs smooth) | Occasional lag (PSI remote desktop) |
| Scoring | Strict — partial credit is rare | More forgiving — partial credit exists |
| Passing Score | No pass/fail — just a percentage | 66% to pass |
| Copy/Paste | Normal Ctrl+C/V | Ctrl+Shift+C/V (catches everyone) |
| Clusters | Usually 2–3 contexts | 4–6 different contexts |
My scores:
- killer.sh attempt #1: 52%
- killer.sh attempt #2 (1 week later): 81%
- Real CKAD exam: 91% (finished with about 20 min left)
From what I've read online, scoring 70%+ on killer.sh is a pretty reliable indicator that you'll pass the real thing.
Two comprehensive practice exams matching real CKAD format:
- Mock Exam 01 — 15 questions, 73% weight, 2 hours (answers in MOCK-EXAM-01-SOLUTIONS.md)
- Mock Exam 02 — 16 questions, 80% weight, 2 hours (answers in MOCK-EXAM-02-SOLUTIONS.md)
Each exam:
- Covers all domains with realistic weight distribution
- Requires 5–8 minutes per question (like real exam)
- Has separate question and solution files (don't look at solutions until done)
- Focuses on integration across domains, not single-domain skills
Scoring: 10+ correct (66%) = pass. 12+ = strong. 15+ = ready.
Study approach: Complete all 14 exercises first, then take both mock exams under timed conditions. Track weak areas and review corresponding exercises before taking the real exam.
Here's the checklist I used. Covers a week before through exam completion.
- Confirm your exam date and time in the PSI portal
- Make sure your government-issued ID isn't expired
- Run the PSI system check on the computer you'll use
- Do killer.sh simulator #2 if you haven't already
- Review the Candidate Handbook one more time
- Clear your desk completely — nothing but laptop, keyboard, mouse, webcam
- Remove all papers, sticky notes, books, phones from the desk
- Unplug extra monitors (or turn them face-down)
- Make sure your webcam works and your room is well-lit
- Close all browser extensions (especially password managers — proctor will flag them)
- Review the Docs Pages I Used section — click through each one
- Go to bed early. Seriously.
- Close ALL other applications on your computer
- Disable notifications (Focus mode on Windows/Mac)
- Have your ID ready next to you
- Use the bathroom now (you can take a break during the exam but it eats your time)
- Have water in a clear, label-free bottle (some proctors are picky about labels)
- Run the alias setup (see first 60 seconds)
- Run
k get nodesto verify everything works - Read Question 1 — but first run the context switch command!
- Switch context at the start of EVERY question
- Flag hard questions — come back in Pass 2
- Verify every answer (
kubectl get,kubectl describe) - Use
kubectl explainif you forget a field name - Don't panic if the environment lags — just wait
- Results arrive within 24 hours via email (I got mine in about 10 hours)
- Check the Linux Foundation portal for your certificate
- Update your LinkedIn
- Come back here and star this repo
Fork this repo and check these off as you go.
- Can write a Dockerfile from scratch
- Know the difference between COPY and ADD
- Can create a Deployment, DaemonSet, Job, CronJob from memory
- Know how to set
schedule,restartPolicy,successfulJobsHistoryLimit - Can create a multi-container pod with init container
- Can create a sidecar using
restartPolicy: Alwaysin initContainers (v1.35 GA) - Know
kubectl debugfor ephemeral containers - Can create PV, PVC, and mount into a Pod
- Know emptyDir vs PVC vs hostPath differences
- Know access modes (RWO, ROX, RWX, RWOP)
- Can create a Deployment with rolling update strategy
- Know
maxSurgeandmaxUnavailable - Can rollout update + rollback + undo to specific revision
- Understand blue/green and canary patterns with labels/selectors
- Can
helm install,helm upgrade --set,helm rollback,helm uninstall - Can
kubectl apply -kfor Kustomize - Know how to create a Kustomize overlay
- Know the three probe types (liveness, readiness, startup)
- Know the three probe mechanisms (httpGet, exec, tcpSocket)
- Can check logs with
kubectl logs,kubectl logs --previous - Can use
kubectl top pods,kubectl top nodes - Can use
kubectl describeto read events - Know how to detect deprecated APIs
- Can debug a CrashLoopBackOff pod systematically
- Can create ConfigMaps (from-literal + from-file)
- Can inject ConfigMaps as env and volume
- Can create Secrets and mount them as volume or env
- Can create Roles, RoleBindings with imperative commands
- Know
kubectl auth can-isyntax - Can set SecurityContext (runAsUser, capabilities, drop ALL)
- Can create ServiceAccounts and assign to pods
- Know how to work with CRDs (list, describe, create custom resources)
- Can create LimitRange and ResourceQuota
- Understand requests vs limits (OOMKilled, throttled, Pending)
- Can write a NetworkPolicy from scratch (ingress + egress + DNS)
- Can write a default-deny NetworkPolicy
- Remember to always allow DNS egress (port 53 UDP)
- Know ClusterIP vs NodePort vs LoadBalancer
- Can create Services with
kubectl expose - Can troubleshoot empty endpoints (selector mismatch)
- Can create an Ingress resource with host/path routing
- Can create Gateway API resources (GatewayClass, Gateway, HTTPRoute)
- Know DNS format:
service.namespace.svc.cluster.local - Can troubleshoot DNS failures
- Aliases and vim config memorized (can type in under 30 seconds)
- killer.sh attempt #1 completed (score: ___)
- killer.sh attempt #2 completed (score: ___)
- Completed at least 3 killercoda scenarios
- Comfortable with Ctrl+Shift+C / Ctrl+Shift+V
- Know where to find YAML on kubernetes.io/docs
- Can finish 15 questions in under 2 hours
- READY TO BOOK THE EXAM
The night before the exam, I went through each of these once. They're the bare minimum YAML for each resource type. On the real exam, kubectl create --dry-run=client -o yaml generates most of these, but having the structure in your head means you can modify faster.
These are also available as standalone files in the skeletons/ folder.
Pod
apiVersion: v1
kind: Pod
metadata:
name: mypod
namespace: default
labels:
app: myapp
spec:
containers:
- name: app
image: nginx
ports:
- containerPort: 80Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: app
image: nginx:1.25Service (ClusterIP)
apiVersion: v1
kind: Service
metadata:
name: myapp-svc
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 8080NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-specific
namespace: default
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 80
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1GiIngress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-svc
port:
number: 80Gateway API (GatewayClass + Gateway + HTTPRoute)
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: example-gc
spec:
controllerName: example.com/gateway-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
spec:
gatewayClassName: example-gc
listeners:
- name: http
protocol: HTTP
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-route
spec:
parentRefs:
- name: my-gateway
hostnames:
- "app.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: myapp-svc
port: 80RBAC (Role + RoleBinding)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: default
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: ServiceAccount
name: my-sa
namespace: default
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.ioCronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: my-cronjob
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: job
image: busybox
command: ["echo", "hello"]
restartPolicy: OnFailureJob
apiVersion: batch/v1
kind: Job
metadata:
name: my-job
spec:
backoffLimit: 4
activeDeadlineSeconds: 120
template:
spec:
containers:
- name: job
image: busybox
command: ["echo", "hello"]
restartPolicy: NeverConfigMap + Secret (as env + volume)
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_MODE: production
LOG_LEVEL: info
---
# Pod using both
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: nginx
envFrom:
- configMapRef:
name: app-config
env:
- name: DB_PASS
valueFrom:
secretKeyRef:
name: db-creds
key: password
volumeMounts:
- name: secret-vol
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-vol
secret:
secretName: db-credsStatefulSet + Headless Service
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: myapp
spec:
serviceName: myapp-headless
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: app
image: nginx:1.25
ports:
- containerPort: 80
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: myapp-headless
spec:
clusterIP: None
selector:
app: myapp
ports:
- port: 80
targetPort: 80DaemonSet
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: myagent
spec:
selector:
matchLabels:
app: myagent
template:
metadata:
labels:
app: myagent
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: agent
image: fluentd:v1.16-1
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
volumeMounts:
- name: host-logs
mountPath: /var/log
readOnly: true
volumes:
- name: host-logs
hostPath:
path: /var/log
type: DirectorySecurityContext
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
containers:
- name: app
image: nginx
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]Questions I kept getting asked after passing.
I'd say 6/10 difficulty. CNCF doesn't publish official pass rates, but forum consensus puts it around 65–75%. The hard part isn't any single concept — it's doing everything under time pressure across multiple cluster contexts in a laggy remote desktop. 3–5 weeks of hands-on practice should be enough. See the study progress tracker and common mistakes sections for specifics.
The cert itself is a checkbox. The real value is what you learn preparing for it. That said, the checkbox does open doors — I noticed more recruiter interest after adding it to LinkedIn, and more job postings are listing it as a requirement rather than a nice-to-have. The cert is valid for 2 years.
Depends on where you're starting from:
| Background | Suggested Prep Time |
|---|---|
| Already using Kubernetes daily at work | 1–2 weeks (just learn the exam format) |
| Familiar with Docker, played with K8s a bit | 3–5 weeks (this was me) |
| Know Linux well but new to Kubernetes | 6–8 weeks |
| New to all of this | 10+ weeks (don't rush it) |
None. No other cert, no minimum experience, no degree. Anyone can book it.
But realistically, you'll struggle if you aren't comfortable with:
- Linux command line (navigating, editing files)
- Basic YAML (indentation will haunt you if you don't practice)
- Containers (you should know what a container is)
- vim basics (the exam terminal uses it)
No. You get one browser tab with:
- kubernetes.io/docs
- kubernetes.io/blog
- GitHub repos under the kubernetes org
Nothing else. Learn where things are in the docs before exam day.
You get one free retake with your purchase, valid for 12 months. If you fail both, you buy a new exam.
I had 16. Others report 15–20. Each has a weight (mine ranged from 3% to 7%). You need 66% to pass.
You can browse kubernetes.io/docs during the exam, but nothing else. I used the docs maybe 4–5 times, mostly for NetworkPolicy and Gateway API syntax.
Yes. I took mine from home. You need a webcam, mic, and stable internet. A proctor watches via webcam the entire time. Your desk needs to be completely clear — no papers, no phone.
- Go to training.linuxfoundation.org and create an account
- Pay $445 (or wait for a discount)
- Schedule the exam through the PSI portal — you have 12 months to use it
- Use your killer.sh simulator sessions before the exam
What worked for me (91%):
- Set up aliases at the start of the exam — see the cheat sheet
- Practice in a real cluster daily — kind, minikube, or killercoda
- Spend most study time on Security/Config (25%) and Networking (20%) — 45% of the score combined
- Do killer.sh twice
- Use
--dry-run=client -o yamlfor everything - Do easy questions first, come back to hard ones
- Switch context before every question
- Verify your work with
kubectl get/kubectl describe
| CKAD | CKA | |
|---|---|---|
| Focus | Application development on K8s | Cluster administration |
| Topics | Pods, Deployments, Services, ConfigMaps, Secrets, Helm, Probes, NetworkPolicy | Cluster setup, etcd backup, networking, upgrades, troubleshooting |
| Who it's for | Developers who deploy apps to K8s | Admins who set up and maintain K8s clusters |
| Overlap | ~40% shared content | ~40% shared content |
| Passing score | 66% | 66% |
| Duration | 2 hours | 2 hours |
If you build and deploy apps, go for CKAD first. If you manage clusters, start with CKA. The shared overlap means studying for one makes the other easier.
Most people say CKA is slightly harder. CKA covers cluster internals (etcd backup, kubeadm upgrades, troubleshooting kubelet) that CKAD doesn't touch. CKAD is more focused but the time pressure per question feels tighter.
The stuff I practiced was the stuff that showed up. If something in this guide is wrong or outdated, open a PR.
Good luck.
If this guide helped you, star the repo. ⭐
techwithmohamed.com · Blog Post
ckad ckad-exam ckad-certification kubernetes kubernetes-certification ckad-study-guide ckad-practice-questions kubectl helm networkpolicy gateway-api killer-sh kubernetes-v1-35 ckad-2026 cloud-native
