Skip to main content
mastering ckad certified kubernetes application developer

Mock Exam 2 — Tasks 1–10

17 min read Chapter 83 of 87
Summary

Ten exam tasks at elevated difficulty: namespace ResourceQuotas,...

Ten exam tasks at elevated difficulty: namespace ResourceQuotas, Pod SecurityContext lockdown, debugging ImagePullBackOff and probe failures, canary deployment patterns, CronJob-to-Job extraction, combined Secret and ConfigMap consumption, deny-all NetworkPolicy with namespace-scoped exceptions, StatefulSet with headless Service and volumeClaimTemplates, Service port debugging, and Helm install-upgrade-rollback workflow.

Mock Exam 2 — Tasks 1–10

Start your timer. You have 120 minutes to complete all 20 tasks across both sections. Each task specifies the cluster context and namespace. Switch contexts when instructed and create namespaces before deploying resources.

Before you begin:

  • Open the Kubernetes documentation (kubernetes.io/docs) in a browser tab. Bookmark the reference pages for Pod, Deployment, Service, Ingress, NetworkPolicy, StatefulSet, Job, CronJob, and ResourceQuota. Having these pages ready saves 30–60 seconds per task compared to searching from scratch.
  • Set your default editor to something fast. In the exam environment, vi or nano are available. Run export KUBE_EDITOR=nano if you prefer a simpler editor, or keep vi if you are comfortable with modal editing.
  • Use kubectl explain <resource>.<field> as your primary reference tool. It is faster than switching to a browser for field names and nesting structure. For example, kubectl explain pod.spec.securityContext gives you every available field with descriptions.
  • Set up shell aliases if the exam environment allows it: alias k=kubectl, alias kgp='kubectl get pods', alias kns='kubectl config set-context --current --namespace'. These compound aliases save significant keystrokes across 20 tasks.

Task 1 — Namespace with ResourceQuota (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: Create namespace quota-lab

Requirements

  1. Create a namespace called quota-lab.

  2. Create a ResourceQuota named pod-memory-quota in namespace quota-lab with the following constraints:

    • Maximum number of Pods: 2
    • Maximum total memory requests across all Pods: 1Gi
    • Maximum total memory limits across all Pods: 1Gi
  3. Verify the quota is active by running:

    kubectl describe resourcequota pod-memory-quota -n quota-lab

    The output must show the hard limits for pods, requests.memory, and limits.memory.

  4. Test the quota by creating three Pods with memory: 256Mi request and limit each. The third Pod must be rejected.

Exam strategy: This task combines namespace creation with a ResourceQuota, which has no imperative shortcut — you must write YAML. Use kubectl explain resourcequota.spec.hard to find the exact field names. The verification step (creating three Pods) is critical — do not skip it, as the grading environment may check whether quota enforcement is active.

Expected time: 4 minutes. Spend no more than 2 minutes writing the ResourceQuota YAML, 1 minute applying and verifying, and 1 minute on the test Pods.

Documentation reference: Search for “Resource Quotas” on kubernetes.io/docs. The Concepts → Policy → ResourceQuotas page lists all supported resource names you can constrain.


Task 2 — Pod with Strict SecurityContext (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: secure-app

Requirements

  1. Create namespace secure-app.

  2. Create a Pod named locked-pod in namespace secure-app with these specifications:

    • Image: nginx:1.25
    • The Pod-level security context must set runAsNonRoot: true
    • The container-level security context must set:
      • readOnlyRootFilesystem: true
      • allowPrivilegeEscalation: false
      • Drop ALL capabilities
    • runAsUser: 1000
    • runAsGroup: 3000
  3. The Pod will crash because nginx needs to write to certain directories. Add two emptyDir volume mounts to make it work:

    • Mount an emptyDir at /var/cache/nginx
    • Mount an emptyDir at /var/run
  4. The Pod must reach Running status.

Exam strategy: Security context tasks require you to know where each field goes — Pod-level vs container-level. The most reliable approach is to draft the YAML first with all security fields, apply it, watch it crash, then add the emptyDir volumes to fix the crash. Do not try to get it right in one shot; applying, observing failure logs (kubectl logs locked-pod -n secure-app), and iterating is faster than trying to memorize every nginx directory that needs write access.

Expected time: 5 minutes. The security context YAML takes 2–3 minutes to write correctly. Debugging the volume mounts takes 2 minutes.

Key fields to remember:

  • spec.securityContext (Pod-level): runAsNonRoot, runAsUser, runAsGroup
  • spec.containers[].securityContext (container-level): readOnlyRootFilesystem, allowPrivilegeEscalation, capabilities.drop

Use kubectl explain pod.spec.securityContext and kubectl explain pod.spec.containers.securityContext to confirm the field placement during the exam.


Task 3 — Fix Broken Deployment (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: debug-deploy

Requirements

  1. Create namespace debug-deploy.

  2. Apply the following Deployment manifest:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: broken-web
      namespace: debug-deploy
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: broken-web
      template:
        metadata:
          labels:
            app: broken-web
        spec:
          containers:
          - name: web
            image: ngnix:1.25
            ports:
            - containerPort: 80
            readinessProbe:
              httpGet:
                path: /healthz
                port: 8080
              initialDelaySeconds: 5
              periodSeconds: 5
  3. The Deployment has two errors. Identify and fix both:

    • Error 1: The image name is misspelled, causing ImagePullBackOff.
    • Error 2: The readiness probe targets port 8080, but nginx listens on port 80. Additionally, nginx does not serve a /healthz endpoint — change the path to /.
  4. After applying fixes, all 2 replicas must be Running and Ready.

Exam strategy: Debugging tasks require a systematic approach. Follow this sequence every time:

  1. kubectl get pods -n <namespace> — check the STATUS column for clues (ImagePullBackOff, CrashLoopBackOff, Running but not Ready).
  2. kubectl describe pod <pod-name> -n <namespace> — read the Events section at the bottom for scheduling errors, pull failures, or probe failures.
  3. kubectl logs <pod-name> -n <namespace> — check container stdout/stderr for application-level errors.

Applying fixes in order of severity (image pull first, then probe configuration) avoids masking one error with another.

Expected time: 4 minutes. Diagnosis takes 1–2 minutes, and applying fixes (image name correction + probe adjustment) takes 2 minutes. Use kubectl set image for the image fix — it is the fastest imperative option.

Watch out for: The readiness probe path /healthz is a common Kubernetes convention, but nginx does not have this endpoint by default. The correct path for a basic nginx readiness check is / (which returns 200 on the default welcome page). Some candidates change the path to /index.html, which also works but is less idiomatic.


Task 4 — Canary Deployment (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: canary-ns

Requirements

  1. Create namespace canary-ns.

  2. Create a Deployment named web-stable in namespace canary-ns:

    • Image: nginx:1.24
    • Replicas: 4
    • Labels on the Pod template: app: web, track: stable
  3. Create a Service named web-svc in namespace canary-ns:

    • Type: ClusterIP
    • Selector: app: web (note: only the app label, not track)
    • Port: 80, targetPort: 80
  4. Create a second Deployment named web-canary in the same namespace:

    • Image: nginx:1.25
    • Replicas: 1
    • Labels on the Pod template: app: web, track: canary
  5. Verify the Service endpoints include all 5 Pods (4 stable + 1 canary):

    kubectl get endpoints web-svc -n canary-ns

    The endpoint list must contain 5 IP addresses.

Exam strategy: The canary pattern is a deployment strategy question. The core concept: a Service that selects on a shared label (app: web) routes traffic to all matching Pods across multiple Deployments. The track label differentiates stable from canary but is not in the Service selector. If you include track in the Service selector, only one set of Pods receives traffic — defeating the entire purpose of a canary deployment.

The traffic split ratio is determined by the replica counts: 4 stable + 1 canary = 80% stable, 20% canary. This is a coarse-grained approach compared to service mesh-based canary deployments, but it is the method testable within the CKAD exam scope.

Expected time: 5 minutes. Create both Deployments (2 minutes), create the Service with careful selector choice (1 minute), verify endpoints (1 minute), and review labels (1 minute).

Documentation reference: The Kubernetes documentation does not have a dedicated “canary deployment” page. The relevant concept is covered under “Managing Deployments” and “Labels and Selectors”. Practice this pattern from memory.


Task 5 — CronJob and Job Extraction (5%)

Context: kubectl config use-context ckad-cluster2

Namespace: batch-ns

Requirements

  1. Create namespace batch-ns.

  2. Create a CronJob named log-timestamp in namespace batch-ns:

    • Schedule: */5 * * * * (every 5 minutes)
    • Image: busybox:1.36
    • Command: sh -c "echo Timestamp: $(date) && sleep 5"
    • restartPolicy: Never
    • successfulJobsHistoryLimit: 3
    • failedJobsHistoryLimit: 1
  3. Without waiting for the CronJob to trigger, manually create a Job from it:

    kubectl create job log-now --from=cronjob/log-timestamp -n batch-ns
  4. Wait for the Job log-now to complete. Verify its logs contain the timestamp output:

    kubectl logs job/log-now -n batch-ns
  5. Confirm the CronJob still exists and is scheduled:

    kubectl get cronjob log-timestamp -n batch-ns

Exam strategy: The kubectl create job --from=cronjob/<name> command is a powerful shortcut that many candidates do not know exists. It creates a one-off Job using the CronJob’s exact template — identical container spec, volumes, restart policy — without waiting for the scheduled trigger. Memorize this command: it appears frequently in CKAD exams and takes only seconds to execute.

For the CronJob creation itself, the imperative command kubectl create cronjob supports --schedule, --image, and the container command. However, it does not support successfulJobsHistoryLimit or failedJobsHistoryLimit — those require a YAML manifest or a post-creation kubectl patch.

Expected time: 4 minutes. CronJob YAML (2 minutes), create job --from (30 seconds), verification (1.5 minutes).

Cron schedule syntax refresher: */5 * * * * means “every 5 minutes”. The five fields are: minute, hour, day-of-month, month, day-of-week. Use kubectl explain cronjob.spec.schedule to confirm this during the exam.


Task 6 — Pod with Secret (env) and ConfigMap (volume) (5%)

Context: kubectl config use-context ckad-cluster2

Namespace: config-ns

Requirements

  1. Create namespace config-ns.

  2. Create a Secret named db-credentials in namespace config-ns with the following key-value pairs:

    • DB_USER=admin
    • DB_PASS=exam-secret-2026
  3. Create a ConfigMap named app-config in namespace config-ns with a file-like key:

    • Key: app.properties
    • Value:
      log.level=INFO
      cache.ttl=300
      feature.flag=true
  4. Create a Pod named config-consumer in namespace config-ns:

    • Image: busybox:1.36
    • Command: sh -c "echo DB_USER=$DB_USER DB_PASS=$DB_PASS && cat /config/app.properties && sleep 3600"
    • Environment variables DB_USER and DB_PASS injected from Secret db-credentials using secretKeyRef
    • The ConfigMap app-config mounted as a volume at /config
  5. Verify the Pod is running and the environment variables and mounted file are correct:

    kubectl exec config-consumer -n config-ns -- env | grep DB_
    kubectl exec config-consumer -n config-ns -- cat /config/app.properties

Exam strategy: This task tests two separate injection mechanisms in a single Pod. The critical distinction is:

  • Secret → environment variable via valueFrom.secretKeyRef on individual keys. This injects specific key-value pairs as env vars.
  • ConfigMap → volume via volumes[].configMap and volumeMounts. This mounts all keys in the ConfigMap as files inside the container.

The common mistake is to use envFrom for both (which works but does not match the requirement) or to mount the Secret as a volume (which also works but is not what was asked). Read the task carefully: it specifies secretKeyRef for the Secret and volume mount for the ConfigMap.

Expected time: 5 minutes. Create the Secret imperatively (30 seconds), create the ConfigMap using --from-file or --from-literal (1 minute), write the Pod YAML with both env and volume blocks (2.5 minutes), verify (1 minute).

Speed optimization: Create the Secret and ConfigMap imperatively before writing any YAML. Then write the Pod manifest with references to both. This is faster than writing a single large YAML that includes all three resources.


Task 7 — NetworkPolicy: Deny All Then Allow by Namespace (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: netpol-ns

Requirements

  1. Create two namespaces:

    • netpol-ns
    • trusted-ns
  2. Label the trusted-ns namespace:

    kubectl label namespace trusted-ns purpose=trusted
  3. Create a Pod named web-server in namespace netpol-ns:

    • Image: nginx:1.25
    • Labels: app: web-server
  4. Create a deny-all ingress NetworkPolicy named deny-all-ingress in namespace netpol-ns:

    • Apply to all Pods in the namespace (use podSelector: {})
    • Define policyTypes: ["Ingress"]
    • Provide an empty ingress: [] list (no rules = deny all)
  5. Create a second NetworkPolicy named allow-from-trusted in namespace netpol-ns:

    • Apply to Pods with label app: web-server
    • Allow ingress traffic from Pods in namespaces labeled purpose: trusted
    • Allow traffic on port 80 (TCP)
  6. To verify (conceptually — actual traffic tests depend on CNI support):

    • A Pod in trusted-ns should reach web-server on port 80
    • A Pod in any other namespace should be blocked

    If your cluster CNI supports NetworkPolicy enforcement (Calico, Cilium, Weave Net — but not Flannel by default), you can run an actual traffic test:

    # From trusted-ns — should succeed
    kubectl run test-trusted --image=curlimages/curl --rm -it --restart=Never -n trusted-ns -- curl --max-time 5 http://<web-server-pod-ip>:80
    
    # From default namespace — should time out
    kubectl run test-default --image=curlimages/curl --rm -it --restart=Never -- curl --max-time 5 http://<web-server-pod-ip>:80

Exam strategy: NetworkPolicy tasks have two traps: understanding the deny-all baseline and the additive allow rule. NetworkPolicies are additive — if any policy allows traffic, it passes. The deny-all policy works by selecting all Pods (podSelector: {}) and providing zero ingress rules. The allow-from-trusted policy then adds an exception for a specific namespace.

The namespaceSelector field in the allow rule matches namespaces by their labels — not by name. You must label the source namespace first, then reference that label in the NetworkPolicy. Forgetting to label the namespace is the most common failure mode.

Expected time: 5 minutes. Namespace creation and labeling (1 minute), Pod creation (30 seconds), both NetworkPolicies (2.5 minutes), verification (1 minute).

Documentation reference: Search for “Network Policies” on kubernetes.io/docs. The Concepts → Services, Load Balancing, and Networking → Network Policies page has example deny-all and allow-by-namespace policies you can adapt.


Task 8 — StatefulSet with Headless Service and volumeClaimTemplates (5%)

Context: kubectl config use-context ckad-cluster2

Namespace: stateful-ns

Requirements

  1. Create namespace stateful-ns.

  2. Create a headless Service named db-headless in namespace stateful-ns:

    • clusterIP: None
    • Selector: app: db
    • Port: 5432, targetPort: 5432
  3. Create a StatefulSet named db in namespace stateful-ns:

    • Replicas: 3
    • serviceName: db-headless
    • Selector and Pod template labels: app: db
    • Container:
      • Name: postgres
      • Image: postgres:16
      • Port: 5432
      • Environment variable: POSTGRES_PASSWORD=exam-pass
    • volumeClaimTemplates:
      • Name: data
      • accessModes: ["ReadWriteOnce"]
      • Storage request: 1Gi
      • Mount path: /var/lib/postgresql/data
  4. Wait for all 3 Pods to reach Running status. They should be named db-0, db-1, db-2.

  5. Verify the headless DNS entries exist:

    kubectl exec db-0 -n stateful-ns -- nslookup db-headless.stateful-ns.svc.cluster.local
  6. Verify PVCs were created:

    kubectl get pvc -n stateful-ns

    Three PVCs named data-db-0, data-db-1, data-db-2 must exist.

Exam strategy: StatefulSet tasks have the most YAML of any resource type because they combine a Deployment-like workload spec with volumeClaimTemplates and require a headless Service to function correctly. Write the headless Service first — it must exist before the StatefulSet is applied, otherwise DNS resolution for individual Pods does not work.

Key differences from a Deployment:

  • serviceName field is required and must match the headless Service name.
  • Pods are created sequentially (db-0, then db-1, then db-2). Each Pod must reach Running before the next one starts.
  • PVCs are named <volumeClaimTemplate-name>-<statefulset-name>-<ordinal> — in this case, data-db-0, data-db-1, data-db-2.
  • Deleting a StatefulSet does not delete its PVCs. This is by design to prevent data loss.

The headless Service must have clusterIP: None. Any other value creates a regular ClusterIP Service, which breaks the StatefulSet’s per-Pod DNS records (db-0.db-headless.stateful-ns.svc.cluster.local).

Expected time: 6 minutes. Headless Service YAML (1 minute), StatefulSet YAML (3 minutes), waiting for Pods to start (1 minute), verification of PVCs and DNS (1 minute).

Watch out for: The postgres image requires the POSTGRES_PASSWORD environment variable. Without it, the container exits immediately with an error. This is not a Kubernetes issue — it is a postgres Docker image requirement.


Task 9 — Debug: Pod Returns 503 (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: debug-svc

Requirements

  1. Create namespace debug-svc.

  2. Apply the following resources:

    apiVersion: v1
    kind: Pod
    metadata:
      name: api-server
      namespace: debug-svc
      labels:
        app: api-server
    spec:
      containers:
      - name: api
        image: hashicorp/http-echo:0.2.3
        args:
        - "-listen=:5678"
        - "-text=healthy"
        ports:
        - containerPort: 5678
        readinessProbe:
          httpGet:
            path: /ready
            port: 5678
          initialDelaySeconds: 2
          periodSeconds: 3
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: api-svc
      namespace: debug-svc
    spec:
      selector:
        app: api-server
      ports:
      - port: 80
        targetPort: 8080
  3. The Pod runs but the Service returns no response. There are two problems:

    • Problem 1: The readiness probe hits /ready, but http-echo responds with 200 on any path. However, the probe is fine — the real issue is the Pod never becomes an endpoint. Check the Service targetPort: it points to 8080, but the container listens on 5678. Fix the Service targetPort to 5678.

    • Problem 2: Verify the fix by confirming the Pod appears in the Service endpoints:

      kubectl get endpoints api-svc -n debug-svc
  4. Test the Service:

    kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -n debug-svc -- curl http://api-svc.debug-svc.svc.cluster.local

    Expected output: healthy

Exam strategy: Debugging Service connectivity is a three-step process:

  1. Check the Pod status. Is it Running and Ready? If not, fix the Pod first.
  2. Check the Service endpoints. Run kubectl get endpoints <service-name> -n <namespace>. If the endpoint list is empty, the Service selector does not match any Ready Pods, or the targetPort is wrong.
  3. Check the port mapping. The Service targetPort must match the port the container is actually listening on. In this task, the container listens on 5678 but the Service targets 8080. This mismatch means the Service forwards traffic to a port that nothing is listening on — resulting in connection refused or timeout errors.

The http-echo image is particularly useful for debugging because it responds to any HTTP path with a 200 status code and the configured text. This means the readiness probe at /ready works fine (it returns 200). The problem is purely a Service port mismatch, not a Pod readiness issue.

Expected time: 4 minutes. Diagnosis (2 minutes), fix (1 minute), verification (1 minute).


Task 10 — Helm: Install, Upgrade, Rollback (5%)

Context: kubectl config use-context ckad-cluster2

Namespace: helm-ns

Requirements

  1. Create namespace helm-ns.

  2. Add the Bitnami Helm repository (if not already added):

    helm repo add bitnami https://charts.bitnami.com/bitnami
    helm repo update
  3. Install a Helm release named my-nginx in namespace helm-ns using the bitnami/nginx chart:

    helm install my-nginx bitnami/nginx -n helm-ns --set replicaCount=1
  4. Verify the release is deployed:

    helm list -n helm-ns

    Status must show deployed.

  5. Upgrade the release to set replicaCount=3:

    helm upgrade my-nginx bitnami/nginx -n helm-ns --set replicaCount=3
  6. Verify the upgrade by checking that the Deployment now has 3 replicas:

    kubectl get deployment -n helm-ns
  7. View the release history:

    helm history my-nginx -n helm-ns

    Two revisions must appear.

  8. Roll back to revision 1:

    helm rollback my-nginx 1 -n helm-ns
  9. Confirm rollback: the Deployment must return to 1 replica, and helm history must show 3 revisions (original, upgrade, rollback).

Exam strategy: Helm tasks follow a predictable pattern: add repo, install, upgrade, rollback. The commands are short and imperative — no YAML needed. The main risk is forgetting the -n <namespace> flag, which causes Helm to operate in the default namespace.

Key commands to memorize:

  • helm repo add <name> <url> — adds a chart repository
  • helm repo update — refreshes the local index of all repositories
  • helm install <release> <chart> -n <ns> — installs a release
  • helm upgrade <release> <chart> -n <ns> --set key=value — upgrades with new values
  • helm rollback <release> <revision> -n <ns> — rolls back to a specific revision
  • helm history <release> -n <ns> — shows revision history
  • helm list -n <ns> — shows installed releases in a namespace

A rollback creates a new revision, not a revert. After installing (rev 1), upgrading (rev 2), and rolling back (rev 3), helm history shows three entries. The current state matches revision 1’s configuration but carries revision number 3.

Expected time: 5 minutes. Repo setup (1 minute), install and verify (1 minute), upgrade and verify (1 minute), rollback and verify (2 minutes).

Documentation reference: The Helm documentation at helm.sh/docs covers all commands. In the exam, this site is an allowed reference. Bookmark the “Helm Commands” quickstart page.


End of Section 1. Proceed to Section 2 (Tasks 11–20). Do not stop your timer.