| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Bash-compatible Linux or macOS shell with kubectl configured; any compatible Kubernetes cluster |
| Cert prep | CKAD |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm |
| Privilege | Normal workstation user; no sudo required. Kubernetes API permissions vary by task and are listed under Lab Requirements. |
| Scope | Twenty-five original CKAD-style practice scenarios with verification commands, hints, and worked solutions grouped by official exam domain. Does not include a timed mock exam, exam dumps, cluster installation, or scoring against the real passing standard. |
You have worked through the individual CKAD tutorial lessons. These practice labs turn that knowledge into short scenarios you can repeat until the commands feel automatic. Verification steps often use the curl command from client Pods or port-forwarded ports.
How to Use These CKAD Practice Labs
- Complete the relevant course lesson first.
- Attempt the task without opening its solution.
- Use Kubernetes documentation only when necessary.
- Verify the result using the commands in the task card.
- Open the hint only when you are blocked.
- Compare your finished object with the worked solution.
- Repeat the task later with a different resource name or namespace.
Every task uses the same compact card format: scenario, measurable objectives, optional constraints, verification, hint, solution, related lesson, and cleanup.
Lab Requirements
You need:
- A working Kubernetes cluster
- Current
kubectlconfigured for that cluster - Permission to create namespaced application resources
- A default dynamic StorageClass that can provision
ReadWriteOncePVCs for Task 5 - Helm for Task 8
- Metrics Server for Task 14
- Ephemeral-container support and
pods/ephemeralcontainersaccess for Task 13 - An Ingress controller for Task 24
- OpenSSL on the workstation for Task 24
- Permission to inspect
IngressClassobjects for Task 24 - Namespace creation permission for Tasks 19, 23, and 25
- A NetworkPolicy-capable CNI such as Calico for Task 25
- ServiceAccount impersonation permission for the Task 20
kubectl auth can-i --as=...verification
Create one shared practice namespace and make it your default context namespace:
kubectl create namespace ckad-labskubectl config set-context --current --namespace=ckad-labsThis article does not include cluster installation instructions. Use install Kubernetes with kubeadm or your existing lab when you still need a cluster.
Domain Navigation
Difficulty and Time Labels
| Level | Meaning |
|---|---|
| Foundation | One resource or a direct command sequence |
| Intermediate | Several related objects |
| Exam-style | Diagnosis or skills from multiple lessons |
Suggested times are study guidance only. They are not claims about real exam timing.
Application Design and Build
Task 1: Create and Inspect a Pod
| Field | Value |
|---|---|
| Domain | Application Design and Build |
| Difficulty | Foundation |
| Suggested time | 8 minutes |
Scenario: Run a single Pod that serves HTTP on port 8080 and remains available for inspection.
Objectives:
- Pod name:
ckad-pod-01 - Namespace:
ckad-labs - Image:
registry.k8s.io/e2e-test-images/agnhost:2.39 - Container name:
app - Labels:
app=ckad-lab,task=01 - Container port name:
http, port8080 - Restart policy:
Never - Command: run
agnhost netexec --http-port=8080
Constraints: Use a declarative manifest. Do not create a Deployment.
Verification:
kubectl get pod ckad-pod-01 -n ckad-labs -o jsonpath='{.status.phase}{"\n"}'kubectl logs ckad-pod-01 -n ckad-labs -c app --tail=3kubectl get pod ckad-pod-01 -n ckad-labs --show-labelsHint
Declare ports, labels, restartPolicy, and command/args on the container. Use kubectl apply -f and wait until the Pod is Running before checking logs.
Worked solution
apiVersion: v1
kind: Pod
metadata:
name: ckad-pod-01
namespace: ckad-labs
labels:
app: ckad-lab
task: "01"
spec:
restartPolicy: Never
containers:
- name: app
image: registry.k8s.io/e2e-test-images/agnhost:2.39
command: ["/agnhost"]
args: ["netexec", "--http-port=8080"]
ports:
- name: http
containerPort: 8080kubectl apply -f ckad-pod-01.yaml
kubectl wait pod/ckad-pod-01 -n ckad-labs --for=condition=Ready --timeout=90sRelated lesson: Kubernetes Pods and Pod lifecycle
Cleanup:
kubectl delete pod ckad-pod-01 -n ckad-labs --ignore-not-foundTask 2: Add an Init Container
| Field | Value |
|---|---|
| Domain | Application Design and Build |
| Difficulty | Intermediate |
| Suggested time | 12 minutes |
Scenario: An init container must write a greeting file into a shared volume before the main container starts. The main container prints the file contents to stdout and then sleeps.
Objectives:
- Pod name:
ckad-init-02 - Init container
prepwriteshello from initto/work/message.txt - Main container
apprunscat /work/message.txtthensleep 3600 - Shared volume:
emptyDirmounted at/work
Constraints: Do not use a hostPath volume.
Verification:
kubectl get pod ckad-init-02 -n ckad-labs -o jsonpath='{.status.initContainerStatuses[0].state.terminated.reason}{"\n"}'kubectl logs ckad-init-02 -n ckad-labs -c appHint
Mount the same volume in both init and app containers. The init container must finish before Kubernetes starts app.
Worked solution
apiVersion: v1
kind: Pod
metadata:
name: ckad-init-02
namespace: ckad-labs
spec:
restartPolicy: Never
initContainers:
- name: prep
image: busybox:1.36
command: ["sh", "-c", "echo 'hello from init' > /work/message.txt"]
volumeMounts:
- name: work
mountPath: /work
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "cat /work/message.txt; sleep 3600"]
volumeMounts:
- name: work
mountPath: /work
volumes:
- name: work
emptyDir: {}kubectl apply -f ckad-init-02.yaml
kubectl wait pod/ckad-init-02 -n ckad-labs --for=condition=Ready --timeout=90sSample output from the main container log:
hello from initRelated lessons: Init containers · Kubernetes volumes
Cleanup:
kubectl delete pod ckad-init-02 -n ckad-labs --ignore-not-foundTask 3: Build a Sidecar Pod
| Field | Value |
|---|---|
| Domain | Application Design and Build |
| Difficulty | Intermediate |
| Suggested time | 12 minutes |
Scenario: A main container appends lines to a log file in a shared emptyDir. A sidecar tails that file so you can read new lines independently from either container.
Objectives:
- Pod name:
ckad-sidecar-03 - Main container
writerappendsline-1,line-2,line-3to/var/log/app.logevery five seconds - Sidecar
tailerrunstail -F /var/log/app.log - Both containers share one
emptyDirat/var/log
Verification:
kubectl logs ckad-sidecar-03 -n ckad-labs -c tailer --tail=3kubectl exec ckad-sidecar-03 -n ckad-labs -c writer -- wc -l /var/log/app.logHint
Use two containers in one Pod spec and one shared volume mount path.
Worked solution
apiVersion: v1
kind: Pod
metadata:
name: ckad-sidecar-03
namespace: ckad-labs
spec:
containers:
- name: writer
image: busybox:1.36
command:
- sh
- -c
- |
i=1
while true; do
echo "line-$i" >> /var/log/app.log
i=$((i+1))
sleep 5
done
volumeMounts:
- name: logs
mountPath: /var/log
- name: tailer
image: busybox:1.36
command: ["tail", "-F", "/var/log/app.log"]
volumeMounts:
- name: logs
mountPath: /var/log
volumes:
- name: logs
emptyDir: {}kubectl apply -f ckad-sidecar-03.yaml
kubectl wait pod/ckad-sidecar-03 -n ckad-labs --for=condition=Ready --timeout=90s
sleep 6Related lesson: Sidecar and multi-container patterns
Cleanup:
kubectl delete pod ckad-sidecar-03 -n ckad-labs --ignore-not-foundTask 4: Create a Job and CronJob
| Field | Value |
|---|---|
| Domain | Application Design and Build |
| Difficulty | Intermediate |
| Suggested time | 15 minutes |
Scenario: Run a finite task once with a Job, then schedule the same command with a CronJob.
Objectives:
- Job name:
ckad-job-04, command printsjob completeand exits successfully backoffLimit: 2- CronJob name:
ckad-cron-04, schedule*/5 * * * * - CronJob concurrency policy:
Forbid - Successful Jobs history limit:
1
Verification:
kubectl wait job/ckad-job-04 -n ckad-labs --for=condition=complete --timeout=120skubectl get cronjob ckad-cron-04 -n ckad-labsHint
A Job needs restartPolicy: Never on the Pod template. A CronJob wraps a Job template and owns the schedule fields.
Worked solution
apiVersion: batch/v1
kind: Job
metadata:
name: ckad-job-04
namespace: ckad-labs
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo job complete"]
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: ckad-cron-04
namespace: ckad-labs
spec:
schedule: "*/5 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo cron tick"]kubectl apply -f ckad-job-cron-04.yaml
kubectl wait job/ckad-job-04 -n ckad-labs --for=condition=complete --timeout=120s
kubectl get cronjob ckad-cron-04 -n ckad-labsRelated lessons: Kubernetes Jobs · Kubernetes CronJobs
Cleanup:
kubectl delete job ckad-job-04 -n ckad-labs --ignore-not-found
kubectl delete cronjob ckad-cron-04 -n ckad-labs --ignore-not-foundTask 5: Mount Persistent Storage
| Field | Value |
|---|---|
| Domain | Application Design and Build |
| Difficulty | Exam-style |
| Suggested time | 20 minutes |
Scenario: Persist data across Pod replacement using a PVC backed by the cluster default StorageClass.
Objectives:
- PVC name:
ckad-data-05, size1Gi, access modeReadWriteOnce - First Pod
ckad-pvc-writer-05writespersist-meto/data/value.txt - Delete the writer Pod and create
ckad-pvc-reader-05that reads the same file - Both Pods mount the PVC at
/data
Verification:
kubectl get pvc ckad-data-05 -n ckad-labskubectl logs ckad-pvc-reader-05 -n ckad-labsHint
A StorageClass with volumeBindingMode: WaitForFirstConsumer leaves the PVC pending until a Pod references it. Apply the writer Pod first, then wait for the PVC to bind.
Worked solution
Save the PVC and writer Pod in ckad-pvc-writer-05.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ckad-data-05
namespace: ckad-labs
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: ckad-pvc-writer-05
namespace: ckad-labs
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo persist-me > /data/value.txt"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: ckad-data-05kubectl apply -f ckad-pvc-writer-05.yamlkubectl wait pvc/ckad-data-05 -n ckad-labs --for=jsonpath='{.status.phase}'=Bound --timeout=120skubectl wait pod/ckad-pvc-writer-05 -n ckad-labs --for=jsonpath='{.status.phase}'=Succeeded --timeout=120sSave the reader Pod in ckad-pvc-reader-05.yaml:
apiVersion: v1
kind: Pod
metadata:
name: ckad-pvc-reader-05
namespace: ckad-labs
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["cat", "/data/value.txt"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: ckad-data-05kubectl delete pod ckad-pvc-writer-05 -n ckad-labs --wait=truekubectl apply -f ckad-pvc-reader-05.yamlkubectl wait pod/ckad-pvc-reader-05 -n ckad-labs --for=jsonpath='{.status.phase}'=Succeeded --timeout=120skubectl logs ckad-pvc-reader-05 -n ckad-labsSample output:
persist-meRelated lesson: PersistentVolume and PVC
Cleanup:
kubectl delete pod ckad-pvc-writer-05 ckad-pvc-reader-05 -n ckad-labs --ignore-not-found
kubectl delete pvc ckad-data-05 -n ckad-labs --ignore-not-foundApplication Deployment
Task 6: Create and Scale a Deployment
| Field | Value |
|---|---|
| Domain | Application Deployment |
| Difficulty | Foundation |
| Suggested time | 10 minutes |
Scenario: Deploy nginx, scale it, and add a Pod-template label without recreating the Deployment object.
Objectives:
- Deployment
ckad-deploy-06, imagenginx:1.27-alpine, initial replicas1 - Scale to
3replicas - Add label
tier=webto the Pod template - Selector must remain
app=ckad-deploy-06
Constraints: Patch or scale the existing Deployment. Do not delete and recreate it.
Verification:
kubectl get deployment ckad-deploy-06 -n ckad-labskubectl get rs -n ckad-labs -l app=ckad-deploy-06kubectl get pods -n ckad-labs -l app=ckad-deploy-06 --show-labelsHint
kubectl scale changes replica count. kubectl patch or kubectl edit can add template labels and trigger a rolling update.
Worked solution
kubectl create deployment ckad-deploy-06 -n ckad-labs --image=nginx:1.27-alpine --replicas=1
kubectl scale deployment ckad-deploy-06 -n ckad-labs --replicas=3
kubectl patch deployment ckad-deploy-06 -n ckad-labs -p '{"spec":{"template":{"metadata":{"labels":{"tier":"web"}}}}}'
kubectl rollout status deployment/ckad-deploy-06 -n ckad-labs --timeout=120sRelated lesson: Deployments and rolling updates
Cleanup:
kubectl delete deployment ckad-deploy-06 -n ckad-labs --ignore-not-foundTask 7: Perform an Update and Rollback
| Field | Value |
|---|---|
| Domain | Application Deployment |
| Difficulty | Intermediate |
| Suggested time | 12 minutes |
Scenario: Roll out a new image, confirm the change, then roll back to the previous revision.
Objectives:
- Deployment
ckad-roll-07starts withnginx:1.27-alpine - Update image to
nginx:1.25-alpine - Record revision after rollout completes
- Roll back to revision
1 - Final Pods must run
nginx:1.27-alpineagain
Verification:
kubectl rollout status deployment/ckad-roll-07 -n ckad-labs --timeout=120skubectl rollout history deployment/ckad-roll-07 -n ckad-labskubectl get pods -n ckad-labs -l app=ckad-roll-07 -o jsonpath='{range .items[*]}{.spec.containers[0].image}{"\n"}{end}'Hint
kubectl set image starts the rollout. kubectl rollout undo --to-revision=1 restores the first recorded template.
Worked solution
kubectl create deployment ckad-roll-07 -n ckad-labs --image=nginx:1.27-alpine
CONTAINER=$(kubectl get deployment ckad-roll-07 -n ckad-labs -o jsonpath='{.spec.template.spec.containers[0].name}')
kubectl set image deployment/ckad-roll-07 -n ckad-labs "$CONTAINER=nginx:1.25-alpine"
kubectl rollout status deployment/ckad-roll-07 -n ckad-labs --timeout=120s
kubectl rollout history deployment/ckad-roll-07 -n ckad-labs
kubectl rollout undo deployment/ckad-roll-07 -n ckad-labs --to-revision=1
kubectl rollout status deployment/ckad-roll-07 -n ckad-labs --timeout=120sRelated lesson: Deployments and rolling updates
Cleanup:
kubectl delete deployment ckad-roll-07 -n ckad-labs --ignore-not-foundTask 8: Install and Upgrade a Helm Release
| Field | Value |
|---|---|
| Domain | Application Deployment |
| Difficulty | Exam-style |
| Suggested time | 20 minutes |
Scenario: Install a chart, override one value, upgrade it, and inspect release history.
Objectives:
- Release name:
ckad-helm-08inckad-labs - Chart:
bitnami/nginxfromhttps://charts.bitnami.com/bitnami - First install sets
service.type=ClusterIP - Upgrade sets
replicaCount=2 - Show at least two entries in
helm history
Verification:
helm list -n ckad-labshelm history ckad-helm-08 -n ckad-labskubectl get pods -n ckad-labs -l app.kubernetes.io/instance=ckad-helm-08Hint
helm repo add, helm install --set, then helm upgrade --reuse-values --set replicaCount=2.
Worked solution
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install ckad-helm-08 bitnami/nginx --version 25.0.15 -n ckad-labs --set service.type=ClusterIP --wait --timeout 5m
helm upgrade ckad-helm-08 bitnami/nginx --version 25.0.15 -n ckad-labs --reuse-values --set replicaCount=2 --wait --timeout 5m
helm history ckad-helm-08 -n ckad-labs
kubectl wait deployment -n ckad-labs -l app.kubernetes.io/instance=ckad-helm-08 --for=condition=Available --timeout=120sRelated lesson: Deploy applications with Helm
Cleanup:
helm uninstall ckad-helm-08 -n ckad-labsTask 9: Create a Kustomize Overlay
| Field | Value |
|---|---|
| Domain | Application Deployment |
| Difficulty | Exam-style |
| Suggested time | 20 minutes |
Scenario: Reuse a base Deployment and build a production overlay that changes replicas, image, and labels.
Objectives:
- Base path:
kustomize/basewith Deploymentckad-kust-09, imagenginx:1.27-alpine, replicas1 - Overlay path:
kustomize/prod - Production overlay sets replicas
3, imagenginx:1.25-alpine, and labelenv=prod - Preview with
kubectl kustomize kustomize/prod - Apply with
kubectl apply -k kustomize/prod
Constraints: Do not edit files under kustomize/base. Change production values only in the overlay.
Verification:
kubectl get deployment ckad-kust-09 -n ckad-labskubectl get pods -n ckad-labs -l app=ckad-kust-09 --show-labelsHint
From kustomize/prod, reference the base with resources: [../base]. Use the labels transformer with includeSelectors: false and includeTemplates: true so env=prod appears on Pod templates without changing the Deployment selector.
Worked solution
Create kustomize/base/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ckad-labs
resources:
- deployment.yamlCreate kustomize/base/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ckad-kust-09
spec:
replicas: 1
selector:
matchLabels:
app: ckad-kust-09
template:
metadata:
labels:
app: ckad-kust-09
spec:
containers:
- name: app
image: nginx:1.27-alpineCreate kustomize/prod/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
replicas:
- name: ckad-kust-09
count: 3
images:
- name: nginx
newTag: 1.25-alpine
labels:
- pairs:
env: prod
includeSelectors: false
includeTemplates: truekubectl kustomize kustomize/prod
kubectl apply -k kustomize/prod
kubectl rollout status deployment/ckad-kust-09 -n ckad-labs --timeout=120sRelated lesson: Kustomize bases and overlays
Cleanup:
kubectl delete deployment ckad-kust-09 -n ckad-labs --ignore-not-foundApplication Observability and Maintenance
Task 10: Migrate a Deprecated Manifest
| Field | Value |
|---|---|
| Domain | Application Observability and Maintenance |
| Difficulty | Exam-style |
| Suggested time | 18 minutes |
Scenario: A teammate left an old Deployment manifest on extensions/v1beta1. Migrate it to a supported API version and apply it safely.
Objectives:
- Final Deployment name:
ckad-migrate-10 - Use
kubectl api-resourcesorkubectl explainto find the current API - Validate with server-side dry run before apply
- Running Pod count must reach
2
Starting manifest:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: ckad-migrate-10
namespace: ckad-labs
spec:
replicas: 2
template:
metadata:
labels:
app: ckad-migrate-10
spec:
containers:
- name: app
image: nginx:1.27-alpineVerification:
kubectl apply -f ckad-migrate-10.yaml --dry-run=serverkubectl rollout status deployment/ckad-migrate-10 -n ckad-labs --timeout=120sHint
Modern clusters use apps/v1. Add an explicit selector.matchLabels that matches the Pod template labels.
Worked solution
kubectl api-resources | grep -E '^deployments[[:space:]]'Sample output:
deployments deploy apps/v1 true DeploymentapiVersion: apps/v1
kind: Deployment
metadata:
name: ckad-migrate-10
namespace: ckad-labs
spec:
replicas: 2
selector:
matchLabels:
app: ckad-migrate-10
template:
metadata:
labels:
app: ckad-migrate-10
spec:
containers:
- name: app
image: nginx:1.27-alpinekubectl apply -f ckad-migrate-10.yaml --dry-run=server
kubectl apply -f ckad-migrate-10.yaml
kubectl rollout status deployment/ckad-migrate-10 -n ckad-labs --timeout=120sRelated lesson: API deprecations and manifest migration
Cleanup:
kubectl delete deployment ckad-migrate-10 -n ckad-labs --ignore-not-foundTask 11: Configure Three Probe Types
| Field | Value |
|---|---|
| Domain | Application Observability and Maintenance |
| Difficulty | Intermediate |
| Suggested time | 15 minutes |
Scenario: One Deployment must expose startup, readiness, and liveness probes against an HTTP endpoint.
Objectives:
- Deployment
ckad-probes-11, imageregistry.k8s.io/e2e-test-images/agnhost:2.39 - Args:
netexec --http-port=8080 - Startup probe:
httpGet/healthzon port8080,failureThreshold: 12,periodSeconds: 2 - Readiness probe: same path,
periodSeconds: 5 - Liveness probe: same path,
periodSeconds: 10
Verification:
kubectl rollout status deployment/ckad-probes-11 -n ckad-labs --timeout=120skubectl get pods -n ckad-labs -l app=ckad-probes-11 -o jsonpath='{range .items[*]}{.metadata.name}{" Ready="}{.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}'kubectl describe pod -n ckad-labs -l app=ckad-probes-11 | grep -E 'Startup|Readiness|Liveness'Hint
All three probes can target the same HTTP path while using different timing fields.
Worked solution
apiVersion: apps/v1
kind: Deployment
metadata:
name: ckad-probes-11
namespace: ckad-labs
spec:
replicas: 1
selector:
matchLabels:
app: ckad-probes-11
template:
metadata:
labels:
app: ckad-probes-11
spec:
containers:
- name: app
image: registry.k8s.io/e2e-test-images/agnhost:2.39
args: ["netexec", "--http-port=8080"]
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 2
failureThreshold: 12
readinessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10kubectl apply -f ckad-probes-11.yaml
kubectl rollout status deployment/ckad-probes-11 -n ckad-labs --timeout=120sRelated lesson: Liveness, readiness, and startup probes
Cleanup:
kubectl delete deployment ckad-probes-11 -n ckad-labs --ignore-not-foundTask 12: Diagnose a Restarting Pod
| Field | Value |
|---|---|
| Domain | Application Observability and Maintenance |
| Difficulty | Exam-style |
| Suggested time | 18 minutes |
Scenario: A Pod named ckad-crash-12 keeps restarting because its command exits immediately.
Objectives:
- Inspect Pod status and container restart count
- Read Events and previous container logs
- Identify the failing exit code
- Fix the manifest so the container stays running
- Verify the Pod becomes Ready
Broken manifest to apply first:
apiVersion: v1
kind: Pod
metadata:
name: ckad-crash-12
namespace: ckad-labs
spec:
restartPolicy: Always
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo 'application startup failed'; exit 1"]Verification:
kubectl get pod ckad-crash-12 -n ckad-labskubectl logs ckad-crash-12 -n ckad-labs -c app --previouskubectl wait pod/ckad-crash-12 -n ckad-labs --for=condition=Ready --timeout=120sHint
CrashLoopBackOff with exit code 1 means the command itself is wrong. Replace it with a long-running command such as sleep 3600.
Worked solution
kubectl apply -f ckad-crash-12-broken.yamlfor attempt in {1..45}; do
RESTARTS=$(kubectl get pod ckad-crash-12 -n ckad-labs -o jsonpath='{.status.containerStatuses[0].restartCount}')
if [ "${RESTARTS:-0}" -ge 1 ]; then
break
fi
sleep 2
done
if [ "${RESTARTS:-0}" -lt 1 ]; then
echo "Pod did not restart within 90 seconds" >&2
exit 1
fikubectl logs ckad-crash-12 -n ckad-labs -c app --previouskubectl get pod ckad-crash-12 -n ckad-labs -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'Sample output:
1Delete the broken Pod and apply the fixed manifest:
apiVersion: v1
kind: Pod
metadata:
name: ckad-crash-12
namespace: ckad-labs
spec:
restartPolicy: Always
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]kubectl delete pod ckad-crash-12 -n ckad-labs
kubectl apply -f ckad-crash-12-fixed.yaml
kubectl wait pod/ckad-crash-12 -n ckad-labs --for=condition=Ready --timeout=120sRelated lessons: Fix CrashLoopBackOff · kubectl logs, Events, and describe
Cleanup:
kubectl delete pod ckad-crash-12 -n ckad-labs --ignore-not-foundTask 13: Debug a Minimal Container
| Field | Value |
|---|---|
| Domain | Application Observability and Maintenance |
| Difficulty | Exam-style |
| Suggested time | 18 minutes |
Scenario: The application runs in a minimal container without a shell. Use an ephemeral debug container to test connectivity to the app port.
Objectives:
- Deployment
ckad-debug-13with containerapprunningregistry.k8s.io/e2e-test-images/agnhost:2.39andnetexec --http-port=8080 - Add an ephemeral debug container with image
busybox:1.36 - From the debug container, confirm TCP connectivity to
127.0.0.1:8080
Verification:
POD=$(kubectl get pods -n ckad-labs -l app=ckad-debug-13 -o jsonpath='{.items[0].metadata.name}')
kubectl debug "pod/$POD" -n ckad-labs --image=busybox:1.36 --target=app --profile=general -- sh -c 'wget -qO- 127.0.0.1:8080/healthz'Hint
kubectl debug --target attaches an ephemeral container to an existing Pod network namespace. Wait for the Deployment rollout before selecting the Pod name.
Worked solution
apiVersion: apps/v1
kind: Deployment
metadata:
name: ckad-debug-13
namespace: ckad-labs
spec:
replicas: 1
selector:
matchLabels:
app: ckad-debug-13
template:
metadata:
labels:
app: ckad-debug-13
spec:
containers:
- name: app
image: registry.k8s.io/e2e-test-images/agnhost:2.39
args: ["netexec", "--http-port=8080"]
ports:
- containerPort: 8080kubectl apply -f ckad-debug-13.yaml
kubectl rollout status deployment/ckad-debug-13 -n ckad-labs --timeout=120s
POD=$(kubectl get pods -n ckad-labs -l app=ckad-debug-13 -o jsonpath='{.items[0].metadata.name}')
kubectl debug "pod/$POD" -n ckad-labs --image=busybox:1.36 --target=app --profile=general -- sh -c 'wget -qO- 127.0.0.1:8080/healthz'Related lesson: Debug Pods with kubectl debug
Cleanup:
kubectl delete deployment ckad-debug-13 -n ckad-labs --ignore-not-foundTask 14: Compare Resource Usage
| Field | Value |
|---|---|
| Domain | Application Observability and Maintenance |
| Difficulty | Intermediate |
| Suggested time | 12 minutes |
Scenario: Two Deployments run in the namespace. Find which Pod currently uses the most memory and compare live usage with configured requests and limits.
Objectives:
- Create
ckad-top-a-14withrequests.memory=32Miandlimits.memory=64Mi - Create
ckad-top-b-14withrequests.memory=128Miandlimits.memory=256Mi - Use
kubectl topto identify the higher-memory Pod - Show per-container usage and compare with
kubectl describe
Verification:
kubectl top pod -n ckad-labs --sort-by=memorykubectl top pod -n ckad-labs --containerskubectl describe pod -n ckad-labs -l task=14 | grep -A6 -E '^Name:|^[[:space:]]+Limits:|^[[:space:]]+Requests:'Hint
Metrics Server must be installed. Different requests and limits do not guarantee different live usage, so the second Deployment allocates memory in Python to create a measurable gap.
Worked solution
Deploy two workloads with different memory settings. The second Deployment allocates more memory in Python so kubectl top shows a measurable difference.
kubectl apply -f - <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata:
name: ckad-top-a-14
namespace: ckad-labs
labels:
task: "14"
spec:
replicas: 1
selector:
matchLabels:
app: ckad-top-a-14
template:
metadata:
labels:
app: ckad-top-a-14
task: "14"
spec:
containers:
- name: app
image: python:3.12-alpine
command:
- python
- -c
- |
import time
allocation = bytearray(2 * 1024 * 1024)
time.sleep(3600)
resources:
requests:
memory: 32Mi
limits:
memory: 64Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ckad-top-b-14
namespace: ckad-labs
labels:
task: "14"
spec:
replicas: 1
selector:
matchLabels:
app: ckad-top-b-14
template:
metadata:
labels:
app: ckad-top-b-14
task: "14"
spec:
containers:
- name: app
image: python:3.12-alpine
command:
- python
- -c
- |
import time
allocation = bytearray(20 * 1024 * 1024)
time.sleep(3600)
resources:
requests:
memory: 128Mi
limits:
memory: 256Mi
YAMLkubectl rollout status deployment/ckad-top-a-14 -n ckad-labs --timeout=120s
kubectl rollout status deployment/ckad-top-b-14 -n ckad-labs --timeout=120sWait for Metrics Server to report usage, then compare live memory:
for i in $(seq 1 12); do
kubectl top pod -n ckad-labs -l task=14 --sort-by=memory && break
sleep 10
doneckad-top-b-14 should report higher live memory than ckad-top-a-14. Compare those numbers with the Requests and Limits lines from kubectl describe pod.
Related lesson: Monitor resources with kubectl top
Cleanup:
kubectl delete deployment ckad-top-a-14 ckad-top-b-14 -n ckad-labs --ignore-not-foundApplication Environment, Configuration and Security
Task 15: Consume a ConfigMap Three Ways
| Field | Value |
|---|---|
| Domain | Application Environment, Configuration and Security |
| Difficulty | Intermediate |
| Suggested time | 15 minutes |
Scenario: One ConfigMap must feed a Pod through a single key reference, envFrom, and a mounted file.
Objectives:
- ConfigMap
ckad-cm-15with keysMODE=debugandREGION=lab - Pod
ckad-cm-pod-15exposesMODE_SINGLEthroughconfigMapKeyRef - All keys available through
envFrom - File
/etc/config/REGIONmounted from keyREGION
Verification:
kubectl exec ckad-cm-pod-15 -n ckad-labs -- printenv MODE_SINGLE MODE REGIONkubectl exec ckad-cm-pod-15 -n ckad-labs -- cat /etc/config/REGIONHint
You can combine env, envFrom, and volumeMount with items in one Pod spec.
Worked solution
kubectl create configmap ckad-cm-15 -n ckad-labs --from-literal=MODE=debug --from-literal=REGION=labapiVersion: v1
kind: Pod
metadata:
name: ckad-cm-pod-15
namespace: ckad-labs
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
env:
- name: MODE_SINGLE
valueFrom:
configMapKeyRef:
name: ckad-cm-15
key: MODE
envFrom:
- configMapRef:
name: ckad-cm-15
volumeMounts:
- name: cfg
mountPath: /etc/config
volumes:
- name: cfg
configMap:
name: ckad-cm-15
items:
- key: REGION
path: REGIONkubectl apply -f ckad-cm-pod-15.yaml
kubectl wait pod/ckad-cm-pod-15 -n ckad-labs --for=condition=Ready --timeout=90s
kubectl exec ckad-cm-pod-15 -n ckad-labs -- printenv MODE_SINGLE MODE REGION
kubectl exec ckad-cm-pod-15 -n ckad-labs -- cat /etc/config/REGIONRelated lesson: Kubernetes ConfigMap with examples
Cleanup:
kubectl delete pod ckad-cm-pod-15 -n ckad-labs --ignore-not-found
kubectl delete configmap ckad-cm-15 -n ckad-labs --ignore-not-foundTask 16: Create and Consume a Secret
| Field | Value |
|---|---|
| Domain | Application Environment, Configuration and Security |
| Difficulty | Intermediate |
| Suggested time | 12 minutes |
Scenario: Store a demo API token in a Secret and consume it through secretKeyRef and a mounted file.
Objectives:
- Secret
ckad-secret-16, keytoken=demo-token-123 - Pod
ckad-secret-pod-16readsAPI_TOKENfromsecretKeyRef - Same token available at
/etc/secret/token
Verification:
kubectl exec ckad-secret-pod-16 -n ckad-labs -- printenv API_TOKENkubectl exec ckad-secret-pod-16 -n ckad-labs -- cat /etc/secret/tokenHint
Generic Secrets work with stringData or kubectl create secret generic.
Worked solution
kubectl create secret generic ckad-secret-16 -n ckad-labs --from-literal=token=demo-token-123kubectl apply -f - <<'YAML'
apiVersion: v1
kind: Pod
metadata:
name: ckad-secret-pod-16
namespace: ckad-labs
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
env:
- name: API_TOKEN
valueFrom:
secretKeyRef:
name: ckad-secret-16
key: token
volumeMounts:
- name: sec
mountPath: /etc/secret
readOnly: true
volumes:
- name: sec
secret:
secretName: ckad-secret-16
items:
- key: token
path: token
YAMLkubectl wait pod/ckad-secret-pod-16 -n ckad-labs --for=condition=Ready --timeout=90s
kubectl exec ckad-secret-pod-16 -n ckad-labs -- printenv API_TOKEN
kubectl exec ckad-secret-pod-16 -n ckad-labs -- cat /etc/secret/tokenBoth commands should print demo-token-123.
Related lesson: Kubernetes Secrets with examples
Cleanup:
kubectl delete pod ckad-secret-pod-16 -n ckad-labs --ignore-not-found
kubectl delete secret ckad-secret-16 -n ckad-labs --ignore-not-foundTask 17: Override Command and Use the Downward API
| Field | Value |
|---|---|
| Domain | Application Environment, Configuration and Security |
| Difficulty | Intermediate |
| Suggested time | 15 minutes |
Scenario: Replace the container entrypoint, inject literal and downward API values, and expose one resource limit through resourceFieldRef.
Objectives:
- Pod
ckad-down-17 - Command prints environment values then sleeps
- Literal env
BUILD=lab - Downward API env vars for Pod name and namespace
CPU_LIMITfromlimits.cpu
Verification:
kubectl logs ckad-down-17 -n ckad-labsHint
Use fieldRef for metadata and resourceFieldRef for limits.cpu.
Worked solution
apiVersion: v1
kind: Pod
metadata:
name: ckad-down-17
namespace: ckad-labs
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "env | sort; sleep 3600"]
env:
- name: BUILD
value: lab
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: CPU_LIMIT
valueFrom:
resourceFieldRef:
containerName: app
resource: limits.cpu
divisor: 1m
resources:
limits:
cpu: 200mkubectl apply -f ckad-down-17.yaml
kubectl wait pod/ckad-down-17 -n ckad-labs --for=condition=Ready --timeout=90s
kubectl logs ckad-down-17 -n ckad-labsSample output includes CPU_LIMIT=200 and POD_NAME=ckad-down-17.
Related lesson: Commands, args, environment variables, and Downward API
Cleanup:
kubectl delete pod ckad-down-17 -n ckad-labs --ignore-not-foundTask 18: Configure Requests, Limits and QoS
| Field | Value |
|---|---|
| Domain | Application Environment, Configuration and Security |
| Difficulty | Exam-style |
| Suggested time | 18 minutes |
Scenario: Complete a partially written Pod so it reaches Guaranteed QoS and correct a valid but unintended memory quantity.
Objectives:
- Pod
ckad-qos-18 - Container
appmust end with CPU and memory requests equal to their limits - Correct the valid but unintended
memory: 128mquantity to128Mi - Verify
.status.qosClassisGuaranteed
Starting manifest:
apiVersion: v1
kind: Pod
metadata:
name: ckad-qos-18
namespace: ckad-labs
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
resources:
requests:
cpu: 100m
memory: 128m
limits:
cpu: 200m
memory: 256MiVerification:
kubectl get pod ckad-qos-18 -n ckad-labs -o jsonpath='{.status.qosClass}{"\n"}'Hint
128m means millibytes, not mebibytes. Equal requests and limits on both CPU and memory produce Guaranteed.
Worked solution
apiVersion: v1
kind: Pod
metadata:
name: ckad-qos-18
namespace: ckad-labs
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
resources:
requests:
cpu: 200m
memory: 128Mi
limits:
cpu: 200m
memory: 128Mikubectl apply -f ckad-qos-18.yaml
kubectl wait pod/ckad-qos-18 -n ckad-labs --for=condition=Ready --timeout=90s
kubectl get pod ckad-qos-18 -n ckad-labs -o jsonpath='{.status.qosClass}{"\n"}'Sample output:
GuaranteedRelated lesson: Requests, limits, and QoS classes
Cleanup:
kubectl delete pod ckad-qos-18 -n ckad-labs --ignore-not-foundTask 19: Create Namespace Resource Controls
| Field | Value |
|---|---|
| Domain | Application Environment, Configuration and Security |
| Difficulty | Exam-style |
| Suggested time | 20 minutes |
Scenario: Enforce default container resources and cap total namespace consumption.
Objectives:
- Namespace
ckad-quota-19 - LimitRange sets default
cpu=100m,memory=128Miand matching defaults for limits - ResourceQuota allows at most
2Pods and500mtotal CPU requests - Pod without explicit resources inherits defaults
- Second oversized Pod is rejected by quota
Verification:
kubectl describe limitrange -n ckad-quota-19kubectl describe resourcequota -n ckad-quota-19kubectl get pod -n ckad-quota-19 -o yaml | grep -A6 resources:Hint
Create LimitRange and ResourceQuota before the test Pods. Admission injects defaults when the Pod omits resources.
Worked solution
kubectl create namespace ckad-quota-19kubectl apply -f - <<'YAML'
apiVersion: v1
kind: LimitRange
metadata:
name: ckad-lr-19
namespace: ckad-quota-19
spec:
limits:
- type: Container
default:
cpu: 100m
memory: 128Mi
defaultRequest:
cpu: 100m
memory: 128Mi
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: ckad-rq-19
namespace: ckad-quota-19
spec:
hard:
pods: "2"
requests.cpu: 500m
YAMLkubectl apply -f - <<'YAML'
apiVersion: v1
kind: Pod
metadata:
name: ckad-quota-pod-19a
namespace: ckad-quota-19
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
YAML
kubectl wait pod/ckad-quota-pod-19a -n ckad-quota-19 --for=condition=Ready --timeout=90sThe first Pod omits resources, so admission injects 100m CPU and 128Mi memory.
Capture the expected rejection:
kubectl apply -f - <<'YAML' 2>&1 | tee /tmp/ckad-quota-19b.err
apiVersion: v1
kind: Pod
metadata:
name: ckad-quota-pod-19b
namespace: ckad-quota-19
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
resources:
requests:
cpu: 500m
limits:
cpu: 500m
YAMLgrep -E 'exceeded quota: ckad-rq-19|Forbidden' /tmp/ckad-quota-19b.errThe second Pod should fail because 100m is already used and 500m more would exceed the 500m CPU request cap.
Related lesson: ResourceQuota and LimitRange
Cleanup:
kubectl delete namespace ckad-quota-19 --ignore-not-foundTask 20: Configure ServiceAccount, RBAC and SecurityContext
| Field | Value |
|---|---|
| Domain | Application Environment, Configuration and Security |
| Difficulty | Exam-style |
| Suggested time | 25 minutes |
Scenario: Run a hardened Pod under a dedicated ServiceAccount that may read Pods in the namespace.
Objectives:
- ServiceAccount
ckad-sa-20 - Role grants
get,list,watchon Pods - RoleBinding subjects the ServiceAccount
- Pod
ckad-secure-20uses that ServiceAccount - Container runs as UID
1000,runAsNonRoot: true,allowPrivilegeEscalation: false,readOnlyRootFilesystem: true - Writable
emptyDirmounted at/tmp
Verification:
kubectl auth can-i list pods --as=system:serviceaccount:ckad-labs:ckad-sa-20 -n ckad-labskubectl exec ckad-secure-20 -n ckad-labs -- idkubectl exec ckad-secure-20 -n ckad-labs -- sh -c 'touch /tmp/ok && ls /tmp/ok'Hint
Create ServiceAccount, Role, and RoleBinding first. The Pod needs serviceAccountName, SecurityContext fields, and a writable mount when the root filesystem is read-only.
Worked solution
kubectl create serviceaccount ckad-sa-20 -n ckad-labs
kubectl create role ckad-sa-20-reader -n ckad-labs --verb=get,list,watch --resource=pods
kubectl create rolebinding ckad-sa-20-reader -n ckad-labs --role=ckad-sa-20-reader --serviceaccount=ckad-labs:ckad-sa-20kubectl apply -f - <<'YAML'
apiVersion: v1
kind: Pod
metadata:
name: ckad-secure-20
namespace: ckad-labs
spec:
serviceAccountName: ckad-sa-20
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
YAMLkubectl wait pod/ckad-secure-20 -n ckad-labs --for=condition=Ready --timeout=90s
kubectl auth can-i list pods --as=system:serviceaccount:ckad-labs:ckad-sa-20 -n ckad-labs
kubectl exec ckad-secure-20 -n ckad-labs -- id
kubectl exec ckad-secure-20 -n ckad-labs -- sh -c 'touch /tmp/ok && ls /tmp/ok'The kubectl auth can-i --as=... check requires permission to impersonate ServiceAccounts. A user with only namespaced workload permissions may not be allowed to run it.
Related lessons: ServiceAccounts · RBAC · SecurityContext examples
Cleanup:
kubectl delete pod ckad-secure-20 -n ckad-labs --ignore-not-found
kubectl delete rolebinding ckad-sa-20-reader -n ckad-labs --ignore-not-found
kubectl delete role ckad-sa-20-reader -n ckad-labs --ignore-not-found
kubectl delete serviceaccount ckad-sa-20 -n ckad-labs --ignore-not-foundServices and Networking
Task 21: Expose a Deployment with a Service
| Field | Value |
|---|---|
| Domain | Services and Networking |
| Difficulty | Intermediate |
| Suggested time | 12 minutes |
Scenario: Expose a Deployment through a ClusterIP Service with named ports and verify Endpoints.
Objectives:
- Deployment
ckad-svc-21, imagenginx:1.27-alpine, port80 - Service
ckad-svc-21, typeClusterIP - Service port name
http, port8080,targetPort80 - Selector matches Deployment labels
- Client Pod can
curlthe Service short name
Verification:
kubectl get svc ckad-svc-21 -n ckad-labskubectl get endpointslice -n ckad-labs -l kubernetes.io/service-name=ckad-svc-21kubectl run ckad-client-21 -n ckad-labs --image=curlimages/curl:8.11.1 --restart=Never --rm -i -- curl -fsS http://ckad-svc-21:8080Hint
The Service port is what clients use. targetPort must match the container listener.
Worked solution
kubectl create deployment ckad-svc-21 -n ckad-labs --image=nginx:1.27-alpine --port=80
kubectl rollout status deployment/ckad-svc-21 -n ckad-labs --timeout=120s
kubectl expose deployment ckad-svc-21 -n ckad-labs --name=ckad-svc-21 --port=8080 --target-port=80
kubectl patch svc ckad-svc-21 -n ckad-labs -p '{"spec":{"ports":[{"name":"http","port":8080,"targetPort":80}]}}'
kubectl wait endpointslice \
-n ckad-labs \
-l kubernetes.io/service-name=ckad-svc-21 \
--for=jsonpath='{.endpoints[0].conditions.ready}'=true \
--timeout=120s
kubectl run ckad-client-21 -n ckad-labs --image=curlimages/curl:8.11.1 --restart=Never --rm -i -- curl -fsS http://ckad-svc-21:8080Related lesson: Services, Endpoints, and EndpointSlices
Cleanup:
kubectl delete pod ckad-client-21 -n ckad-labs --ignore-not-found
kubectl delete svc ckad-svc-21 -n ckad-labs --ignore-not-found
kubectl delete deployment ckad-svc-21 -n ckad-labs --ignore-not-foundTask 22: Repair a Broken Service
| Field | Value |
|---|---|
| Domain | Services and Networking |
| Difficulty | Exam-style |
| Suggested time | 20 minutes |
Scenario: A Service exists but clients cannot reach the application. Fix the broken objects without deleting the Deployment.
Objectives:
- Deployment
ckad-fix-22stays in place - Repair selector mismatch and wrong
targetPort - Endpoints must list ready backend addresses
curlfrom a client Pod must succeed
Broken starting objects:
- Deployment labels
app=ckad-fix-22 - Service
ckad-fix-22selectorapp=wrong,targetPort: 8080while nginx listens on80
Verification:
kubectl get endpointslice -n ckad-labs -l kubernetes.io/service-name=ckad-fix-22kubectl run ckad-client-22 -n ckad-labs --image=curlimages/curl:8.11.1 --restart=Never --rm -i -- curl -fsS -o /dev/null -w '%{http_code}\n' http://ckad-fix-22Hint
Compare Service selector and targetPort with Pod labels and container ports.
Worked solution
Apply the broken starting objects:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ckad-fix-22
namespace: ckad-labs
labels:
app: ckad-fix-22
spec:
replicas: 1
selector:
matchLabels:
app: ckad-fix-22
template:
metadata:
labels:
app: ckad-fix-22
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: ckad-fix-22
namespace: ckad-labs
spec:
selector:
app: wrong
ports:
- port: 80
targetPort: 8080kubectl apply -f ckad-fix-22-broken.yaml
kubectl rollout status deployment/ckad-fix-22 -n ckad-labs --timeout=120sRepair the Service selector and targetPort:
kubectl patch svc ckad-fix-22 -n ckad-labs -p '{"spec":{"selector":{"app":"ckad-fix-22"},"ports":[{"port":80,"targetPort":80}]}}'
kubectl wait endpointslice \
-n ckad-labs \
-l kubernetes.io/service-name=ckad-fix-22 \
--for=jsonpath='{.endpoints[0].conditions.ready}'=true \
--timeout=120s
kubectl run ckad-client-22 -n ckad-labs --image=curlimages/curl:8.11.1 --restart=Never --rm -i -- curl -fsS -o /dev/null -w '%{http_code}\n' http://ckad-fix-22Sample output:
200Related lesson: Troubleshoot a Service that is not working
Cleanup:
kubectl delete pod ckad-client-22 -n ckad-labs --ignore-not-found
kubectl delete svc,deployment ckad-fix-22 -n ckad-labs --ignore-not-foundTask 23: Resolve Services Across Namespaces
| Field | Value |
|---|---|
| Domain | Services and Networking |
| Difficulty | Intermediate |
| Suggested time | 15 minutes |
Scenario: A client Pod in ckad-front-23 must reach an API Service in ckad-back-23.
Objectives:
- Namespace
ckad-back-23with Serviceapi - Namespace
ckad-front-23with client Pod - Demonstrate that bare
apidoes not resolve fromckad-front-23 - Verify
api.ckad-back-23andapi.ckad-back-23.svc.cluster.local.resolve - Inspect
/etc/resolv.confon the client
Verification:
kubectl exec -n ckad-front-23 ckad-dns-23 -- nslookup api.ckad-back-23kubectl exec -n ckad-front-23 ckad-dns-23 -- cat /etc/resolv.confHint
Short names are namespace-relative. Cross-namespace lookups need the backend namespace in the query.
Worked solution
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: ckad-back-23
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: app
image: nginx:1.27-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: api
namespace: ckad-back-23
spec:
selector:
app: api
ports:
- port: 80
targetPort: 80
---
apiVersion: v1
kind: Pod
metadata:
name: ckad-dns-23
namespace: ckad-front-23
spec:
containers:
- name: app
image: registry.k8s.io/e2e-test-images/agnhost:2.39
imagePullPolicy: IfNotPresent
restartPolicy: Alwayskubectl create namespace ckad-back-23
kubectl create namespace ckad-front-23
kubectl apply -f ckad-dns-23.yaml
kubectl rollout status deployment/api -n ckad-back-23 --timeout=120s
kubectl wait pod/ckad-dns-23 -n ckad-front-23 --for=condition=Ready --timeout=90skubectl exec -n ckad-front-23 ckad-dns-23 -- nslookup api || true
kubectl exec -n ckad-front-23 ckad-dns-23 -- nslookup api.ckad-back-23
kubectl exec -n ckad-front-23 ckad-dns-23 -- nslookup api.ckad-back-23.svc.cluster.local.
kubectl exec -n ckad-front-23 ckad-dns-23 -- cat /etc/resolv.confBare api searches only inside ckad-front-23, so it does not find the backend Service. The namespace-qualified names resolve to the ClusterIP. Replace cluster.local if your lab uses a different cluster domain.
Related lesson: Service DNS and name resolution
Cleanup:
kubectl delete namespace ckad-front-23 ckad-back-23 --ignore-not-foundTask 24: Create an Ingress Rule
| Field | Value |
|---|---|
| Domain | Services and Networking |
| Difficulty | Exam-style |
| Suggested time | 20 minutes |
Scenario: Publish two paths on one host through an Ingress that uses TLS.
Objectives:
- Backend Deployment and Service that answer
/and/healthz - Ingress
ckad-ing-24uses an available IngressClass - Host
ckad.lab.local - Paths
/and/healthzroute to the Service - TLS Secret
ckad-ing-tls-24referenced by the Ingress
Verification:
kubectl describe ingress ckad-ing-24 -n ckad-labsINGRESS_IP=192.168.56.120
curl -sk --resolve "ckad.lab.local:443:$INGRESS_IP" https://ckad.lab.local/healthzHint
Create a TLS Secret with tls.crt and tls.key, then reference it under spec.tls.
Worked solution
List available IngressClasses and pick one that matches your controller:
kubectl get ingressclassCreate the backend Deployment and Service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ckad-ing-svc-24
namespace: ckad-labs
spec:
replicas: 1
selector:
matchLabels:
app: ckad-ing-svc-24
template:
metadata:
labels:
app: ckad-ing-svc-24
spec:
containers:
- name: app
image: registry.k8s.io/e2e-test-images/agnhost:2.39
args: ["netexec", "--http-port=8080"]
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: ckad-ing-svc-24
namespace: ckad-labs
spec:
selector:
app: ckad-ing-svc-24
ports:
- port: 80
targetPort: 8080kubectl apply -f ckad-ing-backend-24.yaml
kubectl rollout status deployment/ckad-ing-svc-24 -n ckad-labs --timeout=120sopenssl req -x509 -nodes -days 1 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj '/CN=ckad.lab.local'
kubectl create secret tls ckad-ing-tls-24 -n ckad-labs --cert=tls.crt --key=tls.keyReplace nginx below if your cluster uses a different IngressClass name:
kubectl apply -f - <<'YAML'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ckad-ing-24
namespace: ckad-labs
spec:
ingressClassName: nginx
tls:
- hosts:
- ckad.lab.local
secretName: ckad-ing-tls-24
rules:
- host: ckad.lab.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ckad-ing-svc-24
port:
number: 80
- path: /healthz
pathType: Prefix
backend:
service:
name: ckad-ing-svc-24
port:
number: 80
YAMLkubectl wait \
--for=jsonpath='{.status.loadBalancer.ingress[0].ip}' ingress/ckad-ing-24 \
-n ckad-labs \
--timeout=180s 2>/dev/null || kubectl wait \
--for=jsonpath='{.status.loadBalancer.ingress[0].hostname}' ingress/ckad-ing-24 \
-n ckad-labs \
--timeout=180s
INGRESS_IP=$(kubectl get ingress ckad-ing-24 -n ckad-labs -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
INGRESS_HOSTNAME=$(kubectl get ingress ckad-ing-24 -n ckad-labs -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
if [ -n "$INGRESS_IP" ]; then
curl -sk --resolve "ckad.lab.local:443:$INGRESS_IP" https://ckad.lab.local/healthz
elif [ -n "$INGRESS_HOSTNAME" ]; then
curl -sk --connect-to "ckad.lab.local:443:$INGRESS_HOSTNAME:443" https://ckad.lab.local/healthz
else
echo "Ingress has not published a reachable address; inspect the controller Service." >&2
exit 1
fi--resolve sets both the request hostname and TLS SNI correctly when the Ingress address is an IP.
Related lesson: Ingress rules with host, path, and TLS
Cleanup:
kubectl delete ingress ckad-ing-24 -n ckad-labs --ignore-not-found
kubectl delete secret ckad-ing-tls-24 -n ckad-labs --ignore-not-found
kubectl delete svc,deployment ckad-ing-svc-24 -n ckad-labs --ignore-not-foundTask 25: Apply Default-Deny NetworkPolicy
| Field | Value |
|---|---|
| Domain | Services and Networking |
| Difficulty | Exam-style |
| Suggested time | 25 minutes |
Scenario: Lock down one application namespace while still allowing DNS and one trusted client.
Objectives:
- Namespace
ckad-net-25and trusted client namespaceckad-client-25 - Default-deny ingress and egress policy
- Allow ingress to
app=serverPods from namespaceckad-client-25on TCP80 - Allow egress to
kube-systemfor DNS on UDP and TCP port53 - Prove allowed traffic from
ckad-client-25and blocked traffic from an untrusted namespace - Run a DNS lookup from an egress-isolated Pod
Verification:
SERVER_IP=$(kubectl get pod -n ckad-net-25 -l app=server -o jsonpath='{.items[0].status.podIP}')
kubectl exec -n ckad-client-25 client -- wget -qO- --timeout=3 "http://$SERVER_IP"kubectl exec -n ckad-untrusted-25 blocked-client -- wget -qO- --timeout=3 "http://$SERVER_IP" || echo "blocked as expected"Hint
Use separate policies or one policy with multiple ingress and egress rules. DNS egress usually targets the kube-system namespace selector.
Worked solution
kubectl create namespace ckad-net-25
kubectl create namespace ckad-client-25
kubectl create namespace ckad-untrusted-25apiVersion: apps/v1
kind: Deployment
metadata:
name: server
namespace: ckad-net-25
spec:
replicas: 1
selector:
matchLabels:
app: server
template:
metadata:
labels:
app: server
spec:
containers:
- name: app
image: nginx:1.27-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: server
namespace: ckad-net-25
spec:
selector:
app: server
ports:
- port: 80
targetPort: 80
---
apiVersion: v1
kind: Pod
metadata:
name: client
namespace: ckad-client-25
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
---
apiVersion: v1
kind: Pod
metadata:
name: blocked-client
namespace: ckad-untrusted-25
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]kubectl apply -f ckad-net-25-workloads.yaml
kubectl rollout status deployment/server -n ckad-net-25 --timeout=120s
kubectl wait pod/client -n ckad-client-25 --for=condition=Ready --timeout=90s
kubectl wait pod/blocked-client -n ckad-untrusted-25 --for=condition=Ready --timeout=90skubectl apply -f - <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: ckad-net-25
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-client
namespace: ckad-net-25
spec:
podSelector:
matchLabels:
app: server
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ckad-client-25
ports:
- protocol: TCP
port: 80
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: ckad-net-25
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
YAMLSERVER_IP=$(kubectl get pod -n ckad-net-25 -l app=server -o jsonpath='{.items[0].status.podIP}')
kubectl exec -n ckad-client-25 client -- wget -qO- --timeout=3 "http://$SERVER_IP" | head -1
kubectl exec -n ckad-untrusted-25 blocked-client -- wget -qO- --timeout=3 "http://$SERVER_IP" || echo "blocked as expected"
SERVER_POD=$(kubectl get pod -n ckad-net-25 -l app=server -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n ckad-net-25 "$SERVER_POD" -- nslookup kubernetes.defaultTraffic from ckad-client-25 should succeed. A client Pod in ckad-untrusted-25 should time out because the ingress allow rule matches only kubernetes.io/metadata.name: ckad-client-25.
Related lesson: NetworkPolicy with examples
Cleanup:
kubectl delete namespace ckad-net-25 ckad-client-25 ckad-untrusted-25 --ignore-not-foundProgress Checklist
Track your own practice on paper or in a local note. No account or JavaScript is required.
| Task | First attempt | No hint | On time | Repeat later |
|---|---|---|---|---|
| 1 | ☐ | ☐ | ☐ | ☐ |
| 2 | ☐ | ☐ | ☐ | ☐ |
| 3 | ☐ | ☐ | ☐ | ☐ |
| 4 | ☐ | ☐ | ☐ | ☐ |
| 5 | ☐ | ☐ | ☐ | ☐ |
| 6 | ☐ | ☐ | ☐ | ☐ |
| 7 | ☐ | ☐ | ☐ | ☐ |
| 8 | ☐ | ☐ | ☐ | ☐ |
| 9 | ☐ | ☐ | ☐ | ☐ |
| 10 | ☐ | ☐ | ☐ | ☐ |
| 11 | ☐ | ☐ | ☐ | ☐ |
| 12 | ☐ | ☐ | ☐ | ☐ |
| 13 | ☐ | ☐ | ☐ | ☐ |
| 14 | ☐ | ☐ | ☐ | ☐ |
| 15 | ☐ | ☐ | ☐ | ☐ |
| 16 | ☐ | ☐ | ☐ | ☐ |
| 17 | ☐ | ☐ | ☐ | ☐ |
| 18 | ☐ | ☐ | ☐ | ☐ |
| 19 | ☐ | ☐ | ☐ | ☐ |
| 20 | ☐ | ☐ | ☐ | ☐ |
| 21 | ☐ | ☐ | ☐ | ☐ |
| 22 | ☐ | ☐ | ☐ | ☐ |
| 23 | ☐ | ☐ | ☐ | ☐ |
| 24 | ☐ | ☐ | ☐ | ☐ |
| 25 | ☐ | ☐ | ☐ | ☐ |
Do Not Expect Here
- A timed full mock exam
- Real or recalled certification questions
- Exam dumps
- Long conceptual re-teaching of every lesson
- Complete beginner cluster setup
- Multiple alternative solutions for every task
- Scoring against the real CKAD passing standard
References
- Certified Kubernetes Application Developer (CKAD) — official certification page
- Kubernetes documentation — upstream task and API reference
- kubectl Cheat Sheet — command quick reference
Summary
You now have twenty-five original CKAD practice scenarios grouped under the five official exam domains. Each task card gives you a measurable outcome, verification commands, an optional hint, and a worked solution you can open only after you try the task yourself.
The practice bank is designed for repetition: complete the related lesson, attempt the scenario cold, verify with runtime commands, and return later with different resource names. Foundation tasks build single-object confidence, intermediate tasks combine related objects, and exam-style tasks ask you to diagnose or apply skills from multiple lessons in one namespace.
When a task depends on Helm, Metrics Server, Ingress, or NetworkPolicy support, satisfy that prerequisite before you start. Return to the CKAD tutorial hub for conceptual depth, then use this page as your hands-on rehearsal loop before the exam.

