← All Posts
Kubernetes10 Aug 2026·13 min read

CKAD Hands-On: Build and Deploy 6 Real Applications on Kubernetes

Srinivasa Rao Maganti — Lead Cloud & DevOps Trainer at CloudTechTrainings

Srinivasa Rao Maganti

Cloud Architect & Lead Trainer, CloudTechTrainings

#CKAD#Kubernetes#Certified Kubernetes Application Developer#kubectl#Certification

Info: Exam Reality

The real CKAD gives you 15–20 performance-based tasks over 2 hours in a live cluster, open-book (kubernetes.io and kubectl --help are both allowed), with a 66% pass mark. Every lab below mirrors that format: a requirement to build from scratch, not a concept quiz.

Quick Reference — 6 Builds at a Glance

  • 1. Multi-container Pod with a sidecar — Application Design and Build (20%)
  • 2. Zero-downtime rolling update with a readiness gate — Application Deployment (20%)
  • 3. ConfigMap, Secret, and a non-root SecurityContext — Environment, Config & Security (25%)
  • 4. NetworkPolicy lockdown with a ResourceQuota — Environment, Config & Security (25%)
  • 5. Liveness, readiness, and startup probes — Application Observability (15%)
  • 6. ClusterIP Service behind an Ingress with path-based routing — Services and Networking (20%)

1. Design & Build — Multi-Container Pod with a Sidecar

Requirement: deploy a Pod named log-shipper in namespace apps with two containers sharing a volume — an app container that writes to /var/log/app.log, and a sidecar that tails that file. This pattern (main container + sidecar sharing an emptyDir) is one of the most exam-relevant multi-container designs.

yaml
apiVersion: v1
kind: Pod
metadata:
  name: log-shipper
  namespace: apps
  labels:
    app: log-shipper
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "while true; do echo \"$(date) - request served\" >> /var/log/app.log; sleep 5; done"]
      volumeMounts:
        - name: log-volume
          mountPath: /var/log
    - name: log-sidecar
      image: busybox:1.36
      command: ["sh", "-c", "tail -f /var/log/app.log"]
      volumeMounts:
        - name: log-volume
          mountPath: /var/log
  volumes:
    - name: log-volume
      emptyDir: {}
bash
$ kubectl apply -f log-shipper.yaml
pod/log-shipper created

$ kubectl get pod log-shipper -n apps
NAME          READY   STATUS    RESTARTS   AGE
log-shipper   2/2     Running   0          12s

$ kubectl logs log-shipper -n apps -c log-sidecar --tail=3
Mon Aug 10 09:14:32 UTC 2026 - request served
Mon Aug 10 09:14:37 UTC 2026 - request served
Mon Aug 10 09:14:42 UTC 2026 - request served

2. Deployment — Zero-Downtime Rolling Update with a Readiness Gate

Requirement: deploy checkout-api as a Deployment with 4 replicas, a rolling update strategy that never drops below 3 available Pods, and a readiness probe so traffic only reaches Pods that are actually ready. Then perform a rolling update to a new image tag and verify it completes without dropping availability.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-api
  namespace: apps
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: checkout-api
  template:
    metadata:
      labels:
        app: checkout-api
    spec:
      containers:
        - name: checkout-api
          image: myregistry.io/checkout-api:1.4.0
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
bash
$ kubectl apply -f checkout-api.yaml
deployment.apps/checkout-api created

$ kubectl set image deployment/checkout-api checkout-api=myregistry.io/checkout-api:1.5.0 -n apps
deployment.apps/checkout-api image updated

$ kubectl rollout status deployment/checkout-api -n apps
Waiting for deployment "checkout-api" rollout to finish: 2 out of 4 new replicas have been updated...
deployment "checkout-api" successfully rolled out

$ kubectl rollout undo deployment/checkout-api -n apps
deployment.apps/checkout-api rolled back

Tip: Know the Rollback Command Cold

kubectl rollout undo is easy to forget under time pressure because you rarely need it outside an incident. Practice it explicitly — CKAD scenarios sometimes require rolling back a bad deploy as part of the task.

3. Environment, Config & Security — ConfigMap, Secret, and a Non-Root SecurityContext

Requirement: create a ConfigMap with a database host setting, a Secret with a database password, and a Pod that consumes both as environment variables — running as a non-root user with no privilege escalation allowed.

bash
$ kubectl create configmap db-config -n apps --from-literal=DB_HOST=postgres.internal
configmap/db-config created

$ kubectl create secret generic db-secret -n apps --from-literal=DB_PASSWORD='S3cure!Pass'
secret/db-secret created
yaml
apiVersion: v1
kind: Pod
metadata:
  name: billing-worker
  namespace: apps
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
  containers:
    - name: billing-worker
      image: myregistry.io/billing-worker:2.1.0
      securityContext:
        allowPrivilegeEscalation: false
      envFrom:
        - configMapRef:
            name: db-config
      env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: DB_PASSWORD
bash
$ kubectl apply -f billing-worker.yaml
pod/billing-worker created

$ kubectl exec billing-worker -n apps -- whoami
1000

$ kubectl exec billing-worker -n apps -- printenv DB_HOST
postgres.internal

4. Environment, Config & Security — ResourceQuota and a NetworkPolicy Lockdown

Requirement: in namespace restricted-ns, cap total CPU and memory requests with a ResourceQuota, and lock down network traffic so only Pods labeled role=frontend can reach Pods labeled role=backend on port 8080 — everything else denied by default.

yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: restricted-quota
  namespace: restricted-ns
spec:
  hard:
    requests.cpu: "2"
    requests.memory: 2Gi
    limits.cpu: "4"
    limits.memory: 4Gi
yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-allow-frontend
  namespace: restricted-ns
spec:
  podSelector:
    matchLabels:
      role: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              role: frontend
      ports:
        - protocol: TCP
          port: 8080
bash
$ kubectl apply -f restricted-quota.yaml -f backend-allow-frontend.yaml
resourcequota/restricted-quota created
networkpolicy.networking.k8s.io/backend-allow-frontend created

$ kubectl describe quota restricted-quota -n restricted-ns
Name:            restricted-quota
Namespace:       restricted-ns
Resource         Used  Hard
--------         ----  ----
limits.cpu       0     4
limits.memory    0     4Gi
requests.cpu     0     2
requests.memory  0     2Gi

Tip: NetworkPolicy Only Works if the CNI Supports It

On the real exam the cluster's CNI plugin (Calico, Cilium, etc.) already enforces NetworkPolicy — you just write the manifest. When practicing locally with kind or minikube, confirm your CNI actually enforces NetworkPolicy, or your policy will silently do nothing.

5. Observability — Liveness, Readiness, and Startup Probes

Requirement: deploy a Pod for a slow-starting legacy app that needs up to 60 seconds before it is ready, but should be restarted if it ever stops responding after that. Use all three probe types together — this exact combination (startup + liveness + readiness) is a common exam pattern for apps with a long boot time.

yaml
apiVersion: v1
kind: Pod
metadata:
  name: legacy-reporting
  namespace: apps
spec:
  containers:
    - name: legacy-reporting
      image: myregistry.io/legacy-reporting:3.0.0
      ports:
        - containerPort: 9000
      startupProbe:
        httpGet:
          path: /startupz
          port: 9000
        failureThreshold: 12
        periodSeconds: 5
      livenessProbe:
        httpGet:
          path: /healthz
          port: 9000
        periodSeconds: 10
        failureThreshold: 3
      readinessProbe:
        httpGet:
          path: /readyz
          port: 9000
        periodSeconds: 5
bash
$ kubectl apply -f legacy-reporting.yaml
pod/legacy-reporting created

$ kubectl describe pod legacy-reporting -n apps | grep -A2 "Startup\|Liveness\|Readiness"
    Startup:    http-get http://:9000/startupz delay=0s timeout=1s period=5s #success=1 #failure=12
    Liveness:   http-get http://:9000/healthz delay=0s timeout=1s period=10s #success=1 #failure=3
    Readiness:  http-get http://:9000/readyz delay=0s timeout=1s period=5s #success=1 #failure=3

6. Services & Networking — ClusterIP Service Behind an Ingress

Requirement: expose Deployment catalog-api internally via a ClusterIP Service on port 80, then route external traffic to it at path /catalog using an existing Ingress controller — with a second backend at /orders routing to a different Service, on the same Ingress resource.

yaml
apiVersion: v1
kind: Service
metadata:
  name: catalog-api-svc
  namespace: apps
spec:
  type: ClusterIP
  selector:
    app: catalog-api
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: storefront-ingress
  namespace: apps
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - http:
        paths:
          - path: /catalog
            pathType: Prefix
            backend:
              service:
                name: catalog-api-svc
                port:
                  number: 80
          - path: /orders
            pathType: Prefix
            backend:
              service:
                name: orders-api-svc
                port:
                  number: 80
bash
$ kubectl apply -f catalog-api-svc.yaml -f storefront-ingress.yaml
service/catalog-api-svc created
ingress.networking.k8s.io/storefront-ingress created

$ kubectl get ingress storefront-ingress -n apps
NAME                  CLASS   HOSTS   ADDRESS        PORTS   AGE
storefront-ingress    nginx   *       10.96.201.44   80      8s

$ kubectl get endpoints catalog-api-svc -n apps
NAME               ENDPOINTS                        AGE
catalog-api-svc    10.244.1.7:8080,10.244.2.9:8080   14s

Tip: Practice Until It's Muscle Memory

The real exam rewards speed as much as correctness. Rebuild each of these six tasks yourself in a local cluster (kind or minikube both work) using imperative kubectl commands first, then only fall back to hand-writing YAML for fields imperative commands can't set — that's the actual time-saving skill CKAD is testing.

Put These Builds to the Test

Ready to Start Your Cloud Journey?

Live batches Mon–Sat — Azure 9–10 AM IST (starts 24 august 2026) · AWS 10:30–11:45 AM IST (starts 31 august 2026). Hands-on labs, exam prep, and community support.

Join Free Demo →WhatsApp Us

Keep Reading

Kubernetes

CKA Troubleshooting Labs: 6 Real Exam-Style Cluster Failures

Troubleshooting is 30% of the CKA exam — the single highest-weighted domain, and the #1 reason candidates run out of time. Six realistic broken-cluster labs, each worked symptom to root cause to fix with the exact kubectl commands.

5 Aug 2026·13 min read
Read →
Kubernetes

Kubernetes vs Docker Swarm: Which to Learn in 2026?

An honest comparison for DevOps engineers choosing between K8s and Swarm. Career demand data, feature comparison, and a clear recommendation included.

17 Jun 2026·8 min read
Read →