7. Practice Tasks¶
25 exam-style tasks. Set a 2-hour timer and try each one before opening its solution. Each task's weight is relative, like in the real exam. Aim for 66% or more of the total weight (see Scoring yourself).
Run these on a practice cluster: a kubeadm lab with 1 control plane + 1–2 workers, killer.sh, or Killercoda. Names, namespaces and paths are exact. In the real exam, getting them wrong costs the points.
| # | Domain | Weight | Topic |
|---|---|---|---|
| 1–5 | Cluster Architecture | 4–8% | etcd, upgrade, RBAC, CSR, Helm |
| 6–8 | Cluster Architecture | 3–5% | Kustomize, CRD, sysctl/runtime |
| 9–13 | Workloads & Scheduling | 3–5% | Rollback, HPA, sidecar, PriorityClass, scheduling |
| 14–18 | Services & Networking | 4–6% | NetworkPolicy, Gateway API, Ingress, DNS, Service |
| 19–20 | Storage | 3–4% | StorageClass, PV/PVC |
| 21–25 | Troubleshooting | 5–8% | Node, control plane, app, service, logs |
Cluster Architecture, Installation & Configuration¶
Task 1: etcd backup and restore (8%)¶
Take a snapshot of etcd running on the control plane at https://127.0.0.1:2379 and save it to /opt/etcd-snap.db. Then create a Deployment canary (nginx). Then restore the snapshot so that canary no longer exists.
Solution
ssh controlplane; sudo -i
grep -E 'cert-file|key-file|trusted-ca-file' /etc/kubernetes/manifests/etcd.yaml
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save /opt/etcd-snap.db
etcdutl snapshot status /opt/etcd-snap.db -w table
kubectl create deploy canary --image=nginx
etcdutl snapshot restore /opt/etcd-snap.db --data-dir /var/lib/etcd-restore
vim /etc/kubernetes/manifests/etcd.yaml # etcd-data hostPath -> /var/lib/etcd-restore
watch crictl ps # wait for etcd + apiserver
kubectl get deploy canary # NotFound = success
Task 2: Upgrade the control plane (7%)¶
Upgrade the control plane node controlplane from the current version to the next patch or minor version given (for example X.Y.Z). Upgrade kubeadm, kubelet and kubectl on that node only. Leave workers untouched.
Solution
kubectl drain controlplane --ignore-daemonsets
ssh controlplane; sudo -i
# If minor version changes: update /etc/apt/sources.list.d/kubernetes.list to vX.Y
apt-get update && apt-cache madison kubeadm
apt-mark unhold kubeadm && apt-get install -y kubeadm='X.Y.Z-*' && apt-mark hold kubeadm
kubeadm upgrade plan
kubeadm upgrade apply vX.Y.Z
apt-mark unhold kubelet kubectl && apt-get install -y kubelet='X.Y.Z-*' kubectl='X.Y.Z-*' && apt-mark hold kubelet kubectl
systemctl daemon-reload && systemctl restart kubelet
exit; exit
kubectl uncordon controlplane
kubectl get nodes # controlplane shows vX.Y.Z
Task 3: RBAC for a ServiceAccount (5%)¶
In namespace app-team, create ServiceAccount cicd. Allow it to create, update and delete Deployments, StatefulSets and DaemonSets in app-team only. Use a ClusterRole named deployment-clusterrole and a binding named cicd-binding.
Solution
k create ns app-team
k create sa cicd -n app-team
k create clusterrole deployment-clusterrole --verb=create,update,delete \
--resource=deployments,statefulsets,daemonsets
k create rolebinding cicd-binding -n app-team \
--clusterrole=deployment-clusterrole --serviceaccount=app-team:cicd
k auth can-i create deployments -n app-team --as=system:serviceaccount:app-team:cicd # yes
k auth can-i create deployments -n default --as=system:serviceaccount:app-team:cicd # no
Task 4: User certificate and kubeconfig (6%)¶
Create a CSR for user john (group devs) using the key /opt/john.key (generate it). Approve it, save the cert to /opt/john.crt, and give john permission to get,list pods in namespace dev. Add a context john to your kubeconfig.
Solution
openssl genrsa -out /opt/john.key 2048
openssl req -new -key /opt/john.key -subj "/CN=john/O=devs" -out /opt/john.csr
cat <<EOF | k apply -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: john
spec:
request: $(base64 -w0 < /opt/john.csr)
signerName: kubernetes.io/kube-apiserver-client
usages: ["client auth"]
EOF
k certificate approve john
k get csr john -o jsonpath='{.status.certificate}' | base64 -d > /opt/john.crt
k create ns dev
k create role pod-reader -n dev --verb=get,list --resource=pods
k create rolebinding john-pods -n dev --role=pod-reader --user=john
k config set-credentials john --client-key=/opt/john.key --client-certificate=/opt/john.crt --embed-certs
k config set-context john --cluster=kubernetes --user=john --namespace=dev
k --context=john get pods # works (may be empty)
k --context=john get pods -n default # Forbidden
Task 5: Helm install (5%)¶
Add the repo https://stefanprodan.github.io/podinfo as podinfo. Install chart podinfo/podinfo as release web into namespace web (create it) with replicaCount=3. Then write the rendered manifests of that same configuration to /opt/podinfo.yaml.
Solution
Task 6: Kustomize overlay (4%)¶
A base exists in /opt/app/base (a Deployment web with image nginx:1.25). Create overlay /opt/app/overlays/prod that deploys it into namespace prod with image tag 1.27 and 4 replicas, then apply it.
Solution
mkdir -p /opt/app/overlays/prod
cat > /opt/app/overlays/prod/kustomization.yaml <<EOF
resources:
- ../../base
namespace: prod
images:
- name: nginx
newTag: "1.27"
replicas:
- name: web
count: 4
EOF
k create ns prod
k kustomize /opt/app/overlays/prod # preview
k apply -k /opt/app/overlays/prod
k get deploy web -n prod -o wide # 4 replicas, nginx:1.27
Task 7: CRDs (3%)¶
List all CRDs whose group contains cert-manager and save their names to /opt/crds.txt. Save the documentation of the spec field of the Certificate kind to /opt/cert-spec.txt.
Solution
Task 8: Node preparation (4%)¶
On node node01, make the following kernel parameters persistent and active: net.ipv4.ip_forward=1 and net.bridge.bridge-nf-call-iptables=1. Install the package /root/cri-dockerd.deb and make sure the cri-docker service is enabled and running.
Solution
ssh node01; sudo -i
modprobe br_netfilter
cat <<EOF > /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sysctl --system
sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables
dpkg -i /root/cri-dockerd.deb
systemctl enable --now cri-docker.service
systemctl is-active cri-docker
Workloads & Scheduling¶
Task 9: Rollout and rollback (4%)¶
Create Deployment api in namespace prod with image nginx:1.25 and 3 replicas. Update it to nginx:1.27-broken (an image that doesn't exist), observe the failure, then roll back to the working version. Write the revision number you rolled back to into /opt/rev.txt.
Solution
k create deploy api -n prod --image=nginx:1.25 --replicas=3
k set image deploy/api -n prod nginx=nginx:1.27-broken
k rollout status deploy/api -n prod --timeout=30s # stalls: ImagePullBackOff
k rollout history deploy/api -n prod # revision 1 = nginx:1.25
k rollout undo deploy/api -n prod --to-revision=1
echo 1 > /opt/rev.txt
k get deploy api -n prod -o jsonpath='{.spec.template.spec.containers[0].image}'
Task 10: HPA (4%)¶
Create an HPA api-hpa for Deployment api in prod: min 2, max 8, target 60% average CPU, and a scale-down stabilization window of 120 seconds.
Solution
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
namespace: prod
spec:
scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: api}
minReplicas: 2
maxReplicas: 8
metrics:
- type: Resource
resource:
name: cpu
target: {type: Utilization, averageUtilization: 60}
behavior:
scaleDown:
stabilizationWindowSeconds: 120
Task 11: Logging sidecar (5%)¶
Deployment legacy in namespace default writes logs to /var/log/legacy.log inside container app. Add a sidecar container log-tail (image busybox:1.36) that streams that file to stdout, using a shared emptyDir volume logs. The logs must be visible with kubectl logs ... -c log-tail.
Solution
spec:
template:
spec:
containers:
- name: app
# ...existing...
volumeMounts:
- {name: logs, mountPath: /var/log}
- name: log-tail # classic sidecar
image: busybox:1.36
command: ["sh", "-c", "tail -n+1 -F /var/log/legacy.log"]
volumeMounts:
- {name: logs, mountPath: /var/log}
volumes:
- name: logs
emptyDir: {}
initContainers with restartPolicy: Always.
Task 12: PriorityClass (3%)¶
Create PriorityClass high-priority with a value one less than the highest existing user-defined PriorityClass. Patch Deployment busybox-logger in namespace priority to use it.
Solution
k get pc # ignore system-* classes; say highest user one = 1000000
k create priorityclass high-priority --value=999999 --description="high priority"
k patch deploy busybox-logger -n priority \
-p '{"spec":{"template":{"spec":{"priorityClassName":"high-priority"}}}}'
k get pods -n priority -o jsonpath='{.items[*].spec.priorityClassName}'
Task 13: Scheduling with taints and affinity (4%)¶
Node node01 is tainted dedicated=batch:NoSchedule and labeled workload=batch. Create pod batch-job (image busybox:1.36, command sleep 3600) that runs only on node01.
Solution
The toleration allows the pod onto the node, and the nodeSelector forces it there. You need both.Services & Networking¶
Task 14: NetworkPolicy (6%)¶
In namespace backend, allow ingress to pods labeled app=api on TCP 8080 only from pods labeled app=web in namespace frontend. Deny all other ingress to those pods. Name it api-allow-web.
Solution
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow-web
namespace: backend
spec:
podSelector:
matchLabels: {app: api}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: frontend}
podSelector: # same item = AND
matchLabels: {app: web}
ports:
- {protocol: TCP, port: 8080}
Task 15: Gateway API (6%)¶
A GatewayClass nginx exists. In namespace default, create Gateway web-gateway with an HTTP listener on port 80 for host shop.example.com. Create HTTPRoute shop-route that sends /api to Service api-svc:8080, and everything else 90/10 to shop-v1:80 and shop-v2:80.
Solution
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata: {name: web-gateway, namespace: default}
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
hostname: shop.example.com
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: {name: shop-route, namespace: default}
spec:
parentRefs:
- name: web-gateway
hostnames: ["shop.example.com"]
rules:
- matches:
- path: {type: PathPrefix, value: /api}
backendRefs:
- {name: api-svc, port: 8080}
- backendRefs:
- {name: shop-v1, port: 80, weight: 90}
- {name: shop-v2, port: 80, weight: 10}
Task 16: Ingress (4%)¶
Create Ingress shop (class nginx) in namespace shop routing shop.example.com/ to Service front:80 and shop.example.com/pay to Service pay:9000, with TLS using the existing secret shop-tls.
Solution
k create ingress shop -n shop --class=nginx \
--rule="shop.example.com/*=front:80,tls=shop-tls" \
--rule="shop.example.com/pay*=pay:9000"
k describe ingress shop -n shop
Prefix and the TLS section lists shop.example.com. Edit with k edit ingress if needed.
Task 17: Expose and DNS (4%)¶
Expose pod nginx-resolver (create it, image nginx) as Service nginx-resolver-svc on port 80. From a temporary busybox pod, look up the Service and the pod's DNS records. Save the outputs to /opt/svc.dns and /opt/pod.dns.
Solution
k run nginx-resolver --image=nginx
k expose pod nginx-resolver --name=nginx-resolver-svc --port=80
k run t --rm -i --image=busybox:1.28 --restart=Never -- nslookup nginx-resolver-svc > /opt/svc.dns
IP=$(k get pod nginx-resolver -o jsonpath='{.status.podIP}' | tr . -)
k run t --rm -i --image=busybox:1.28 --restart=Never -- nslookup $IP.default.pod.cluster.local > /opt/pod.dns
cat /opt/svc.dns /opt/pod.dns
busybox:1.28 has the most reliable nslookup output for this classic task.)
Task 18: Fix a NodePort Service (5%)¶
Service web in namespace shop must be reachable on every node at port 30080, forwarding to container port 8080 of pods labeled app=web. It currently doesn't work. Fix it.
Solution
k get svc web -n shop -o yaml # check type, selector, targetPort, nodePort
k get pods -n shop --show-labels
k get endpointslices -n shop -l kubernetes.io/service-name=web
k edit svc web -n shop
# type: NodePort
# selector: {app: web} # fix mismatched label
# ports: [{port: 80, targetPort: 8080, nodePort: 30080}]
curl -s <node-ip>:30080
Storage¶
Task 19: Default StorageClass (3%)¶
Create StorageClass local-fast (provisioner rancher.io/local-path, volumeBindingMode: WaitForFirstConsumer, allowVolumeExpansion: true) and make it the only default StorageClass.
Solution
Task 20: PV, PVC and pod (4%)¶
Create PV app-pv (2Gi, RWO, hostPath /srv/app, reclaim Retain, storageClassName manual). Create PVC app-pvc in namespace storage requesting 1Gi that binds to it. Mount it at /data in pod app (image nginx).
Solution
apiVersion: v1
kind: PersistentVolume
metadata: {name: app-pv}
spec:
capacity: {storage: 2Gi}
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath: {path: /srv/app}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: app-pvc, namespace: storage}
spec:
accessModes: [ReadWriteOnce]
resources: {requests: {storage: 1Gi}}
storageClassName: manual
---
apiVersion: v1
kind: Pod
metadata: {name: app, namespace: storage}
spec:
containers:
- name: app
image: nginx
volumeMounts: [{name: data, mountPath: /data}]
volumes:
- name: data
persistentVolumeClaim: {claimName: app-pvc}
Troubleshooting¶
Task 21: NotReady worker (8%)¶
Node node01 is NotReady. Fix it and make sure the fix survives a reboot.
Solution
k describe node node01 | grep -A5 Conditions
ssh node01; sudo -i
systemctl status kubelet # inactive? failed?
journalctl -u kubelet --no-pager | tail -30 # read the actual error
# Typical fixes:
systemctl enable --now kubelet # stopped/disabled
vim /var/lib/kubelet/config.yaml # wrong clientCAFile / path
systemctl status containerd # runtime down -> enable --now containerd
systemctl daemon-reload && systemctl restart kubelet
exit; exit
k get nodes -w # Ready
systemctl enable, not just start.
Task 22: Broken API server (8%)¶
After a config change, kubectl returns connection refused. Fix the cluster.
Solution
ssh controlplane; sudo -i
crictl ps -a | grep kube-apiserver # Exited / not present
crictl logs <id> # flag error? cert path? etcd URL?
tail -30 /var/log/pods/kube-system_kube-apiserver-*/kube-apiserver/*.log
journalctl -u kubelet | tail -20 # YAML parse errors show here
vim /etc/kubernetes/manifests/kube-apiserver.yaml
# common: --etcd-servers=https://127.0.0.1:2379 (not 2380), typo'd flag,
# wrong --tls-cert-file path, invalid YAML indentation
watch crictl ps # apiserver Running
kubectl get nodes
Task 23: Pods stuck Pending (5%)¶
New pods in namespace default stay Pending with no events. Find and fix the cause.
Solution
No events at all means nothing tried to schedule them, so the scheduler is down.
Then confirm the pods get scheduled.Task 24: CrashLoopBackOff (5%)¶
Pod payments in namespace prod keeps restarting. Find why, fix it, and write the original error message to /opt/payments-error.txt.
Solution
k describe pod payments -n prod | sed -n '/Last State/,/Ready/p' # OOMKilled? exit code?
k logs payments -n prod --previous > /opt/payments-error.txt
# Fix per the cause: missing env/ConfigMap key, wrong command, liveness probe, memory limit
k get pod payments -n prod -o yaml > p.yaml && vim p.yaml
k replace --force -f p.yaml
k get pod payments -n prod -w # Running, restarts stop increasing
Task 25: Resource usage (5%)¶
Among pods with label tier=backend in all namespaces, find the one using the most memory. Write <namespace>/<pod> to /opt/top-mem.txt.
Solution
Scoring yourself¶
Add up the weights of the tasks you solved fully and correctly (verified, exact names). The weights total 125, so 83 or more (66%) means you're on track. For every miss, write the one command or concept you lacked and drill it tonight.
Last stop: 8. Cheat Sheet →