Model Solution: Kubernetes Assessment 02 – Intermediate
Difficulty level: Intermediate
Language: English
Type: model solution / expected answer guide
This file is the matching solution for
02-intermediate-kubernetes-assessment-EN.md.
The manifests are lab examples. Tool and cluster dependencies such as StorageClass, Ingress Controller, Gateway API or NetworkPolicy support must be checked in the respective lab.
1. Usage Notes
Recommended namespace:
export NS=exam-intermediate
A complete solution does not have to use exactly the same images as long as they are safe, demonstrate the required Kubernetes concepts and can be validated reproducibly. Where a tool is unavailable, a technically correct substitute assessment is acceptable.
2. Part A: Model Answers to Knowledge Questions
1. Deployment, StatefulSet, Job and CronJob
| Resource | Use case |
|---|---|
| Deployment | continuously running stateless services with rollout and rollback |
| StatefulSet | workloads with stable identity and stable PVC assignment |
| Job | one-time or finite task, for example a migration |
| CronJob | scheduled recurring jobs |
A Deployment is unsuitable for one-time migrations because it tries to keep processes alive continuously.
2. Init Containers, Sidecars and Ephemeral Containers
Init Containers run before the app containers and must complete successfully. Sidecars run in parallel with the app and share network, volumes and Pod lifecycle. Ephemeral Containers are added afterwards for debugging and are not part of the normal deployment model.
3. Startup, Readiness and Liveness Probe
| Probe | Effect |
|---|---|
| Startup Probe | protects slow starts; Readiness/Liveness are held back until it succeeds |
| Readiness Probe | decides whether the Pod receives Service traffic |
| Liveness Probe | decides whether kubelet restarts the container |
4. Risk of aggressive Liveness Probes
An overly aggressive Liveness Probe can restart healthy containers under load or during slow startup. That can create crash loops, unnecessary downtime and amplified load.
5. Effect of Requests on scheduling and HPA
CPU and Memory Requests are planning values for the scheduler. CPU Requests are also the basis for CPU-based HPA utilization. Missing or unrealistic Requests lead to poor placement, misleading autoscaling and noisy-neighbor problems.
6. QoS classes
| QoS | Condition | Effect |
|---|---|---|
| Guaranteed | CPU and Memory Requests equal Limits for all containers | highest stability under resource pressure |
| Burstable | Requests are set but not identical to Limits | medium priority |
| BestEffort | no Requests and no Limits | evicted first under resource pressure |
7. LimitRange and ResourceQuota
LimitRange sets defaults and limits for individual Pods or containers in a namespace. ResourceQuota limits the total consumption of a namespace, for example Pods, CPU, Memory or PVCs.
8. nodeSelector, affinity, taints and tolerations
nodeSelector is a simple hard node selection. Affinity is more expressive and can define hard or soft rules. Taints mark Nodes so that Pods without a matching toleration are not scheduled there. Tolerations allow placement on tainted Nodes, but they do not force it by themselves.
9. Why a Pod can remain Pending
Typical causes are insufficient resources, non-matching node selectors, affinity rules, missing tolerations for taints, missing PVCs or StorageClass problems. Diagnosis is done through kubectl describe pod and events.
10. Service, EndpointSlice, Pod readiness and DNS
A Service defines a stable address and a selector. EndpointSlices contain the actually reachable Pod endpoints. Only ready Pods are normally included for Service traffic. CoreDNS resolves Service names.
11. DNS egress under default deny
With egress default deny, DNS queries are blocked unless explicitly allowed. Without DNS egress, Pods cannot resolve Service names such as backend.
12. Ingress resource and Ingress Controller
The Ingress resource describes HTTP routing rules. An Ingress Controller implements these rules as proxy or load-balancer configuration. Without a controller, the resource has little practical effect.
13. PV, PVC, StorageClass and CSI
| Term | Role |
|---|---|
| PV | provisioned storage object in the cluster |
| PVC | storage request by a Pod or workload |
| StorageClass | describes dynamic provisioning and parameters |
| CSI | interface/plugin for storage systems |
14. Why hostPath is not a production solution
hostPath mounts a Node filesystem directly. It is not portable, binds data to a Node and can create security risks. It is practical in labs, but not a generic production storage model.
15. Checks for PVC Pending, Service without Endpoints and CreateContainerConfigError
| Error pattern | Check |
|---|---|
| PVC Pending | kubectl describe pvc, StorageClass, provisioner, events |
| Service without Endpoints | Pod Labels, Service selector, readiness, EndpointSlices |
| CreateContainerConfigError | missing ConfigMap/Secret, wrong key, invalid env/volume reference |
3. Part B: Architecture Decision
Model for architecture-decision.md:
# Architecture Decision – Shop Lab
| Requirement | Decision | Justification |
| --- | --- | --- |
| HTTP frontend runs continuously and is updated through rolling updates | Deployment | stateless, replicas, rollout, rollback |
| Backend API with health checks and resources | Deployment + Service | continuously running internal service, Readiness for traffic, Liveness for real defects |
| Migration once per release | Job | successful completion is the target state |
| Cleanup hourly without parallel jobs | CronJob with `concurrencyPolicy: Forbid` | scheduled jobs, no overlap |
| Small stateful workload with data after Pod recreation | StatefulSet with `volumeClaimTemplates` | stable Pod identity and PVC assignment |
| External reachability | Ingress or Gateway API | resource requires a matching controller |
| Protect namespace from overuse | LimitRange + ResourceQuota | defaults and total limits |
| Frontend may reach only backend and DNS | NetworkPolicy default deny + allow rules | allow ingress/egress deliberately |
4. Part C: Practical Solution
C1: Namespace, LimitRange and ResourceQuota
namespace.yml
apiVersion: v1
kind: Namespace
metadata:
name: exam-intermediate
labels:
environment: exam
level: intermediate
resources/limitrange.yml
apiVersion: v1
kind: LimitRange
metadata:
name: default-container-limits
namespace: exam-intermediate
spec:
limits:
- type: Container
defaultRequest:
cpu: 100m
memory: 128Mi
default:
cpu: 500m
memory: 512Mi
max:
cpu: "2"
memory: 2Gi
resources/resourcequota.yml
apiVersion: v1
kind: ResourceQuota
metadata:
name: exam-quota
namespace: exam-intermediate
spec:
hard:
pods: "10"
requests.cpu: "2"
requests.memory: 2Gi
limits.cpu: "4"
limits.memory: 4Gi
Apply:
kubectl apply -f namespace.yml
kubectl apply -f resources/limitrange.yml
kubectl apply -f resources/resourcequota.yml
kubectl describe limitrange default-container-limits -n exam-intermediate
kubectl describe resourcequota exam-quota -n exam-intermediate
Quota violation example:
apiVersion: v1
kind: Pod
metadata:
name: too-large
namespace: exam-intermediate
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests:
cpu: "3"
memory: 3Gi
limits:
cpu: "3"
memory: 3Gi
Expected: The API Server rejects the Pod because of the ResourceQuota.
C2: Frontend and backend with probes
workloads/backend.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: exam-intermediate
labels:
app: backend
component: api
environment: exam
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
component: api
environment: exam
spec:
containers:
- name: backend
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["netexec", "--http-port=8080"]
ports:
- name: http
containerPort: 8080
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 2
readinessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
workloads/frontend.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: exam-intermediate
labels:
app: frontend
component: web
environment: exam
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
component: web
environment: exam
spec:
containers:
- name: frontend
image: curlimages/curl:8.8.0
command: ["sh", "-c", "sleep 3600"]
readinessProbe:
exec:
command: ["sh", "-c", "command -v curl >/dev/null"]
periodSeconds: 10
livenessProbe:
exec:
command: ["sh", "-c", "test -d /tmp"]
initialDelaySeconds: 10
periodSeconds: 20
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
networking/services.yml
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: exam-intermediate
spec:
selector:
app: backend
ports:
- name: http
port: 8080
targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: frontend
namespace: exam-intermediate
spec:
selector:
app: frontend
ports:
- name: http
port: 80
targetPort: 8080
Apply and check:
kubectl apply -f workloads/backend.yml
kubectl apply -f workloads/frontend.yml
kubectl apply -f networking/services.yml
kubectl rollout status deployment/backend -n exam-intermediate
kubectl rollout status deployment/frontend -n exam-intermediate
kubectl get deploy,pod,svc,endpoints,endpointslices -n exam-intermediate
Readiness versus Liveness:
- Readiness removes Pods from Service traffic without restarting them.
- Liveness restarts containers and must only detect real, non-self-healing defects.
C3: Job and CronJob
workloads/migration-job.yml
apiVersion: batch/v1
kind: Job
metadata:
name: migration
namespace: exam-intermediate
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: migration
image: busybox:1.36
command: ["sh", "-c", "echo Migration starts; sleep 3; echo Migration successful"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
workloads/cleanup-cronjob.yml
apiVersion: batch/v1
kind: CronJob
metadata:
name: cleanup
namespace: exam-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 done"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
Check:
kubectl apply -f workloads/migration-job.yml
kubectl apply -f workloads/cleanup-cronjob.yml
kubectl get job,cronjob,pod -n exam-intermediate
kubectl logs job/migration -n exam-intermediate
C4: Scheduling check
Label a node:
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl label node "$NODE" exam-role=apps
kubectl get nodes --show-labels | grep exam-role
Pod with matching selector:
apiVersion: v1
kind: Pod
metadata:
name: scheduled-ok
namespace: exam-intermediate
spec:
nodeSelector:
exam-role: apps
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
Pod with non-matching selector:
apiVersion: v1
kind: Pod
metadata:
name: scheduled-pending
namespace: exam-intermediate
spec:
nodeSelector:
exam-role: does-not-exist
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
Taint and toleration:
kubectl taint node "$NODE" dedicated=apps:NoSchedule
Pod with toleration:
apiVersion: v1
kind: Pod
metadata:
name: tolerated
namespace: exam-intermediate
spec:
nodeSelector:
exam-role: apps
tolerations:
- key: dedicated
operator: Equal
value: apps
effect: NoSchedule
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
Validation:
kubectl get pods -o wide -n exam-intermediate
kubectl describe pod scheduled-pending -n exam-intermediate
kubectl get events -n exam-intermediate --sort-by=.metadata.creationTimestamp
Rollback node changes:
kubectl taint node "$NODE" dedicated=apps:NoSchedule-
kubectl label node "$NODE" exam-role-
C5: NetworkPolicy default deny plus allowances
networking/networkpolicies.yml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: exam-intermediate
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: exam-intermediate
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-egress-to-backend
namespace: exam-intermediate
spec:
podSelector:
matchLabels:
app: frontend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-ingress-from-frontend
namespace: exam-intermediate
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
Check:
kubectl apply -f networking/networkpolicies.yml
kubectl get networkpolicy -n exam-intermediate
FRONTEND=$(kubectl get pod -l app=frontend -n exam-intermediate -o jsonpath='{.items[0].metadata.name}')
kubectl exec "$FRONTEND" -n exam-intermediate -- nslookup backend
kubectl exec "$FRONTEND" -n exam-intermediate -- curl -s --max-time 5 http://backend:8080/hostname
CNI classification:
kubectl get pods -A | grep -Ei 'calico|cilium|weave|antrea|flannel|kindnet'
A good solution documents whether the concrete CNI enforces NetworkPolicies. For example, Flannel without a policy extension is evaluated differently from Calico or Cilium.
C6: Ingress or Gateway API
Check controller availability:
kubectl get ingressclass
kubectl get gatewayclass 2>/dev/null || true
kubectl get pods -A | grep -Ei 'ingress|gateway|nginx|traefik|envoy|haproxy'
Example networking/ingress-or-gateway.yml as Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: exam-intermediate
spec:
rules:
- host: shop.localtest.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backend
port:
number: 8080
Check:
kubectl apply -f networking/ingress-or-gateway.yml
kubectl describe ingress shop -n exam-intermediate
kubectl get ingress shop -n exam-intermediate
If no controller exists:
No Ingress or Gateway Controller is installed in the lab. The resource can be accepted by the API Server, but it has no practical routing effect because no controller translates it into proxy or load-balancer configuration.
Gateway API example, if Gateway API is available
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop
namespace: exam-intermediate
spec:
parentRefs:
- name: shared-gateway
namespace: gateway-system
hostnames:
- shop.localtest.me
rules:
- backendRefs:
- name: backend
port: 8080
C7: PVC and StatefulSet
Check StorageClasses:
kubectl get storageclass
storage/statefulset.yml
apiVersion: v1
kind: Service
metadata:
name: data-demo
namespace: exam-intermediate
spec:
clusterIP: None
selector:
app: data-demo
ports:
- name: dummy
port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: data-demo
namespace: exam-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
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
Apply and validate:
kubectl apply -f storage/statefulset.yml
kubectl rollout status statefulset/data-demo -n exam-intermediate
POD=data-demo-0
kubectl exec "$POD" -n exam-intermediate -- sh -c 'echo persistent > /data/test.txt'
kubectl delete pod "$POD" -n exam-intermediate
kubectl wait --for=condition=Ready pod/data-demo-0 -n exam-intermediate --timeout=90s
kubectl exec data-demo-0 -n exam-intermediate -- cat /data/test.txt
Expected: The file still exists after Pod recreation if dynamic provisioning and storage behavior support it.
C8: Helm and Kustomize rendering
Kustomize example:
packaging/kustomize/base/deployment.yml
packaging/kustomize/base/service.yml
packaging/kustomize/base/kustomization.yml
packaging/kustomize/overlays/dev/kustomization.yml
Render:
kubectl kustomize packaging/kustomize/overlays/dev > packaging/kustomize-output.md
Helm example:
helm create packaging/shop-demo
helm template shop-demo packaging/shop-demo > packaging/helm-output.md
Comparison:
| Criterion | Kustomize | Helm |
|---|---|---|
| Model | patch and overlay existing YAML | template-based chart package |
| Reviewability | rendered YAML is often close to source | rendered YAML must be reviewed carefully |
| Parameterization | overlays and patches | values files and templates |
| Error risk | patch drift and overlay complexity | template logic and values mistakes |
5. Part D: Troubleshooting Course
| Error pattern | Symptom | Diagnosis | Cause | Fix | Validation |
|---|---|---|---|---|---|
wrong nodeSelector |
Pod remains Pending |
kubectl describe pod, events |
no Node matches the selector | correct selector or label Node | Pod scheduled |
| wrong image tag | ImagePullBackOff |
Pod events show pull failure | tag/image does not exist | set valid image tag | Pod running |
| wrong startup command | CrashLoopBackOff |
kubectl logs --previous, exit code |
process exits | fix command/config | restarts stop |
| wrong StorageClass | PVC remains Pending |
kubectl describe pvc, events |
no provisioner/class | use valid StorageClass or omit if default exists | PVC Bound |
| wrong Service selector | Service has no Endpoints | labels, selector, EndpointSlices | selector mismatch | correct selector | Endpoints populated |
6. Expected Submission Structure
02-intermediate-submission/
answers.md
architecture-decision.md
workloads/
frontend.yml
backend.yml
migration-job.yml
cleanup-cronjob.yml
resources/
limitrange.yml
resourcequota.yml
networking/
services.yml
networkpolicies.yml
ingress-or-gateway.yml
storage/
statefulset.yml
packaging/
kustomize-output.md
helm-output.md
troubleshooting.md
evidence.md
cleanup.md
7. Cleanup
Preferred cleanup:
kubectl delete namespace exam-intermediate
If node labels or taints were changed, always roll them back explicitly:
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl taint node "$NODE" dedicated=apps:NoSchedule- || true
kubectl label node "$NODE" exam-role- || true
Verify:
kubectl get namespace exam-intermediate
kubectl get nodes --show-labels
kubectl describe node "$NODE" | grep -i taint -A2
8. Grading and Examiner Notes
A strong solution:
- selects workload resources based on lifecycle and operating model,
- separates Readiness, Liveness and Startup Probe cleanly,
- sets meaningful Requests and Limits,
- demonstrates quota rejection,
- diagnoses scheduling via events rather than guessing,
- treats NetworkPolicy and DNS separately,
- understands that Ingress/Gateway requires a controller,
- demonstrates or correctly qualifies persistent storage behavior,
- renders Helm and Kustomize before applying resources,
- documents troubleshooting as symptom, diagnosis, cause, fix and validation.
Common deductions:
| Issue | Deduction |
|---|---|
| Deployment used for a one-time migration | medium |
| Readiness and Liveness explained as equivalent | high |
| NetworkPolicy misses DNS egress | medium to high |
| no CNI enforcement note | medium |
| node taint not rolled back | high |
| PVC behavior asserted without evidence | medium |
| no rendered Helm/Kustomize output | low to medium |