Skip to main content
mastering ckad certified kubernetes application developer

Mock Exam 2 — Tasks 11–20

16 min read Chapter 84 of 87
Summary

Ten exam tasks continuing the advanced mock exam:...

Ten exam tasks continuing the advanced mock exam: multi-container Pod with init container, DaemonSet with control-plane tolerations, TLS-terminated Ingress with manually created Secret, ephemeral volumes with resource limits and OOMKilled observation, ServiceAccount with Role binding, Deployment-to-Ingress exposure chain, diagnosing Pending Pods from nodeSelector mismatch, Job failure handling with backoffLimit, blue-green deployment switchover, and a comprehensive aggregate task building a full application stack.

Mock Exam 2 — Tasks 11–20

Continue from Section 1. Your timer should still be running. These tasks increase in complexity, with the final task requiring you to assemble a full application stack from scratch.

Pacing check: If you are past the 60-minute mark and have not finished Section 1, skip remaining Section 1 tasks and start here. Tasks 17 and 20 carry the same weight as Task 1 — do not let sunk time on a difficult earlier task prevent you from collecting points on tasks you can finish quickly. Return to skipped tasks if time permits.


Task 11 — Multi-Container Pod with Init Container (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: init-ns

Requirements

  1. Create namespace init-ns.

  2. Create a Pod named app-with-init in namespace init-ns with the following specification:

    • Init container:

      • Name: config-downloader
      • Image: busybox:1.36
      • Command: sh -c "echo 'server.port=8080' > /shared/config.txt && echo 'Config downloaded'"
      • Volume mount: mount a shared volume at /shared
    • Main container:

      • Name: app
      • Image: busybox:1.36
      • Command: sh -c "cat /config/config.txt && sleep 3600"
      • Volume mount: mount the same shared volume at /config
    • Volume:

      • Name: shared-data
      • Type: emptyDir
  3. Wait for the Pod to reach Running status. The init container must complete before the main container starts.

  4. Verify the config file was passed from init container to main container:

    kubectl logs app-with-init -c app -n init-ns

    Expected output must include server.port=8080.

  5. Confirm the init container completed:

    kubectl get pod app-with-init -n init-ns -o jsonpath='{.status.initContainerStatuses[0].state}'

Exam strategy: Init containers run to completion before any regular containers start. If the init container fails, the main container never launches — the Pod stays in Init:Error or Init:CrashLoopBackOff status. When debugging multi-container Pods, always check init container logs first: kubectl logs <pod> -c <init-container-name>.

The volume sharing pattern here is the foundation of the sidecar/init pattern: one container prepares data, another consumes it. The emptyDir volume is shared between all containers in the same Pod and is created when the Pod is assigned to a node.

Expected time: 4 minutes. Writing the YAML with both containers and the shared volume is the most time-consuming part. Use kubectl run --dry-run=client -o yaml to generate the Pod scaffold, then add the init container manually.


Task 12 — DaemonSet with Control-Plane Tolerations (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: daemon-ns

Requirements

  1. Create namespace daemon-ns.

  2. Create a DaemonSet named log-agent in namespace daemon-ns:

    • Image: busybox:1.36
    • Command: sh -c "while true; do echo 'Collecting logs from $(hostname)'; sleep 60; done"
    • Labels on Pod template: app: log-agent
  3. Add tolerations so the DaemonSet schedules on all nodes, including control-plane nodes:

    tolerations:
    - key: "node-role.kubernetes.io/control-plane"
      operator: "Exists"
      effect: "NoSchedule"
  4. Verify the DaemonSet has Pods scheduled on every node:

    kubectl get daemonset log-agent -n daemon-ns

    The DESIRED count must equal the total number of nodes in the cluster, and the READY count must match.

  5. Check that at least one Pod is running on a control-plane node:

    kubectl get pods -n daemon-ns -o wide

Exam strategy: There is no kubectl create daemonset imperative command. The fastest approach is to generate a Deployment YAML with kubectl create deployment --dry-run=client -o yaml, change the kind to DaemonSet, remove the replicas and strategy fields, and add the tolerations block. This conversion takes under 60 seconds once you have practiced it.

Tolerations must match the taint key, operator, and effect. The Exists operator matches any value for the given key, making it the safest choice when you do not know the exact taint value. The NoSchedule effect means the taint prevents scheduling but does not evict already-running Pods.

To verify that the DaemonSet is running on control-plane nodes, compare the NODE column in kubectl get pods -o wide against kubectl get nodes and identify which nodes have the control-plane role.

Expected time: 4 minutes.


Task 13 — Ingress with TLS (5%)

Context: kubectl config use-context ckad-cluster2

Namespace: tls-ns

Requirements

  1. Create namespace tls-ns.

  2. Generate a self-signed TLS certificate (for exam simulation purposes):

    openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
      -keyout tls.key -out tls.crt \
      -subj "/CN=secure-app.example.com"
  3. Create a Kubernetes Secret of type kubernetes.io/tls named tls-secret in namespace tls-ns:

    kubectl create secret tls tls-secret --cert=tls.crt --key=tls.key -n tls-ns
  4. Create a Deployment named secure-app in namespace tls-ns:

    • Image: nginx:1.25
    • Replicas: 2
    • Labels: app: secure-app
  5. Create a ClusterIP Service named secure-app-svc in namespace tls-ns:

    • Selector: app: secure-app
    • Port: 80, targetPort: 80
  6. Create an Ingress named secure-app-ingress in namespace tls-ns:

    • Host: secure-app.example.com
    • TLS section referencing Secret tls-secret for host secure-app.example.com
    • Rule: route traffic for host secure-app.example.com, path /, pathType Prefix, to Service secure-app-svc on port 80
    • ingressClassName: nginx
  7. Verify the Ingress is created with TLS configured:

    kubectl describe ingress secure-app-ingress -n tls-ns

    The TLS section must show secure-app.example.com with Secret tls-secret.

Exam strategy: TLS Ingress tasks have four distinct steps: generate a certificate, create a TLS Secret, create the backend (Deployment + Service), and wire up the Ingress. The openssl command and kubectl create secret tls commands are both provided in the task — you do not need to memorize them, but you must type them accurately.

The Ingress manifest requires a tls section that lists the hosts and the Secret name. The tls[].hosts must exactly match the rules[].host — a mismatch means TLS termination does not apply to the hostname. Use kubectl explain ingress.spec.tls during the exam to confirm the field structure.

The Deployment and Service are standard — use imperative commands (kubectl create deployment, kubectl expose) to save time and focus your YAML writing on the Ingress, which lacks an imperative equivalent.

Expected time: 5 minutes.


Task 14 — Pod with Ephemeral Volume and OOMKilled Behavior (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: oom-ns

Requirements

  1. Create namespace oom-ns.

  2. Create a Pod named memory-hog in namespace oom-ns:

    • Image: polinux/stress
    • Command: stress --vm 1 --vm-bytes 128M --vm-hang 0
    • Resource requests: memory: 64Mi, cpu: 100m
    • Resource limits: memory: 100Mi, cpu: 200m
    • Mount an emptyDir volume named tmp-data at /tmp/data
  3. Observe the Pod status. Because the stress command allocates 128Mi but the memory limit is 100Mi, the container must be OOMKilled.

  4. Verify the OOMKilled status:

    kubectl get pod memory-hog -n oom-ns -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'

    Expected output: OOMKilled

  5. Fix the Pod by creating a new Pod named memory-ok with the same specification but increase the memory limit to 256Mi and the memory request to 128Mi. This Pod must reach and stay in Running status.

Exam strategy: The OOMKilled state is triggered by the Linux kernel’s Out-Of-Memory killer when a process exceeds the cgroup memory limit set by Kubernetes. The key is understanding that the memory limit is an enforced ceiling — the container is killed if it exceeds this value. The memory request is a scheduling guarantee — it ensures the node has that much memory available when placing the Pod.

To verify OOMKilled, check lastState.terminated.reason (not state, because the Pod may have restarted by the time you look). The status.containerStatuses[0].restartCount field shows how many times the container has been OOMKilled and restarted.

The fix is to create a new Pod (not edit the existing one) because Pods are immutable for resource limits. You cannot change limits on a running Pod — you must delete and recreate it (or create a new one with a different name as the task specifies).

Expected time: 4 minutes.


Task 15 — ServiceAccount with Role Binding (5%)

Context: kubectl config use-context ckad-cluster2

Namespace: rbac-ns

Requirements

  1. Create namespace rbac-ns.

  2. Create a ServiceAccount named pod-reader-sa in namespace rbac-ns.

  3. Create a Role named pod-reader-role in namespace rbac-ns with the following permissions:

    • API group: "" (core)
    • Resources: pods
    • Verbs: get, list, watch
  4. Create a RoleBinding named pod-reader-binding in namespace rbac-ns:

    • Bind Role pod-reader-role to ServiceAccount pod-reader-sa
  5. Create a Pod named reader-pod in namespace rbac-ns:

    • Image: bitnami/kubectl:latest
    • Command: sh -c "kubectl get pods -n rbac-ns && sleep 3600"
    • serviceAccountName: pod-reader-sa
  6. Verify the Pod can list Pods in namespace rbac-ns:

    kubectl logs reader-pod -n rbac-ns

    The output must list Pods (at minimum, the reader-pod itself).

  7. Verify the ServiceAccount cannot list Pods in other namespaces:

    kubectl exec reader-pod -n rbac-ns -- kubectl get pods -n default

    This command must return a Forbidden error.

Exam strategy: RBAC tasks are fast when you use imperative commands: kubectl create sa, kubectl create role, kubectl create rolebinding — three commands in sequence, under 30 seconds total. The YAML equivalent is longer and more error-prone because RoleBinding subjects require the ServiceAccount’s namespace in the subjects[].namespace field.

The distinction between Role/RoleBinding (namespace-scoped) and ClusterRole/ClusterRoleBinding (cluster-scoped) is critical. This task requires namespace-scoped access only. Using a ClusterRoleBinding would grant cross-namespace access, which violates step 7.

The bitnami/kubectl image includes kubectl inside the container, making it ideal for testing RBAC from inside a Pod. The Pod must specify serviceAccountName to use the custom ServiceAccount instead of the default one.

Expected time: 5 minutes.


Task 16 — Deployment → Service → Ingress Chain (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: app-stack

Requirements

  1. Create namespace app-stack.

  2. Create a Deployment named frontend in namespace app-stack:

    • Image: nginx:1.25
    • Replicas: 3
    • Labels on Pod template: app: frontend
  3. Expose the Deployment with a ClusterIP Service named frontend-svc:

    • Port: 80, targetPort: 80
    • Selector: app: frontend
  4. Verify the Service has 3 endpoints:

    kubectl get endpoints frontend-svc -n app-stack
  5. Create an Ingress named frontend-ingress in namespace app-stack:

    • ingressClassName: nginx
    • Annotation: nginx.ingress.kubernetes.io/rewrite-target: /
    • Rule for host frontend.example.com:
      • Path: /app
      • PathType: Prefix
      • Backend: Service frontend-svc, port 80
  6. Verify the Ingress:

    kubectl describe ingress frontend-ingress -n app-stack

    The rules section must show the path /app pointing to frontend-svc:80.

Exam strategy: The three-resource chain (Deployment → Service → Ingress) is a common exam pattern. Use imperative commands for the Deployment and Service, then write YAML only for the Ingress. The rewrite-target annotation is essential when the Ingress path (/app) differs from the backend path (/). Without it, nginx receives requests at /app and returns 404 because the backend application serves content at /.

The kubectl expose deployment command creates the Service with a selector that matches the Deployment’s Pod labels. Verify the generated selector matches your expectations with kubectl get service frontend-svc -n app-stack -o yaml.

Expected time: 4 minutes.


Task 17 — Fix: Pod Stuck Pending (nodeSelector Mismatch) (5%)

Context: kubectl config use-context ckad-cluster2

Namespace: schedule-ns

Requirements

  1. Create namespace schedule-ns.

  2. Apply the following Pod manifest:

    apiVersion: v1
    kind: Pod
    metadata:
      name: picky-pod
      namespace: schedule-ns
    spec:
      containers:
      - name: app
        image: nginx:1.25
      nodeSelector:
        disktype: ssd
  3. The Pod stays in Pending status. Diagnose the problem:

    kubectl describe pod picky-pod -n schedule-ns

    The Events section shows the scheduler cannot find a node matching disktype=ssd.

  4. Fix the issue by labeling a node with the required label:

    kubectl get nodes --show-labels

    Pick any worker node and apply the label:

    kubectl label node <node-name> disktype=ssd
  5. Wait for the Pod to be scheduled and reach Running status:

    kubectl get pod picky-pod -n schedule-ns -w
  6. Clean up the label after the Pod is running (optional but good practice):

    kubectl label node <node-name> disktype-

Exam strategy: Pending Pod diagnosis is one of the fastest tasks on the exam if you know where to look. Run kubectl describe pod <name> and read the Events section. The scheduler event message tells you exactly what constraint failed. In this case, it reports that no nodes match the nodeSelector. The fix is to label a node, not to modify the Pod (which would require deletion and recreation because nodeSelector is immutable).

After labeling the node, the scheduler re-evaluates pending Pods automatically. You do not need to restart anything. Watch the Pod with kubectl get pod -w to see it transition from Pending to Running.

Node label cleanup (kubectl label node <name> disktype-) removes the label without affecting the running Pod. nodeSelector is evaluated only at scheduling time.

Expected time: 3 minutes.


Task 18 — Job with backoffLimit and Failure Handling (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: job-ns

Requirements

  1. Create namespace job-ns.

  2. Create a Job named failing-job in namespace job-ns:

    apiVersion: batch/v1
    kind: Job
    metadata:
      name: failing-job
      namespace: job-ns
    spec:
      backoffLimit: 3
      template:
        spec:
          containers:
          - name: worker
            image: busybox:1.36
            command: ["sh", "-c", "echo 'Attempting task...' && exit 1"]
          restartPolicy: Never
  3. Watch the Job create Pods that fail repeatedly:

    kubectl get pods -n job-ns -w

    After 4 total attempts (1 initial + 3 retries), the Job must stop creating new Pods.

  4. Check the Job status:

    kubectl describe job failing-job -n job-ns

    The Job condition must show Failed with reason BackoffLimitExceeded.

  5. Delete the failed Job and create a fixed version named success-job in namespace job-ns:

    • Same specification but change the command to: sh -c "echo 'Task completed' && exit 0"
    • backoffLimit: 3
  6. Verify the success-job completes:

    kubectl get job success-job -n job-ns

    The COMPLETIONS column must show 1/1.

Exam strategy: Job failure behavior is controlled by two fields: backoffLimit (how many retries before the Job is marked failed) and restartPolicy (whether to restart in-place or create new Pods). With restartPolicy: Never, each failure creates a new Pod. With restartPolicy: OnFailure, the same Pod restarts in-place.

backoffLimit: 3 means 3 retries after the initial attempt, for a total of 4 Pod creations. The naming is counterintuitive — it limits the number of retries, not the total attempts.

Watching the failure sequence with kubectl get pods -w teaches you the exponential back-off behavior: each retry waits longer than the previous one. The Job controller uses 10s, 20s, 40s, etc. delays between Pod creations.

Expected time: 4 minutes.


Task 19 — Blue-Green Deployment (5%)

Context: kubectl config use-context ckad-cluster2

Namespace: bg-ns

Requirements

  1. Create namespace bg-ns.

  2. Create the blue Deployment named app-blue in namespace bg-ns:

    • Image: hashicorp/http-echo:0.2.3
    • Args: ["-listen=:5678", "-text=blue"]
    • Replicas: 3
    • Labels on Pod template: app: webapp, version: blue
  3. Create a Service named webapp-svc in namespace bg-ns:

    • Type: ClusterIP
    • Selector: app: webapp, version: blue
    • Port: 80, targetPort: 5678
  4. Verify the Service returns “blue”:

    kubectl run test-blue --image=curlimages/curl --rm -it --restart=Never -n bg-ns -- curl http://webapp-svc.bg-ns.svc.cluster.local

    Expected output: blue

  5. Create the green Deployment named app-green in namespace bg-ns:

    • Image: hashicorp/http-echo:0.2.3
    • Args: ["-listen=:5678", "-text=green"]
    • Replicas: 3
    • Labels on Pod template: app: webapp, version: green
  6. Wait for all green Pods to be Running and Ready.

  7. Switch the Service to route traffic to the green Deployment by patching the selector:

    kubectl patch service webapp-svc -n bg-ns -p '{"spec":{"selector":{"version":"green"}}}'
  8. Verify the Service now returns “green”:

    kubectl run test-green --image=curlimages/curl --rm -it --restart=Never -n bg-ns -- curl http://webapp-svc.bg-ns.svc.cluster.local

    Expected output: green

  9. Both Deployments must remain running. The blue Deployment is kept as a rollback target.

Exam strategy: Blue-green deployment is a zero-downtime release strategy. Unlike canary (which splits traffic), blue-green runs both versions simultaneously but routes all traffic to one version at a time. The switchover is instantaneous because it changes the Service selector — no Pod restarts needed.

The key difference from canary: in canary, the Service selector matches a common label shared by both Deployments (like app: web). In blue-green, the Service selector includes the version label (version: blue or version: green) so that traffic goes entirely to one Deployment. The switch is a single kubectl patch command.

Both Deployments must remain running after the switch. The blue Deployment serves as an instant rollback target — if the green version has issues, patch the Service selector back to version: blue.

Expected time: 5 minutes.


Task 20 — Full-Stack Aggregate Task (5%)

Context: kubectl config use-context ckad-cluster1

Namespace: fullstack-ns

Requirements

Build a complete application stack from scratch. Every resource must be in namespace fullstack-ns.

  1. Namespace and Quota:

    • Create namespace fullstack-ns
    • Create a ResourceQuota named stack-quota with: max 10 Pods, requests.cpu: 2, requests.memory: 2Gi
  2. ConfigMap:

    • Name: app-settings
    • Data:
      APP_ENV=production
      LOG_LEVEL=warn
      MAX_CONNECTIONS=100
  3. Secret:

    • Name: app-secrets
    • Data: API_KEY=super-secret-key-2026
  4. Deployment:

    • Name: fullstack-app
    • Image: nginx:1.25
    • Replicas: 3
    • Resource requests per container: cpu: 100m, memory: 128Mi
    • Resource limits per container: cpu: 200m, memory: 256Mi
    • Inject all keys from ConfigMap app-settings as environment variables using envFrom with configMapRef
    • Inject the API_KEY from Secret app-secrets as environment variable API_KEY using secretKeyRef
    • Labels on Pod template: app: fullstack, tier: frontend
  5. Service:

    • Name: fullstack-svc
    • Type: ClusterIP
    • Selector: app: fullstack
    • Port: 80, targetPort: 80
  6. Ingress:

    • Name: fullstack-ingress
    • ingressClassName: nginx
    • Host: fullstack.example.com
    • Path: /, PathType: Prefix
    • Backend: Service fullstack-svc, port 80
  7. Verification:

    # All resources exist
    kubectl get quota,configmap,secret,deployment,service,ingress -n fullstack-ns
    
    # Deployment has 3 ready replicas
    kubectl get deployment fullstack-app -n fullstack-ns
    
    # Environment variables are set
    kubectl exec deploy/fullstack-app -n fullstack-ns -- env | grep -E "APP_ENV|LOG_LEVEL|API_KEY"
    
    # Service has 3 endpoints
    kubectl get endpoints fullstack-svc -n fullstack-ns

    All resources must be present and correctly wired together.

Exam strategy: This is the hardest task on the exam because it combines 6 resource types into a single coherent stack. Work in layers, verifying each layer before moving to the next:

  1. Namespace + Quota — verify the quota is active before creating any workloads.
  2. ConfigMap + Secret — use imperative commands for speed.
  3. Deployment — this is the most complex resource because it must reference both the ConfigMap and Secret, set resource requests/limits (required by the quota), and use the correct labels.
  4. Service — one imperative command.
  5. Ingress — write YAML.

The ResourceQuota is the hidden trap in this task. When a quota specifies requests.cpu or requests.memory, every container in the namespace must declare those fields. If you deploy the Deployment without resource requests, the Pods are rejected by the admission controller. The error message mentions the quota, not the Deployment, which can be confusing.

Budget 8 minutes for this task. If you are running low on time, prioritize getting the Deployment running with correct ConfigMap/Secret injection and the Service exposing it. The Ingress and Quota are easier to verify and less likely to have cascading failures.

Expected time: 8 minutes.


End of Mock Exam 2. Stop your timer. Record your completion time and note which tasks you skipped or struggled with. Check your answers against the solutions in Chapter 29.