Skip to content

Module 7 — Pods & Workloads

Overview

Pods are the smallest deployable unit in Kubernetes. Workload resources (Deployments, ReplicaSets, DaemonSets, StatefulSets, Jobs, CronJobs) manage pods at scale. This module covers the pod lifecycle, every workload type, and rolling update/rollback mechanics — all heavily tested on the CKA exam.


1. Pod Lifecycle and Phases

1.1 Pod Phases

A pod moves through these phases during its lifetime:

                   ┌──────────┐
         ┌─────────│ Pending  │
         │         └────┬─────┘
         │              │ scheduled + images pulled
         │              ▼
         │         ┌──────────┐
         │         │ Running  │
         │         └────┬─────┘
         │              │
         │    ┌─────────┼──────────┐
         │    ▼         ▼          ▼
         │ ┌─────────┐ ┌────────┐ ┌────────┐
         │ │Succeeded│ │ Failed │ │Unknown │
         │ └─────────┘ └────────┘ └────────┘
    ┌──────────┐
    │  Failed  │  (image pull error, insufficient resources, etc.)
    └──────────┘
Phase Meaning
Pending Pod accepted but not yet running — waiting for scheduling, image pull, or volume mount
Running At least one container is running, starting, or restarting
Succeeded All containers terminated successfully (exit code 0) — typical for Jobs
Failed All containers terminated, at least one with a non-zero exit code
Unknown Pod status can't be determined — usually a node communication failure
# Check pod phase
kubectl get pod <name> -o jsonpath='{.status.phase}'

1.2 Container States

Within a running pod, each container has its own state:

State Description
Waiting Container not yet running (pulling image, applying config)
Running Container executing normally
Terminated Container finished execution (success or failure)
1
2
3
4
5
# Check container state details
kubectl get pod <name> -o jsonpath='{.status.containerStatuses[0].state}'

# Human-readable
kubectl describe pod <name> | grep -A5 State:

1.3 Common Status Conditions

What you see in kubectl get pods STATUS column:

Status Phase Meaning
Pending Pending Waiting to be scheduled
ContainerCreating Pending Scheduled, pulling images or mounting volumes
Running Running All containers started
Completed Succeeded All containers exited with code 0
Error Failed Container exited with non-zero code
CrashLoopBackOff Running Container keeps crashing, kubelet backs off restart
ImagePullBackOff Pending Can't pull the container image
ErrImagePull Pending First image pull failure
OOMKilled Running/Failed Container exceeded memory limit
Terminating Running Pod is being deleted (graceful shutdown)

1.4 Restart Policies

Policy Behavior Used by
Always (default) Restart container on any exit Deployments, ReplicaSets, DaemonSets, StatefulSets
OnFailure Restart only on non-zero exit code Jobs
Never Never restart Jobs (when you want to inspect failures)
spec:
  restartPolicy: OnFailure

1.5 Pod Lifecycle Hooks and Probes

Container Start
      ├── postStart hook (runs immediately after container starts)
      ├── startupProbe (runs until success, then stops)
      │         │
      │         ▼ (once startup passes)
      ├── livenessProbe (runs periodically — restarts container on failure)
      ├── readinessProbe (runs periodically — removes from Service endpoints on failure)
Container Stop
      └── preStop hook (runs before SIGTERM is sent)
Probe Purpose On failure
startupProbe Detect slow-starting containers Container killed + restarted
livenessProbe Detect deadlocked/hung containers Container killed + restarted
readinessProbe Detect containers not ready to serve traffic Removed from Service endpoints (not killed)
spec:
  containers:
  - name: app
    image: nginx
    startupProbe:
      httpGet:
        path: /healthz
        port: 8080
      failureThreshold: 30
      periodSeconds: 10
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 10
    readinessProbe:
      httpGet:
        path: /ready
        port: 8080
      periodSeconds: 5

Probe types:

Type Example
httpGet httpGet: { path: /healthz, port: 8080 }
tcpSocket tcpSocket: { port: 3306 }
exec exec: { command: ["cat", "/tmp/healthy"] }
grpc grpc: { port: 50051 }

2. Creating Pods

2.1 Imperative Commands

# Create a pod
kubectl run nginx --image=nginx

# Create a pod with labels
kubectl run nginx --image=nginx --labels="app=web,tier=frontend"

# Create a pod and expose a port
kubectl run nginx --image=nginx --port=80

# Create a pod with a command
kubectl run busybox --image=busybox --command -- sleep 3600

# Create a pod with environment variables
kubectl run myapp --image=nginx --env="DB_HOST=mysql" --env="DB_PORT=3306"

# Dry-run to generate YAML (very useful on the exam)
kubectl run nginx --image=nginx --dry-run=client -o yaml > pod.yaml

# Create a temporary pod for debugging
kubectl run debug --image=busybox -it --rm --restart=Never -- sh

2.2 Minimal Pod YAML

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  containers:
  - name: nginx
    image: nginx:1.25
    ports:
    - containerPort: 80

2.3 Multi-Container Pods

Containers in the same pod share the network namespace (localhost) and can share volumes:

apiVersion: v1
kind: Pod
metadata:
  name: multi-container
spec:
  containers:
  - name: app
    image: nginx
    ports:
    - containerPort: 80
    volumeMounts:
    - name: shared-logs
      mountPath: /var/log/nginx
  - name: sidecar
    image: busybox
    command: ["sh", "-c", "tail -f /var/log/nginx/access.log"]
    volumeMounts:
    - name: shared-logs
      mountPath: /var/log/nginx
  volumes:
  - name: shared-logs
    emptyDir: {}

Common multi-container patterns:

Pattern Description Example
Sidecar Enhances the main container Log shipper, proxy
Ambassador Proxies network connections Local DB proxy
Adapter Transforms output Log format converter

2.4 Init Containers

Run before app containers start. All init containers must complete successfully before the main containers begin:

1
2
3
4
5
6
7
8
spec:
  initContainers:
  - name: wait-for-db
    image: busybox
    command: ["sh", "-c", "until nc -z mysql 3306; do sleep 2; done"]
  containers:
  - name: app
    image: myapp:latest

3. Workload Resources

3.1 ReplicaSet

Ensures a specified number of pod replicas are running at all times.

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-rs
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25

CKA Tip: You rarely create ReplicaSets directly. Deployments manage ReplicaSets for you. But you must understand how they work.

1
2
3
4
5
# List ReplicaSets
kubectl get rs

# Scale a ReplicaSet
kubectl scale rs nginx-rs --replicas=5

3.2 Deployment

The most common workload resource. Manages ReplicaSets and provides rolling updates and rollbacks.

1
2
3
4
5
Deployment
    ├── ReplicaSet (current) ──▶ Pod, Pod, Pod
    └── ReplicaSet (old)     ──▶ (scaled to 0 after rollout)

Imperative Creation

# Create a deployment
kubectl create deployment nginx --image=nginx --replicas=3

# Generate YAML
kubectl create deployment nginx --image=nginx --replicas=3 --dry-run=client -o yaml > deploy.yaml

# Scale
kubectl scale deployment nginx --replicas=5

# Update image
kubectl set image deployment/nginx nginx=nginx:1.26

# Expose as a service
kubectl expose deployment nginx --port=80 --target-port=80 --type=ClusterIP

Declarative YAML

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1            # max pods above desired during update
      maxUnavailable: 0      # max pods unavailable during update
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 250m
            memory: 256Mi

3.3 DaemonSet

Runs exactly one pod per node (or per matching node). Pods are automatically added/removed as nodes join/leave.

1
2
3
4
5
┌──────────┐  ┌──────────┐  ┌──────────┐
│  node-1  │  │  node-2  │  │  node-3  │
│          │  │          │  │          │
│ [ds-pod] │  │ [ds-pod] │  │ [ds-pod] │
└──────────┘  └──────────┘  └──────────┘

Common use cases: - Log collectors (Fluentd, Filebeat) - Monitoring agents (Prometheus node-exporter) - Network plugins (kube-proxy, Calico, Cilium) - Storage daemons (CSI node plugins)

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-collector
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: log-collector
  template:
    metadata:
      labels:
        app: log-collector
    spec:
      tolerations:
      - key: node-role.kubernetes.io/control-plane
        effect: NoSchedule       # run on control plane nodes too
      containers:
      - name: fluentd
        image: fluentd:latest
        volumeMounts:
        - name: varlog
          mountPath: /var/log
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
1
2
3
4
5
# List DaemonSets
kubectl get ds -n kube-system

# Check which nodes have the DaemonSet pod
kubectl get pods -l app=log-collector -o wide -n kube-system

CKA Tip: There's no kubectl create daemonset imperative command. Create a Deployment YAML with --dry-run, then change kind: Deployment to kind: DaemonSet and remove the replicas and strategy fields.

3.4 StatefulSet

For stateful applications that need stable network identities and persistent storage.

1
2
3
4
5
StatefulSet: mysql (replicas: 3)
    ├── mysql-0  ──▶  PVC: data-mysql-0  ──▶  PV
    ├── mysql-1  ──▶  PVC: data-mysql-1  ──▶  PV
    └── mysql-2  ──▶  PVC: data-mysql-2  ──▶  PV

Key differences from Deployments:

Feature Deployment StatefulSet
Pod names Random suffix (nginx-7d4f8b) Ordered index (mysql-0, mysql-1)
Creation order All at once Sequential (0, then 1, then 2)
Deletion order All at once Reverse sequential (2, then 1, then 0)
Stable network ID No Yes — <pod-name>.<headless-service>
Persistent storage Shared or none Per-pod PVC via volumeClaimTemplates
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql-headless     # required — headless Service name
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        ports:
        - containerPort: 3306
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "rootpass"
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:           # creates a PVC per pod
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

Requires a headless Service:

apiVersion: v1
kind: Service
metadata:
  name: mysql-headless
spec:
  clusterIP: None               # headless
  selector:
    app: mysql
  ports:
  - port: 3306

DNS records created:

1
2
3
mysql-0.mysql-headless.default.svc.cluster.local
mysql-1.mysql-headless.default.svc.cluster.local
mysql-2.mysql-headless.default.svc.cluster.local

3.5 Job

Runs a pod to completion — for batch processing, one-time tasks.

1
2
3
4
5
# Imperative
kubectl create job pi --image=perl -- perl -Mbignum=bpi -wle 'print bpi(2000)'

# Generate YAML
kubectl create job pi --image=perl --dry-run=client -o yaml > job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  completions: 1          # how many pods must complete successfully
  parallelism: 1          # how many pods run in parallel
  backoffLimit: 4          # max retries before marking as Failed
  activeDeadlineSeconds: 120  # timeout for the entire Job
  template:
    spec:
      restartPolicy: Never    # or OnFailure
      containers:
      - name: pi
        image: perl
        command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
Field Purpose
completions Total successful completions needed
parallelism Max pods running simultaneously
backoffLimit Max retries (default: 6)
activeDeadlineSeconds Hard timeout for the Job
1
2
3
4
5
6
# Check Job status
kubectl get jobs
kubectl describe job pi

# View completed pod logs
kubectl logs job/pi

3.6 CronJob

Runs Jobs on a schedule (cron syntax).

1
2
3
4
5
# Imperative
kubectl create cronjob backup --image=busybox --schedule="0 2 * * *" -- /bin/sh -c "echo backup"

# Generate YAML
kubectl create cronjob backup --image=busybox --schedule="0 2 * * *" --dry-run=client -o yaml > cj.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: backup
spec:
  schedule: "0 2 * * *"           # every day at 2:00 AM
  concurrencyPolicy: Forbid        # don't run if previous is still running
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  startingDeadlineSeconds: 200     # skip if missed by this many seconds
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: backup
            image: busybox
            command: ["/bin/sh", "-c", "echo backup completed"]

Cron schedule format:

1
2
3
4
5
6
7
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sun=0)
│ │ │ │ │
* * * * *
concurrencyPolicy Behavior
Allow (default) Multiple Jobs can run concurrently
Forbid Skip new Job if previous is still running
Replace Kill the running Job and start a new one

4. Rolling Updates and Rollbacks

4.1 Update Strategies

Strategy Behavior
RollingUpdate (default) Gradually replaces old pods with new ones
Recreate Kills all old pods first, then creates new ones (downtime)

4.2 Rolling Update Mechanics

Deployment: nginx (replicas: 3, maxSurge: 1, maxUnavailable: 0)
Image update: nginx:1.25 → nginx:1.26

Step 1: Create new RS, scale up 1 new pod
  Old RS: ●●● (3 pods)    New RS: ○ (1 pod starting)

Step 2: New pod ready → scale down 1 old pod
  Old RS: ●● (2 pods)     New RS: ● (1 pod ready)

Step 3: Scale up 1 more new pod
  Old RS: ●● (2 pods)     New RS: ●○ (1 ready, 1 starting)

Step 4: New pod ready → scale down 1 old pod
  Old RS: ● (1 pod)       New RS: ●● (2 pods ready)

Step 5: Scale up 1 more new pod
  Old RS: ● (1 pod)       New RS: ●●○ (2 ready, 1 starting)

Step 6: New pod ready → scale down last old pod
  Old RS: (0 pods)        New RS: ●●● (3 pods ready) ✓
Parameter Default Meaning
maxSurge 25% Max extra pods above desired count during update
maxUnavailable 25% Max pods that can be unavailable during update
1
2
3
4
5
6
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0      # zero-downtime: always have all replicas available

4.3 Triggering a Rolling Update

# Method 1: Update the image
kubectl set image deployment/nginx nginx=nginx:1.26

# Method 2: Edit the deployment
kubectl edit deployment nginx

# Method 3: Apply an updated YAML
kubectl apply -f deployment.yaml

# Method 4: Patch
kubectl patch deployment nginx -p '{"spec":{"template":{"spec":{"containers":[{"name":"nginx","image":"nginx:1.26"}]}}}}'

4.4 Monitoring a Rollout

# Watch rollout status (blocks until complete)
kubectl rollout status deployment/nginx

# Check rollout history
kubectl rollout history deployment/nginx
# REVISION  CHANGE-CAUSE
# 1         <none>
# 2         <none>

# See details of a specific revision
kubectl rollout history deployment/nginx --revision=2

# Record the change cause (add --record or annotate)
kubectl annotate deployment/nginx kubernetes.io/change-cause="Updated to nginx:1.26"

4.5 Rollback

1
2
3
4
5
6
7
8
9
# Rollback to the previous revision
kubectl rollout undo deployment/nginx

# Rollback to a specific revision
kubectl rollout undo deployment/nginx --to-revision=1

# Verify
kubectl rollout status deployment/nginx
kubectl describe deployment nginx | grep Image

4.6 Pause and Resume

For making multiple changes without triggering multiple rollouts:

1
2
3
4
5
6
7
8
9
# Pause the deployment
kubectl rollout pause deployment/nginx

# Make multiple changes
kubectl set image deployment/nginx nginx=nginx:1.26
kubectl set resources deployment/nginx -c nginx --limits=cpu=200m,memory=256Mi

# Resume — triggers a single rollout with all changes
kubectl rollout resume deployment/nginx

4.7 Revision History Limit

spec:
  revisionHistoryLimit: 10    # default: 10 — how many old ReplicaSets to keep

Old ReplicaSets (scaled to 0) are kept for rollback. Setting this to 0 means you can't rollback.


5. Useful Commands Reference

# --- Pods ---
kubectl get pods -o wide                    # show node, IP
kubectl get pods --show-labels              # show labels
kubectl get pods -l app=nginx               # filter by label
kubectl get pods --field-selector=status.phase=Running
kubectl describe pod <name>                 # detailed info
kubectl logs <pod>                          # container logs
kubectl logs <pod> -c <container>           # specific container in multi-container pod
kubectl logs <pod> --previous               # logs from previous crashed container
kubectl exec -it <pod> -- sh               # shell into pod
kubectl delete pod <pod> --grace-period=0 --force  # force delete

# --- Deployments ---
kubectl get deployments
kubectl describe deployment <name>
kubectl scale deployment <name> --replicas=5
kubectl autoscale deployment <name> --min=2 --max=10 --cpu-percent=80
kubectl rollout status deployment/<name>
kubectl rollout history deployment/<name>
kubectl rollout undo deployment/<name>

# --- DaemonSets ---
kubectl get ds -A                           # all namespaces
kubectl describe ds <name> -n kube-system

# --- StatefulSets ---
kubectl get sts
kubectl scale sts <name> --replicas=5
kubectl rollout status sts/<name>

# --- Jobs / CronJobs ---
kubectl get jobs
kubectl get cronjobs
kubectl describe job <name>
kubectl logs job/<name>
kubectl delete job <name>                   # also deletes completed pods
kubectl create job test-run --from=cronjob/backup  # manual trigger

6. Practice Exercises

Exercise 1 — Pod Lifecycle

# 1. Create a pod that exits successfully
kubectl run success --image=busybox --restart=Never -- echo "done"
kubectl get pod success    # STATUS: Completed

# 2. Create a pod that fails
kubectl run fail --image=busybox --restart=Never -- sh -c "exit 1"
kubectl get pod fail       # STATUS: Error

# 3. Create a pod with CrashLoopBackOff
kubectl run crash --image=busybox -- sh -c "exit 1"
# Wait 30 seconds
kubectl get pod crash      # STATUS: CrashLoopBackOff

# 4. Inspect the restart count
kubectl describe pod crash | grep "Restart Count"

# 5. Clean up
kubectl delete pod success fail crash

Exercise 2 — Deployments and Scaling

# 1. Create a deployment with 3 replicas
kubectl create deployment web --image=nginx:1.24 --replicas=3

# 2. Verify pods and ReplicaSet
kubectl get deploy,rs,pods

# 3. Scale to 5
kubectl scale deployment web --replicas=5

# 4. Update the image
kubectl set image deployment/web nginx=nginx:1.25

# 5. Watch the rollout
kubectl rollout status deployment/web

# 6. Check history
kubectl rollout history deployment/web

# 7. Rollback
kubectl rollout undo deployment/web

# 8. Verify the image is back to 1.24
kubectl describe deployment web | grep Image

# 9. Clean up
kubectl delete deployment web

Exercise 3 — DaemonSet

# 1. Generate a Deployment YAML and convert to DaemonSet
kubectl create deployment ds-test --image=busybox --dry-run=client -o yaml \
  -- sh -c "while true; do echo running; sleep 60; done" > ds.yaml

# 2. Edit ds.yaml:
#    - Change kind: Deployment → kind: DaemonSet
#    - Remove spec.replicas
#    - Remove spec.strategy

# 3. Apply
kubectl apply -f ds.yaml

# 4. Verify — one pod per node
kubectl get pods -o wide
kubectl get ds

# 5. Clean up
kubectl delete -f ds.yaml

Exercise 4 — Jobs and CronJobs

# 1. Create a Job that runs 5 completions with parallelism of 2
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
  name: counter
spec:
  completions: 5
  parallelism: 2
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: counter
        image: busybox
        command: ["sh", "-c", "echo Processing item; sleep 5"]
EOF

# 2. Watch pods being created
kubectl get pods -w

# 3. Check Job status
kubectl get job counter

# 4. Create a CronJob that runs every minute
kubectl create cronjob minute-job --image=busybox --schedule="*/1 * * * *" \
  -- echo "tick"

# 5. Wait 2 minutes and check
kubectl get cronjobs
kubectl get jobs
kubectl get pods

# 6. Manually trigger a Job from the CronJob
kubectl create job manual-run --from=cronjob/minute-job

# 7. Clean up
kubectl delete job counter
kubectl delete cronjob minute-job
kubectl delete job manual-run

Exercise 5 — Rolling Update with Zero Downtime

# 1. Create a deployment
kubectl create deployment rolling --image=nginx:1.24 --replicas=4

# 2. Set strategy to zero-downtime
kubectl patch deployment rolling -p '{"spec":{"strategy":{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0}}}}'

# 3. Update image and watch
kubectl set image deployment/rolling nginx=nginx:1.25
kubectl rollout status deployment/rolling

# 4. Verify all pods are on the new version
kubectl get pods -o jsonpath='{range .items[*]}{.spec.containers[0].image}{"\n"}{end}'

# 5. Rollback to revision 1
kubectl rollout undo deployment/rolling --to-revision=1

# 6. Verify
kubectl describe deployment rolling | grep Image

# 7. Clean up
kubectl delete deployment rolling

7. Key Takeaways for the CKA Exam

Point Detail
kubectl run creates a Pod Use kubectl create deployment for Deployments
--dry-run=client -o yaml Generate YAML for any resource — fastest way to start
Deployments manage ReplicaSets Don't create ReplicaSets directly
DaemonSet = one pod per node No imperative create — convert from Deployment YAML
StatefulSet needs a headless Service serviceName field is required
Jobs use restartPolicy: Never or OnFailure Never Always
kubectl rollout undo Rollback to previous or specific revision
maxSurge + maxUnavailable Control rolling update speed and availability
kubectl create job --from=cronjob/ Manually trigger a CronJob
Know probe types httpGet, tcpSocket, exec — and the difference between liveness, readiness, startup
Init containers run first All must succeed before app containers start

Previous: 06-kubeconfig-contexts.md — Kubeconfig & Contexts

Next: 08-scheduling.md — Scheduling