Skip to main content
mastering ckad certified kubernetes application developer

Solutions: Tasks 1–8

12 min read Chapter 80 of 87
Summary

Complete solutions for tasks 1-8: Pod with labels...

Complete solutions for tasks 1-8: Pod with labels and resources, Deployment with rolling update, CronJob with concurrency, ConfigMap as env vars, NetworkPolicy, PVC with Pod, debugging a broken Pod, and Service with Ingress.

Solutions: Tasks 1–8

Solution: Task 1 — Pod with Labels and Resource Requests (4%)

Imperative Approach

kubectl config set-context --current --namespace=app-team1

Generate the Pod scaffold:

k run web-frontend --image=nginx:1.25-alpine --port=80 \
  --labels="app=frontend,tier=web,version=v1" \
  --requests="cpu=100m,memory=128Mi" \
  --limits="cpu=250m,memory=256Mi" \
  $do > web-frontend.yaml

Review the generated YAML and apply:

k apply -f web-frontend.yaml

Final YAML

apiVersion: v1
kind: Pod
metadata:
  name: web-frontend
  namespace: app-team1
  labels:
    app: frontend
    tier: web
    version: v1
spec:
  containers:
  - name: web-frontend
    image: nginx:1.25-alpine
    ports:
    - containerPort: 80
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 250m
        memory: 256Mi

Verification

k get pod web-frontend -n app-team1
NAME           READY   STATUS    RESTARTS   AGE
web-frontend   1/1     Running   0          10s
k get pod web-frontend -n app-team1 --show-labels
NAME           READY   STATUS    RESTARTS   AGE   LABELS
web-frontend   1/1     Running   0          15s   app=frontend,tier=web,version=v1
k describe pod web-frontend -n app-team1 | grep -A6 "Limits\|Requests"
    Limits:
      cpu:     250m
      memory:  256Mi
    Requests:
      cpu:     100m
      memory:  128Mi

Key Points

  • This is a quick-win task. The entire Pod can be generated imperatively in one command — no YAML editing required.
  • The --labels flag accepts comma-separated key=value pairs. No spaces after commas.
  • Verify all three labels are present. Missing one label means partial credit at best.
  • Time target: under 2 minutes.

Solution: Task 2 — Deployment with Rolling Update (7%)

Imperative Approach

kubectl config set-context --current --namespace=app-team1

Generate the Deployment:

k create deployment api-server --image=nginx:1.24 --replicas=3 --port=8080 $do > api-server.yaml

Edit the YAML to add the rolling update strategy and the app: api label:

vim api-server.yaml

Final YAML (Step 1)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: app-team1
  labels:
    app: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
      - name: nginx
        image: nginx:1.24
        ports:
        - containerPort: 8080

Apply the Deployment:

k apply -f api-server.yaml

Wait for all replicas:

k rollout status deployment/api-server -n app-team1

Step 2: Rolling Update

k set image deployment/api-server nginx=nginx:1.25 -n app-team1

Step 3: Verify

k rollout status deployment/api-server -n app-team1
deployment "api-server" successfully rolled out
k get pods -n app-team1 -l app=api -o jsonpath='{.items[*].spec.containers[0].image}'
nginx:1.25 nginx:1.25 nginx:1.25

Step 4: Rollout History

k rollout history deployment/api-server -n app-team1
deployment.apps/api-server 
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

Key Points

  • The k create deployment command generates a Deployment with default labels matching the deployment name (e.g., app: api-server). The task requires app: api, so you must edit the selector, template labels, and Deployment labels to match.
  • When you change the label selector, update it in three places: metadata.labels, spec.selector.matchLabels, and spec.template.metadata.labels. They must all be consistent.
  • maxUnavailable: 0 means Kubernetes creates new Pods before terminating old ones, ensuring zero downtime during the update.
  • The container name generated by kubectl create deployment defaults to the image name (e.g., nginx). Use that name in the k set image command.
  • Time target: 5–6 minutes.

Solution: Task 3 — CronJob with Concurrency Settings (7%)

Imperative Approach

kubectl config set-context --current --namespace=batch-ns

Generate the CronJob scaffold:

k create cronjob report-generator --image=busybox:1.36 \
  --schedule="*/5 * * * *" \
  $do -- sh -c "echo Generating report at \$(date) && sleep 20" > report-generator.yaml

Edit the YAML to add concurrency policy, history limits, active deadline, and restart policy:

vim report-generator.yaml

Final YAML

apiVersion: batch/v1
kind: CronJob
metadata:
  name: report-generator
  namespace: batch-ns
spec:
  schedule: "*/5 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      activeDeadlineSeconds: 30
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: report-generator
            image: busybox:1.36
            command:
            - sh
            - -c
            - "echo Generating report at $(date) && sleep 20"

Apply:

k apply -f report-generator.yaml

Verification

k get cronjob report-generator -n batch-ns
NAME               SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
report-generator   */5 * * * *   False     0        <none>          10s
k get cronjob report-generator -n batch-ns -o jsonpath='{.spec.concurrencyPolicy}'
Forbid
k get cronjob report-generator -n batch-ns -o jsonpath='{.spec.successfulJobsHistoryLimit}'
3
k get cronjob report-generator -n batch-ns -o jsonpath='{.spec.jobTemplate.spec.activeDeadlineSeconds}'
30

Key Points

  • activeDeadlineSeconds goes under spec.jobTemplate.spec, not under spec of the CronJob. This is a common mistake — it applies to the Job, not the CronJob.
  • concurrencyPolicy: Forbid prevents a new Job from starting if the previous Job is still running. This matters for tasks that should not overlap.
  • successfulJobsHistoryLimit and failedJobsHistoryLimit go under spec of the CronJob (not the Job template).
  • restartPolicy must be Never or OnFailure for Jobs. Pods in a Job cannot have restartPolicy: Always.
  • Time target: 4–5 minutes.

Solution: Task 4 — ConfigMap as Environment Variables (4%)

Imperative Approach

kubectl config set-context --current --namespace=app-team1

Create the ConfigMap:

k create configmap app-config \
  --from-literal=APP_ENV=production \
  --from-literal=APP_LOG_LEVEL=info \
  --from-literal=APP_PORT=8080 \
  -n app-team1

Generate the Pod scaffold:

k run config-reader --image=busybox:1.36 \
  $do --command -- sh -c "echo \$APP_ENV \$APP_LOG_LEVEL \$APP_PORT && sleep 3600" > config-reader.yaml

Edit the YAML to add envFrom:

vim config-reader.yaml

Final YAML (Pod only — ConfigMap was created imperatively)

apiVersion: v1
kind: Pod
metadata:
  name: config-reader
  namespace: app-team1
spec:
  containers:
  - name: config-reader
    image: busybox:1.36
    command:
    - sh
    - -c
    - "echo $APP_ENV $APP_LOG_LEVEL $APP_PORT && sleep 3600"
    envFrom:
    - configMapRef:
        name: app-config

Apply:

k apply -f config-reader.yaml

Verification

k exec config-reader -n app-team1 -- env | grep APP_
APP_ENV=production
APP_LOG_LEVEL=info
APP_PORT=8080

Key Points

  • envFrom with configMapRef loads all keys from the ConfigMap as environment variables. This is faster to write than individual env entries with valueFrom.
  • The ConfigMap can be created entirely imperatively — no YAML needed. Only the Pod requires YAML editing to add envFrom.
  • Time target: 2–3 minutes.

Solution: Task 5 — NetworkPolicy (7%)

Imperative Approach

There is no imperative command for NetworkPolicies. You must write YAML from scratch or copy from the Kubernetes documentation.

kubectl config set-context --current --namespace=network-ns

Navigate to kubernetes.io/docs/concepts/services-networking/network-policies/ and copy the example NetworkPolicy YAML. Edit it to match the task requirements.

Final YAML

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-netpol
  namespace: network-ns
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 80

Apply:

k apply -f backend-netpol.yaml

Verification

k get networkpolicy backend-netpol -n network-ns
NAME             POD-SELECTOR   AGE
backend-netpol   app=backend    10s
k describe networkpolicy backend-netpol -n network-ns
Name:         backend-netpol
Namespace:    network-ns
Created on:   ...
Labels:       <none>
Annotations:  <none>
Spec:
  PodSelector:     app=backend
  Allowing ingress traffic:
    To Port: 80/TCP
    From:
      PodSelector: role=frontend
  Not affecting egress traffic
  Policy Types: Ingress

Key Points

  • NetworkPolicy is the one common resource type that has no imperative command. You must write or copy YAML.
  • The podSelector under spec selects which Pods this policy applies to (app: backend).
  • The podSelector under ingress[0].from[0] selects which Pods are allowed to send traffic (role: frontend).
  • Specifying policyTypes: [Ingress] without any egress rules means egress is unrestricted. If you add Egress to policyTypes without egress rules, you block all outbound traffic — a common mistake.
  • The ports field under ingress restricts which ports traffic is allowed on. Placing it at the wrong indentation level changes the meaning.
  • Pay close attention to YAML indentation: from and ports are both items under the same ingress list entry. The from and ports arrays are peers, not nested.
  • Time target: 5–6 minutes.

Solution: Task 6 — PersistentVolumeClaim and Pod (7%)

Imperative Approach

kubectl config set-context --current --namespace=storage-ns

There is no imperative command for PVC creation. Write the YAML:

PVC YAML

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
  namespace: storage-ns
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

Apply:

k apply -f data-pvc.yaml

Generate the Pod scaffold:

k run data-writer --image=busybox:1.36 \
  $do --command -- sh -c "echo 'persistent data test' > /data/output.txt && sleep 3600" > data-writer.yaml

Edit to add volume and volume mount:

Final Pod YAML

apiVersion: v1
kind: Pod
metadata:
  name: data-writer
  namespace: storage-ns
spec:
  containers:
  - name: data-writer
    image: busybox:1.36
    command:
    - sh
    - -c
    - "echo 'persistent data test' > /data/output.txt && sleep 3600"
    volumeMounts:
    - name: data-vol
      mountPath: /data
  volumes:
  - name: data-vol
    persistentVolumeClaim:
      claimName: data-pvc

Apply:

k apply -f data-writer.yaml

Verification

k get pvc data-pvc -n storage-ns
NAME       STATUS   VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-pvc   Bound    pv-xxxx    1Gi        RWO            standard       30s
k get pod data-writer -n storage-ns
NAME          READY   STATUS    RESTARTS   AGE
data-writer   1/1     Running   0          15s
k exec data-writer -n storage-ns -- cat /data/output.txt
persistent data test

Key Points

  • Do not specify storageClassName in the PVC if the task says to use the default storage class. Omitting it causes Kubernetes to use the default class automatically.
  • The volume name (data-vol) must match between volumes[].name and volumeMounts[].name. A mismatch causes a validation error.
  • The PVC must be Bound before the Pod can start. If the PVC stays in Pending, check whether a suitable PersistentVolume or dynamic provisioner exists.
  • Time target: 4–5 minutes.

Solution: Task 7 — Debug a Broken Pod (7%)

Investigation

kubectl config set-context --current --namespace=monitoring
k get pod health-checker -n monitoring
NAME             READY   STATUS             RESTARTS   AGE
health-checker   0/1     ImagePullBackOff   0          30s

The status ImagePullBackOff indicates the image cannot be pulled. Check the events:

k describe pod health-checker -n monitoring | grep -A5 Events
Events:
  Type     Reason     Age   From               Message
  ----     ------     ----  ----               -------
  Warning  Failed     10s   kubelet            Failed to pull image "ngnix:latest": 
                                                rpc error: image not found
  Warning  Failed     10s   kubelet            Error: ImagePullBackOff

The image name is ngnix — missing the i. It should be nginx.

Additionally, the probes target port 8080 but the container listens on port 80, and the probe paths /healthz and /ready do not exist on the default nginx image.

Fix

Delete the broken Pod:

k delete pod health-checker -n monitoring

Create the corrected YAML:

apiVersion: v1
kind: Pod
metadata:
  name: health-checker
  namespace: monitoring
spec:
  containers:
  - name: checker
    image: nginx:latest
    ports:
    - containerPort: 80
    livenessProbe:
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 5
      periodSeconds: 10
    readinessProbe:
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 3
      periodSeconds: 5

Apply:

k apply -f health-checker.yaml

Verification

k get pod health-checker -n monitoring
NAME             READY   STATUS    RESTARTS   AGE
health-checker   1/1     Running   0          15s
k describe pod health-checker -n monitoring | grep -A3 "Liveness\|Readiness"
    Liveness:       http-get http://:80/ delay=5s timeout=1s period=10s #success=1 #failure=3
    Readiness:      http-get http://:80/ delay=3s timeout=1s period=5s #success=1 #failure=3

Key Points

  • Debugging tasks require a systematic approach: check status, check events, check logs, identify root cause, fix.
  • Pod spec fields are immutable. You cannot change the image or probe configuration of a running Pod. Delete and recreate.
  • The default nginx image serves on port 80 and returns 200 OK on path /. Using /healthz or /ready returns 404, which the probe interprets as failure.
  • Typos in image names (ngnix vs nginx) are a common exam trap. Always check the image name first when debugging ImagePullBackOff.
  • Time target: 4–5 minutes.

Solution: Task 8 — Service and Ingress (7%)

Imperative Approach

kubectl config set-context --current --namespace=web-prod

Create the Deployment:

k create deployment shop-frontend --image=nginx:1.25 --replicas=2 --port=80 -n web-prod

The default labels from kubectl create deployment will be app: shop-frontend. The task requires app: shop on the Pods. Update the Deployment:

k get deployment shop-frontend -n web-prod -o yaml > shop-deploy.yaml

Edit to set Pod labels to app: shop and update the selector:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-frontend
  namespace: web-prod
  labels:
    app: shop
spec:
  replicas: 2
  selector:
    matchLabels:
      app: shop
  template:
    metadata:
      labels:
        app: shop
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80

Apply:

k apply -f shop-deploy.yaml

Create the Service:

k expose deployment shop-frontend --name=shop-svc --port=80 --target-port=80 -n web-prod

Since we changed the labels, the expose command automatically picks up the selector from the Deployment. Alternatively, create the Service imperatively and it will use the Deployment’s selector:

Verify the Service targets the correct Pods:

k get endpoints shop-svc -n web-prod

Create the Ingress. There is no fully-featured imperative command for Ingress with host-based routing, so write YAML:

Ingress YAML

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-ingress
  namespace: web-prod
spec:
  ingressClassName: nginx
  rules:
  - host: shop.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: shop-svc
            port:
              number: 80

Apply:

k apply -f shop-ingress.yaml

Verification

k get svc shop-svc -n web-prod
NAME       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
shop-svc   ClusterIP   10.96.45.123   <none>        80/TCP    20s
k get ingress shop-ingress -n web-prod
NAME           CLASS   HOSTS              ADDRESS   PORTS   AGE
shop-ingress   nginx   shop.example.com             80      10s
k describe ingress shop-ingress -n web-prod
Name:             shop-ingress
Namespace:        web-prod
...
Rules:
  Host              Path  Backends
  ----              ----  --------
  shop.example.com
                    /   shop-svc:80 (10.244.0.15:80,10.244.0.16:80)

Key Points

  • The Ingress API uses networking.k8s.io/v1. Using the old extensions/v1beta1 API results in an error on modern Kubernetes versions.
  • ingressClassName: nginx replaces the old kubernetes.io/ingress.class annotation. Use the field, not the annotation.
  • pathType is required. The two common values are Prefix (matches the path and its sub-paths) and Exact (matches only the exact path).
  • The Service selector must match the Pod labels. If you changed the Deployment labels from app: shop-frontend to app: shop, verify the Service selector was updated accordingly.
  • Time target: 6–7 minutes.