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.
Objective: Create pods with specific labels and use label selectors.
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.
# 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 labelk 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 onlyapp=web), set-based (app in (web,api)).k annotate pod app-v1 description="Main application pod".Objective: Deploy an application and perform a rolling update.
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.
# 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=1k get deployment frontend -o jsonpath='{.spec.template.spec.containers[0].image}'
# nginx:1.24 (after rollback)
k get pods -l app=frontend
# 4 pods runningmaxSurge: 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.Objective: Manually and automatically scale workloads.
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.
# 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
EOFk get hpa
k describe hpa frontend
k get deployment frontendObjective: Add and query annotations on resources.
Annotate the frontend deployment with team=platform, oncall=user@example.com, and change-cause="Updated to nginx:1.25".
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-kubernetes.io/change-cause populates the "CHANGE-CAUSE" column in k rollout history.Objective: Run batch workloads with specific completion requirements.
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.
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
EOFk 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-processorObjective: Schedule recurring tasks.
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.
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"
}
}'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-cleanupObjective: Run two versions of an application simultaneously.
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.
# 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
EOFk get endpoints myapp-svc
# Should show 5 pod IPs (4 stable + 1 canary)
# ~20% of traffic goes to canary (1 out of 5 pods)Objective: Switch traffic between two complete versions instantly.
Deploy v1 (blue) and v2 (green) of an application. Route all traffic to v1 initially. Then switch to v2 by updating the Service selector.
# 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"}}}'k get endpoints app-service
# Should show green pod IPs after the switchExam 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.
Objective: Inject configuration into pods via environment variables and file mounts.
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.
# 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
EOFk 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.comenvFrom imports ALL keys. env[].valueFrom.configMapKeyRef imports a single key.Objective: Store sensitive data and inject it into pods securely.
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.
# 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
EOFk logs secret-pod
# DB_USER=admin
# S3cureP4ss!
# View secret (base64 encoded)
k get secret db-credentials -o jsonpath='{.data.password}' | base64 -dstringData in YAML for plain text values (Kubernetes encodes them).defaultMode (e.g., 0400) to limit file permissions.Opaque (default), kubernetes.io/tls, kubernetes.io/dockerconfigjson.Objective: Run containers with specific user/group IDs and capability restrictions.
Create a pod with the following security constraints:
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: {}
EOFk 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)Objective: Control CPU and memory allocation for containers.
Create a pod named resource-pod with:
Then create a pod that exceeds its memory limit and observe OOMKill.
# 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"
EOFk 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: OOMKilledObjective: Set default resource limits for a namespace.
Create a LimitRange in the dev namespace that sets:
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# 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)Objective: Limit total resource consumption in a namespace.
Create a ResourceQuota in the dev namespace that limits:
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"
EOFk get resourcequota dev-quota -n dev
k describe resourcequota dev-quota -n dev
# Shows Used vs Hard for each resourceObjective: Control API access for pods using ServiceAccounts.
Create a ServiceAccount named app-runner and a pod that uses it. Verify the token is mounted.
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"]
EOFk 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
# defaultObjective: Create configuration that cannot be changed after creation.
Create an immutable ConfigMap and verify it cannot be modified.
k apply -f - <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
name: app-version
data:
version: "2.5.0"
build: "12345"
immutable: true
EOF# 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.0Get the full Kubernetes CKAD Lab Workbook and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.