Contents

Chapter 1

Chapter 1: Pod Design — Labels, Annotations, Deployments, Jobs, CronJobs

Exam weight: 20% (Application Design and Build) + 20% (Application Deployment) = 40% — This is the core of the CKAD. You must create, manage, and update pods, deployments, jobs, and cronjobs fluently.


Task 1: Create and Manage Pods with Labels

Objective: Create pods with specific labels and use label selectors.

Task Statement

1. Create three pods: app-v1 (image: nginx:1.24, labels: app=myapp, version=v1), app-v2 (image: nginx:1.25, labels: app=myapp, version=v2), and cache (image: redis:7, labels: app=cache, tier=backend).

2. List only pods with app=myapp.

3. Add a label env=production to all pods with app=myapp.

Solution

bash
# Create pods with labels
k run app-v1 --image=nginx:1.24 --labels="app=myapp,version=v1" --restart=Never
k run app-v2 --image=nginx:1.25 --labels="app=myapp,version=v2" --restart=Never
k run cache --image=redis:7 --labels="app=cache,tier=backend" --restart=Never

# List by label selector
k get pods -l app=myapp
k get pods -l 'app in (myapp,cache)'    # Set-based selector
k get pods -l app=myapp,version=v2      # Multiple labels (AND)

# Add labels to matching pods
k label pods -l app=myapp env=production

# Remove a label
# k label pods -l app=myapp version-    # Trailing dash removes the label

Verification

bash
k get pods --show-labels
# app-v1   Running   app=myapp,env=production,version=v1
# app-v2   Running   app=myapp,env=production,version=v2
# cache    Running   app=cache,tier=backend

k get pods -l env=production
# Should show app-v1 and app-v2 only

Key Takeaways

  • Labels are key-value pairs for organizing and selecting resources.
  • Label selectors: equality-based (app=web), set-based (app in (web,api)).
  • Annotations are for metadata only (not used for selection): k annotate pod app-v1 description="Main application pod".

Task 2: Create a Deployment with Rolling Update Strategy

Objective: Deploy an application and perform a rolling update.

Task Statement

Create a Deployment named frontend with 4 replicas of nginx:1.24. Configure it with a rolling update strategy: maxSurge=1, maxUnavailable=0 (zero-downtime). Update the image to nginx:1.25. Then roll back.

Solution

bash
# Create the deployment
k create deployment frontend --image=nginx:1.24 --replicas=4

# Configure the rolling update strategy (edit the deployment)
k patch deployment frontend -p '{
  "spec": {
    "strategy": {
      "type": "RollingUpdate",
      "rollingUpdate": {
        "maxSurge": 1,
        "maxUnavailable": 0
      }
    }
  }
}'

# Update the image
k set image deployment/frontend nginx=nginx:1.25

# Watch the rollout
k rollout status deployment/frontend

# View rollout history
k rollout history deployment/frontend

# Roll back to the previous version
k rollout undo deployment/frontend

# Roll back to a specific revision
# k rollout undo deployment/frontend --to-revision=1

Verification

bash
k get deployment frontend -o jsonpath='{.spec.template.spec.containers[0].image}'
# nginx:1.24 (after rollback)

k get pods -l app=frontend
# 4 pods running

Key Takeaways

  • maxSurge: Maximum extra pods above the desired count during update.
  • maxUnavailable: Maximum pods that can be unavailable during update. Set to 0 for zero-downtime.
  • maxSurge=0 + maxUnavailable=0 is invalid (update would never make progress).
  • k rollout pause/resume deployment/ lets you do canary-style partial rollouts.

Task 3: Scale a Deployment and Use Horizontal Pod Autoscaler

Objective: Manually and automatically scale workloads.

Task Statement

1. Scale the frontend deployment to 6 replicas manually.

2. Configure a HorizontalPodAutoscaler to scale between 2 and 10 replicas based on 50% CPU utilization.

Solution

bash
# Manual scaling
k scale deployment frontend --replicas=6

# Create HPA
k autoscale deployment frontend --min=2 --max=10 --cpu-percent=50

# Or with YAML:
k apply -f - <<'EOF'
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: frontend-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v2
    kind: Deployment
    name: frontend
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
EOF

Verification

bash
k get hpa
k describe hpa frontend
k get deployment frontend

Task 4: Use Annotations for Metadata

Objective: Add and query annotations on resources.

Task Statement

Annotate the frontend deployment with team=platform, oncall=user@example.com, and change-cause="Updated to nginx:1.25".

Solution

bash
k annotate deployment frontend \
  team=platform \
  oncall=user@example.com \
  kubernetes.io/change-cause="Updated to nginx:1.25"

# View annotations
k describe deployment frontend | grep Annotations -A5

# Remove an annotation
# k annotate deployment frontend team-

Key Takeaways

  • Annotations hold arbitrary metadata. Unlike labels, they can contain large values (URLs, JSON, etc.).
  • kubernetes.io/change-cause populates the "CHANGE-CAUSE" column in k rollout history.

Task 5: Create a Job with Completion Count

Objective: Run batch workloads with specific completion requirements.

Task Statement

Create a Job named batch-processor that runs busybox with command echo "Processing item $(hostname)". It should complete 6 items total, running 3 at a time, with a backoff limit of 2.

Solution

bash
k apply -f - <<'EOF'
apiVersion: batch/v1
kind: Job
metadata:
  name: batch-processor
spec:
  completions: 6
  parallelism: 3
  backoffLimit: 2
  template:
    spec:
      containers:
      - name: worker
        image: busybox:1.36
        command: ["sh", "-c", "echo Processing item $(hostname) && sleep 3"]
      restartPolicy: Never
EOF

Verification

bash
k get job batch-processor
# COMPLETIONS   DURATION
# 6/6           15s

k get pods -l job-name=batch-processor
# 6 pods, all Completed

k logs -l job-name=batch-processor

Task 6: Create a CronJob

Objective: Schedule recurring tasks.

Task Statement

Create a CronJob named log-cleanup that runs every 30 minutes. It should execute echo "Cleaned up logs at $(date)". Keep only the last 3 successful and 1 failed job history.

Solution

bash
k create cronjob log-cleanup \
  --image=busybox:1.36 \
  --schedule="*/30 * * * *" \
  -- sh -c 'echo "Cleaned up logs at $(date)"'

# Patch to set history limits
k patch cronjob log-cleanup -p '{
  "spec": {
    "successfulJobsHistoryLimit": 3,
    "failedJobsHistoryLimit": 1,
    "concurrencyPolicy": "Forbid"
  }
}'

Verification

bash
k get cronjob log-cleanup
# SCHEDULE       SUSPEND   ACTIVE   LAST SCHEDULE
# */30 * * * *   False     0        <none>

# Manually trigger to test
k create job test-cleanup --from=cronjob/log-cleanup
k logs -l job-name=test-cleanup

Task 7: Use Canary Deployments

Objective: Run two versions of an application simultaneously.

Task Statement

Deploy nginx:1.24 as the stable version with 4 replicas and nginx:1.25 as the canary with 1 replica. Both should be behind the same Service.

Solution

bash
# Stable deployment
k apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-stable
spec:
  replicas: 4
  selector:
    matchLabels:
      app: myapp
      track: stable
  template:
    metadata:
      labels:
        app: myapp
        track: stable
    spec:
      containers:
      - name: app
        image: nginx:1.24
EOF

# Canary deployment
k apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
      track: canary
  template:
    metadata:
      labels:
        app: myapp
        track: canary
    spec:
      containers:
      - name: app
        image: nginx:1.25
EOF

# Service selects both (using the shared label)
k apply -f - <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: myapp-svc
spec:
  selector:
    app: myapp     # Matches BOTH stable and canary
  ports:
  - port: 80
    targetPort: 80
EOF

Verification

bash
k get endpoints myapp-svc
# Should show 5 pod IPs (4 stable + 1 canary)

# ~20% of traffic goes to canary (1 out of 5 pods)

Task 8: Blue-Green Deployment Pattern

Objective: Switch traffic between two complete versions instantly.

Task Statement

Deploy v1 (blue) and v2 (green) of an application. Route all traffic to v1 initially. Then switch to v2 by updating the Service selector.

Solution

bash
# Blue (v1) deployment
k create deployment blue --image=nginx:1.24 --replicas=3
k label deployment blue version=v1 --overwrite

# Green (v2) deployment
k create deployment green --image=nginx:1.25 --replicas=3
k label deployment green version=v2 --overwrite

# Service pointing to blue (v1)
k apply -f - <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: app-service
spec:
  selector:
    app: blue          # Points to blue
  ports:
  - port: 80
    targetPort: 80
EOF

# Switch to green: update the Service selector
k patch svc app-service -p '{"spec":{"selector":{"app":"green"}}}'

# Rollback to blue:
# k patch svc app-service -p '{"spec":{"selector":{"app":"blue"}}}'

Verification

bash
k get endpoints app-service
# Should show green pod IPs after the switch

Key Takeaways

  • Blue-green = instant cutover by changing the Service selector.
  • Both versions run simultaneously; only one receives traffic.
  • Rollback is instant (switch selector back).
Chapter 2

Chapter 2: Configuration — ConfigMaps, Secrets, SecurityContexts, Resources

Exam weight: 25% — The largest domain. You must create ConfigMaps and Secrets, inject them into pods, set security contexts, define resource limits, and work with ServiceAccounts.


Task 1: Create and Use ConfigMaps

Objective: Inject configuration into pods via environment variables and file mounts.

Task Statement

1. Create a ConfigMap named app-config with DATABASE_HOST=db.example.com, LOG_LEVEL=info, and WORKERS=4.

2. Create a pod that uses these values as environment variables.

3. Create another pod that mounts the ConfigMap as a file at /etc/app/config.

Solution

bash
# Create ConfigMap
k create configmap app-config \
  --from-literal=DATABASE_HOST=db.example.com \
  --from-literal=LOG_LEVEL=info \
  --from-literal=WORKERS=4

# Pod with env vars from ConfigMap
k apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: env-pod
spec:
  containers:
  - name: app
    image: busybox:1.36
    command: ["sh", "-c", "env | grep -E 'DATABASE|LOG|WORKERS' && sleep 3600"]
    envFrom:                    # All keys as env vars
    - configMapRef:
        name: app-config
EOF

# Pod with ConfigMap mounted as files
k apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: file-pod
spec:
  containers:
  - name: app
    image: busybox:1.36
    command: ["sh", "-c", "ls /etc/app/config && cat /etc/app/config/LOG_LEVEL && sleep 3600"]
    volumeMounts:
    - name: config-vol
      mountPath: /etc/app/config
      readOnly: true
  volumes:
  - name: config-vol
    configMap:
      name: app-config
EOF

Verification

bash
k logs env-pod
# DATABASE_HOST=db.example.com
# LOG_LEVEL=info
# WORKERS=4

k exec file-pod -- ls /etc/app/config
# DATABASE_HOST  LOG_LEVEL  WORKERS
k exec file-pod -- cat /etc/app/config/DATABASE_HOST
# db.example.com

Key Takeaways

  • envFrom imports ALL keys. env[].valueFrom.configMapKeyRef imports a single key.
  • Volume-mounted ConfigMaps update automatically when the ConfigMap changes (with a small delay).
  • Environment variables from ConfigMaps do NOT update — the pod must be restarted.

Task 2: Create and Use Secrets

Objective: Store sensitive data and inject it into pods securely.

Task Statement

1. Create a Secret named db-credentials with username=admin and password=S3cureP4ss!.

2. Create a pod that uses the username as an env var and mounts the entire Secret at /etc/db-creds.

Solution

bash
# Create Secret (values are automatically base64-encoded)
k create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password='S3cureP4ss!'

# Pod using Secret
k apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: secret-pod
spec:
  containers:
  - name: app
    image: busybox:1.36
    command: ["sh", "-c", "echo DB_USER=$DB_USER && cat /etc/db-creds/password && sleep 3600"]
    env:
    - name: DB_USER
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: username
    volumeMounts:
    - name: creds
      mountPath: /etc/db-creds
      readOnly: true
  volumes:
  - name: creds
    secret:
      secretName: db-credentials
      defaultMode: 0400          # Read-only by owner
EOF

Verification

bash
k logs secret-pod
# DB_USER=admin
# S3cureP4ss!

# View secret (base64 encoded)
k get secret db-credentials -o jsonpath='{.data.password}' | base64 -d

Key Takeaways

  • Secrets are base64-encoded, NOT encrypted (unless encryption at rest is configured).
  • Use stringData in YAML for plain text values (Kubernetes encodes them).
  • Mount with restrictive defaultMode (e.g., 0400) to limit file permissions.
  • Secret types: Opaque (default), kubernetes.io/tls, kubernetes.io/dockerconfigjson.

Task 3: Configure Security Contexts

Objective: Run containers with specific user/group IDs and capability restrictions.

Task Statement

Create a pod with the following security constraints:

  • Run as user 1000, group 3000
  • Read-only root filesystem
  • No privilege escalation
  • Drop all capabilities, add only NET_BIND_SERVICE

Solution

bash
k apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
    runAsNonRoot: true
    fsGroup: 2000
  containers:
  - name: app
    image: nginx
    securityContext:
      readOnlyRootFilesystem: true
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]
        add: ["NET_BIND_SERVICE"]
    volumeMounts:
    - name: tmp
      mountPath: /tmp
    - name: cache
      mountPath: /var/cache/nginx
    - name: run
      mountPath: /var/run
  volumes:
  - name: tmp
    emptyDir: {}
  - name: cache
    emptyDir: {}
  - name: run
    emptyDir: {}
EOF

Verification

bash
k exec secure-app -- id
# uid=1000 gid=3000 groups=2000,3000

k exec secure-app -- touch /test 2>&1
# Read-only file system

k exec secure-app -- touch /tmp/test
# Works (emptyDir is writable)

Task 4: Set Resource Requests and Limits

Objective: Control CPU and memory allocation for containers.

Task Statement

Create a pod named resource-pod with:

  • CPU request: 100m, limit: 500m
  • Memory request: 64Mi, limit: 128Mi

Then create a pod that exceeds its memory limit and observe OOMKill.

Solution

bash
# Pod with proper resource constraints
k apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: resource-pod
spec:
  containers:
  - name: app
    image: nginx
    resources:
      requests:
        cpu: "100m"
        memory: "64Mi"
      limits:
        cpu: "500m"
        memory: "128Mi"
EOF

# Pod that will be OOMKilled (allocates more memory than allowed)
k apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: oom-pod
spec:
  containers:
  - name: stress
    image: busybox:1.36
    command: ["sh", "-c", "dd if=/dev/zero of=/dev/null bs=1M count=500"]
    resources:
      limits:
        memory: "50Mi"
EOF

Verification

bash
k get pod resource-pod
k describe pod resource-pod | grep -A4 Limits

k get pod oom-pod
# STATUS: OOMKilled or CrashLoopBackOff

k describe pod oom-pod | grep -A2 "Last State"
# Reason: OOMKilled

Task 5: Create a LimitRange

Objective: Set default resource limits for a namespace.

Task Statement

Create a LimitRange in the dev namespace that sets:

  • Default CPU limit: 500m, default CPU request: 100m
  • Default memory limit: 256Mi, default memory request: 64Mi
  • Maximum CPU per container: 2, maximum memory: 1Gi

Solution

bash
k create namespace dev

k apply -f - <<'EOF'
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: dev
spec:
  limits:
  - type: Container
    default:
      cpu: "500m"
      memory: "256Mi"
    defaultRequest:
      cpu: "100m"
      memory: "64Mi"
    max:
      cpu: "2"
      memory: "1Gi"
    min:
      cpu: "50m"
      memory: "32Mi"
EOF

Verification

bash
# Create a pod without resource specs in dev namespace
k run test-limits --image=nginx --restart=Never -n dev

# Check that default limits were applied
k describe pod test-limits -n dev | grep -A6 "Limits"
# cpu: 500m, memory: 256Mi (defaults applied)

Task 6: Create a ResourceQuota

Objective: Limit total resource consumption in a namespace.

Task Statement

Create a ResourceQuota in the dev namespace that limits:

  • Maximum 10 pods
  • Total CPU requests: 4 cores, Total CPU limits: 8 cores
  • Total memory requests: 4Gi, Total memory limits: 8Gi

Solution

bash
k apply -f - <<'EOF'
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: dev
spec:
  hard:
    pods: "10"
    requests.cpu: "4"
    requests.memory: "4Gi"
    limits.cpu: "8"
    limits.memory: "8Gi"
    configmaps: "20"
    secrets: "20"
    services: "10"
EOF

Verification

bash
k get resourcequota dev-quota -n dev
k describe resourcequota dev-quota -n dev
# Shows Used vs Hard for each resource

Task 7: Use ServiceAccounts in Pods

Objective: Control API access for pods using ServiceAccounts.

Task Statement

Create a ServiceAccount named app-runner and a pod that uses it. Verify the token is mounted.

Solution

bash
k create serviceaccount app-runner

k apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: sa-pod
spec:
  serviceAccountName: app-runner
  containers:
  - name: app
    image: busybox:1.36
    command: ["sh", "-c", "ls /var/run/secrets/kubernetes.io/serviceaccount/ && sleep 3600"]
EOF

Verification

bash
k exec sa-pod -- ls /var/run/secrets/kubernetes.io/serviceaccount/
# ca.crt  namespace  token

k exec sa-pod -- cat /var/run/secrets/kubernetes.io/serviceaccount/namespace
# default

Task 8: Immutable ConfigMaps and Secrets

Objective: Create configuration that cannot be changed after creation.

Task Statement

Create an immutable ConfigMap and verify it cannot be modified.

Solution

bash
k apply -f - <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-version
data:
  version: "2.5.0"
  build: "12345"
immutable: true
EOF

Verification

bash
# Try to update it
k edit configmap app-version
# Error: "field is immutable"

# To change it, you must delete and recreate
k delete configmap app-version
k create configmap app-version --from-literal=version=2.6.0

Key Takeaways

  • Immutable ConfigMaps/Secrets reduce API server load (no watch updates).
  • Use for versioned config that should never change during a deployment.
Chapter 3
🔒 Available in full product

Chapter 3: Multi-Container Pod Patterns

Chapter 4
🔒 Available in full product

Chapter 4: Observability — Probes, Logging, Monitoring

Chapter 5
🔒 Available in full product

Chapter 5: Services & Networking

Chapter 6
🔒 Available in full product

Chapter 6: State Persistence

You’ve reached the end of the free preview

Get the full Kubernetes CKAD Lab Workbook and unlock everything.

All Chapters

Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.

Full Tool Suite

Access all interactive tools with complete data, all workload profiles, and the full scenario library.

Source Files

Downloadable source code, configuration files, and working examples from every chapter.

Lifetime Updates

Free updates for life. Every new chapter, tool, and improvement included.

Buy Now — $69 →
📦 Free sample included — download another copy for the full product.
Kubernetes CKAD Lab Workbook v1.0.0 — Free Preview