Kubernetes Workshop: Intermediate – Exercises with Solutions
Reference: 02-intermediate-EN.md
Difficulty level: Intermediate
Format: Hands-on lab with sample solutions and walkthroughs
Goal of this exercise document
This exercise document complements the Intermediate workshop with practical tasks, solutions, and walkthroughs. The focus is on workload selection, Init Containers, sidecars, Ephemeral Containers, probes, graceful shutdown, PodDisruptionBudgets, requests, limits, QoS, LimitRange, ResourceQuota, scheduling, NetworkPolicy, DNS, Ingress or Gateway API, storage, Helm, Kustomize, and systematic troubleshooting.
Prerequisites
You need:
- a lab cluster with a working CNI
- CoreDNS
kubectl- optional:
helm - optional:
kustomizeorkubectl kustomize - optional: a StorageClass
- optional: an Ingress Controller or Gateway Controller
Recommended Namespace:
export NS=workshop-intermediate
kubectl create namespace "$NS"
kubectl config set-context --current --namespace="$NS"
Exercise overview
| No. | Topic | Result |
|---|---|---|
| 1 | Workload selection | Deployment, Job, and CronJob distinguished technically |
| 2 | Init Container and sidecar | Pod lifecycles traced |
| 3 | Probes and lifecycle | Startup, Readiness, and Liveness cleanly separated |
| 4 | Resources, QoS, LimitRange, Quota | Namespace boundaries and scheduling impact checked |
| 5 | Scheduling | Node Labels, nodeSelector, Taints, and Tolerations tested |
| 6 | NetworkPolicy and DNS | Default Deny plus targeted allows created |
| 7 | Ingress or Gateway | HTTP app exposed externally or cleanly classified |
| 8 | PVC and StatefulSet | persistent state tested |
| 9 | Helm and Kustomize | rendered manifests compared |
| 10 | Troubleshooting course | typical errors systematically fixed |
Part A – Exercises
Exercise 1: Select the workload resource
Goal
You decide whether Deployment, Job, or CronJob is technically appropriate.
Scenario
You need to model three requirements:
- A web server must run continuously and be updated with rolling updates.
- A data migration must run once and then finish.
- A cleanup task must run every hour.
Task
- Create a minimal manifest for each case.
- Apply the manifests.
- Check status and Events.
- Document why the selected resource fits.
Exercise 2: Classify Init Container and sidecar
Goal
You understand when an Init Container must run before the application and when an additional container shares the same Pod lifecycle.
Task
Create a Pod where:
- an Init Container writes a file to an
emptyDir, - the App Container reads this file,
- a sidecar container periodically writes a heartbeat file.
Check:
- container startup order,
- logs of all containers,
- contents of the shared volume.
Exercise 3: Separate probes correctly
Goal
You correct a faulty probe configuration that causes restarts.
Scenario
A sample app starts slowly. The Liveness Probe checks the same path as Readiness too early.
Task
- Create a Deployment with an intentionally too aggressive Liveness Probe.
- Observe Events and restarts.
- Add a Startup Probe.
- Separate Readiness and Liveness conceptually.
- Explain why Readiness controls traffic and Liveness triggers restarts.
Exercise 4: Resources, QoS, LimitRange, and ResourceQuota
Goal
You set requests and limits, observe QoS classes, and create Namespace boundaries.
Task
- Create a LimitRange with default requests and limits.
- Create a ResourceQuota for CPU, memory, and Pod count.
- Roll out three Pods:
- without explicit resources,
- with requests lower than limits,
- with requests equal to limits.
- Determine the QoS classes.
- Intentionally create a Quota violation.
Exercise 5: Scheduling with Node Labels, nodeSelector, Taint, and Toleration
Goal
You control Pod placement and interpret Pending Events.
Task
- Label a lab Node with
workshop-role=apps. - Create a Pod with a matching
nodeSelector. - Create a Pod with a non-matching
nodeSelector. - Set a Taint on the Node.
- Create one Pod without a Toleration and one with a Toleration.
- Document the Events.
Exercise 6: NetworkPolicy Default Deny plus DNS allow
Goal
You create a Default Deny policy and then explicitly allow DNS and app traffic.
Task
- Create two Deployments:
frontendandbackend. - Create Services for both.
- Check that
frontendcan reachbackend. - Create a Default Deny policy for Ingress and Egress.
- Check that traffic is blocked.
- Allow DNS Egress.
- Allow
frontendtobackendon port 8080. - Check again.
Exercise 7: Provide Ingress or Gateway
Goal
You expose an HTTP application through Ingress or alternatively describe the Gateway API resources if no controller is installed.
Task
- Check whether an Ingress Controller or Gateway Controller is present.
- Deploy an HTTP app.
- Create either an Ingress resource or a Gateway/HTTPRoute example.
- Check controller Events, backend Service, and routing.
- Document why an Ingress resource has no effect without a controller.
Exercise 8: Use PVC and StatefulSet
Goal
You run a small stateful workload with a PVC and check whether data survives Pod recreation.
Task
- Check existing StorageClasses.
- Create a StatefulSet with
volumeClaimTemplates. - Write a file to the volume.
- Delete the Pod.
- After recreation, check whether the file still exists.
- Document why a
hostPathlab is not a production assumption.
Exercise 9: Compare Helm and Kustomize
Goal
You render manifests before applying them and compare Helm with Kustomize.
Task
- Create a small Kustomize base and a
devoverlay. - Render the overlay.
- Create a minimal Helm chart or use a local sample chart.
- Render the Helm chart.
- Compare reviewability, parameterization, and error risk.
Exercise 10: Troubleshooting course
Goal
You examine five typical error patterns methodically.
Error patterns
- Pod stays
Pendingbecause of an incorrectnodeSelector. - Pod shows
ImagePullBackOffbecause of a wrong image tag. - Pod shows
CrashLoopBackOffbecause of a faulty startup command. - PVC stays
Pendingbecause of an incorrect StorageClass. - Service has no Endpoints because of a wrong Selector.
Task
For each error, document:
- symptom,
- diagnostic command,
- relevant Events or logs,
- cause,
- fix,
- validation.
Part B – Solutions and walkthroughs
Solution 1: Select the workload resource
Deployment for a continuously running web server
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: workshop-intermediate
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
Job for a one-time migration
apiVersion: batch/v1
kind: Job
metadata:
name: migration
namespace: workshop-intermediate
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: migration
image: busybox:1.36
command: ["sh", "-c", "echo Migration running; sleep 3; echo Migration complete"]
CronJob for hourly cleanup
apiVersion: batch/v1
kind: CronJob
metadata:
name: cleanup
namespace: workshop-intermediate
spec:
schedule: "0 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: cleanup
image: busybox:1.36
command: ["sh", "-c", "date; echo cleanup"]
Check:
kubectl apply -f deployment.yml
kubectl apply -f job.yml
kubectl apply -f cronjob.yml
kubectl get deployment,job,cronjob,pod
kubectl logs job/migration
Technical decision:
| Requirement | Resource | Reason |
|---|---|---|
| continuously running HTTP service | Deployment | replicas, rollouts, rollbacks |
| one-time migration | Job | completed state is technically desired |
| hourly cleanup | CronJob | time-based Job creation |
Solution 2: Init Container and sidecar
Manifest:
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
namespace: workshop-intermediate
labels:
app: lifecycle-demo
spec:
volumes:
- name: shared
emptyDir: {}
initContainers:
- name: init-write
image: busybox:1.36
command: ["sh", "-c", "echo prepared > /shared/init.txt"]
volumeMounts:
- name: shared
mountPath: /shared
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "while true; do cat /shared/init.txt; sleep 30; done"]
volumeMounts:
- name: shared
mountPath: /shared
- name: sidecar
image: busybox:1.36
command: ["sh", "-c", "while true; do date > /shared/heartbeat.txt; sleep 10; done"]
volumeMounts:
- name: shared
mountPath: /shared
Check:
kubectl apply -f lifecycle-demo.yml
kubectl get pod lifecycle-demo
kubectl describe pod lifecycle-demo
kubectl logs lifecycle-demo -c init-write
kubectl logs lifecycle-demo -c app
kubectl exec lifecycle-demo -c app -- ls -l /shared
kubectl exec lifecycle-demo -c app -- cat /shared/heartbeat.txt
Explanation:
- Init Containers run before App Containers and must finish successfully.
- Sidecars run in parallel with the application.
emptyDirshares files within the same Pod, but it does not survive Pod deletion.
Solution 3: Correct probes
Robust sample manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: probe-demo
namespace: workshop-intermediate
spec:
replicas: 1
selector:
matchLabels:
app: probe-demo
template:
metadata:
labels:
app: probe-demo
spec:
containers:
- name: app
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["netexec", "--http-port=8080"]
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 2
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
Check:
kubectl apply -f probe-demo.yml
kubectl rollout status deployment/probe-demo
kubectl describe pod -l app=probe-demo
kubectl get pods -w
Sample explanation:
- Startup Probe protects slow starts; while it is not successful, Liveness and Readiness are not active.
- Readiness decides whether a Pod receives Endpoints for Services.
- Liveness decides whether kubelet restarts the container.
- A too aggressive Liveness Probe creates restart loops.
Solution 4: Resources, QoS, LimitRange, and ResourceQuota
LimitRange:
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: workshop-intermediate
spec:
limits:
- type: Container
defaultRequest:
cpu: 100m
memory: 128Mi
default:
cpu: 500m
memory: 256Mi
max:
cpu: "1"
memory: 512Mi
ResourceQuota:
apiVersion: v1
kind: ResourceQuota
metadata:
name: namespace-quota
namespace: workshop-intermediate
spec:
hard:
pods: "5"
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "3"
limits.memory: 2Gi
Check:
kubectl apply -f limitrange.yml
kubectl apply -f quota.yml
kubectl describe limitrange defaults
kubectl describe resourcequota namespace-quota
Pod with Burstable QoS:
apiVersion: v1
kind: Pod
metadata:
name: burstable-pod
namespace: workshop-intermediate
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
Pod with Guaranteed QoS:
apiVersion: v1
kind: Pod
metadata:
name: guaranteed-pod
namespace: workshop-intermediate
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 100m
memory: 128Mi
Check QoS:
kubectl apply -f burstable-pod.yml
kubectl apply -f guaranteed-pod.yml
kubectl get pod burstable-pod -o jsonpath='{.status.qosClass}{"\n"}'
kubectl get pod guaranteed-pod -o jsonpath='{.status.qosClass}{"\n"}'
Create a Quota violation:
apiVersion: v1
kind: Pod
metadata:
name: too-large
namespace: workshop-intermediate
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests:
cpu: "2"
memory: 2Gi
limits:
cpu: "2"
memory: 2Gi
Expected result:
kubectl apply -f too-large.yml
The API Server rejects the resource because of ResourceQuota.
Solution 5: Scheduling
Select Node:
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl label node "$NODE" workshop-role=apps
kubectl get nodes --show-labels | grep workshop-role
Pod with matching nodeSelector:
apiVersion: v1
kind: Pod
metadata:
name: scheduled-ok
namespace: workshop-intermediate
spec:
nodeSelector:
workshop-role: apps
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
Pod with non-matching nodeSelector:
apiVersion: v1
kind: Pod
metadata:
name: scheduled-pending
namespace: workshop-intermediate
spec:
nodeSelector:
workshop-role: does-not-exist
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
Check:
kubectl apply -f scheduled-ok.yml
kubectl apply -f scheduled-pending.yml
kubectl get pods -o wide
kubectl describe pod scheduled-pending
Set Taint:
kubectl taint node "$NODE" dedicated=apps:NoSchedule
Pod with Toleration:
apiVersion: v1
kind: Pod
metadata:
name: tolerated
namespace: workshop-intermediate
spec:
nodeSelector:
workshop-role: apps
tolerations:
- key: dedicated
operator: Equal
value: apps
effect: NoSchedule
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
Cleanup:
kubectl delete pod scheduled-ok scheduled-pending tolerated --ignore-not-found
kubectl taint node "$NODE" dedicated=apps:NoSchedule-
kubectl label node "$NODE" workshop-role-
Explanation:
nodeSelectoris hard: no matching Node meansPending.- A Taint rejects Pods.
- A Toleration allows a Pod to tolerate a Taint; by itself, it does not force placement.
Solution 6: NetworkPolicy and DNS
Sample app:
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: workshop-intermediate
spec:
replicas: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["netexec", "--http-port=8080"]
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: workshop-intermediate
spec:
selector:
app: backend
ports:
- name: http
port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: workshop-intermediate
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: curlimages/curl:8.8.0
command: ["sh", "-c", "sleep 3600"]
Test before Policy:
kubectl apply -f net-demo.yml
kubectl wait --for=condition=Available deployment/backend --timeout=90s
kubectl wait --for=condition=Available deployment/frontend --timeout=90s
FRONTEND=$(kubectl get pod -l app=frontend -o jsonpath='{.items[0].metadata.name}')
kubectl exec "$FRONTEND" -- curl -s http://backend:8080/hostname
Default Deny:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: workshop-intermediate
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Allow DNS Egress:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: workshop-intermediate
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Allow frontend to backend:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: workshop-intermediate
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-egress-to-backend
namespace: workshop-intermediate
spec:
podSelector:
matchLabels:
app: frontend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 8080
Check:
kubectl apply -f default-deny.yml
kubectl apply -f allow-dns-egress.yml
kubectl apply -f allow-frontend-backend.yml
kubectl exec "$FRONTEND" -- nslookup backend
kubectl exec "$FRONTEND" -- curl -s --max-time 5 http://backend:8080/hostname
Note:
NetworkPolicies are effective only if the CNI plugin enforces them.
Solution 7: Ingress or Gateway
Check controller:
kubectl get ingressclass
kubectl get gatewayclass 2>/dev/null || true
kubectl get pods -A | grep -Ei 'ingress|gateway|traefik|nginx|envoy|haproxy'
Ingress example:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
namespace: workshop-intermediate
spec:
rules:
- host: web.localtest.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backend
port:
number: 8080
Check:
kubectl apply -f ingress.yml
kubectl describe ingress web
kubectl get ingress web
Gateway API example if Gateway API is installed:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: web
namespace: workshop-intermediate
spec:
parentRefs:
- name: shared-gateway
namespace: gateway-system
hostnames:
- web.localtest.me
rules:
- backendRefs:
- name: backend
port: 8080
Technical explanation:
- Ingress is only a rule description.
- An Ingress Controller must translate that rule into proxy/load-balancer configuration.
- Gateway API separates infrastructure responsibility more clearly from application routing.
Solution 8: PVC and StatefulSet
Check StorageClass:
kubectl get storageclass
StatefulSet:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: data-demo
namespace: workshop-intermediate
spec:
serviceName: data-demo
replicas: 1
selector:
matchLabels:
app: data-demo
template:
metadata:
labels:
app: data-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "while true; do sleep 3600; done"]
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
Apply and test:
kubectl apply -f statefulset.yml
kubectl rollout status statefulset/data-demo
kubectl exec data-demo-0 -- sh -c 'echo persistence-test > /data/test.txt'
kubectl delete pod data-demo-0
kubectl wait --for=condition=Ready pod/data-demo-0 --timeout=120s
kubectl exec data-demo-0 -- cat /data/test.txt
Expected state:
The file still exists after Pod recreation because the PVC is reattached to data-demo-0.
Classification:
- StatefulSets provide stable Pod names and stable PVC assignment.
- A local
hostPathvolume in a lab is convenient, but it is not portable and not production storage.
Solution 9: Helm and Kustomize
Kustomize structure:
kustomize-demo/
base/
deployment.yml
service.yml
kustomization.yml
overlays/
dev/
kustomization.yml
base/kustomization.yml:
resources:
- deployment.yml
- service.yml
overlays/dev/kustomization.yml:
resources:
- ../../base
namePrefix: dev-
labels:
- pairs:
environment: dev
images:
- name: nginx
newTag: "1.27"
Render:
kubectl kustomize kustomize-demo/overlays/dev
kubectl apply -k kustomize-demo/overlays/dev --dry-run=server
Minimal Helm chart:
helm create webchart
helm template web ./webchart
helm lint ./webchart
Comparison:
| Criterion | Helm | Kustomize |
|---|---|---|
| Model | package with templates and values | base/overlay without template language |
| Strength | many parameters, releases, chart ecosystem | close to YAML, easy to review |
| Risk | complex templates can be hard to verify | overlays can become confusing |
| Mandatory check | helm template, helm lint |
kubectl kustomize, dry-run |
Solution 10: Troubleshooting course
Sample diagnosis:
| Error pattern | Diagnosis | Cause | Fix |
|---|---|---|---|
Pending |
kubectl describe pod |
no Node matches nodeSelector |
correct Label or remove Selector |
ImagePullBackOff |
Events in the Pod | image/tag/registry wrong | set valid image |
CrashLoopBackOff |
kubectl logs --previous |
process exits with an error | correct startup command/app configuration |
PVC Pending |
kubectl describe pvc |
StorageClass missing or provisioner missing | correct or provide StorageClass |
| Service without Endpoints | kubectl get endpoints, check Labels |
Selector does not match | correct Selector or Pod Labels |
Standard sequence:
kubectl get all
kubectl get pod -o wide
kubectl describe pod <pod>
kubectl get events --sort-by=.metadata.creationTimestamp
kubectl logs <pod>
kubectl logs <pod> --previous
kubectl get svc,endpoints,endpointslices
kubectl describe pvc <pvc>
Cleanup
kubectl delete namespace workshop-intermediate
Assessment checklist
| Criterion | Completed? |
|---|---|
| Workload resources distinguished technically | ☐ |
| Init Container, sidecar, and App Container explained | ☐ |
| Probes separated correctly | ☐ |
| Requests, limits, QoS, LimitRange, and Quota applied | ☐ |
| Scheduling Events interpreted | ☐ |
| NetworkPolicy including DNS considered | ☐ |
| Ingress/Gateway Controller role explained | ☐ |
| PVC and StatefulSet tested | ☐ |
| Helm and Kustomize rendered | ☐ |
| Troubleshooting course documented | ☐ |