What You'll Build
In this lab you will operate the IPL scorecard Deployment through its full lifecycle of update, failure, and recovery, developing the operational reflexes that distinguish an engineer who runs Kubernetes from one who only deploys to it. You will perform four scenarios: a successful rolling update with annotated change cause and real-time progress monitoring; a simulated failed rollout where a bad image tag causes CrashLoopBackOff and the rollout halts; a rollback to the last stable revision; and a health probe tuning session that modifies `initialDelaySeconds`, `failureThreshold`, and `periodSeconds` on a slow-starting service to eliminate false-positive liveness probe restarts without increasing the real failure detection window beyond an acceptable SLA. Each scenario is designed around a realistic operational situation where the correct action depends on correctly reading the Kubernetes resource hierarchy.
The probe tuning scenario addresses one of the most common misconfigurations in production Kubernetes deployments: liveness probes that are too aggressive for the application's actual startup time, causing Kubernetes to restart perfectly healthy containers during load spikes or cold starts. The probe values in the Deployment manifest from Lesson 16 are conservative starting points; this lab teaches you to measure actual startup time under load, calculate the minimum `initialDelaySeconds` that avoids false positives, and verify that the resulting failure detection window still satisfies the application's availability SLA — the analytical discipline that separates probe values based on measurement from probe values based on guesswork.
Prerequisites
- The IPL scorecard platform from M3 Exercise running and verified — all Pods in Ready state in the `ipl-production` namespace before beginning any scenario in this lab.
- Two additional image tags pre-built and loaded into the cluster: one that represents a 'slow start' variant with a 20-second startup delay (for probe tuning) and one non-existent tag `ipl-scorecard:bad-tag` (to simulate a failed pull for the rollback scenario).
- A terminal window running `kubectl get pods -n ipl-production -w` to observe Pod state transitions in real time throughout all four scenarios.
- Prometheus and Grafana running with the kube-state-metrics exporter to observe `kube_deployment_status_replicas_ready` and `kube_pod_container_status_restarts_total` metrics — optional but recommended for the probe tuning scenario.
- A text editor open with the Deployment manifest from M3 Exercise for the probe tuning scenario — you will modify `initialDelaySeconds`, `periodSeconds`, and `failureThreshold` values in three measured iterations.
Setup & Project Structure
Verify the baseline state before running any scenario: all Pods must be Ready, the Deployment's rollout history must show the initial deployment, and the ResourceQuota must show sufficient headroom for the rolling update to create surge Pods. Running a scenario against an already-degraded cluster produces compound failures whose root cause is ambiguous.
# Verify baseline before beginning scenarios
# All Pods ready
kubectl get pods -n ipl-production -o wide
# Expected: all Pods in Running state, READY column shows 1/1 for all
# Deployment shows healthy status
kubectl get deployment ipl-api -n ipl-production
# Expected: READY=2/2, UP-TO-DATE=2, AVAILABLE=2
# Rollout history shows initial deployment
kubectl rollout history deployment/ipl-api -n ipl-production
# Expected: REVISION 1, CHANGE-CAUSE: "initial deployment from M3 exercise"
# ResourceQuota has headroom for rolling update (maxSurge=1 requires one more Pod slot)
kubectl describe resourcequota ipl-production-quota -n ipl-production | grep "count/pods"
# Expected: count/pods: 4/20 — plenty of headroom for 1 surge Pod
# Create the "slow-start" image variant for probe tuning
cat > Dockerfile.slowstart << 'EOF'
FROM localhost:5001/ipl-scorecard:latest
# Add artificial startup delay to simulate slow JVM/Python import time
ENTRYPOINT ["sh", "-c", "echo 'Simulating slow startup...'; sleep 20; uvicorn main:app --host 0.0.0.0 --port 8000"]
EOF
docker build -f Dockerfile.slowstart -t localhost:5001/ipl-scorecard:slow-start .
kind load docker-image localhost:5001/ipl-scorecard:slow-start --name ipl-cluster
echo "Baseline verified. Ready to begin lab scenarios."
# Open a watch terminal in a second window:
# kubectl get pods -n ipl-production -wStep 1 — Foundation
Perform a successful rolling update with annotated change cause, monitoring the rollout in real time. The annotate-before-apply discipline is critical: `kubernetes.io/change-cause` must be set before the rollout begins so that `kubectl rollout history` shows a meaningful description rather than an empty string. Watch the Pod state transitions — old Pods moving to Terminating, new Pods progressing through ContainerCreating → Running → Ready — to confirm that `maxUnavailable: 0` keeps two Ready Pods available at all times throughout the transition.
# Scenario 1: Successful rolling update with real-time monitoring
# ── Step 1: Annotate BEFORE applying ──────────────────────────────────────
kubectl annotate deployment/ipl-api kubernetes.io/change-cause="feat: Scenario 1 — update to verify rolling mechanics" -n ipl-production --overwrite
# ── Step 2: Trigger rolling update by changing the image ──────────────────
# (In practice this would be a new digest — we re-apply same image to trigger
# a new rollout while keeping the application functionally identical)
kubectl patch deployment ipl-api -n ipl-production -p '{"spec":{"template":{"metadata":{"annotations":{"rollout-trigger":"'$(date +%s)'"}}}}}'
# ── Step 3: Monitor rollout in real time ──────────────────────────────────
kubectl rollout status deployment/ipl-api -n ipl-production --timeout=120s
# ── Step 4: Verify rollout history ────────────────────────────────────────
kubectl rollout history deployment/ipl-api -n ipl-production
# REVISION CHANGE-CAUSE
# 1 initial deployment from M3 exercise
# 2 feat: Scenario 1 — update to verify rolling mechanics
# ── Step 5: Confirm availability was maintained throughout ─────────────────
# Availability was maintained if maxUnavailable=0 is set
kubectl describe deployment ipl-api -n ipl-production | grep "Max Unavailable"
# Expected: Max Unavailable: 0 — no Pod was ever removed before its replacement was Ready
echo "Scenario 1 complete: rolling update succeeded with zero downtime ✓" Step 2 — Core Logic
Simulate a failed rollout: update the Deployment to reference a non-existent image tag, watch Pods enter ImagePullBackOff and then ErrImagePull state, observe the rollout halt as the progressDeadlineSeconds timer approaches, then execute a rollback to the previous revision and confirm the Deployment returns to healthy status. The key diagnostic skill is distinguishing ImagePullBackOff (a transient registry authentication or network failure that may self-resolve) from ErrImagePull (a permanent 404 error indicating the tag does not exist) — a distinction that determines whether the correct response is to wait, to fix the registry credentials, or to roll back.
# Scenario 2: Failed rollout and recovery via rollback
# ── Step 1: Inject a bad image tag ────────────────────────────────────────
kubectl annotate deployment/ipl-api kubernetes.io/change-cause="INTENTIONAL FAILURE: bad image tag for lab" -n ipl-production --overwrite
kubectl set image deployment/ipl-api ipl-api=localhost:5001/ipl-scorecard:tag-does-not-exist -n ipl-production
# ── Step 2: Observe the failure progression ────────────────────────────────
kubectl get pods -n ipl-production -w
# New Pod enters: Pending → ContainerCreating → ErrImagePull → ImagePullBackOff
# Old Pods remain Running (maxUnavailable=0: old Pods not removed until new are Ready)
# Diagnose the failure
kubectl describe pod $(kubectl get pod -l app=ipl-api -n ipl-production --field-selector=status.phase=Pending -o name | head -1) -n ipl-production | tail -20
# Events show: Failed to pull image: tag does not exist
# ── Step 3: Check rollout status (shows stuck, not failed yet) ─────────────
kubectl rollout status deployment/ipl-api -n ipl-production
# Waiting for deployment "ipl-api" rollout to finish...
# (will eventually say 'has timed out' after progressDeadlineSeconds=600s)
# Don't wait 10 minutes — roll back immediately once ErrImagePull is confirmed
# ── Step 4: Rollback to last stable revision ──────────────────────────────
kubectl rollout undo deployment/ipl-api -n ipl-production
# deployment.apps/ipl-api rolled back
# ── Step 5: Verify recovery ────────────────────────────────────────────────
kubectl rollout status deployment/ipl-api -n ipl-production --timeout=60s
# deployment "ipl-api" successfully rolled out
kubectl get pods -n ipl-production
# All Pods Running and Ready again ✓
kubectl rollout history deployment/ipl-api -n ipl-production
# Shows the bad revision AND the rollback entry in history
echo "Scenario 2 complete: failed rollout detected and rolled back ✓" Step 3 — Integration & Enhancement
Tune the liveness probe for the slow-start image variant using a three-iteration measurement approach: deploy with aggressive probe values that trigger false positives, observe the restart count, calculate the correct `initialDelaySeconds` from measured startup time, redeploy with corrected values, and verify that the restart count drops to zero while a genuine liveness failure still triggers a restart within the SLA window. This iterative measurement-and-adjustment workflow is the professional approach to probe configuration — not guessing at values, but measuring the application's behaviour and deriving settings from data.
# Scenario 3: Health probe tuning for a slow-starting application
# ── Iteration 1: Deploy with aggressive probes (will cause false positives) ──
kubectl annotate deployment/ipl-api kubernetes.io/change-cause="lab: probe tuning iteration 1 — aggressive probes" -n ipl-production --overwrite
kubectl patch deployment ipl-api -n ipl-production -p '{
"spec": {
"template": {
"spec": {
"containers": [{
"name": "ipl-api",
"image": "localhost:5001/ipl-scorecard:slow-start",
"livenessProbe": {
"httpGet": {"path": "/health", "port": 8000},
"initialDelaySeconds": 5, # too short: 20s startup > 5s delay
"periodSeconds": 10,
"failureThreshold": 2 # kills after only 2 failures (20s)
}
}]
}
}
}
}'
# Wait 60 seconds, then observe false restarts
sleep 60
kubectl get pod -l app=ipl-api -n ipl-production
# Expected: RESTARTS column shows 2-3 — liveness killed the container
# before startup completed, triggering CrashLoopBackOff
# ── Measure actual startup time ────────────────────────────────────────────
kubectl logs -l app=ipl-api -n ipl-production --previous | grep "Uvicorn running"
# Find timestamp difference: sleep 20 + uvicorn startup ≈ 22-25 seconds
# ── Iteration 2: Apply corrected probe values based on measurement ──────────
kubectl annotate deployment/ipl-api kubernetes.io/change-cause="lab: probe tuning iteration 2 — measured values" -n ipl-production --overwrite
kubectl patch deployment ipl-api -n ipl-production -p '{
"spec": {
"template": {
"spec": {
"containers": [{
"name": "ipl-api",
"image": "localhost:5001/ipl-scorecard:slow-start",
"livenessProbe": {
"httpGet": {"path": "/health", "port": 8000},
"initialDelaySeconds": 35, # measured startup (25s) + 10s margin
"periodSeconds": 15, # check every 15s after initial delay
"failureThreshold": 3 # tolerate 3 failures before restart (45s window)
}
}]
}
}
}
}'
# ── Verify: no false restarts, genuine failure still detected ──────────────
sleep 120
RESTARTS=$(kubectl get pod -l app=ipl-api -n ipl-production -o jsonpath='{.items[0].status.containerStatuses[0].restartCount}')
echo "Restart count after probe tuning: $RESTARTS"
[ "$RESTARTS" -eq "0" ] && echo "No false positives ✓" || echo "Still false positives — increase initialDelaySeconds"
# Detection window: initialDelaySeconds=35 means first check at 35s
# failureThreshold=3 × periodSeconds=15 = 45s to detect a genuine failure
# Total worst-case detection time: 35s + 45s = 80 seconds — acceptable for this SLA
echo "Scenario 3 complete: probe tuning eliminates false positives ✓"
# Reset to standard image for clean lab completion
kubectl set image deployment/ipl-api ipl-api=localhost:5001/ipl-scorecard:latest -n ipl-production
kubectl rollout status deployment/ipl-api -n ipl-productionStep 4 — Testing & Verification
Run the final lab checklist confirming all four scenarios completed successfully: the rollout history shows the expected revision sequence, the Deployment is healthy after rollback, the probe configuration no longer causes false restarts, and the application serves correct API responses. The lab is complete when all checklist items pass — and more importantly, when you can explain the operational reasoning behind each scenario without consulting the lab instructions.
# Final lab verification checklist
echo "=== M3 Lab — Rolling Updates and Probe Tuning Verification ==="
# 1. Rollout history shows all scenario revisions
echo -n "1. Rollout history complete: "
REVISIONS=$(kubectl rollout history deployment/ipl-api -n ipl-production | grep -v "REVISION" | wc -l)
[ "$REVISIONS" -ge "4" ] && echo "PASS ✓ ($REVISIONS revisions)" || echo "FAIL ✗"
# 2. Deployment is healthy after rollback
echo -n "2. Deployment healthy post-rollback: "
READY=$(kubectl get deployment ipl-api -n ipl-production -o jsonpath='{.status.readyReplicas}')
DESIRED=$(kubectl get deployment ipl-api -n ipl-production -o jsonpath='{.spec.replicas}')
[ "$READY" -eq "$DESIRED" ] && echo "PASS ✓ ($READY/$DESIRED ready)" || echo "FAIL ✗"
# 3. No containers in CrashLoopBackOff (probe tuning resolved false restarts)
echo -n "3. No CrashLoopBackOff pods: "
CRASH=$(kubectl get pods -n ipl-production --field-selector=status.phase=Running -o jsonpath='{.items[*].status.containerStatuses[*].state.waiting.reason}' | grep -c "CrashLoopBackOff" || true)
[ "$CRASH" -eq "0" ] && echo "PASS ✓" || echo "FAIL ✗ ($CRASH crashing)"
# 4. API responds correctly after all scenario transitions
echo -n "4. API serves correct responses: "
kubectl port-forward service/ipl-api 8080:8000 -n ipl-production &
PF_PID=$!
sleep 3
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health)
kill $PF_PID 2>/dev/null
[ "$STATUS" -eq "200" ] && echo "PASS ✓ (HTTP 200)" || echo "FAIL ✗ (HTTP $STATUS)"
# 5. PDB protects the Deployment (verify it exists)
echo -n "5. PodDisruptionBudget present: "
kubectl get pdb -n ipl-production 2>/dev/null | grep -q "ipl-api" && echo "PASS ✓" || echo "FAIL ✗ — apply PDB from Lesson 16"
echo "=== Lab complete ==="
echo ""
echo "Operational skills demonstrated:"
echo " - Rolling update with annotated change cause ✓"
echo " - Failed rollout detection (ErrImagePull) ✓"
echo " - Rollback to stable revision ✓"
echo " - Probe tuning from measurement, not guesswork ✓" Warning: Never set `livenessProbe.failureThreshold` to 1 in production. A single probe failure is not evidence of a genuine application liveness failure — it may be a momentary network hiccup, a brief garbage collection pause, or a probe executor scheduling delay on a loaded node. A `failureThreshold` of 1 will restart containers during normal transient network conditions, creating unnecessary downtime and masking real performance issues behind a noisy restart count. The minimum recommended `failureThreshold` for production is 3, giving the probe three consecutive failures over `failureThreshold × periodSeconds` before restarting, which is enough to filter transient blips while still detecting genuine application hangs within an acceptable window.
Extension Challenge: Implement a canary deployment for the IPL API using Kubernetes label selectors and weighted traffic splitting. Create a second Deployment named `ipl-api-canary` with one replica and the new image tag, using the same labels as the primary Deployment so both Deployments' Pods receive traffic from the existing Service. The canary receives approximately 25% of traffic (1 of 4 total Pods) without any changes to the Service definition. Monitor the canary's error rate separately using a label selector on the Pod label `version: canary` in Prometheus, and only proceed to a full rollout if the canary's error rate matches the baseline. This is the manual equivalent of the Argo Rollouts operator's canary strategy, which M4 will cover with automated traffic weight management.
- Always annotate `kubernetes.io/change-cause` before every rollout — the annotation becomes the rollout history entry that enables post-mortem root cause analysis by correlating incident timelines with specific deployment revisions.
- ErrImagePull indicates a permanent 404 (tag not found) while ImagePullBackOff indicates exponential backoff after any pull failure — ErrImagePull warrants immediate rollback, while ImagePullBackOff may resolve with a registry fix.
- Kubernetes never automatically rolls back a failed rollout — it halts progress after `progressDeadlineSeconds` and marks the Deployment Failed, requiring an explicit `kubectl rollout undo` decision from an operator.
- Probe tuning requires measurement: deploy with a watch on `kube_pod_container_status_restarts_total`, observe actual startup time, set `initialDelaySeconds` to measured startup + margin, and verify zero restarts before considering the configuration correct.
- The detection window for a genuine liveness failure is `initialDelaySeconds + (failureThreshold × periodSeconds)` — balance this against the desired time-to-restart SLA to avoid both false positives and delayed genuine failure detection.
- A `failureThreshold` of 1 causes restarts on any transient probe failure; production minimum is 3, providing three consecutive check failures over the period window as evidence before triggering a container restart.