CKAD Practice Tasks by Exam Domain

Practice CKAD skills with 25 hands-on Kubernetes scenarios grouped by exam domain, each with verification steps, hints, and worked solutions.

Published

Updated

Read time 32 min read

Reviewed byDeepak Prasad

CKAD practice labs and hands-on Kubernetes tasks grouped by official exam domain
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

  1. Complete the relevant course lesson first.
  2. Attempt the task without opening its solution.
  3. Use Kubernetes documentation only when necessary.
  4. Verify the result using the commands in the task card.
  5. Open the hint only when you are blocked.
  6. Compare your finished object with the worked solution.
  7. 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 kubectl configured for that cluster
  • Permission to create namespaced application resources
  • A default dynamic StorageClass that can provision ReadWriteOnce PVCs for Task 5
  • Helm for Task 8
  • Metrics Server for Task 14
  • Ephemeral-container support and pods/ephemeralcontainers access for Task 13
  • An Ingress controller for Task 24
  • OpenSSL on the workstation for Task 24
  • Permission to inspect IngressClass objects 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:

bash
kubectl create namespace ckad-labs
bash
kubectl config set-context --current --namespace=ckad-labs

This article does not include cluster installation instructions. Use install Kubernetes with kubeadm or your existing lab when you still need a cluster.


Domain Navigation

Domain Weight
Application Design and Build 20%
Application Deployment 20%
Application Observability and Maintenance 15%
Application Environment, Configuration and Security 25%
Services and Networking 20%

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, port 8080
  • Restart policy: Never
  • Command: run agnhost netexec --http-port=8080

Constraints: Use a declarative manifest. Do not create a Deployment.

Verification:

bash
kubectl get pod ckad-pod-01 -n ckad-labs -o jsonpath='{.status.phase}{"\n"}'
bash
kubectl logs ckad-pod-01 -n ckad-labs -c app --tail=3
bash
kubectl get pod ckad-pod-01 -n ckad-labs --show-labels
Hint

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
yaml
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: 8080
bash
kubectl apply -f ckad-pod-01.yaml
kubectl wait pod/ckad-pod-01 -n ckad-labs --for=condition=Ready --timeout=90s

Related lesson: Kubernetes Pods and Pod lifecycle

Cleanup:

bash
kubectl delete pod ckad-pod-01 -n ckad-labs --ignore-not-found

Task 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 prep writes hello from init to /work/message.txt
  • Main container app runs cat /work/message.txt then sleep 3600
  • Shared volume: emptyDir mounted at /work

Constraints: Do not use a hostPath volume.

Verification:

bash
kubectl get pod ckad-init-02 -n ckad-labs -o jsonpath='{.status.initContainerStatuses[0].state.terminated.reason}{"\n"}'
bash
kubectl logs ckad-init-02 -n ckad-labs -c app
Hint

Mount the same volume in both init and app containers. The init container must finish before Kubernetes starts app.

Worked solution
yaml
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: {}
bash
kubectl apply -f ckad-init-02.yaml
kubectl wait pod/ckad-init-02 -n ckad-labs --for=condition=Ready --timeout=90s

Sample output from the main container log:

output
hello from init

Related lessons: Init containers · Kubernetes volumes

Cleanup:

bash
kubectl delete pod ckad-init-02 -n ckad-labs --ignore-not-found

Task 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 writer appends line-1, line-2, line-3 to /var/log/app.log every five seconds
  • Sidecar tailer runs tail -F /var/log/app.log
  • Both containers share one emptyDir at /var/log

Verification:

bash
kubectl logs ckad-sidecar-03 -n ckad-labs -c tailer --tail=3
bash
kubectl exec ckad-sidecar-03 -n ckad-labs -c writer -- wc -l /var/log/app.log
Hint

Use two containers in one Pod spec and one shared volume mount path.

Worked solution
yaml
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: {}
bash
kubectl apply -f ckad-sidecar-03.yaml
kubectl wait pod/ckad-sidecar-03 -n ckad-labs --for=condition=Ready --timeout=90s
sleep 6

Related lesson: Sidecar and multi-container patterns

Cleanup:

bash
kubectl delete pod ckad-sidecar-03 -n ckad-labs --ignore-not-found

Task 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 prints job complete and exits successfully
  • backoffLimit: 2
  • CronJob name: ckad-cron-04, schedule */5 * * * *
  • CronJob concurrency policy: Forbid
  • Successful Jobs history limit: 1

Verification:

bash
kubectl wait job/ckad-job-04 -n ckad-labs --for=condition=complete --timeout=120s
bash
kubectl get cronjob ckad-cron-04 -n ckad-labs
Hint

A Job needs restartPolicy: Never on the Pod template. A CronJob wraps a Job template and owns the schedule fields.

Worked solution
yaml
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"]
bash
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-labs

Related lessons: Kubernetes Jobs · Kubernetes CronJobs

Cleanup:

bash
kubectl delete job ckad-job-04 -n ckad-labs --ignore-not-found
kubectl delete cronjob ckad-cron-04 -n ckad-labs --ignore-not-found

Task 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, size 1Gi, access mode ReadWriteOnce
  • First Pod ckad-pvc-writer-05 writes persist-me to /data/value.txt
  • Delete the writer Pod and create ckad-pvc-reader-05 that reads the same file
  • Both Pods mount the PVC at /data

Verification:

bash
kubectl get pvc ckad-data-05 -n ckad-labs
bash
kubectl logs ckad-pvc-reader-05 -n ckad-labs
Hint

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:

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-05
bash
kubectl apply -f ckad-pvc-writer-05.yaml
bash
kubectl wait pvc/ckad-data-05   -n ckad-labs   --for=jsonpath='{.status.phase}'=Bound   --timeout=120s
bash
kubectl wait pod/ckad-pvc-writer-05   -n ckad-labs   --for=jsonpath='{.status.phase}'=Succeeded   --timeout=120s

Save the reader Pod in ckad-pvc-reader-05.yaml:

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-05
bash
kubectl delete pod ckad-pvc-writer-05 -n ckad-labs --wait=true
bash
kubectl apply -f ckad-pvc-reader-05.yaml
bash
kubectl wait pod/ckad-pvc-reader-05   -n ckad-labs   --for=jsonpath='{.status.phase}'=Succeeded   --timeout=120s
bash
kubectl logs ckad-pvc-reader-05 -n ckad-labs

Sample output:

output
persist-me

Related lesson: PersistentVolume and PVC

Cleanup:

bash
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-found

Application 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, image nginx:1.27-alpine, initial replicas 1
  • Scale to 3 replicas
  • Add label tier=web to the Pod template
  • Selector must remain app=ckad-deploy-06

Constraints: Patch or scale the existing Deployment. Do not delete and recreate it.

Verification:

bash
kubectl get deployment ckad-deploy-06 -n ckad-labs
bash
kubectl get rs -n ckad-labs -l app=ckad-deploy-06
bash
kubectl get pods -n ckad-labs -l app=ckad-deploy-06 --show-labels
Hint

kubectl scale changes replica count. kubectl patch or kubectl edit can add template labels and trigger a rolling update.

Worked solution
bash
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=120s

Related lesson: Deployments and rolling updates

Cleanup:

bash
kubectl delete deployment ckad-deploy-06 -n ckad-labs --ignore-not-found

Task 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-07 starts with nginx: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-alpine again

Verification:

bash
kubectl rollout status deployment/ckad-roll-07 -n ckad-labs --timeout=120s
bash
kubectl rollout history deployment/ckad-roll-07 -n ckad-labs
bash
kubectl 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
bash
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=120s

Related lesson: Deployments and rolling updates

Cleanup:

bash
kubectl delete deployment ckad-roll-07 -n ckad-labs --ignore-not-found

Task 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-08 in ckad-labs
  • Chart: bitnami/nginx from https://charts.bitnami.com/bitnami
  • First install sets service.type=ClusterIP
  • Upgrade sets replicaCount=2
  • Show at least two entries in helm history

Verification:

bash
helm list -n ckad-labs
bash
helm history ckad-helm-08 -n ckad-labs
bash
kubectl get pods -n ckad-labs -l app.kubernetes.io/instance=ckad-helm-08
Hint

helm repo add, helm install --set, then helm upgrade --reuse-values --set replicaCount=2.

Worked solution
bash
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=120s

Related lesson: Deploy applications with Helm

Cleanup:

bash
helm uninstall ckad-helm-08 -n ckad-labs

Task 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/base with Deployment ckad-kust-09, image nginx:1.27-alpine, replicas 1
  • Overlay path: kustomize/prod
  • Production overlay sets replicas 3, image nginx:1.25-alpine, and label env=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:

bash
kubectl get deployment ckad-kust-09 -n ckad-labs
bash
kubectl get pods -n ckad-labs -l app=ckad-kust-09 --show-labels
Hint

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:

yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ckad-labs
resources:
- deployment.yaml

Create kustomize/base/deployment.yaml:

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-alpine

Create kustomize/prod/kustomization.yaml:

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: true
bash
kubectl kustomize kustomize/prod
kubectl apply -k kustomize/prod
kubectl rollout status deployment/ckad-kust-09 -n ckad-labs --timeout=120s

Related lesson: Kustomize bases and overlays

Cleanup:

bash
kubectl delete deployment ckad-kust-09 -n ckad-labs --ignore-not-found

Application 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-resources or kubectl explain to find the current API
  • Validate with server-side dry run before apply
  • Running Pod count must reach 2

Starting manifest:

yaml
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-alpine

Verification:

bash
kubectl apply -f ckad-migrate-10.yaml --dry-run=server
bash
kubectl rollout status deployment/ckad-migrate-10 -n ckad-labs --timeout=120s
Hint

Modern clusters use apps/v1. Add an explicit selector.matchLabels that matches the Pod template labels.

Worked solution
bash
kubectl api-resources | grep -E '^deployments[[:space:]]'

Sample output:

output
deployments   deploy   apps/v1   true   Deployment
yaml
apiVersion: 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-alpine
bash
kubectl 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=120s

Related lesson: API deprecations and manifest migration

Cleanup:

bash
kubectl delete deployment ckad-migrate-10 -n ckad-labs --ignore-not-found

Task 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, image registry.k8s.io/e2e-test-images/agnhost:2.39
  • Args: netexec --http-port=8080
  • Startup probe: httpGet /healthz on port 8080, failureThreshold: 12, periodSeconds: 2
  • Readiness probe: same path, periodSeconds: 5
  • Liveness probe: same path, periodSeconds: 10

Verification:

bash
kubectl rollout status deployment/ckad-probes-11 -n ckad-labs --timeout=120s
bash
kubectl get pods -n ckad-labs -l app=ckad-probes-11 -o jsonpath='{range .items[*]}{.metadata.name}{" Ready="}{.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}'
bash
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
yaml
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: 10
bash
kubectl apply -f ckad-probes-11.yaml
kubectl rollout status deployment/ckad-probes-11 -n ckad-labs --timeout=120s

Related lesson: Liveness, readiness, and startup probes

Cleanup:

bash
kubectl delete deployment ckad-probes-11 -n ckad-labs --ignore-not-found

Task 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:

yaml
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:

bash
kubectl get pod ckad-crash-12 -n ckad-labs
bash
kubectl logs ckad-crash-12 -n ckad-labs -c app --previous
bash
kubectl wait pod/ckad-crash-12 -n ckad-labs --for=condition=Ready --timeout=120s
Hint

CrashLoopBackOff with exit code 1 means the command itself is wrong. Replace it with a long-running command such as sleep 3600.

Worked solution
bash
kubectl apply -f ckad-crash-12-broken.yaml
bash
for 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
fi
bash
kubectl logs ckad-crash-12 -n ckad-labs -c app --previous
bash
kubectl get pod ckad-crash-12 -n ckad-labs -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'

Sample output:

output
1

Delete the broken Pod and apply the fixed manifest:

yaml
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"]
bash
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=120s

Related lessons: Fix CrashLoopBackOff · kubectl logs, Events, and describe

Cleanup:

bash
kubectl delete pod ckad-crash-12 -n ckad-labs --ignore-not-found

Task 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-13 with container app running registry.k8s.io/e2e-test-images/agnhost:2.39 and netexec --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:

bash
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
yaml
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: 8080
bash
kubectl 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:

bash
kubectl delete deployment ckad-debug-13 -n ckad-labs --ignore-not-found

Task 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-14 with requests.memory=32Mi and limits.memory=64Mi
  • Create ckad-top-b-14 with requests.memory=128Mi and limits.memory=256Mi
  • Use kubectl top to identify the higher-memory Pod
  • Show per-container usage and compare with kubectl describe

Verification:

bash
kubectl top pod -n ckad-labs --sort-by=memory
bash
kubectl top pod -n ckad-labs --containers
bash
kubectl 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.

bash
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
YAML
bash
kubectl rollout status deployment/ckad-top-a-14 -n ckad-labs --timeout=120s
kubectl rollout status deployment/ckad-top-b-14 -n ckad-labs --timeout=120s

Wait for Metrics Server to report usage, then compare live memory:

bash
for i in $(seq 1 12); do
  kubectl top pod -n ckad-labs -l task=14 --sort-by=memory && break
  sleep 10
done

ckad-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:

bash
kubectl delete deployment ckad-top-a-14 ckad-top-b-14 -n ckad-labs --ignore-not-found

Application 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-15 with keys MODE=debug and REGION=lab
  • Pod ckad-cm-pod-15 exposes MODE_SINGLE through configMapKeyRef
  • All keys available through envFrom
  • File /etc/config/REGION mounted from key REGION

Verification:

bash
kubectl exec ckad-cm-pod-15 -n ckad-labs -- printenv MODE_SINGLE MODE REGION
bash
kubectl exec ckad-cm-pod-15 -n ckad-labs -- cat /etc/config/REGION
Hint

You can combine env, envFrom, and volumeMount with items in one Pod spec.

Worked solution
bash
kubectl create configmap ckad-cm-15 -n ckad-labs --from-literal=MODE=debug --from-literal=REGION=lab
yaml
apiVersion: 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: REGION
bash
kubectl 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/REGION

Related lesson: Kubernetes ConfigMap with examples

Cleanup:

bash
kubectl delete pod ckad-cm-pod-15 -n ckad-labs --ignore-not-found
kubectl delete configmap ckad-cm-15 -n ckad-labs --ignore-not-found

Task 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, key token=demo-token-123
  • Pod ckad-secret-pod-16 reads API_TOKEN from secretKeyRef
  • Same token available at /etc/secret/token

Verification:

bash
kubectl exec ckad-secret-pod-16 -n ckad-labs -- printenv API_TOKEN
bash
kubectl exec ckad-secret-pod-16 -n ckad-labs -- cat /etc/secret/token
Hint

Generic Secrets work with stringData or kubectl create secret generic.

Worked solution
bash
kubectl create secret generic ckad-secret-16 -n ckad-labs --from-literal=token=demo-token-123
bash
kubectl 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
YAML
bash
kubectl 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/token

Both commands should print demo-token-123.

Related lesson: Kubernetes Secrets with examples

Cleanup:

bash
kubectl delete pod ckad-secret-pod-16 -n ckad-labs --ignore-not-found
kubectl delete secret ckad-secret-16 -n ckad-labs --ignore-not-found

Task 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_LIMIT from limits.cpu

Verification:

bash
kubectl logs ckad-down-17 -n ckad-labs
Hint

Use fieldRef for metadata and resourceFieldRef for limits.cpu.

Worked solution
yaml
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: 200m
bash
kubectl 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-labs

Sample output includes CPU_LIMIT=200 and POD_NAME=ckad-down-17.

Related lesson: Commands, args, environment variables, and Downward API

Cleanup:

bash
kubectl delete pod ckad-down-17 -n ckad-labs --ignore-not-found

Task 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 app must end with CPU and memory requests equal to their limits
  • Correct the valid but unintended memory: 128m quantity to 128Mi
  • Verify .status.qosClass is Guaranteed

Starting manifest:

yaml
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: 256Mi

Verification:

bash
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
yaml
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: 128Mi
bash
kubectl 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:

output
Guaranteed

Related lesson: Requests, limits, and QoS classes

Cleanup:

bash
kubectl delete pod ckad-qos-18 -n ckad-labs --ignore-not-found

Task 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=128Mi and matching defaults for limits
  • ResourceQuota allows at most 2 Pods and 500m total CPU requests
  • Pod without explicit resources inherits defaults
  • Second oversized Pod is rejected by quota

Verification:

bash
kubectl describe limitrange -n ckad-quota-19
bash
kubectl describe resourcequota -n ckad-quota-19
bash
kubectl 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
bash
kubectl create namespace ckad-quota-19
bash
kubectl 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
YAML
bash
kubectl 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=90s

The first Pod omits resources, so admission injects 100m CPU and 128Mi memory.

Capture the expected rejection:

bash
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
YAML
bash
grep -E 'exceeded quota: ckad-rq-19|Forbidden' /tmp/ckad-quota-19b.err

The 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:

bash
kubectl delete namespace ckad-quota-19 --ignore-not-found

Task 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, watch on Pods
  • RoleBinding subjects the ServiceAccount
  • Pod ckad-secure-20 uses that ServiceAccount
  • Container runs as UID 1000, runAsNonRoot: true, allowPrivilegeEscalation: false, readOnlyRootFilesystem: true
  • Writable emptyDir mounted at /tmp

Verification:

bash
kubectl auth can-i list pods --as=system:serviceaccount:ckad-labs:ckad-sa-20 -n ckad-labs
bash
kubectl exec ckad-secure-20 -n ckad-labs -- id
bash
kubectl 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
bash
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-20
bash
kubectl 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: {}
YAML
bash
kubectl 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:

bash
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-found

Services 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, image nginx:1.27-alpine, port 80
  • Service ckad-svc-21, type ClusterIP
  • Service port name http, port 8080, targetPort 80
  • Selector matches Deployment labels
  • Client Pod can curl the Service short name

Verification:

bash
kubectl get svc ckad-svc-21 -n ckad-labs
bash
kubectl get endpointslice -n ckad-labs -l kubernetes.io/service-name=ckad-svc-21
bash
kubectl run ckad-client-21 -n ckad-labs --image=curlimages/curl:8.11.1 --restart=Never --rm -i -- curl -fsS http://ckad-svc-21:8080
Hint

The Service port is what clients use. targetPort must match the container listener.

Worked solution
bash
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:8080

Related lesson: Services, Endpoints, and EndpointSlices

Cleanup:

bash
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-found

Task 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-22 stays in place
  • Repair selector mismatch and wrong targetPort
  • Endpoints must list ready backend addresses
  • curl from a client Pod must succeed

Broken starting objects:

  • Deployment labels app=ckad-fix-22
  • Service ckad-fix-22 selector app=wrong, targetPort: 8080 while nginx listens on 80

Verification:

bash
kubectl get endpointslice -n ckad-labs -l kubernetes.io/service-name=ckad-fix-22
bash
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-22
Hint

Compare Service selector and targetPort with Pod labels and container ports.

Worked solution

Apply the broken starting objects:

yaml
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: 8080
bash
kubectl apply -f ckad-fix-22-broken.yaml
kubectl rollout status deployment/ckad-fix-22 -n ckad-labs --timeout=120s

Repair the Service selector and targetPort:

bash
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-22

Sample output:

output
200

Related lesson: Troubleshoot a Service that is not working

Cleanup:

bash
kubectl delete pod ckad-client-22 -n ckad-labs --ignore-not-found
kubectl delete svc,deployment ckad-fix-22 -n ckad-labs --ignore-not-found

Task 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-23 with Service api
  • Namespace ckad-front-23 with client Pod
  • Demonstrate that bare api does not resolve from ckad-front-23
  • Verify api.ckad-back-23 and api.ckad-back-23.svc.cluster.local. resolve
  • Inspect /etc/resolv.conf on the client

Verification:

bash
kubectl exec -n ckad-front-23 ckad-dns-23 -- nslookup api.ckad-back-23
bash
kubectl exec -n ckad-front-23 ckad-dns-23 -- cat /etc/resolv.conf
Hint

Short names are namespace-relative. Cross-namespace lookups need the backend namespace in the query.

Worked solution
yaml
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: Always
bash
kubectl 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=90s
bash
kubectl 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.conf

Bare 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:

bash
kubectl delete namespace ckad-front-23 ckad-back-23 --ignore-not-found

Task 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-24 uses an available IngressClass
  • Host ckad.lab.local
  • Paths / and /healthz route to the Service
  • TLS Secret ckad-ing-tls-24 referenced by the Ingress

Verification:

bash
kubectl describe ingress ckad-ing-24 -n ckad-labs
bash
INGRESS_IP=192.168.56.120
curl -sk --resolve "ckad.lab.local:443:$INGRESS_IP" https://ckad.lab.local/healthz
Hint

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:

bash
kubectl get ingressclass

Create the backend Deployment and Service:

yaml
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: 8080
bash
kubectl apply -f ckad-ing-backend-24.yaml
kubectl rollout status deployment/ckad-ing-svc-24 -n ckad-labs --timeout=120s
bash
openssl 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.key

Replace nginx below if your cluster uses a different IngressClass name:

bash
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
YAML
bash
kubectl 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:

bash
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-found

Task 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-25 and trusted client namespace ckad-client-25
  • Default-deny ingress and egress policy
  • Allow ingress to app=server Pods from namespace ckad-client-25 on TCP 80
  • Allow egress to kube-system for DNS on UDP and TCP port 53
  • Prove allowed traffic from ckad-client-25 and blocked traffic from an untrusted namespace
  • Run a DNS lookup from an egress-isolated Pod

Verification:

bash
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"
bash
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
bash
kubectl create namespace ckad-net-25
kubectl create namespace ckad-client-25
kubectl create namespace ckad-untrusted-25
yaml
apiVersion: 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"]
bash
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=90s
bash
kubectl 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
YAML
bash
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" | 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.default

Traffic 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:

bash
kubectl delete namespace ckad-net-25 ckad-client-25 ckad-untrusted-25 --ignore-not-found

Progress 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

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.


Frequently Asked Questions

1. Are these real CKAD exam questions?

No. These are original practice scenarios written for skill rehearsal. They follow the official CKAD competency areas but are not copied from the certification exam or any exam dump.

2. Do I need a full kubeadm cluster for every task?

Most tasks need a working Kubernetes cluster with permission to create namespaced workloads. A few tasks also need Helm, Metrics Server, an Ingress controller, or a NetworkPolicy-capable CNI. Each task states its extra dependency in the scenario or lab requirements.

3. Should I read the solution before attempting a task?

Try the task first using course lessons and official Kubernetes documentation. Open the hint only when blocked, then compare your finished object with the worked solution.

4. How long should each task take?

Suggested times are study guidance only. Foundation tasks target about 5 to 10 minutes, intermediate tasks about 10 to 15 minutes, and exam-style tasks about 15 to 25 minutes on a familiar cluster.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)