702 lines
41 KiB
Python
702 lines
41 KiB
Python
"""CKAD curriculum as a study path: the 24 objectives of the five domains, in the curriculum's own
|
|
words and order (CKAD_Curriculum_v1.37.pdf, cncf/curriculum), each with what to know, the fastest
|
|
commands, the traps, the documentation pages the exam lets you open, and the tasks that practise it.
|
|
|
|
./exam objectives the path, with your progress per objective
|
|
./exam notes 3.2 the notes for one objective (id or key: 3.2, probes)
|
|
./exam study 3.2 those notes, then its tasks with no clock and solutions visible
|
|
./exam drill 3.2 the same tasks against the clock
|
|
|
|
"tasks" lists every task that really exercises the objective, most direct first; a task may sit
|
|
under several objectives. Study and drill sessions use the pool tasks; the Simulator Set A tasks
|
|
(38-57) are listed too but stay out unless you ask for them (--all), so the mock stays unspoiled.
|
|
Every task must be listed at least once and every objective needs two pool tasks -
|
|
`./exam objectives` says so when that stops being true.
|
|
"""
|
|
|
|
OBJECTIVES = [
|
|
# ------------------------------------------------- 1. Application Design and Build (20%) ----
|
|
{
|
|
"id": "1.1", "key": "images", "domain": "DESIGN",
|
|
"text": "Define, build and modify container images",
|
|
"tasks": [13, 60, 41],
|
|
"know": [
|
|
"Read and change a Dockerfile: FROM, COPY, RUN, ENV, ARG (build time only), WORKDIR, USER, LABEL, EXPOSE, ENTRYPOINT, CMD.",
|
|
"ENTRYPOINT is the program, CMD its default arguments. In a Pod, `command` replaces ENTRYPOINT and `args` replaces CMD.",
|
|
"Build, tag, list, inspect, run, save and remove images. The exam terminal has docker or podman - the verbs are the same.",
|
|
"A tag is only a pointer: one image can carry several tags, and rebuilding under an old tag moves it.",
|
|
],
|
|
"fast": """
|
|
podman build -t localhost/app:v1 . # -f path/Dockerfile --build-arg KEY=val
|
|
podman tag localhost/app:v1 localhost/app:latest
|
|
podman images | grep app
|
|
podman inspect localhost/app:v1 --format '{{.Config.User}} {{.Config.Entrypoint}} {{.Config.Cmd}} {{.Config.Env}}'
|
|
podman run --rm localhost/app:v1 [args replacing CMD] # -d --name x keeps it running; podman logs x
|
|
podman save --format oci-archive -o app.tar localhost/app:v1 # docker save -o app.tar ...
|
|
podman rmi localhost/app:v0
|
|
""",
|
|
"traps": [
|
|
"An ARG is gone at run time unless you copy it into an ENV (`ARG V` then `ENV APP_VERSION=$V`).",
|
|
"`podman save` writes a docker-archive unless you pass `--format oci-archive`.",
|
|
"Build before you edit when a task wants the old version kept under its own tag.",
|
|
"imagePullPolicy defaults to Always for `:latest` (or no tag) and IfNotPresent for any other tag.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/containers/images/",
|
|
"https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/"],
|
|
},
|
|
{
|
|
"id": "1.2", "key": "workloads", "domain": "DESIGN",
|
|
"text": "Choose and use the right workload resource (Deployment, DaemonSet, CronJob, etc.)",
|
|
"tasks": [3, 14, 15, 39, 38, 44, 56],
|
|
"know": [
|
|
"Choose by behaviour: Deployment (interchangeable replicas, rolling updates), StatefulSet (stable name, DNS and storage per replica), DaemonSet (one Pod per node), Job (run to completion), CronJob (Jobs on a schedule), bare Pod (one-off).",
|
|
"Job: completions, parallelism, backoffLimit, activeDeadlineSeconds, ttlSecondsAfterFinished; restartPolicy must be Never or OnFailure.",
|
|
"CronJob: schedule, startingDeadlineSeconds, concurrencyPolicy (Allow, Forbid, Replace), suspend, successfulJobsHistoryLimit, failedJobsHistoryLimit.",
|
|
"DaemonSet: no replicas; a toleration for node-role.kubernetes.io/control-plane:NoSchedule puts it on control-plane nodes too.",
|
|
],
|
|
"fast": """
|
|
k create deploy web --image=nginx:1.27 --replicas=3 $do > deploy.yaml
|
|
k create job render --image=busybox:1.36 $do -- sh -c 'echo done' > job.yaml
|
|
k create cronjob digest --image=busybox:1.36 --schedule='*/5 * * * *' $do -- sh -c 'echo digest' > cj.yaml
|
|
k create job digest-manual --from=cronjob/digest # run a CronJob once, now
|
|
k run tmp --image=busybox:1.36 --restart=Never --rm -it -- sh
|
|
# DaemonSet has no generator: create a Deployment with $do, change kind, delete replicas/strategy/status
|
|
k explain cronjob.spec.jobTemplate.spec --recursive | less
|
|
""",
|
|
"traps": [
|
|
"Three nested specs in a CronJob: spec (schedule, startingDeadlineSeconds) > jobTemplate.spec (completions, activeDeadlineSeconds) > template.spec (containers, restartPolicy).",
|
|
"A Job's Pod template is immutable - delete the Job and create it again instead of editing it.",
|
|
"A StatefulSet needs `serviceName` naming a headless Service (clusterIP: None) before its Pods get their own DNS names.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/workloads/controllers/job/",
|
|
"https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/",
|
|
"https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/"],
|
|
},
|
|
{
|
|
"id": "1.3", "key": "multicontainer", "domain": "DESIGN",
|
|
"text": "Understand multi-container Pod design patterns (e.g. sidecar, init and others)",
|
|
"tasks": [1, 2, 16, 40],
|
|
"know": [
|
|
"Containers of a Pod share its network (they reach each other on localhost) and any volume both mount - that is all a sidecar, adapter or ambassador is.",
|
|
"Init containers run one at a time, in order, each to completion, before any app container starts.",
|
|
"A native sidecar is an init container with `restartPolicy: Always`: it starts before the app containers, keeps running beside them, and does not keep a Job from completing.",
|
|
"Address one container with `-c`: `k logs POD -c NAME`, `k exec POD -c NAME -- ...`. Status `Init:0/1` means an init container is still running.",
|
|
],
|
|
"fast": """
|
|
k run app --image=nginx:1.27 $do > pod.yaml # then add the second container by hand
|
|
spec:
|
|
initContainers:
|
|
- {name: wait, image: busybox:1.36, command: [sh, -c, "until wget -qO- -T2 http://backend; do sleep 2; done"]}
|
|
- {name: shipper, image: busybox:1.36, restartPolicy: Always, command: [sh, -c, "tail -F /logs/app.log"],
|
|
volumeMounts: [{name: logs, mountPath: /logs}]} # native sidecar
|
|
containers:
|
|
- {name: app, image: nginx:1.27, volumeMounts: [{name: logs, mountPath: /var/log/app}]}
|
|
volumes: [{name: logs, emptyDir: {}}]
|
|
k logs app -c shipper; k logs app --all-containers --prefix
|
|
""",
|
|
"traps": [
|
|
"`kubectl run` creates one container only - generate the Pod and add the others in the YAML.",
|
|
"A Service with no endpoints still resolves in DNS, so an init container waiting on `nslookup` is not waiting for the app. Wait on a real request.",
|
|
"A classic sidecar that never exits keeps a Job running for ever; make it a native sidecar.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/workloads/pods/init-containers/",
|
|
"https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/"],
|
|
},
|
|
{
|
|
"id": "1.4", "key": "volumes", "domain": "DESIGN",
|
|
"text": "Utilize persistent and ephemeral volumes",
|
|
"tasks": [17, 10, 52, 40, 50],
|
|
"know": [
|
|
"Ephemeral: emptyDir (lives as long as the Pod; `medium: Memory` is tmpfs, `sizeLimit` caps it), and the configMap, secret, downwardAPI and projected volumes.",
|
|
"Persistent: a PersistentVolumeClaim asks for storage (accessModes, resources.requests.storage, storageClassName); a StorageClass provisions the PersistentVolume, or a pre-made one binds.",
|
|
"Access modes: ReadWriteOnce, ReadOnlyMany, ReadWriteMany, ReadWriteOncePod.",
|
|
"volumeMounts: mountPath, readOnly, and subPath to mount one file or directory without hiding the rest of the target directory.",
|
|
],
|
|
"fast": """
|
|
apiVersion: v1
|
|
kind: PersistentVolumeClaim
|
|
metadata: {name: data}
|
|
spec:
|
|
accessModes: [ReadWriteOnce]
|
|
storageClassName: standard
|
|
resources: {requests: {storage: 100Mi}}
|
|
--- # in the Pod spec:
|
|
volumes:
|
|
- {name: data, persistentVolumeClaim: {claimName: data}}
|
|
- {name: scratch, emptyDir: {medium: Memory, sizeLimit: 32Mi}}
|
|
containers:
|
|
- volumeMounts: [{name: data, mountPath: /data}, {name: scratch, mountPath: /scratch}]
|
|
k get pvc,pv; k describe pvc data # Pending? the Events say why
|
|
""",
|
|
"traps": [
|
|
"There is no `kubectl create pvc` - copy the claim from the docs.",
|
|
"With a WaitForFirstConsumer StorageClass (`standard` here) a claim stays Pending until a Pod uses it. That is not an error.",
|
|
"emptyDir survives a container restart but not a new Pod. To prove data persists, delete the Pod and read the file again.",
|
|
"A non-root container cannot write to a fresh volume unless the Pod sets `securityContext.fsGroup`.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/storage/persistent-volumes/",
|
|
"https://kubernetes.io/docs/concepts/storage/volumes/",
|
|
"https://kubernetes.io/docs/tutorials/configuration/configure-persistent-volume-storage/"],
|
|
},
|
|
# ------------------------------------------------------- 2. Application Deployment (20%) ----
|
|
{
|
|
"id": "2.1", "key": "strategies", "domain": "DEPLOY",
|
|
"text": "Use Kubernetes primitives to implement common deployment strategies (e.g. blue/green or canary)",
|
|
"tasks": [5, 18],
|
|
"know": [
|
|
"Blue/green: two Deployments, and the Service selector decides which one gets all the traffic. Cut over by changing the selector; roll back by changing it back.",
|
|
"Canary: two Deployments whose Pods share the label the Service selects. Traffic splits by the number of ready Pods: 1 canary beside 4 stable is 20%.",
|
|
"A Deployment's own strategy is RollingUpdate (default) or Recreate (every old Pod stops first - downtime).",
|
|
],
|
|
"fast": """
|
|
k get svc web -o wide; k get pods --show-labels
|
|
k set selector svc web 'app=web,version=green' # blue/green cut-over
|
|
k scale deploy web-stable --replicas=4 # canary ratio: 4 stable + 1 canary = 20%
|
|
k get endpointslices -l kubernetes.io/service-name=web -o wide
|
|
k get pods -l app=web -L version,track
|
|
""",
|
|
"traps": [
|
|
"Wait until the new Deployment is fully available before you switch the selector.",
|
|
"`kubectl set selector` replaces the whole selector - name every label you want in it.",
|
|
"A canary only shares traffic if the Service selector leaves out the label that tells the two versions apart.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/workloads/management/",
|
|
"https://kubernetes.io/docs/concepts/workloads/controllers/deployment/"],
|
|
},
|
|
{
|
|
"id": "2.2", "key": "rollouts", "domain": "DEPLOY",
|
|
"text": "Understand Deployments and how to perform rolling updates",
|
|
"tasks": [4, 19, 42, 44],
|
|
"know": [
|
|
"Any change to `spec.template` starts a rollout: a new ReplicaSet scales up while the old one scales down, within maxSurge and maxUnavailable (25% each by default).",
|
|
"minReadySeconds (how long a Pod must be Ready to count as available), revisionHistoryLimit (old ReplicaSets kept), progressDeadlineSeconds.",
|
|
"`kubectl rollout`: status, history, undo, pause, resume, restart.",
|
|
],
|
|
"fast": """
|
|
k set image deploy/web nginx=nginx:1.27 # CONTAINER=IMAGE
|
|
k rollout status deploy/web
|
|
k rollout history deploy/web # --revision=3 shows that revision's template
|
|
k rollout undo deploy/web # --to-revision=2
|
|
k rollout restart deploy/web # same spec, new Pods
|
|
k annotate deploy/web kubernetes.io/change-cause='pick 1.27'
|
|
k patch deploy web -p '{"spec":{"strategy":{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0}}}}'
|
|
""",
|
|
"traps": [
|
|
"`--record` is deprecated: write the kubernetes.io/change-cause annotation yourself.",
|
|
"An undo makes a new revision out of the old one, so the active revision number is not the one you rolled back to.",
|
|
"A rollout stuck on a bad image does not fail by itself - `rollout status` just waits, and the old Pods keep serving.",
|
|
"maxSurge and maxUnavailable cannot both be 0.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/workloads/controllers/deployment/"],
|
|
},
|
|
{
|
|
"id": "2.3", "key": "helm", "domain": "DEPLOY",
|
|
"text": "Use the Helm package manager to deploy existing packages",
|
|
"tasks": [6, 21, 43],
|
|
"know": [
|
|
"A chart is the package, a release is one installation of it in a Namespace, and every install, upgrade or rollback adds a revision.",
|
|
"Values: the chart's defaults (`helm show values`), overridden with `-f file.yaml` and `--set key=value`; `helm get values` shows what a release was given.",
|
|
"Life cycle: repo add/update, search, install, upgrade, rollback, uninstall, list, history, status.",
|
|
],
|
|
"fast": """
|
|
helm repo add NAME URL && helm repo update; helm search repo nginx --versions
|
|
helm show values CHART > values.yaml
|
|
helm install REL CHART -n NS --create-namespace -f values.yaml --set replicaCount=2 # --version X
|
|
helm upgrade REL CHART -n NS --reuse-values --set replicaCount=4
|
|
helm list -n NS # -A for every Namespace; --failed, --pending filter by status
|
|
helm history REL -n NS; helm rollback REL 1 -n NS; helm uninstall REL -n NS
|
|
helm get values REL -n NS # --all adds the defaults; helm get manifest REL -n NS
|
|
""",
|
|
"traps": [
|
|
"Releases live in a Namespace: without `-n` Helm looks in `default` and finds nothing.",
|
|
"An upgrade without `--reuse-values` (or the same `-f`/`--set` again) puts every value back to the chart's default.",
|
|
"A rollback is a new revision, not a step back in the numbering.",
|
|
"Helm 3's `helm list` leaves out pending releases unless you add `-a`; Helm 4 (on this machine) lists every status and has no `-a`. `--pending` and `--failed` work in both.",
|
|
],
|
|
"docs": ["https://helm.sh/docs/intro/using_helm/", "https://helm.sh/docs/helm/helm_upgrade/"],
|
|
},
|
|
{
|
|
"id": "2.4", "key": "kustomize", "domain": "DEPLOY",
|
|
"text": "Kustomize",
|
|
"tasks": [20, 61, 45],
|
|
"know": [
|
|
"A kustomization.yaml lists `resources` (files or other kustomizations) and transforms them: namespace, namePrefix, nameSuffix, labels, images, replicas, patches.",
|
|
"Generators build ConfigMaps and Secrets from files, env files or literals, add a content hash to the name and rewrite every reference to it.",
|
|
"Base and overlays: an overlay lists `../../base` under resources and changes only what differs.",
|
|
"`kubectl kustomize DIR` prints the result; `kubectl apply -k DIR` applies it.",
|
|
],
|
|
"fast": """
|
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
kind: Kustomization
|
|
resources: [../../base] # or files: [deployment.yaml, service.yaml]
|
|
namespace: prod
|
|
namePrefix: prod-
|
|
labels: [{pairs: {env: prod}, includeSelectors: false}]
|
|
images: [{name: nginx, newTag: "1.27"}]
|
|
replicas: [{name: web, count: 3}]
|
|
patches: [{path: patch.yaml}] # a partial Deployment with the same name
|
|
configMapGenerator: [{name: settings, envs: [settings.env]}]
|
|
secretGenerator: [{name: token, literals: [token=abc]}]
|
|
generatorOptions: {disableNameSuffixHash: true}
|
|
k kustomize DIR | less; k apply -k DIR; k diff -k DIR
|
|
""",
|
|
"traps": [
|
|
"Always read the render before you apply it.",
|
|
"`images`, `replicas` and patches name things as the base does, before any prefix or suffix.",
|
|
"Refer to a generated ConfigMap by its plain name - Kustomize appends the hash everywhere it is used.",
|
|
"`namespace:` does not create the Namespace.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/"],
|
|
},
|
|
# --------------------------------- 3. Application Observability and Maintenance (15%) ----
|
|
{
|
|
"id": "3.1", "key": "deprecations", "domain": "OBS",
|
|
"text": "Understand API deprecations",
|
|
"tasks": [22, 58],
|
|
"know": [
|
|
"Every object is a kind in an API group at a version (apps/v1 Deployment). Versions are deprecated, keep being served for some releases, then are removed.",
|
|
"What this cluster serves: `kubectl api-resources` (the APIVERSION column), `kubectl api-versions`, `kubectl explain KIND`.",
|
|
"A removed version fails with `no matches for kind \"X\" in version \"g/v\"`; a deprecated one still works and prints `Warning: ... is deprecated`.",
|
|
"Migrating is the new apiVersion plus whatever changed shape: an Ingress backend becomes `service: {name, port: {number}}` with a required pathType, and an apps/v1 Deployment needs `spec.selector`.",
|
|
],
|
|
"fast": """
|
|
k api-resources | grep -i -E 'cronjob|ingress' # NAME SHORTNAMES APIVERSION NAMESPACED KIND
|
|
k api-versions | grep batch
|
|
k explain hpa | head -4 # GROUP, KIND, VERSION
|
|
k apply -f old.yaml --dry-run=server # the error or warning, without changing anything
|
|
# removed, and what replaced them:
|
|
# extensions/v1beta1, networking.k8s.io/v1beta1 Ingress -> networking.k8s.io/v1 (1.22)
|
|
# extensions/v1beta1, apps/v1beta1, apps/v1beta2 Deployment -> apps/v1 (1.16)
|
|
# batch/v1beta1 CronJob -> batch/v1 (1.25)
|
|
# policy/v1beta1 PodDisruptionBudget -> policy/v1 (1.25)
|
|
# autoscaling/v2beta1, v2beta2 HorizontalPodAutoscaler -> autoscaling/v2 (1.25, 1.26)
|
|
""",
|
|
"traps": [
|
|
"The `kubectl convert` plugin is usually not installed - know the Ingress and Deployment rewrites by hand.",
|
|
"`kubectl get` shows an object in the API's preferred version whatever version it was written with.",
|
|
"A warning is not an error: the object was created. Read stderr.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/reference/using-api/deprecation-guide/",
|
|
"https://kubernetes.io/docs/reference/deprecation-policy/"],
|
|
},
|
|
{
|
|
"id": "3.2", "key": "probes", "domain": "OBS",
|
|
"text": "Implement probes and health checks",
|
|
"tasks": [7, 62, 46],
|
|
"know": [
|
|
"Liveness: failing restarts the container. Readiness: failing takes the Pod out of Service endpoints, no restart. Startup: holds the other two off until it has passed once.",
|
|
"Handlers: httpGet (path, port), tcpSocket (port), exec (command), grpc (port).",
|
|
"Timing: initialDelaySeconds (0), periodSeconds (10), timeoutSeconds (1), failureThreshold (3), successThreshold (1). A startup probe allows failureThreshold x periodSeconds to come up.",
|
|
],
|
|
"fast": """
|
|
containers:
|
|
- name: app
|
|
startupProbe: {httpGet: {path: /, port: 80}, periodSeconds: 5, failureThreshold: 12} # 60 s to start
|
|
readinessProbe: {httpGet: {path: /ready, port: 8080}, periodSeconds: 5, failureThreshold: 1}
|
|
livenessProbe: {tcpSocket: {port: 80}, initialDelaySeconds: 15, periodSeconds: 10}
|
|
# exec: readinessProbe: {exec: {command: [cat, /tmp/ready]}, periodSeconds: 5}
|
|
k describe pod POD | grep -E 'Liveness|Readiness|Startup'
|
|
k get events --field-selector reason=Unhealthy
|
|
k explain pod.spec.containers.livenessProbe
|
|
""",
|
|
"traps": [
|
|
"No kubectl flag adds a probe: generate the YAML and edit it, or `kubectl edit`.",
|
|
"Probes belong to a container, not to the Pod spec.",
|
|
"Restarts climbing with `Unhealthy` and `Killing` events but nothing wrong in the logs: the liveness probe is wrong or too impatient, not the app.",
|
|
"Running, 0/1 Ready and no restarts: the readiness probe is failing.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/",
|
|
"https://kubernetes.io/docs/concepts/workloads/pods/probes/"],
|
|
},
|
|
{
|
|
"id": "3.3", "key": "monitoring", "domain": "OBS",
|
|
"text": "Use built-in CLI tools to monitor Kubernetes applications",
|
|
"tasks": [23, 63, 48, 38],
|
|
"know": [
|
|
"`kubectl get` output: -o wide, yaml, json, name, jsonpath=, custom-columns=; narrowed with -l (labels), --field-selector, -n / -A; ordered with --sort-by.",
|
|
"`kubectl top pod|node` shows live CPU and memory from metrics-server; `--containers` splits a Pod up.",
|
|
"`kubectl describe` for the story of one object, `kubectl get events` for the Namespace.",
|
|
],
|
|
"fast": """
|
|
k top pod -A --sort-by=memory; k top pod --containers; k top node
|
|
k get pods -o wide --sort-by=.spec.nodeName
|
|
k get pods -o custom-columns=POD:.metadata.name,NODE:.spec.nodeName,IMAGE:.spec.containers[0].image
|
|
k get pod POD -o jsonpath='{.status.phase}'
|
|
k get pods -l tier=web --field-selector=status.phase!=Running -o name
|
|
k get pods -A --sort-by='.status.containerStatuses[0].restartCount'
|
|
k get events --sort-by=.lastTimestamp; k events --types=Warning --for pod/POD
|
|
""",
|
|
"traps": [
|
|
"`kubectl get --sort-by` sorts ascending (the largest is last); `kubectl top --sort-by` sorts descending (the largest is first).",
|
|
"`kubectl top` shows usage, not requests or limits, and needs about a minute after a Pod starts.",
|
|
"Quote jsonpath for the shell: '{.items[*].metadata.name}'.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/reference/kubectl/quick-reference/",
|
|
"https://kubernetes.io/docs/reference/kubectl/jsonpath/"],
|
|
},
|
|
{
|
|
"id": "3.4", "key": "logs", "domain": "OBS",
|
|
"text": "Utilize container logs",
|
|
"tasks": [24, 64, 49, 54, 39],
|
|
"know": [
|
|
"`kubectl logs` shows what a container wrote to stdout and stderr. The kubelet keeps it on the node, and it goes when the Pod is deleted.",
|
|
"Pick the container with -c (init containers too), or take --all-containers; --previous is the run before the last restart.",
|
|
"Narrow with --tail=N, --since=5m, --timestamps; follow with -f; by label with -l.",
|
|
],
|
|
"fast": """
|
|
k logs POD -c CONTAINER # --all-containers --prefix
|
|
k logs POD --previous # the crashed run
|
|
k logs POD --tail=20 --since=10m --timestamps
|
|
k logs -l app=web --prefix --tail=5 # every Pod with the label
|
|
k logs deploy/web; k logs job/render # one Pod of it
|
|
k logs POD | grep ERROR > errors.log
|
|
""",
|
|
"traps": [
|
|
"With -l, only the last 10 lines of each Pod are shown unless you set --tail (--tail=-1 for everything).",
|
|
"`kubectl logs deploy/x` reads one Pod, not all replicas.",
|
|
"--previous fails when the container has never restarted.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/cluster-administration/logging/",
|
|
"https://kubernetes.io/docs/reference/kubectl/quick-reference/"],
|
|
},
|
|
{
|
|
"id": "3.5", "key": "debugging", "domain": "OBS",
|
|
"text": "Debugging in Kubernetes",
|
|
"tasks": [8, 25, 26, 36, 47, 48],
|
|
"know": [
|
|
"Always the same order: get (status), describe (Events at the bottom), logs (--previous), then exec or debug.",
|
|
"Pending: nothing can schedule it (resources, taints, nodeSelector, an unbound claim). ImagePullBackOff / ErrImagePull: wrong image or tag. CreateContainerConfigError: a ConfigMap, Secret or key it references is missing.",
|
|
"CrashLoopBackOff: the process exits - read the previous log and lastState.terminated (reason, exitCode). OOMKilled (137): the memory limit is too low.",
|
|
"A Deployment with no Pods at all: describe its ReplicaSet - a quota, a LimitRange or Pod Security admission is rejecting them.",
|
|
],
|
|
"fast": """
|
|
k get pods -o wide; k describe pod POD | tail -20
|
|
k get pod POD -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}'
|
|
k get pod POD -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}' # .exitCode
|
|
k logs POD --previous; k exec -it POD -- sh
|
|
k debug -it POD --image=busybox:1.36 --target=CONTAINER # ephemeral container, sees its processes
|
|
k run tmp --rm -it --restart=Never --image=busybox:1.36 -- wget -qO- -T3 http://SVC:PORT
|
|
k describe rs -l app=web --show-events | tail; k get events --sort-by=.lastTimestamp
|
|
k get pod POD -o yaml > pod.yaml && vi pod.yaml && k replace --force -f pod.yaml
|
|
""",
|
|
"traps": [
|
|
"Most of a running Pod is immutable (the image is not): fix a bare Pod with `replace --force`, fix a Deployment's Pods in the Deployment.",
|
|
"Fix what the task names and nothing else - graders check that the ConfigMap, Service or Namespace you were told to leave is unchanged.",
|
|
"After fixing a Deployment wait for the rollout; old Pods linger in Terminating.",
|
|
"`kubectl describe` with a selector that matches several objects leaves their Events out - add `--show-events`, or describe one by name.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/tasks/debug/debug-application/debug-pods/",
|
|
"https://kubernetes.io/docs/tasks/debug/debug-application/debug-running-pod/",
|
|
"https://kubernetes.io/docs/tasks/debug/debug-application/determine-reason-pod-failure/"],
|
|
},
|
|
# -------------- 4. Application Environment, Configuration and Security (25%) ----
|
|
{
|
|
"id": "4.1", "key": "crds", "domain": "ENV",
|
|
"text": "Discover and use resources that extend Kubernetes (CRD, Operators)",
|
|
"tasks": [27, 59, 58],
|
|
"know": [
|
|
"A CustomResourceDefinition adds a kind to the API; its objects (custom resources) work with every kubectl verb. An operator is a CRD plus a controller acting on those objects.",
|
|
"Discover: `k get crd`, `k api-resources --api-group=GROUP` (kind, short names, namespaced or not), `k explain KIND.spec`.",
|
|
"The CRD's schema validates custom resources: required fields, enum values, minimum and maximum. The controller reports back in `.status`.",
|
|
],
|
|
"fast": """
|
|
k get crd | grep -i backup # the CRD's name is PLURAL.GROUP
|
|
k api-resources --api-group=data.example.com
|
|
k explain backup.spec # --recursive for the whole tree
|
|
k get backups -A; k get bkp NAME -o yaml; k describe backup NAME
|
|
k get crd backups.data.example.com -o yaml | less # served versions, schema, enum, maximum
|
|
k apply -f cr.yaml --dry-run=server # every validation error, nothing created
|
|
""",
|
|
"traps": [
|
|
"A custom resource's apiVersion is the CRD's GROUP/VERSION, never apiextensions.k8s.io/v1.",
|
|
"Field names come from the CRD's schema - check with `kubectl explain` instead of guessing.",
|
|
"Deleting a CRD deletes every object of that kind.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/",
|
|
"https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/",
|
|
"https://kubernetes.io/docs/concepts/extend-kubernetes/operator/"],
|
|
},
|
|
{
|
|
"id": "4.2", "key": "access", "domain": "ENV",
|
|
"text": "Understand authentication, authorization and admission control",
|
|
"tasks": [28, 29, 49, 53],
|
|
"know": [
|
|
"Every request passes authentication (who is it: a user certificate or token, a ServiceAccount token), authorization (may they: RBAC), then admission control (change or reject it: LimitRanger, ResourceQuota, PodSecurity).",
|
|
"RBAC: a Role or ClusterRole lists verbs on resources (and subresources such as pods/log, deployments/scale); a RoleBinding or ClusterRoleBinding grants it to users, groups or ServiceAccounts.",
|
|
"Pod Security admission: Namespace labels pod-security.kubernetes.io/enforce|warn|audit = privileged | baseline | restricted.",
|
|
"`Forbidden: User ... cannot list` is authorization. `violates PodSecurity`, `exceeded quota` and `maximum memory usage` are admission.",
|
|
],
|
|
"fast": """
|
|
k create role ci --verb=get,list,watch --resource=pods,pods/log -n NS
|
|
k create role ci --verb=get,update,patch --resource=deployments.apps,deployments.apps/scale -n NS
|
|
k create rolebinding ci --role=ci --serviceaccount=NS:SA -n NS # --user=jane --clusterrole=view
|
|
k auth can-i list pods -n NS --as=system:serviceaccount:NS:SA
|
|
k auth can-i --list -n NS --as=system:serviceaccount:NS:SA
|
|
k get ns NS --show-labels
|
|
# what `restricted` wants from every container:
|
|
securityContext: {allowPrivilegeEscalation: false, runAsNonRoot: true, capabilities: {drop: [ALL]},
|
|
seccompProfile: {type: RuntimeDefault}}
|
|
""",
|
|
"traps": [
|
|
"The same ServiceAccount is `--serviceaccount=NS:NAME` in a binding and `system:serviceaccount:NS:NAME` after `--as`.",
|
|
"`can-i create pods/exec` asks about a Pod named exec; use `can-i create pods --subresource=exec`.",
|
|
"Admission rejects Pods, not Deployments: the Deployment exists and shows 0 Pods, and the reason is in its ReplicaSet's events.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/reference/access-authn-authz/rbac/",
|
|
"https://kubernetes.io/docs/concepts/security/pod-security-admission/",
|
|
"https://kubernetes.io/docs/concepts/security/pod-security-standards/"],
|
|
},
|
|
{
|
|
"id": "4.3", "key": "quotas", "domain": "ENV",
|
|
"text": "Understand requests, limits, quotas",
|
|
"tasks": [30, 26, 53],
|
|
"know": [
|
|
"Requests are what the scheduler reserves on a node. Limits are the ceiling: CPU above it is throttled, memory above it gets the container OOMKilled.",
|
|
"A ResourceQuota caps a Namespace's totals (requests.cpu, limits.memory, pods, count/...). Once it covers CPU or memory, every new Pod must state them.",
|
|
"A LimitRange gives containers a default request and limit and enforces a min and max per container, at admission.",
|
|
],
|
|
"fast": """
|
|
k create quota compute-quota -n NS --hard=requests.cpu=1,requests.memory=1Gi,limits.cpu=2,limits.memory=2Gi,pods=6
|
|
k describe quota -n NS # Used against Hard
|
|
k describe limitrange -n NS
|
|
apiVersion: v1
|
|
kind: LimitRange
|
|
metadata: {name: container-defaults}
|
|
spec:
|
|
limits:
|
|
- type: Container
|
|
defaultRequest: {cpu: 100m, memory: 128Mi} # the request a container gets
|
|
default: {cpu: 250m, memory: 256Mi} # the LIMIT a container gets
|
|
max: {cpu: 500m, memory: 512Mi}
|
|
""",
|
|
"traps": [
|
|
"`default` is the default limit and `defaultRequest` the default request - they are easy to swap.",
|
|
"Quotas and LimitRanges only act on Pods created afterwards.",
|
|
"The rejection is not on the Deployment: `k describe rs` shows `exceeded quota` or `maximum memory usage per Container`.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/policy/resource-quotas/",
|
|
"https://kubernetes.io/docs/concepts/policy/limit-range/"],
|
|
},
|
|
{
|
|
"id": "4.4", "key": "configmaps", "domain": "ENV",
|
|
"text": "Understand ConfigMaps",
|
|
"tasks": [32, 10, 25, 51, 45],
|
|
"know": [
|
|
"Create from literals, files, directories or env files. Each key becomes an environment variable (configMapKeyRef, envFrom) or a file (a configMap volume; `items` picks keys).",
|
|
"Mounted files follow a changed ConfigMap after a short delay. Environment variables and subPath mounts never do - restart the Pods.",
|
|
"A Pod that references a missing ConfigMap or key waits in CreateContainerConfigError unless the reference is `optional: true`.",
|
|
],
|
|
"fast": """
|
|
k create cm app --from-literal=TIER=backend --from-file=app.properties # --from-file=KEY=path --from-env-file=x.env
|
|
env:
|
|
- {name: APP_TIER, valueFrom: {configMapKeyRef: {name: app, key: TIER}}}
|
|
envFrom: [{configMapRef: {name: app}}]
|
|
volumes: [{name: cfg, configMap: {name: app, items: [{key: app.properties, path: app.properties}]}}]
|
|
volumeMounts: [{name: cfg, mountPath: /etc/app}] # one file into a full directory: add subPath: app.properties
|
|
k set env deploy/web --from=configmap/app; k exec POD -- env | grep TIER
|
|
k rollout restart deploy/web # pick up a change
|
|
""",
|
|
"traps": [
|
|
"`--from-file` makes one key named after the file; `--from-env-file` makes one key per KEY=VALUE line.",
|
|
"A volume mounted on a directory hides what the image had there.",
|
|
"`immutable: true` cannot be edited or undone - delete the ConfigMap and create it again.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/configuration/configmap/",
|
|
"https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/"],
|
|
},
|
|
{
|
|
"id": "4.5", "key": "resources", "domain": "ENV",
|
|
"text": "Define resource requirements",
|
|
"tasks": [31, 26, 15, 53],
|
|
"know": [
|
|
"Per container: resources.requests and resources.limits for cpu (cores or millicores: 250m) and memory (Mi, Gi).",
|
|
"QoS class: Guaranteed (every container has requests equal to limits for both CPU and memory), Burstable (some are set), BestEffort (none). BestEffort is evicted first.",
|
|
"A container with a limit but no request gets a request equal to the limit.",
|
|
],
|
|
"fast": """
|
|
k set resources deploy/web -c app --requests=cpu=250m,memory=128Mi --limits=cpu=250m,memory=128Mi
|
|
resources:
|
|
requests: {cpu: 100m, memory: 64Mi}
|
|
limits: {cpu: 250m, memory: 128Mi}
|
|
k get pod POD -o jsonpath='{.status.qosClass}'
|
|
k describe node NODE | grep -A8 'Allocated resources'
|
|
""",
|
|
"traps": [
|
|
"`kubectl run` has no --requests or --limits any more: generate the YAML, or use `kubectl set resources` on a workload.",
|
|
"Guaranteed needs every container of the Pod, sidecars included.",
|
|
"Memory is Mi, not m: `128m` is a fraction of a byte.",
|
|
"A request no node can fit leaves the Pod Pending with `Insufficient cpu` or `Insufficient memory`.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
|
|
"https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/"],
|
|
},
|
|
{
|
|
"id": "4.6", "key": "secrets", "domain": "ENV",
|
|
"text": "Create & consume Secrets",
|
|
"tasks": [33, 10, 37, 50],
|
|
"know": [
|
|
"A Secret's `data` is base64 - an encoding, not encryption. `stringData` takes plain text when you write one.",
|
|
"Types: Opaque (generic), kubernetes.io/tls, kubernetes.io/dockerconfigjson, kubernetes.io/service-account-token.",
|
|
"Consumed like a ConfigMap: secretKeyRef, envFrom secretRef, or a secret volume (`secretName`, `defaultMode`, `items`).",
|
|
],
|
|
"fast": """
|
|
k create secret generic api --from-literal=username=svc --from-literal=password='S3cr3t!'
|
|
k create secret tls web-tls --cert=tls.crt --key=tls.key
|
|
k create secret docker-registry regcred --docker-server=REG --docker-username=U --docker-password=P
|
|
k get secret api -o jsonpath='{.data.password}' | base64 -d
|
|
env: [{name: API_USER, valueFrom: {secretKeyRef: {name: api, key: username}}}]
|
|
volumes: [{name: creds, secret: {secretName: api, defaultMode: 0400}}]
|
|
volumeMounts: [{name: creds, mountPath: /etc/creds, readOnly: true}]
|
|
""",
|
|
"traps": [
|
|
"A secret volume names it with `secretName`; a configMap volume uses `name`.",
|
|
"Encoding by hand: `echo -n value | base64`, or the newline becomes part of the secret.",
|
|
"defaultMode is octal in YAML (0400) and shows as decimal in JSON (256).",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/configuration/secret/",
|
|
"https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/"],
|
|
},
|
|
{
|
|
"id": "4.7", "key": "serviceaccounts", "domain": "ENV",
|
|
"text": "Understand ServiceAccounts",
|
|
"tasks": [34, 28, 10, 49],
|
|
"know": [
|
|
"Every Pod runs as a ServiceAccount (`default` if you name none); its short-lived token is mounted at /var/run/secrets/kubernetes.io/serviceaccount.",
|
|
"Choose it with spec.serviceAccountName. Stop the mount with `automountServiceAccountToken: false` on the ServiceAccount or on the Pod (the Pod wins).",
|
|
"Tokens: `kubectl create token` for a short-lived one; a Secret of type kubernetes.io/service-account-token annotated with the account's name for a long-lived one.",
|
|
"A ServiceAccount can do nothing until RBAC binds a Role to it.",
|
|
],
|
|
"fast": """
|
|
k create sa ci -n NS
|
|
k set serviceaccount deploy/web ci -n NS # = spec.template.spec.serviceAccountName
|
|
k patch sa ci -n NS -p '{"automountServiceAccountToken": false}'
|
|
k create token ci -n NS --duration=1h
|
|
k auth can-i list pods -n NS --as=system:serviceaccount:NS:ci
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata: {name: ci-token, annotations: {kubernetes.io/service-account.name: ci}}
|
|
type: kubernetes.io/service-account-token
|
|
""",
|
|
"traps": [
|
|
"No token Secret is made for a new ServiceAccount (since 1.24) - create it yourself when a task wants one.",
|
|
"serviceAccountName cannot be changed on a running Pod; on a Deployment it starts a rollout.",
|
|
"A Pod naming a ServiceAccount that does not exist is never created - see the ReplicaSet's events.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/security/service-accounts/",
|
|
"https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/"],
|
|
},
|
|
{
|
|
"id": "4.8", "key": "security", "domain": "ENV",
|
|
"text": "Understand Application Security (SecurityContexts, Capabilities, etc.)",
|
|
"tasks": [9, 29, 44, 52],
|
|
"know": [
|
|
"Pod level (spec.securityContext): runAsUser, runAsGroup, runAsNonRoot, fsGroup, supplementalGroups, seccompProfile - for every container.",
|
|
"Container level adds allowPrivilegeEscalation, privileged, readOnlyRootFilesystem and capabilities, and overrides the Pod level.",
|
|
"Capabilities are written without the CAP_ prefix: NET_BIND_SERVICE, NET_ADMIN, SYS_TIME. Drop ALL, then add back what the app needs.",
|
|
],
|
|
"fast": """
|
|
spec:
|
|
securityContext: {runAsUser: 1000, runAsGroup: 1000, runAsNonRoot: true, fsGroup: 2000}
|
|
containers:
|
|
- name: app
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
capabilities: {drop: [ALL], add: [NET_BIND_SERVICE]}
|
|
k exec POD -- id
|
|
k explain pod.spec.securityContext; k explain pod.spec.containers.securityContext
|
|
""",
|
|
"traps": [
|
|
"capabilities exist only on a container, fsGroup only on the Pod.",
|
|
"runAsNonRoot with an image that runs as root and no runAsUser: CreateContainerConfigError.",
|
|
"readOnlyRootFilesystem breaks an app that writes to /tmp or /var/cache - mount an emptyDir there.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/tasks/configure-pod-container/security-context/"],
|
|
},
|
|
# -------------------------------------------------- 5. Services and Networking (20%) ----
|
|
{
|
|
"id": "5.1", "key": "netpol", "domain": "NET",
|
|
"text": "Demonstrate basic understanding of NetworkPolicies",
|
|
"tasks": [12, 35, 55],
|
|
"know": [
|
|
"Pods accept and send everything until a NetworkPolicy selects them. From then on, for the policyTypes it lists, only what some policy allows gets through.",
|
|
"Policies only allow, and they add up: there is no deny rule and no ordering.",
|
|
"Peers: podSelector (same Namespace), namespaceSelector, ipBlock. Two selectors in one list item mean AND; two list items mean OR.",
|
|
"Default deny: `podSelector: {}` with a policyType and no rules. Locking down egress also needs DNS allowed (UDP and TCP 53).",
|
|
],
|
|
"fast": """
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata: {name: db-allow, namespace: NS}
|
|
spec:
|
|
podSelector: {matchLabels: {app: db}}
|
|
policyTypes: [Ingress]
|
|
ingress:
|
|
- from:
|
|
- podSelector: {matchLabels: {app: api}} # OR
|
|
- namespaceSelector: {matchLabels: {purpose: monitoring}}
|
|
ports: [{protocol: TCP, port: 5432}]
|
|
# egress that keeps DNS: egress: [{ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}]}, ...]
|
|
k describe netpol db-allow -n NS
|
|
k exec CLIENT -- wget -qO- -T2 http://db:5432 # test it with real traffic
|
|
""",
|
|
"traps": [
|
|
"There is no `kubectl create networkpolicy` - copy the skeleton from the docs.",
|
|
"`- podSelector: ...` followed by `namespaceSelector:` in the same item is AND; a second `- namespaceSelector:` is OR.",
|
|
"A missing or empty `ingress` denies everything; `ingress: [{}]` allows everything.",
|
|
"The policy lives in the Namespace of the Pods it selects, and a Namespace is matched by its labels (kubernetes.io/metadata.name=NAME always exists).",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/services-networking/network-policies/"],
|
|
},
|
|
{
|
|
"id": "5.2", "key": "services", "domain": "NET",
|
|
"text": "Provide and troubleshoot access to applications via services",
|
|
"tasks": [11, 36, 54, 56],
|
|
"know": [
|
|
"A Service selects Pods by label and forwards `port` to `targetPort` (a number, or the name of a container port).",
|
|
"Types: ClusterIP (inside the cluster), NodePort (30000-32767 on every node), LoadBalancer, ExternalName; headless (`clusterIP: None`) gives each Pod its own DNS record.",
|
|
"DNS names: SVC, SVC.NS, SVC.NS.svc.cluster.local.",
|
|
"Troubleshoot in this order: are there endpoints, does the selector match the Pod labels, are the Pods Ready, is targetPort the port the container listens on.",
|
|
],
|
|
"fast": """
|
|
k expose deploy web --port=8080 --target-port=80 # --name=web-svc --type=NodePort
|
|
k create svc nodeport web-ext --tcp=80:80 --node-port=30080
|
|
k get svc,endpointslices -o wide; k describe svc web # Endpoints: <none> = nothing selected
|
|
k get pods -l app=web --show-labels
|
|
k run tmp --rm -it --restart=Never --image=busybox:1.36 -- wget -qO- -T3 http://web:8080
|
|
k port-forward svc/web 8080:80
|
|
""",
|
|
"traps": [
|
|
"`kubectl expose` copies the workload's selector; `kubectl create svc` selects app=<service name>, which may match nothing.",
|
|
"`expose` cannot choose the nodePort - use `create svc nodeport --node-port` or edit the YAML.",
|
|
"A Service without endpoints still resolves in DNS; the connection is what fails.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/services-networking/service/",
|
|
"https://kubernetes.io/docs/tasks/debug/debug-application/debug-service/",
|
|
"https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/"],
|
|
},
|
|
{
|
|
"id": "5.3", "key": "ingress", "domain": "NET",
|
|
"text": "Use Ingress rules to expose applications",
|
|
"tasks": [37, 11, 57],
|
|
"know": [
|
|
"An Ingress routes HTTP(S) by host and path to Services of its own Namespace; an ingress controller, chosen with ingressClassName, does the work.",
|
|
"Each path has a pathType (Prefix, Exact or ImplementationSpecific) and a backend: service.name plus service.port.number or .name.",
|
|
"TLS: spec.tls lists hosts and the kubernetes.io/tls Secret that holds their certificate.",
|
|
],
|
|
"fast": """
|
|
k create ingress shop --class=nginx \\
|
|
--rule="shop.example.com/api*=api:8080,tls=shop-tls" --rule="shop.example.com/*=web:80,tls=shop-tls"
|
|
k create secret tls shop-tls --cert=tls.crt --key=tls.key
|
|
k get ingress,ingressclass; k describe ingress shop # a missing backend shows as <error: services "x" not found>
|
|
k explain ingress.spec.rules.http.paths
|
|
""",
|
|
"traps": [
|
|
"In --rule a trailing * means pathType Prefix; without it the path is Exact.",
|
|
"The backend port is the Service's port, not the container's.",
|
|
"No ingress controller runs in this simulator, so Ingress tasks are graded on the object - on the exam, curl it.",
|
|
],
|
|
"docs": ["https://kubernetes.io/docs/concepts/services-networking/ingress/"],
|
|
},
|
|
]
|