[ { "id": "pod-basics-mcq", "title": "Pod Lifecycle States", "category": "Core Concepts", "difficulty": "Easy", "type": "mcq", "weight": 3, "description": "## What does the `CrashLoopBackOff` status mean for a Pod?\n\nYou observe the following when running `kubectl get pods`:\n\n```\nNAME READY STATUS RESTARTS AGE\nmy-app-xyz 0/1 CrashLoopBackOff 5 3m\n```\n\nWhat is the Kubernetes control plane communicating with this status?", "options": [ { "id": "a", "text": "The pod image could not be pulled from the container registry" }, { "id": "b", "text": "The container starts, crashes, and Kubernetes keeps restarting it with exponential backoff delay" }, { "id": "c", "text": "The pod is waiting for a PersistentVolume to become available" }, { "id": "d", "text": "The pod has been evicted from the node due to resource pressure" } ], "correct_option": "b", "explanation": "`CrashLoopBackOff` means the container is repeatedly crashing after startup. Kubernetes restarts it automatically but introduces increasing delays (backoff) between attempts to avoid overwhelming the system. Common causes include a bad entrypoint command, missing environment variables, or application errors on startup.", "hints": [ { "title": "Interpreting pod status", "body": "Use `kubectl describe pod ` to see the Events section — it shows exactly why the container is failing.", "command": "kubectl describe pod my-app-xyz" }, { "title": "Reading container logs", "body": "Even a crashed container leaves logs behind. Use `--previous` to read the logs from the last crash.", "command": "kubectl logs my-app-xyz --previous" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "deploy-nginx", "title": "Deploy and Expose Nginx", "category": "Workloads", "difficulty": "Easy", "type": "task", "weight": 5, "description": "## Deploy and Expose an Nginx Application\n\nThe platform team needs a simple web server running in the cluster.\n\n**Your tasks:**\n1. Create a **Deployment** named `webserver` in the `default` namespace\n2. Use the image `nginx:1.25`\n3. Set replica count to **2**\n4. Expose it via a **ClusterIP Service** named `webserver-svc` on port **80**\n\n> 💡 Tip: You can use `kubectl create` for both resources imperatively, or write YAML manifests.", "hints": [ { "title": "Create the Deployment", "body": "Use `kubectl create deployment` with the `--image` and `--replicas` flags.", "command": "kubectl create deployment webserver --image=nginx:1.25 --replicas=2" }, { "title": "Expose the Deployment", "body": "Use `kubectl expose` to create a ClusterIP service targeting port 80.", "command": "kubectl expose deployment webserver --name=webserver-svc --port=80 --target-port=80" }, { "title": "Verify", "body": "Check that both resources are up and the service endpoints are populated.", "command": "kubectl get deployment webserver && kubectl get svc webserver-svc && kubectl get endpoints webserver-svc" } ], "setup_commands": [], "validation": { "description": "Checks that the deployment exists with 2 replicas, uses the correct image, and the service exists targeting port 80.", "commands": [ { "description": "Deployment 'webserver' exists", "command": "kubectl get deployment webserver -o jsonpath='{.metadata.name}'", "expected_output": "webserver", "match": "exact" }, { "description": "Deployment has 2 ready replicas", "command": "kubectl get deployment webserver -o jsonpath='{.status.readyReplicas}'", "expected_output": "2", "match": "exact" }, { "description": "Deployment uses nginx:1.25 image", "command": "kubectl get deployment webserver -o jsonpath='{.spec.template.spec.containers[0].image}'", "expected_output": "nginx:1.25", "match": "exact" }, { "description": "Service 'webserver-svc' exists", "command": "kubectl get svc webserver-svc -o jsonpath='{.metadata.name}'", "expected_output": "webserver-svc", "match": "exact" }, { "description": "Service exposes port 80", "command": "kubectl get svc webserver-svc -o jsonpath='{.spec.ports[0].port}'", "expected_output": "80", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete deployment webserver --ignore-not-found" }, { "command": "kubectl delete svc webserver-svc --ignore-not-found" } ] }, { "id": "configmap-volume", "title": "Mount ConfigMap as Volume", "category": "Configuration", "difficulty": "Medium", "type": "task", "weight": 7, "description": "## Inject Configuration via a ConfigMap Volume\n\nAn application reads its configuration from files on disk at `/etc/app-config/`.\n\n**Your tasks:**\n1. Create a **ConfigMap** named `app-config` in the `default` namespace with the following key-value pairs:\n - `environment`: `production`\n - `log_level`: `warn`\n - `max_connections`: `200`\n2. Create a **Pod** named `config-reader` using image `busybox:1.36`\n3. Mount the ConfigMap as a **volume** at `/etc/app-config` inside the container\n4. The pod should run the command: `sleep 3600`", "hints": [ { "title": "Create the ConfigMap", "body": "Use `--from-literal` for each key-value pair.", "command": "kubectl create configmap app-config --from-literal=environment=production --from-literal=log_level=warn --from-literal=max_connections=200" }, { "title": "Write the Pod manifest", "body": "Use a heredoc piped directly to kubectl apply -f - to avoid writing to a temp file.", "command": "cat < Start by describing the deployment and its pods to find clues about what's wrong.", "hints": [ { "title": "Inspect the deployment", "body": "Start by listing pods in the debug namespace and describing the failing ones.", "command": "kubectl get pods -n debug\nkubectl describe pod -n debug -l app=broken-app" }, { "title": "Check events", "body": "The Events section in `kubectl describe` usually tells you exactly what's wrong — look for ImagePullBackOff, OOMKilled, or probe failures.", "command": "kubectl describe deployment broken-app -n debug" }, { "title": "Fix the image tag", "body": "If the image tag doesn't exist, update it to a valid one using `kubectl set image`.", "command": "kubectl set image deployment/broken-app app=nginx:1.25 -n debug" } ], "setup_commands": [ { "command": "kubectl create namespace debug" }, { "command": "kubectl create deployment broken-app --image=nginx:invalid-tag-99999 --replicas=3 -n debug" } ], "validation": { "description": "Checks that the broken-app deployment in the debug namespace has 3 ready replicas.", "commands": [ { "description": "Deployment 'broken-app' has 3 ready replicas", "command": "kubectl get deployment broken-app -n debug -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -v '^$' || echo 0", "expected_output": "3", "match": "exact" }, { "description": "No pods in ImagePullBackOff state", "command": "kubectl get pods -n debug --no-headers 2>/dev/null | awk '{print $3}' | grep -c 'ImagePullBackOff\\|ErrImagePull' || true", "expected_output": "0", "match": "exact" } ] }, "default_namespace": "debug", "teardown_commands": [ { "command": "kubectl delete namespace debug --ignore-not-found --wait=false" } ] }, { "id": "pv-pvc-mount", "title": "Persistent Storage with PV and PVC", "category": "Storage", "difficulty": "Medium", "type": "task", "weight": 7, "description": "## Attach Persistent Storage to a Pod\n\nA stateful application needs data to survive pod restarts.\n\n**Your tasks:**\n1. Create a **PersistentVolumeClaim** named `local-pvc` in the `default` namespace requesting `200Mi` with:\n - Access mode: `ReadWriteOnce`\n - StorageClass: `local-path` (available in this cluster via k3s)\n2. Create a **Pod** named `storage-pod` using image `nginx:1.25` that mounts the PVC at `/data`\n3. Write a file inside the pod at `/data/hello.txt` with content `hello-k8s`", "hints": [ { "title": "Create the PVC", "body": "Use storageClassName: local-path to use the k3s built-in dynamic provisioner. No PV needs to be created manually.", "command": "cat < /data/hello.txt'" } ], "setup_commands": [], "validation": { "description": "Validates PV, PVC binding, pod running, and file written to the volume.", "commands": [ { "description": "PVC local-pvc is Bound", "command": "kubectl get pvc local-pvc -o jsonpath='{.status.phase}'", "expected_output": "Bound", "match": "exact" }, { "description": "Pod 'storage-pod' is Running", "command": "kubectl get pod storage-pod -o jsonpath='{.status.phase}'", "expected_output": "Running", "match": "exact" }, { "description": "File /data/hello.txt contains 'hello-k8s'", "command": "kubectl exec storage-pod -- cat /data/hello.txt", "expected_output": "hello-k8s", "match": "contains" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete pod storage-pod --ignore-not-found --grace-period=0 --force" }, { "command": "kubectl delete pvc local-pvc --ignore-not-found" }, { "command": "kubectl delete pv local-pv --ignore-not-found" } ] }, { "id": "network-policy", "title": "Isolate Traffic with NetworkPolicy", "category": "Networking", "difficulty": "Hard", "type": "task", "weight": 9, "description": "## Implement Network Isolation\n\nA pre-created `database` pod in the `netpol` namespace must only accept traffic from pods labeled `role=backend`.\n\n**Your tasks:**\n1. Create a **NetworkPolicy** named `db-isolate` in the `netpol` namespace that:\n - Targets pods with label `app=database`\n - Allows **ingress** only from pods with label `role=backend` in the **same namespace**\n - Denies all other ingress traffic\n2. Verify the policy is applied correctly\n\n> The `database` and `backend` pods are pre-created for you.", "hints": [ { "title": "Understand NetworkPolicy selectors", "body": "A NetworkPolicy uses `podSelector` to pick which pods it applies to, and `ingress.from` to define allowed sources.", "command": "kubectl explain networkpolicy.spec.ingress.from" }, { "title": "Write the NetworkPolicy", "body": "An empty ingress rule (no `from`) denies everything. Specifying a `from` allows only those sources.", "command": "cat <` to create a new namespace.", "command": "kubectl create namespace team-alpha" }, { "title": "Deploy into a specific namespace", "body": "Use the `-n` or `--namespace` flag with `kubectl create deployment`. Alternatively use `kubectl apply -f` with `namespace` set in the manifest metadata.", "command": "kubectl create deployment web --image=nginx:alpine --replicas=2 -n team-alpha" } ], "setup_commands": [], "validation": { "commands": [ { "description": "Namespace 'team-alpha' exists", "command": "kubectl get namespace team-alpha -o jsonpath='{.metadata.name}'", "expected_output": "team-alpha", "match": "exact" }, { "description": "Deployment 'web' exists in team-alpha", "command": "kubectl get deployment web -n team-alpha -o jsonpath='{.metadata.name}'", "expected_output": "web", "match": "exact" }, { "description": "Deployment 'web' has 2 replicas", "command": "kubectl get deployment web -n team-alpha -o jsonpath='{.spec.replicas}'", "expected_output": "2", "match": "exact" }, { "description": "Deployment uses nginx image", "command": "kubectl get deployment web -n team-alpha -o jsonpath='{.spec.template.spec.containers[0].image}'", "expected_output": "nginx", "match": "contains" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete namespace team-alpha --ignore-not-found --wait=false" } ] }, { "id": "labels-selectors-mcq", "title": "Labels and Selectors", "category": "Core Concepts", "difficulty": "Easy", "type": "mcq", "weight": 3, "description": "## Labels and Selectors\n\nYou run the following command:\n\n```bash\nkubectl get pods -l env=prod,tier=frontend\n```\n\nWhat does this command return?", "options": [ { "id": "a", "text": "Pods that have BOTH `env=prod` AND `tier=frontend` labels" }, { "id": "b", "text": "Pods that have either `env=prod` OR `tier=frontend` labels" }, { "id": "c", "text": "Pods that do NOT have `env=prod` or `tier=frontend` labels" }, { "id": "d", "text": "All pods, sorted by the `env` and `tier` label values" } ], "correct_option": "a", "explanation": "When you specify multiple label selector expressions separated by commas, Kubernetes applies a logical **AND** — all conditions must be true. Only pods that have both `env=prod` **and** `tier=frontend` are returned. Use `-l 'env in (prod,staging)'` for OR-style matching.", "hints": [ { "title": "How comma-separated selectors work", "body": "The `-l` flag accepts a comma-separated list of `key=value` expressions. Multiple expressions are ANDed together — all must match for a pod to be included in the result.", "command": "kubectl get pods -l env=prod,tier=frontend" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "resource-limits-task", "title": "Resource Requests and Limits", "category": "Configuration", "difficulty": "Easy", "type": "task", "weight": 5, "description": "## Resource Requests and Limits\n\nProper resource management prevents noisy-neighbour issues and enables the Kubernetes scheduler to make good placement decisions.\n\n**Your task:**\n\nCreate a Pod named `limited-pod` using the `nginx:alpine` image with the following resource configuration:\n\n| | CPU | Memory |\n|---|---|---|\n| **Request** | `100m` | `64Mi` |\n| **Limit** | `200m` | `128Mi` |\n\n```bash\n# Tip: write a manifest and apply it\nkubectl apply -f limited-pod.yaml\n```", "hints": [ { "title": "Pod manifest with resources", "body": "Add a `resources` block under `spec.containers[].resources` with `requests` and `limits` sub-keys.", "command": "cat < =\n```", "hints": [ { "title": "Set a new image on a Deployment", "body": "Use `kubectl set image deployment/ =` to trigger a rolling update.", "command": "kubectl set image deployment/app-v1 nginx=nginx:1.25" }, { "title": "Check rollout progress", "body": "Monitor the rollout with `kubectl rollout status`. Once complete, all pods will run the new image.", "command": "kubectl get deployment app-v1 -o jsonpath='{.spec.template.spec.containers[0].image}'" } ], "setup_commands": [ { "command": "kubectl create deployment app-v1 --image=nginx:1.24 --replicas=2" }, { "command": "kubectl rollout status deployment/app-v1 --timeout=60s" } ], "validation": { "commands": [ { "description": "Deployment 'app-v1' image is nginx:1.25", "command": "kubectl get deployment app-v1 -o jsonpath='{.spec.template.spec.containers[0].image}'", "expected_output": "nginx:1.25", "match": "exact" }, { "description": "Deployment has 2 ready replicas", "command": "kubectl get deployment app-v1 -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -v '^$' || echo 0", "expected_output": "2", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete deployment app-v1 --ignore-not-found" } ] }, { "id": "init-containers-mcq", "title": "Init Containers Behaviour", "category": "Core Concepts", "difficulty": "Easy", "type": "mcq", "weight": 3, "description": "## Init Containers Behaviour\n\nA pod spec defines two init containers (`init-db-check` and `init-config`) followed by one main app container (`web`).\n\nWhich statement **correctly** describes how Kubernetes runs these containers?", "options": [ { "id": "a", "text": "All three containers start simultaneously; init containers simply have lower priority" }, { "id": "b", "text": "Init containers run sequentially to completion before the main `web` container starts" }, { "id": "c", "text": "The `web` container starts first, and init containers run as sidecars alongside it" }, { "id": "d", "text": "Init containers cannot share volumes with the main container" } ], "correct_option": "b", "explanation": "Init containers always run **sequentially** and must each exit with a success (exit code 0) before the next one starts. Only after **all** init containers complete successfully does Kubernetes start the main application containers. They can share volumes with main containers, making them ideal for setup tasks like seeding configs, waiting for dependencies, or initialising databases.", "hints": [ { "title": "Init container execution order", "body": "Check `kubectl explain pod.spec.initContainers`. Each init container runs to completion before the next begins, and all must succeed before app containers start.", "command": "kubectl explain pod.spec.initContainers" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "env-vars-configmap", "title": "Environment Variables from ConfigMap", "category": "Configuration", "difficulty": "Easy", "type": "task", "weight": 4, "description": "## Environment Variables from ConfigMap\n\nConfigMaps can inject configuration as environment variables into pods, keeping application images generic and portable.\n\n**Your task:**\n\n1. Create a ConfigMap named `app-env` with two keys:\n - `LOG_LEVEL=debug`\n - `APP_PORT=8080`\n\n2. Create a Pod named `env-pod` using `busybox:1.36` with command `sleep 3600` that loads **all keys** from `app-env` as environment variables using `envFrom`.\n\n**Verify:**\n```bash\nkubectl exec env-pod -- env | grep -E 'LOG_LEVEL|APP_PORT'\n```", "hints": [ { "title": "Create the ConfigMap", "body": "Use `kubectl create configmap` with multiple `--from-literal` flags.", "command": "kubectl create configmap app-env --from-literal=LOG_LEVEL=debug --from-literal=APP_PORT=8080" }, { "title": "Load all ConfigMap keys via envFrom", "body": "Use `envFrom` with `configMapRef` to load all keys at once — simpler than mapping each key individually.", "command": "cat </dev/null | grep -v '^$' || echo 0", "expected_output": "4", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete deployment webapp --ignore-not-found" } ] }, { "id": "kubectl-essentials-mcq", "title": "Essential kubectl Commands", "category": "Core Concepts", "difficulty": "Easy", "type": "mcq", "weight": 2, "description": "## Essential kubectl Commands\n\nYou need to quickly view the **logs** of a container named `api` inside a pod named `backend-7d9f`. The pod has multiple containers.\n\nWhich command is correct?", "options": [ { "id": "a", "text": "`kubectl describe pod backend-7d9f`" }, { "id": "b", "text": "`kubectl logs backend-7d9f`" }, { "id": "c", "text": "`kubectl logs backend-7d9f -c api`" }, { "id": "d", "text": "`kubectl exec backend-7d9f -- cat /var/log/api.log`" } ], "correct_option": "c", "explanation": "When a pod has **multiple containers**, you must specify which container's logs you want using the `-c ` flag: `kubectl logs -c `. Without `-c`, kubectl returns an error if the pod has more than one container. `kubectl describe` shows metadata and events, not live logs. `kubectl exec` can work but is cumbersome and container-runtime dependent.", "hints": [ { "title": "kubectl logs flags", "body": "Key flags: `-c` (container name for multi-container pods), `-f` (follow/stream), `--previous` (crashed container logs), `--since=1h` (time filter).", "command": "kubectl logs --help | head -30" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "rbac-clusterrole", "title": "ClusterRole and ClusterRoleBinding", "category": "Cluster Administration", "difficulty": "Medium", "type": "task", "weight": 8, "description": "## ClusterRole and ClusterRoleBinding\n\nRBAC **ClusterRoles** grant permissions cluster-wide (across all namespaces), unlike Roles which are namespace-scoped.\n\n**Your task:**\n\n1. Create a **ClusterRole** named `pod-reader` that allows `get`, `list`, `watch` on `pods`\n2. Create a **ClusterRoleBinding** named `pod-reader-binding` that binds `pod-reader` to the ServiceAccount `default` in the `default` namespace\n\n```bash\nkubectl create clusterrole --help\nkubectl create clusterrolebinding --help\n```", "hints": [ { "title": "Create the ClusterRole", "body": "Use `kubectl create clusterrole` with `--verb` and `--resource` flags.", "command": "kubectl create clusterrole pod-reader --verb=get,list,watch --resource=pods" }, { "title": "Create the ClusterRoleBinding", "body": "Bind the ClusterRole to a ServiceAccount using `--serviceaccount=namespace:name`.", "command": "kubectl create clusterrolebinding pod-reader-binding --clusterrole=pod-reader --serviceaccount=default:default" } ], "setup_commands": [], "validation": { "commands": [ { "description": "ClusterRole 'pod-reader' exists", "command": "kubectl get clusterrole pod-reader -o jsonpath='{.metadata.name}'", "expected_output": "pod-reader", "match": "exact" }, { "description": "ClusterRole allows 'get' on pods", "command": "kubectl get clusterrole pod-reader -o jsonpath='{.rules[0].verbs[*]}'", "expected_output": "get", "match": "contains" }, { "description": "ClusterRoleBinding 'pod-reader-binding' exists", "command": "kubectl get clusterrolebinding pod-reader-binding -o jsonpath='{.metadata.name}'", "expected_output": "pod-reader-binding", "match": "exact" }, { "description": "Binding references the 'pod-reader' ClusterRole", "command": "kubectl get clusterrolebinding pod-reader-binding -o jsonpath='{.roleRef.name}'", "expected_output": "pod-reader", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete clusterrole pod-reader --ignore-not-found" }, { "command": "kubectl delete clusterrolebinding pod-reader-binding --ignore-not-found" } ] }, { "id": "node-label-selector", "title": "Node Labels and nodeSelector", "category": "Cluster Administration", "difficulty": "Easy", "type": "task", "weight": 5, "description": "## Node Labels and nodeSelector\n\nLabelling nodes lets you constrain which nodes a pod can be scheduled on, using `nodeSelector` in the pod spec.\n\n**Your task:**\n\n1. Add the label `disk=ssd` to node `k8s-lab`\n2. Create a Pod named `ssd-pod` using `nginx:alpine` that uses `nodeSelector` to target nodes with `disk=ssd`", "hints": [ { "title": "Label the node", "body": "Use `kubectl label node key=value`.", "command": "kubectl label node k8s-lab disk=ssd --overwrite" }, { "title": "Use nodeSelector in pod spec", "body": "Add `nodeSelector: disk: ssd` under `spec` in the pod manifest.", "command": "cat </dev/null || true" }, { "command": "kubectl delete pod ssd-pod --ignore-not-found --grace-period=0 --force" } ] }, { "id": "node-taint-toleration", "title": "Taints and Tolerations", "category": "Cluster Administration", "difficulty": "Medium", "type": "task", "weight": 7, "description": "## Taints and Tolerations\n\nTaints allow a node to **repel** pods. A toleration on a pod allows it to be scheduled onto a tainted node.\n\nThe node `k8s-lab` has been tainted with `env=gpu:NoSchedule`.\n\n**Your task:**\n\nCreate a Pod named `gpu-pod` using `nginx:alpine` that tolerates the taint `env=gpu:NoSchedule` so it can be scheduled on this node.\n\n```bash\nkubectl describe node k8s-lab | grep -A5 Taints\n```", "hints": [ { "title": "Toleration structure", "body": "Add a `tolerations` block under `spec`. Match `key`, `operator`, `value`, and `effect` to the node taint.", "command": "cat </dev/null || true" } ], "validation": { "commands": [ { "description": "Node has taint env=gpu:NoSchedule", "command": "kubectl get node k8s-lab -o jsonpath='{.spec.taints[*].key}'", "expected_output": "env", "match": "contains" }, { "description": "Pod 'gpu-pod' has toleration for key 'env'", "command": "kubectl get pod gpu-pod -o jsonpath='{.spec.tolerations[*].key}'", "expected_output": "env", "match": "contains" }, { "description": "Pod 'gpu-pod' toleration effect is NoSchedule", "command": "kubectl get pod gpu-pod -o jsonpath='{.spec.tolerations[*].effect}'", "expected_output": "NoSchedule", "match": "contains" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl taint nodes k8s-lab env:NoSchedule- 2>/dev/null || true" }, { "command": "kubectl delete pod gpu-pod --ignore-not-found --grace-period=0 --force" } ] }, { "id": "etcd-backup-mcq", "title": "etcd Backup and Restore", "category": "Cluster Administration", "difficulty": "Hard", "type": "mcq", "weight": 5, "description": "## etcd Backup and Restore\n\nIn the CKA exam, you may need to back up and restore an etcd cluster. Which command correctly creates an etcd snapshot?\n\n```bash\n# The etcdctl binary is available on the control-plane node.\n# Assume certificates are at /etc/kubernetes/pki/etcd/\n```", "options": [ { "id": "a", "text": "`etcdctl snapshot save /backup/etcd.db` (no flags needed — etcdctl auto-detects certs)" }, { "id": "b", "text": "`ETCDCTL_API=3 etcdctl snapshot save /backup/etcd.db --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`" }, { "id": "c", "text": "`kubectl exec etcd-controlplane -- etcdctl backup --data-dir=/var/lib/etcd`" }, { "id": "d", "text": "`etcdctl snapshot save /backup/etcd.db --kubeconfig=/root/.kube/config`" } ], "correct_option": "b", "explanation": "etcd requires **TLS certificates** and explicit endpoint specification. You must set `ETCDCTL_API=3`, point to the etcd endpoint (`https://127.0.0.1:2379` on control-plane), and provide the CA cert, server cert, and key from `/etc/kubernetes/pki/etcd/`. To restore: `ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd.db --data-dir=/var/lib/etcd-restored`, then update the etcd static pod manifest to point to the new data dir.", "hints": [ { "title": "etcdctl API version", "body": "Always set `ETCDCTL_API=3`. etcdctl v2 and v3 APIs are different — the CKA exam uses v3. The certs live at `/etc/kubernetes/pki/etcd/`.", "command": "ETCDCTL_API=3 etcdctl snapshot status /backup/etcd.db --write-out=table" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "resource-quota-ns", "title": "ResourceQuota for a Namespace", "category": "Cluster Administration", "difficulty": "Medium", "type": "task", "weight": 7, "description": "## ResourceQuota for a Namespace\n\nResourceQuotas enforce aggregate resource constraints per namespace, preventing any single team from consuming all cluster resources.\n\n**Your task:**\n\n1. Create a namespace named `team-quota`\n2. Create a **ResourceQuota** named `compute-quota` in `team-quota` that limits:\n - `pods`: `5`\n - `requests.cpu`: `\"1\"`\n - `requests.memory`: `\"500Mi\"`", "hints": [ { "title": "Create the ResourceQuota", "body": "Use `kubectl create resourcequota` with `--hard` flag or apply a YAML manifest.", "command": "cat </dev/null || true" } ], "validation": { "commands": [ { "description": "ResourceQuota 'compute-quota' exists in team-quota", "command": "kubectl get resourcequota compute-quota -n team-quota -o jsonpath='{.metadata.name}'", "expected_output": "compute-quota", "match": "exact" }, { "description": "Pod limit is 5", "command": "kubectl get resourcequota compute-quota -n team-quota -o jsonpath='{.spec.hard.pods}'", "expected_output": "5", "match": "exact" }, { "description": "CPU request limit is 1", "command": "kubectl get resourcequota compute-quota -n team-quota -o jsonpath='{.spec.hard.requests\\.cpu}'", "expected_output": "1", "match": "exact" } ] }, "default_namespace": "team-quota", "teardown_commands": [ { "command": "kubectl delete namespace team-quota --ignore-not-found --wait=false" } ] }, { "id": "deployment-rollback", "title": "Rolling Back a Deployment", "category": "Workloads & Scheduling", "difficulty": "Easy", "type": "task", "weight": 5, "description": "## Rolling Back a Deployment\n\nA deployment `api-server` was mistakenly updated to a broken image. Your task is to roll it back to the previous working revision.\n\n**Your task:**\n\nRoll back the `api-server` deployment to its previous revision.\n\n```bash\n# Check rollout history\nkubectl rollout history deployment/api-server\n# Rollback\nkubectl rollout undo deployment/api-server\n```", "hints": [ { "title": "Undo a rollout", "body": "`kubectl rollout undo deployment/` reverts to the previous revision. Use `--to-revision=N` to go to a specific revision number.", "command": "kubectl rollout undo deployment/api-server" } ], "setup_commands": [ { "command": "kubectl create deployment api-server --image=nginx:1.24 --replicas=2 2>/dev/null || true" }, { "command": "kubectl rollout status deployment/api-server --timeout=60s" }, { "command": "kubectl set image deployment/api-server nginx=nginx:broken-image" } ], "validation": { "commands": [ { "description": "Deployment 'api-server' image rolled back to nginx:1.24", "command": "kubectl get deployment api-server -o jsonpath='{.spec.template.spec.containers[0].image}'", "expected_output": "nginx:1.24", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete deployment api-server --ignore-not-found" } ] }, { "id": "cronjob-task", "title": "Create a CronJob", "category": "Workloads & Scheduling", "difficulty": "Easy", "type": "task", "weight": 4, "description": "## Create a CronJob\n\nCronJobs create Jobs on a repeating schedule, perfect for maintenance tasks like log rotation, backups, or report generation.\n\n**Your task:**\n\nCreate a CronJob named `date-printer` that:\n- Runs **every minute** (`* * * * *`)\n- Uses `busybox:1.36`\n- Executes: `date`\n- Has `successfulJobsHistoryLimit: 3`", "hints": [ { "title": "CronJob manifest", "body": "Use `kubectl create cronjob` or apply a manifest. The schedule uses standard cron syntax.", "command": "kubectl create cronjob date-printer --image=busybox:1.36 --schedule='* * * * *' -- date" } ], "setup_commands": [], "validation": { "commands": [ { "description": "CronJob 'date-printer' exists", "command": "kubectl get cronjob date-printer -o jsonpath='{.metadata.name}'", "expected_output": "date-printer", "match": "exact" }, { "description": "CronJob schedule is every minute", "command": "kubectl get cronjob date-printer -o jsonpath='{.spec.schedule}'", "expected_output": "* * * * *", "match": "exact" }, { "description": "CronJob uses busybox image", "command": "kubectl get cronjob date-printer -o jsonpath='{.spec.jobTemplate.spec.template.spec.containers[0].image}'", "expected_output": "busybox", "match": "contains" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete cronjob date-printer --ignore-not-found" } ] }, { "id": "readiness-probe-task", "title": "Configure a Readiness Probe", "category": "Workloads & Scheduling", "difficulty": "Medium", "type": "task", "weight": 6, "description": "## Configure a Readiness Probe\n\nA **readiness probe** tells Kubernetes when a container is ready to accept traffic. Unlike a liveness probe (which restarts containers), a failed readiness probe removes the pod from Service endpoints until it recovers.\n\n**Your task:**\n\nCreate a Deployment named `ready-app` with:\n- Image: `nginx:alpine`, 2 replicas\n- A **readiness probe**: HTTP GET `/` on port `80`, `initialDelaySeconds: 3`, `periodSeconds: 5`", "hints": [ { "title": "Readiness probe YAML", "body": "Add `readinessProbe` under the container spec — same structure as `livenessProbe` but controls traffic routing, not restarts.", "command": "cat </dev/null || true" } ], "validation": { "commands": [ { "description": "Service 'frontend-np' exists", "command": "kubectl get service frontend-np -o jsonpath='{.metadata.name}'", "expected_output": "frontend-np", "match": "exact" }, { "description": "Service type is NodePort", "command": "kubectl get service frontend-np -o jsonpath='{.spec.type}'", "expected_output": "NodePort", "match": "exact" }, { "description": "NodePort is 30080", "command": "kubectl get service frontend-np -o jsonpath='{.spec.ports[0].nodePort}'", "expected_output": "30080", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete deployment frontend --ignore-not-found" }, { "command": "kubectl delete svc frontend-np --ignore-not-found" } ] }, { "id": "dns-resolution-mcq", "title": "Kubernetes DNS Resolution", "category": "Services & Networking", "difficulty": "Easy", "type": "mcq", "weight": 3, "description": "## Kubernetes DNS Resolution\n\nA pod in namespace `frontend` needs to reach a service named `db-service` in namespace `backend`. Which DNS name correctly resolves to that service from within the cluster?", "options": [ { "id": "a", "text": "`db-service`" }, { "id": "b", "text": "`db-service.backend`" }, { "id": "c", "text": "`db-service.backend.svc.cluster.local`" }, { "id": "d", "text": "`backend.db-service.cluster.local`" } ], "correct_option": "c", "explanation": "The full Kubernetes DNS format is `..svc.`. The default cluster domain is `cluster.local`, so the FQDN is `db-service.backend.svc.cluster.local`. When pods are in the **same namespace**, just `db-service` works. Across namespaces, you need at minimum `db-service.backend` (short form). Option C is the fully-qualified name that always works regardless of the caller's namespace.", "hints": [ { "title": "Kubernetes DNS format", "body": "CoreDNS resolves: `..svc.cluster.local`. You can verify with: `kubectl exec -- nslookup `.", "command": "kubectl exec -it -- nslookup db-service.backend.svc.cluster.local" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "storageclass-mcq", "title": "StorageClass Reclaim Policies", "category": "Storage", "difficulty": "Easy", "type": "mcq", "weight": 3, "description": "## StorageClass Reclaim Policies\n\nA PersistentVolumeClaim bound to a PV is deleted. The StorageClass has `reclaimPolicy: Retain`.\n\nWhat happens to the underlying PersistentVolume?", "options": [ { "id": "a", "text": "The PV is immediately deleted along with all data" }, { "id": "b", "text": "The PV remains but enters a `Released` state; the data is preserved and must be manually reclaimed" }, { "id": "c", "text": "The PV is automatically rebound to the next PVC that requests the same storage class" }, { "id": "d", "text": "The PV is converted to an emptyDir volume attached to the node" } ], "correct_option": "b", "explanation": "With `reclaimPolicy: Retain`, when a PVC is deleted the PV moves to `Released` state — the data is **preserved** on the storage backend, but the PV is not available for automatic rebinding. An admin must manually reclaim it (delete the PV, clean the storage, recreate the PV). Contrast with `reclaimPolicy: Delete`, which deletes both the PV object and the underlying storage asset automatically.", "hints": [ { "title": "Reclaim policies", "body": "Three policies exist: `Retain` (manual reclaim), `Delete` (auto-delete storage), `Recycle` (deprecated, basic scrub). Check with: `kubectl get storageclass`.", "command": "kubectl get storageclass -o jsonpath='{range .items[*]}{.metadata.name}={.reclaimPolicy}{\"\\n\"}{end}'" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "emptydir-pod", "title": "Shared emptyDir Volume Between Containers", "category": "Storage", "difficulty": "Easy", "type": "task", "weight": 5, "description": "## Shared emptyDir Volume\n\nAn `emptyDir` volume is created when a Pod is assigned to a node and exists as long as the Pod runs. It is ideal for sharing data between containers in the same pod (e.g., sidecar patterns).\n\n**Your task:**\n\nCreate a Pod named `shared-data` with two containers:\n1. **writer** (`busybox:1.36`) — writes `hello-kube` to `/shared/message.txt` then sleeps\n2. **reader** (`busybox:1.36`) — reads and prints `/shared/message.txt` then sleeps\n\nBoth containers mount an `emptyDir` volume at `/shared`.", "hints": [ { "title": "emptyDir shared volume manifest", "body": "Define a single volume of type `emptyDir: {}` and mount it in both containers under the same path.", "command": "cat < /shared/message.txt && sleep 3600\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n - name: reader\n image: busybox:1.36\n command: [\"sh\",\"-c\",\"sleep 5 && cat /shared/message.txt && sleep 3600\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n volumes:\n - name: shared-vol\n emptyDir: {}\nEOF" } ], "setup_commands": [], "validation": { "commands": [ { "description": "Pod 'shared-data' exists", "command": "kubectl get pod shared-data -o jsonpath='{.metadata.name}'", "expected_output": "shared-data", "match": "exact" }, { "description": "Pod has 2 containers", "command": "kubectl get pod shared-data -o jsonpath='{range .spec.containers[*]}{.name}{\"\\n\"}{end}'", "expected_output": "writer", "match": "contains" }, { "description": "Volume type is emptyDir", "command": "kubectl get pod shared-data -o jsonpath='{.spec.volumes[0].emptyDir}'", "expected_output": "{}", "match": "contains" }, { "description": "Writer container mounts the shared volume", "command": "kubectl get pod shared-data -o jsonpath='{.spec.containers[0].volumeMounts[0].mountPath}'", "expected_output": "/shared", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete pod shared-data --ignore-not-found --grace-period=0 --force" } ] }, { "id": "pod-security-context", "title": "Pod Security Context", "category": "Security", "difficulty": "Medium", "type": "task", "weight": 6, "description": "## Pod Security Context\n\nA security context defines privilege and access control settings for a pod or container.\n\n**Your task:**\n\nCreate a Pod named `secure-pod` using `nginx:alpine` with the following security settings:\n- `runAsUser: 1000`\n- `runAsNonRoot: true`\n- `allowPrivilegeEscalation: false`\n\n```bash\n# Verify after creation:\nkubectl exec secure-pod -- id\n```", "hints": [ { "title": "securityContext structure", "body": "Security context can be set at Pod level (`spec.securityContext`) or container level (`spec.containers[].securityContext`). Container-level settings override pod-level.", "command": "cat </dev/null || true" }, { "command": "kubectl patch deployment crash-app --type=json -p='[{\"op\":\"add\",\"path\":\"/spec/template/spec/containers/0/command\",\"value\":[\"/bin/sh\",\"-c\",\"exit 1\"]}]'" } ], "validation": { "commands": [ { "description": "Deployment 'crash-app' has 1 ready replica", "command": "kubectl get deployment crash-app -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -v '^$' || echo 0", "expected_output": "1", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete deployment crash-app --ignore-not-found" } ] }, { "id": "endpoint-fix-task", "title": "Fix a Broken Service Selector", "category": "Troubleshooting", "difficulty": "Medium", "type": "task", "weight": 7, "description": "## Fix a Broken Service Selector\n\nA service `my-svc` has been created but no traffic reaches the pods. The pods are labelled `app=backend`, but the service selector is misconfigured.\n\n**Your task:**\n\nFix the `my-svc` service selector so it correctly targets the `backend` pods.\n\n```bash\n# Investigate:\nkubectl describe service my-svc\nkubectl get endpoints my-svc\nkubectl get pods --show-labels\n```", "hints": [ { "title": "Identify the mismatch", "body": "Check what label the pods actually carry, then compare it against the service selector.", "command": "kubectl get pods --show-labels && kubectl get service my-svc -o jsonpath='{.spec.selector}'" }, { "title": "Patch the service selector", "body": "The pods carry `app=backend` but the service selector says `app=wrong-label`. Use `kubectl patch` to point the selector at the correct label.", "command": "kubectl patch service my-svc --type=json -p='[{\"op\":\"replace\",\"path\":\"/spec/selector/app\",\"value\":\"backend\"}]'" }, { "title": "Verify endpoints are populated", "body": "After fixing the selector, Kubernetes will automatically update the endpoint slice. Confirm the service now has live pod addresses.", "command": "kubectl get endpoints my-svc" } ], "setup_commands": [ { "command": "kubectl create deployment backend --image=nginx:alpine --replicas=2 2>/dev/null || true" }, { "command": "kubectl apply -f - </dev/null | grep -v '^$' || echo empty", "expected_output": "empty", "match": "not_contains" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete deployment backend --ignore-not-found" }, { "command": "kubectl delete svc my-svc --ignore-not-found" } ] }, { "id": "ingress-task", "title": "Create an Ingress Resource", "category": "Services & Networking", "difficulty": "Medium", "type": "task", "weight": 7, "description": "## Create an Ingress Resource\n\nIngress exposes HTTP/HTTPS routes from outside the cluster to services within it. k3s includes Traefik as its default Ingress controller.\n\n**Your task:**\n\nA deployment `web-app` and service `web-svc` (port 80) already exist. Create an **Ingress** named `web-ingress` that:\n- Routes traffic for host `app.lab.local`\n- Path `/` → service `web-svc` on port `80`\n- Uses `pathType: Prefix`", "hints": [ { "title": "Ingress manifest", "body": "Use `networking.k8s.io/v1` for the Ingress API. Define `spec.rules[].host` and `spec.rules[].http.paths[]`.", "command": "cat </dev/null || true" }, { "command": "kubectl expose deployment web-app --name=web-svc --port=80 --target-port=80 2>/dev/null || true" } ], "validation": { "commands": [ { "description": "Ingress 'web-ingress' exists", "command": "kubectl get ingress web-ingress -o jsonpath='{.metadata.name}'", "expected_output": "web-ingress", "match": "exact" }, { "description": "Ingress host is app.lab.local", "command": "kubectl get ingress web-ingress -o jsonpath='{.spec.rules[0].host}'", "expected_output": "app.lab.local", "match": "exact" }, { "description": "Backend service is web-svc", "command": "kubectl get ingress web-ingress -o jsonpath='{.spec.rules[0].http.paths[0].backend.service.name}'", "expected_output": "web-svc", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete deployment web-app --ignore-not-found" }, { "command": "kubectl delete svc web-svc --ignore-not-found" }, { "command": "kubectl delete ingress web-ingress --ignore-not-found" } ] }, { "id": "networkpolicy-egress", "title": "Egress NetworkPolicy", "category": "Security", "difficulty": "Hard", "type": "task", "weight": 8, "description": "## Egress NetworkPolicy\n\nEgress NetworkPolicies control **outbound** traffic from pods. By default, all egress is allowed; once you apply an egress policy to a pod, only explicitly allowed egress is permitted.\n\n**Your task:**\n\nCreate a NetworkPolicy named `restrict-egress` in the `default` namespace that:\n- Applies to pods with label `role=isolated`\n- Allows **egress only to pods with label `role=allowed`** on port `80`\n- Blocks all other egress", "hints": [ { "title": "Egress NetworkPolicy structure", "body": "Set `spec.policyTypes: [Egress]` and define `spec.egress[]` with `to` and `ports`. Omitting a type means it's not affected.", "command": "cat </dev/null || true" } ], "validation": { "commands": [ { "description": "LimitRange 'default-limits' exists in bounded-ns", "command": "kubectl get limitrange default-limits -n bounded-ns -o jsonpath='{.metadata.name}'", "expected_output": "default-limits", "match": "exact" }, { "description": "Default CPU limit is 200m", "command": "kubectl get limitrange default-limits -n bounded-ns -o jsonpath='{.spec.limits[0].default.cpu}'", "expected_output": "200m", "match": "exact" }, { "description": "Default memory limit is 256Mi", "command": "kubectl get limitrange default-limits -n bounded-ns -o jsonpath='{.spec.limits[0].default.memory}'", "expected_output": "256Mi", "match": "exact" } ] }, "default_namespace": "bounded-ns", "teardown_commands": [ { "command": "kubectl delete namespace bounded-ns --ignore-not-found --wait=false" } ] }, { "id": "job-task", "title": "Create a One-Shot Job", "category": "Workloads & Scheduling", "difficulty": "Easy", "type": "task", "weight": 4, "description": "## Create a One-Shot Job\n\nA **Job** creates one or more pods and ensures a specified number of them successfully terminate. Unlike Deployments, Jobs are meant for finite tasks that run to completion.\n\n**Your task:**\n\nCreate a Job named `pi-job` that:\n- Uses `perl:5.34` image\n- Runs: `perl -Mbignum=bpi -wle 'print bpi(100)'` (computes π to 100 digits)\n- `completions: 1`\n- `restartPolicy: Never`", "hints": [ { "title": "Job manifest", "body": "Set `restartPolicy: Never` or `OnFailure` in the pod template spec (not at pod spec level).", "command": "cat <` (before worker upgrade), `kubectl uncordon ` (after).", "command": "kubeadm upgrade plan" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "container-logging-mcq", "title": "Container Logging and Monitoring", "category": "Troubleshooting", "difficulty": "Easy", "type": "mcq", "weight": 3, "description": "## Container Logging and Monitoring\n\nA pod `worker-abc` has crashed and restarted. You want to see the logs from the **previous** (crashed) container instance, not the current one.\n\nWhich command retrieves logs from the previously crashed container?", "options": [ { "id": "a", "text": "`kubectl logs worker-abc --all-containers`" }, { "id": "b", "text": "`kubectl logs worker-abc --previous`" }, { "id": "c", "text": "`kubectl logs worker-abc --restart`" }, { "id": "d", "text": "`kubectl describe pod worker-abc | grep log`" } ], "correct_option": "b", "explanation": "`kubectl logs --previous` (or `-p`) retrieves logs from the **terminated previous container** instance in a pod. This is critical for debugging CrashLoopBackOff scenarios where the container restarts before you can inspect its logs. `--all-containers` shows logs from all containers in the current run. `kubectl describe` shows events and status but not stdout/stderr logs.", "hints": [ { "title": "Useful kubectl logs flags", "body": "`--previous`/`-p`: previous container | `--follow`/`-f`: stream | `--since=1h`: last hour | `--tail=50`: last 50 lines | `--timestamps`: show timestamps", "command": "kubectl logs worker-abc --previous --tail=50" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "multi-container-pod-basics", "title": "Create a Multi-Container Pod", "category": "Core Concepts", "difficulty": "Easy", "type": "task", "weight": 5, "description": "## Multi-Container Pods\n\nPods can contain multiple containers that share the same network namespace and storage volumes. This is often used for the 'sidecar' pattern.\n\n**Your task:**\n\nCreate a Pod named `web-sidecar` in the `default` namespace with two containers:\n1. Name: `app`, Image: `nginx:alpine`\n2. Name: `sidecar`, Image: `busybox:1.36`, Command: `sleep 3600`\n\nVerify it is running:\n```bash\nkubectl get pod web-sidecar\n```", "hints": [ { "title": "Manifest Structure", "body": "Your `spec.containers` array needs two items.", "command": "cat < Tip: Use `kubectl logs log-generator | grep ERROR` to filter.", "hints": [ { "title": "View pod logs", "body": "Use kubectl logs to stream the output of a running container.", "command": "kubectl logs log-generator" }, { "title": "Filter logs", "body": "Pipe kubectl logs into grep to find specific messages.", "command": "kubectl logs log-generator | grep ERROR" } ], "setup_commands": [ { "command": "kubectl run log-generator --image=busybox:1.36 --command -- sh -c 'echo \"ERROR: Database connection failed\" && sleep 3600'" } ], "validation": { "commands": [ { "description": "Pod log-generator emits ERROR: Database connection failed", "command": "kubectl logs log-generator 2>/dev/null", "expected_output": "ERROR: Database connection failed", "match": "contains" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete pod log-generator --ignore-not-found --grace-period=0 --force" } ] }, { "id": "edit-deployment-basics", "title": "Updating Deployments", "category": "Workloads", "difficulty": "Easy", "type": "task", "weight": 4, "description": "## Updating a Deployment\n\nA deployment named `web-app` is currently running `nginx:1.24`. A security vulnerability was found in this version.\n\n**Your task:**\n\nUpdate the image of the `web-app` deployment to `nginx:1.25` to patch the vulnerability. Wait for the new pods to become ready.\n\n```bash\n# Verify your change:\nkubectl describe deployment web-app | grep Image\n```", "hints": [ { "title": "Set image command", "body": "You can use `kubectl set image` to update a deployment's container image immediately.", "command": "kubectl set image deployment/web-app nginx=nginx:1.25" } ], "setup_commands": [ { "command": "kubectl create deployment web-app --image=nginx:1.24" }, { "command": "kubectl rollout status deployment/web-app --timeout=60s" } ], "validation": { "commands": [ { "description": "Deployment uses image nginx:1.25", "command": "kubectl get deployment web-app -o jsonpath='{.spec.template.spec.containers[0].image}'", "expected_output": "nginx:1.25", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete deployment web-app --ignore-not-found" } ] }, { "id": "jsonpath-basics", "title": "JSONPath Data Extraction", "category": "Core Concepts", "difficulty": "Medium", "type": "task", "weight": 5, "description": "## Extracting Data with JSONPath\n\nKubectl allows you to extract specific fields from resource manifests using JSONPath, which is incredibly useful for scripting and automation.\n\n**Your task:**\n\nA deployment named `hidden-app` is running in the `default` namespace. Use `kubectl` with a JSONPath expression to output **only the container image name** used by that deployment.\n\n> Expected output: `nginx:1.25.3`", "hints": [ { "title": "JSONPath syntax", "body": "The image is located at `.spec.template.spec.containers[0].image`. Use -o jsonpath to extract it.", "command": "kubectl get deployment hidden-app -o jsonpath='{.spec.template.spec.containers[0].image}'" } ], "setup_commands": [ { "command": "kubectl create deployment hidden-app --image=nginx:1.25.3" } ], "validation": { "commands": [ { "description": "kubectl jsonpath returns the correct image", "command": "kubectl get deployment hidden-app -o jsonpath='{.spec.template.spec.containers[0].image}'", "expected_output": "nginx:1.25.3", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete deployment hidden-app --ignore-not-found" } ] }, { "id": "dry-run-manifest-basics", "title": "Generating YAML Manifests", "category": "Core Concepts", "difficulty": "Easy", "type": "task", "weight": 4, "description": "## Generating and Applying Manifests with Dry Run\n\nInstead of writing YAML from scratch, you can use `kubectl` to generate templates using `--dry-run=client -o yaml` and then apply them.\n\n**Your task:**\n\nUse `kubectl run` with `--dry-run=client -o yaml` to generate a Pod manifest for `web-pod` using image `httpd:alpine`, then pipe it directly to `kubectl apply -f -` to create the pod.\n\n```bash\nkubectl run web-pod --image=httpd:alpine --dry-run=client -o yaml | kubectl apply -f -\n```", "hints": [ { "title": "Combine dry-run with apply", "body": "Chain kubectl run --dry-run=client -o yaml with kubectl apply -f - using a pipe.", "command": "kubectl run web-pod --image=httpd:alpine --dry-run=client -o yaml | kubectl apply -f -" } ], "setup_commands": [], "validation": { "commands": [ { "description": "Pod 'web-pod' exists", "command": "kubectl get pod web-pod -o jsonpath='{.metadata.name}'", "expected_output": "web-pod", "match": "exact" }, { "description": "Pod uses httpd:alpine image", "command": "kubectl get pod web-pod -o jsonpath='{.spec.containers[0].image}'", "expected_output": "httpd:alpine", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete pod web-pod --ignore-not-found --grace-period=0 --force" } ] }, { "id": "delete-by-label-basics", "title": "Bulk Deletion by Label", "category": "Core Concepts", "difficulty": "Easy", "type": "task", "weight": 3, "description": "## Bulk Deletion by Label\n\nYou can use label selectors (`-l`) to perform bulk operations on resources.\n\n**Your task:**\n\nThere are 5 pods running in the default namespace. Delete **only** the pods that have the label `env=dev`. Leave the `env=prod` pods running.\n\n```bash\n# Verify which pods are left:\nkubectl get pods --show-labels\n```", "hints": [ { "title": "Delete with selector", "body": "Use `kubectl delete pods` with the `-l` flag.", "command": "kubectl delete pods -l env=dev" } ], "setup_commands": [ { "command": "kubectl run dev-1 --image=nginx:alpine -l env=dev" }, { "command": "kubectl run dev-2 --image=nginx:alpine -l env=dev" }, { "command": "kubectl run dev-3 --image=nginx:alpine -l env=dev" }, { "command": "kubectl run prod-1 --image=nginx:alpine -l env=prod" }, { "command": "kubectl run prod-2 --image=nginx:alpine -l env=prod" } ], "validation": { "commands": [ { "description": "No 'env=dev' pods exist", "command": "kubectl get pods -l env=dev --no-headers 2>/dev/null | wc -l | tr -d ' '", "expected_output": "0", "match": "exact" }, { "description": "Prod pods are still running", "command": "kubectl get pods -l env=prod --no-headers 2>/dev/null | wc -l | tr -d ' '", "expected_output": "2", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete pods -l env=dev --ignore-not-found --grace-period=0 --force" }, { "command": "kubectl delete pods -l env=prod --ignore-not-found --grace-period=0 --force" } ] }, { "id": "kubectl-cp-basics", "title": "Copy Files to Containers", "category": "Troubleshooting", "difficulty": "Medium", "type": "task", "weight": 4, "description": "## Copy Files Between Containers and the Cluster\n\nThe `kubectl cp` command allows you to copy files and directories to and from running containers.\n\n**Your task:**\n\nA pod named `config-pod` is running with a file at `/src/config.json`. Copy it **into** the same pod at `/app/config.json` using `kubectl cp`.\n\n> Tip: `kubectl cp` syntax is `kubectl cp :`", "hints": [ { "title": "Use kubectl cp", "body": "First copy the file out from /src, then copy it back to /app, or use kubectl exec to verify.", "command": "kubectl cp config-pod:/src/config.json /tmp/config.json && kubectl cp /tmp/config.json config-pod:/app/config.json" } ], "setup_commands": [ { "command": "kubectl run config-pod --image=busybox:1.36 --command -- sleep 3600" }, { "command": "kubectl wait --for=condition=Ready pod/config-pod --timeout=60s" }, { "command": "kubectl exec config-pod -- sh -c 'mkdir -p /src && echo \\'{ \"status\": \"ok\" }\\' > /src/config.json'" }, { "command": "kubectl exec config-pod -- mkdir -p /app" } ], "validation": { "commands": [ { "description": "File /app/config.json exists inside pod", "command": "kubectl exec config-pod -- cat /app/config.json 2>/dev/null", "expected_output": "{\"status\": \"ok\"}", "match": "contains" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete pod config-pod --ignore-not-found --grace-period=0 --force" } ] }, { "id": "exec-command-basics", "title": "Execute Commands in Pods", "category": "Troubleshooting", "difficulty": "Easy", "type": "task", "weight": 3, "description": "## Execute Commands in Pods\n\nSometimes you need to run arbitrary commands inside a running container to debug or change state.\n\n**Your task:**\n\nA pod named `worker-pod` is running. Use `kubectl exec` to create an empty file at `/tmp/ready` inside the container.\n\n```bash\n# Verify the file was created:\nkubectl exec worker-pod -- ls /tmp/ready\n```", "hints": [ { "title": "Use kubectl exec", "body": "You can pass commands to the container by adding `--` after the pod name.", "command": "kubectl exec worker-pod -- touch /tmp/ready" } ], "setup_commands": [ { "command": "kubectl run worker-pod --image=busybox:1.36 --command -- sleep 3600" }, { "command": "kubectl wait --for=condition=Ready pod/worker-pod --timeout=30s" } ], "validation": { "commands": [ { "description": "File /tmp/ready exists inside worker-pod", "command": "kubectl exec worker-pod -- ls /tmp/ready 2>/dev/null", "expected_output": "/tmp/ready", "match": "contains" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete pod worker-pod --ignore-not-found --grace-period=0 --force" } ] }, { "id": "cks-network-policy", "title": "Default Deny Network Policy", "category": "Network Security", "difficulty": "Medium", "type": "task", "weight": 5, "description": "## Network Policies\n\nIn a zero-trust architecture, you should deny all traffic by default and explicitly allow what is needed.\n\n**Your task:**\n\nCreate a NetworkPolicy named `default-deny-all` in the `default` namespace that denies all ingress and egress traffic for all pods in the namespace.\n\n```bash\n# Verify your policy:\nkubectl get networkpolicy default-deny-all\n```", "hints": [ { "title": "Default Deny YAML", "body": "Use a podSelector with an empty matchLabels `{}` to select all pods, and provide empty lists for ingress and egress.", "command": "cat </dev/null", "expected_output": "169.254.169.254/32", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete networkpolicy deny-metadata --ignore-not-found" } ] }, { "id": "cks-rbac-clusterrole", "title": "Cluster-Level RBAC", "category": "Cluster Security", "difficulty": "Medium", "type": "task", "weight": 5, "description": "## Cluster Roles and Bindings\n\nSome resources, like Nodes, are cluster-scoped and cannot be accessed using normal Roles and RoleBindings.\n\n**Your task:**\n\n1. Create a ServiceAccount named `monitor-sa` in the `monitoring` namespace.\n2. Create a ClusterRole named `node-viewer` that grants `get`, `list`, and `watch` permissions on `nodes`.\n3. Create a ClusterRoleBinding named `monitor-node-binding` to bind the ClusterRole to the ServiceAccount.\n\n```bash\n# Verify your permissions:\nkubectl auth can-i list nodes --as=system:serviceaccount:monitoring:monitor-sa\n```", "hints": [ { "title": "Imperative Commands", "body": "Create the SA, then the ClusterRole, then the ClusterRoleBinding.", "command": "kubectl create sa monitor-sa -n monitoring && kubectl create clusterrole node-viewer --verb=get,list,watch --resource=nodes && kubectl create clusterrolebinding monitor-node-binding --clusterrole=node-viewer --serviceaccount=monitoring:monitor-sa" } ], "setup_commands": [ { "command": "kubectl create namespace monitoring --dry-run=client -o yaml | kubectl apply -f -" } ], "validation": { "commands": [ { "description": "monitor-sa exists in monitoring namespace", "command": "kubectl get sa monitor-sa -n monitoring -o jsonpath='{.metadata.name}'", "expected_output": "monitor-sa", "match": "exact" }, { "description": "ServiceAccount can list nodes", "command": "kubectl auth can-i list nodes --as=system:serviceaccount:monitoring:monitor-sa", "expected_output": "yes", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete sa monitor-sa -n monitoring --ignore-not-found" }, { "command": "kubectl delete clusterrole node-viewer --ignore-not-found" }, { "command": "kubectl delete clusterrolebinding monitor-node-binding --ignore-not-found" } ] }, { "id": "cks-psa-namespace", "title": "Pod Security Admission", "category": "Cluster Security", "difficulty": "Easy", "type": "task", "weight": 4, "description": "## Pod Security Admission (PSA)\n\nPod Security Admission replaced PodSecurityPolicies (PSP) to enforce security standards at the namespace level.\n\n**Your task:**\n\nA namespace named `secure-workloads` already exists. Add the necessary label to this namespace to **enforce** the `restricted` pod security standard.\n\n```bash\n# Verify your label:\nkubectl get ns secure-workloads --show-labels\n```", "hints": [ { "title": "Namespace Label", "body": "Label the namespace with the `pod-security.kubernetes.io/enforce=restricted` key-value pair.", "command": "kubectl label ns secure-workloads pod-security.kubernetes.io/enforce=restricted" } ], "setup_commands": [ { "command": "kubectl create namespace secure-workloads --dry-run=client -o yaml | kubectl apply -f -" } ], "validation": { "commands": [ { "description": "Namespace secure-workloads is labeled", "command": "kubectl get ns secure-workloads -o jsonpath='{.metadata.labels.pod-security\\.kubernetes\\.io/enforce}'", "expected_output": "restricted", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete namespace secure-workloads --ignore-not-found --wait=false" } ] }, { "id": "cks-mcq-runtime-security", "title": "Runtime Security Tools", "category": "System Hardening", "difficulty": "Medium", "type": "mcq", "weight": 2, "description": "Which of the following is an open-source tool specifically designed for **runtime security** in Kubernetes (e.g., detecting unexpected behavior or shell execution in running containers)?", "options": [ { "id": "a", "text": "Trivy" }, { "id": "b", "text": "Kube-bench" }, { "id": "c", "text": "Falco" }, { "id": "d", "text": "SonarQube" } ], "correct_option": "c", "explanation": "Falco is a CNCF incubating project that acts as a runtime security tool. It parses Linux system calls at runtime and asserts the stream against a powerful rules engine. Trivy is for image scanning, and kube-bench checks CIS benchmarks.", "hints": [], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "cks-mcq-api-server", "title": "API Server Authentication", "category": "Cluster Setup", "difficulty": "Easy", "type": "mcq", "weight": 2, "description": "To secure the `kube-apiserver`, you should prevent unauthorized users from interacting with the cluster anonymously. Which flag is used to disable anonymous authentication on the API server?", "options": [ { "id": "a", "text": "--disable-anonymous=true" }, { "id": "b", "text": "--anonymous-auth=false" }, { "id": "c", "text": "--auth-mode=Node,RBAC" }, { "id": "d", "text": "--secure-port=6443" } ], "correct_option": "b", "explanation": "The `--anonymous-auth=false` flag instructs the kube-apiserver to reject any request that is not associated with a known user or service account. By default, it is enabled.", "hints": [], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "cks-mcq-image-footprint", "title": "Minimizing Image Footprint", "category": "Supply Chain Security", "difficulty": "Easy", "type": "mcq", "weight": 2, "description": "Why is it highly recommended in the CKS exam to use base images like **Alpine Linux** or **Distroless** for your containerized applications?", "options": [ { "id": "a", "text": "They automatically encrypt data at rest" }, { "id": "b", "text": "They have a significantly reduced attack surface with fewer packages" }, { "id": "c", "text": "They automatically configure NetworkPolicies for the pod" }, { "id": "d", "text": "They guarantee that the container will not run as root" } ], "correct_option": "b", "explanation": "Minimal images like Alpine or Distroless contain only the bare minimum files and dependencies needed to run the application. This drastically reduces the attack surface and the number of potential CVEs.", "hints": [], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "cks-mcq-psp-replacement", "title": "Pod Security Evolution", "category": "Cluster Security", "difficulty": "Easy", "type": "mcq", "weight": 2, "description": "PodSecurityPolicies (PSP) were completely removed from Kubernetes in version 1.25. What is the built-in native replacement that enforces Pod Security Standards at the namespace level?", "options": [ { "id": "a", "text": "NetworkPolicies" }, { "id": "b", "text": "OPA Gatekeeper" }, { "id": "c", "text": "Pod Security Admission (PSA)" }, { "id": "d", "text": "AppArmor Profiles" } ], "correct_option": "c", "explanation": "Pod Security Admission (PSA) is the built-in admission controller that evaluates Pods against the predefined Pod Security Standards (Privileged, Baseline, and Restricted) based on namespace labels.", "hints": [], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "cks-mcq-kubelet-auth", "title": "Securing the Kubelet", "category": "Cluster Setup", "difficulty": "Medium", "type": "mcq", "weight": 2, "description": "The kubelet exposes its own API on port 10250, which can allow an attacker to run `exec` commands on pods if left unsecured. Which configuration setting in the kubelet config file ensures unauthenticated requests are rejected?", "options": [ { "id": "a", "text": "authorization.mode: Webhook" }, { "id": "b", "text": "readOnlyPort: 0" }, { "id": "c", "text": "authentication.anonymous.enabled: false" }, { "id": "d", "text": "protectKernelDefaults: true" } ], "correct_option": "c", "explanation": "Setting `authentication.anonymous.enabled: false` ensures that the kubelet will reject any API requests that do not present a valid client certificate or bearer token.", "hints": [], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "cks-readonly-filesystem", "title": "Read-Only Root Filesystem", "category": "Workload Security", "difficulty": "Easy", "type": "task", "weight": 4, "description": "## Read-Only Root Filesystem\n\nSetting `readOnlyRootFilesystem: true` prevents any process inside the container from writing to the container's root filesystem layer, significantly reducing the blast radius of a compromised container.\n\n**Your task:**\n\nCreate a Pod named `readonly-pod` using `busybox:1.36` (command: `sleep 3600`) with:\n- `readOnlyRootFilesystem: true` in the container's `securityContext`\n- An `emptyDir` volume mounted at `/tmp` to provide writable scratch space\n\n```bash\n# Verify:\nkubectl get pod readonly-pod -o jsonpath='{.spec.containers[0].securityContext.readOnlyRootFilesystem}'\n```", "hints": [ { "title": "readOnlyRootFilesystem with emptyDir", "body": "Set `readOnlyRootFilesystem: true` in the container's `securityContext`. Mount an `emptyDir` volume at `/tmp` so the process has a writable location if needed.", "command": "cat </dev/null" }, { "command": "kubectl create deployment tls-app --image=nginx:alpine --replicas=1 2>/dev/null || true" }, { "command": "kubectl expose deployment tls-app --name=tls-svc --port=80 --target-port=80 2>/dev/null || true" } ], "validation": { "commands": [ { "description": "Secret 'app-tls' is of type kubernetes.io/tls", "command": "kubectl get secret app-tls -o jsonpath='{.type}'", "expected_output": "kubernetes.io/tls", "match": "exact" }, { "description": "Ingress 'tls-ingress' exists", "command": "kubectl get ingress tls-ingress -o jsonpath='{.metadata.name}'", "expected_output": "tls-ingress", "match": "exact" }, { "description": "Ingress uses TLS secret 'app-tls'", "command": "kubectl get ingress tls-ingress -o jsonpath='{.spec.tls[0].secretName}'", "expected_output": "app-tls", "match": "exact" }, { "description": "Ingress routes host secure.lab.local", "command": "kubectl get ingress tls-ingress -o jsonpath='{.spec.rules[0].host}'", "expected_output": "secure.lab.local", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete ingress tls-ingress --ignore-not-found" }, { "command": "kubectl delete secret app-tls --ignore-not-found" }, { "command": "kubectl delete svc tls-svc --ignore-not-found" }, { "command": "kubectl delete deployment tls-app --ignore-not-found" }, { "command": "rm -f /tmp/tls.crt /tmp/tls.key" } ] }, { "id": "cks-image-pull-secret", "title": "Private Registry Image Pull Secret", "category": "Supply Chain Security", "difficulty": "Easy", "type": "task", "weight": 4, "description": "## Private Registry Image Pull Secret\n\nIn production, images are stored in private registries that require authentication. Kubernetes uses `imagePullSecrets` to securely store registry credentials and inject them at image pull time.\n\n**Your task:**\n\n1. Create a Docker registry Secret named `registry-creds` for registry `registry.company.com` with:\n - Username: `ci-bot`\n - Password: `s3cr3t-token`\n - Email: `ci@company.com`\n2. Create a Pod named `private-pod` using `nginx:alpine` that references `registry-creds` as an `imagePullSecret`\n\n```bash\n# Verify:\nkubectl get pod private-pod -o jsonpath='{.spec.imagePullSecrets[0].name}'\n```", "hints": [ { "title": "Create a docker-registry Secret", "body": "Use `kubectl create secret docker-registry` with the four required flags.", "command": "kubectl create secret docker-registry registry-creds \\\n --docker-server=registry.company.com \\\n --docker-username=ci-bot \\\n --docker-password=s3cr3t-token \\\n --docker-email=ci@company.com" }, { "title": "Reference imagePullSecrets in a Pod", "body": "Add `spec.imagePullSecrets[].name` to the pod spec with the name of your secret.", "command": "cat <`.", "command": "kubectl explain runtimeclass\nkubectl explain pod.spec.runtimeClassName" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "cks-mcq-audit-policy", "title": "Kubernetes Audit Logging Levels", "category": "Cluster Setup", "difficulty": "Medium", "type": "mcq", "weight": 3, "description": "## Kubernetes Audit Logging\n\nYou are writing an audit policy for the kube-apiserver. You need to capture the **complete request AND response body** for all write operations on `secrets` in any namespace, so you have a full record of every secret modification.\n\nWhich audit level must you specify for these events?", "options": [ { "id": "a", "text": "`None` — discard all events matching this rule; nothing is logged" }, { "id": "b", "text": "`Metadata` — logs request metadata (user, verb, resource, timestamp) but not the request or response body" }, { "id": "c", "text": "`Request` — logs metadata and the request body, but not the response body" }, { "id": "d", "text": "`RequestResponse` — logs metadata, the full request body, and the full response body" } ], "correct_option": "d", "explanation": "Kubernetes audit levels in ascending verbosity: **None → Metadata → Request → RequestResponse**. `RequestResponse` is required when you need the complete picture — what was sent and what the API server returned. This is most important for sensitive resources like Secrets. The audit policy file is passed to kube-apiserver via `--audit-policy-file`. Note that `RequestResponse` is expensive; use `Metadata` for most resources.", "hints": [ { "title": "Audit policy levels", "body": "The four levels: `None` (discard), `Metadata` (headers only), `Request` (+ request body), `RequestResponse` (+ response body). Configure via `--audit-policy-file` and `--audit-log-path` on the kube-apiserver.", "command": "# Example rule targeting Secrets at RequestResponse level:\n# - level: RequestResponse\n# resources:\n# - group: \"\"\n# resources: [\"secrets\"]" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "cks-runtime-class", "title": "RuntimeClass for Sandboxed Workloads", "category": "System Hardening", "difficulty": "Medium", "type": "task", "weight": 5, "description": "## RuntimeClass for Stronger Workload Isolation\n\nKubernetes `RuntimeClass` lets you select an alternative container runtime (such as gVisor's `runsc`) for specific pods, providing stronger kernel-level isolation than the default `runc`.\n\n**Your task:**\n\n1. Create a `RuntimeClass` named `gvisor` with `handler: runsc`.\n2. Create a Pod named `sandbox-pod` using the `nginx:alpine` image that references this RuntimeClass via `spec.runtimeClassName: gvisor`.\n\n```bash\n# Verify the RuntimeClass:\nkubectl get runtimeclass gvisor\n# Verify the pod spec:\nkubectl get pod sandbox-pod -o jsonpath='{.spec.runtimeClassName}'\n```\n\n> **Note:** The pod may not reach `Running` state if `runsc` is not installed on the node — that is expected in this lab. Validation only checks the API object configuration.", "hints": [ { "title": "Create the RuntimeClass", "body": "Use a manifest with `apiVersion: node.k8s.io/v1`, `kind: RuntimeClass`, and a `handler` field.", "command": "cat < /etc/kubernetes/audit-policy.yaml\napiVersion: audit.k8s.io/v1\nkind: Policy\nrules:\n- level: RequestResponse\n resources:\n - group: \"\"\n resources: [\"secrets\"]\n- level: Metadata\n verbs: [\"get\", \"list\", \"watch\"]\n resources:\n - group: \"\"\n resources: [\"pods\"]\n- level: None\n userGroups: [\"system:masters\"]\n- level: Metadata\nEOF" } ], "setup_commands": [ { "command": "mkdir -p /etc/kubernetes" } ], "validation": { "commands": [ { "description": "Audit policy file is valid YAML with kind: Policy", "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); print(p.get('kind',''))\"", "expected_output": "Policy", "match": "exact" }, { "description": "Rule 1: level is RequestResponse and targets secrets", "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); r=p['rules'][0]; print('ok' if r['level']=='RequestResponse' and any('secrets' in x.get('resources',[]) for x in r.get('resources',[])) else 'fail')\"", "expected_output": "ok", "match": "exact" }, { "description": "Rule 2: level is Metadata, targets pods, verbs include get/list/watch", "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); print('ok' if any(r.get('level')=='Metadata' and any('pods' in x.get('resources',[]) for x in r.get('resources',[])) and set(r.get('verbs',[])) >= {'get','list','watch'} for r in p['rules']) else 'fail')\"", "expected_output": "ok", "match": "exact" }, { "description": "Rule 3: level is None for system:masters group", "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); print('ok' if any(r.get('level')=='None' and 'system:masters' in r.get('userGroups',[]) for r in p['rules']) else 'fail')\"", "expected_output": "ok", "match": "exact" }, { "description": "Rule 4: catch-all is level Metadata with no other selectors", "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); last=p['rules'][-1]; print('ok' if set(last.keys())=={'level'} and last['level']=='Metadata' else 'fail')\"", "expected_output": "ok", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "rm -f /etc/kubernetes/audit-policy.yaml" } ] }, { "id": "cks-egress-namespace", "title": "Restrict Egress to Namespace", "category": "Network Security", "difficulty": "Hard", "type": "task", "weight": 6, "description": "## Namespace-Scoped Egress Network Policy\n\nFine-grained network policies can restrict pod traffic so that only pods within a specific namespace can communicate with each other.\n\n**Your task:**\n\nA namespace `frontend` and a namespace `backend` already exist. Create a NetworkPolicy named `allow-backend-only` in the `frontend` namespace that:\n\n- Applies to **all pods** in the `frontend` namespace\n- **Allows egress only** to pods in the `backend` namespace (matched by `namespaceSelector`)\n- **Denies all other egress** traffic\n\n```bash\n# Verify:\nkubectl get networkpolicy allow-backend-only -n frontend\n```", "hints": [ { "title": "namespaceSelector in Egress rule", "body": "Use `spec.policyTypes: [Egress]` with an empty egress rule except for a `namespaceSelector` that matches the `backend` namespace label.", "command": "cat </dev/null || true" }, { "command": "kubectl rollout status deployment/myapp --timeout=60s" } ], "validation": { "commands": [ { "description": "Deployment 'myapp' exists", "command": "kubectl get deployment myapp -o jsonpath='{.metadata.name}'", "expected_output": "myapp", "match": "exact" }, { "description": "Annotation 'contact' is ops@company.com", "command": "kubectl get deployment myapp -o jsonpath='{.metadata.annotations.contact}'", "expected_output": "ops@company.com", "match": "exact" }, { "description": "Annotation 'reviewed' is true", "command": "kubectl get deployment myapp -o jsonpath='{.metadata.annotations.reviewed}'", "expected_output": "true", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete deployment myapp --ignore-not-found" } ] }, { "id": "init-container-task-basics", "title": "Pod with Init Container", "category": "Core Concepts", "difficulty": "Medium", "type": "task", "weight": 5, "description": "## Pod with Init Container\n\nInit containers run **before** the main application containers start. Each init container must complete successfully before the next one begins, and all must finish before the main containers are started. They are ideal for setup tasks like seeding config files or waiting for a dependency.\n\n**Your task:**\n\nCreate a Pod named `init-demo` with:\n- An **init container** named `setup` using `busybox:1.36` that writes `ready` into `/shared/status.txt`\n- A **main container** named `app` using `busybox:1.36` with command `sleep 3600`\n- Both containers mount an `emptyDir` volume at `/shared`\n\n```bash\n# Verify the init container wrote the file:\nkubectl exec init-demo -- cat /shared/status.txt\n```", "hints": [ { "title": "Pod manifest with initContainers", "body": "Define `spec.initContainers[]` above `spec.containers[]`. Both containers share the emptyDir volume mounted at /shared.", "command": "cat < /shared/status.txt\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n containers:\n - name: app\n image: busybox:1.36\n command: [\"sleep\", \"3600\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n volumes:\n - name: shared-vol\n emptyDir: {}\nEOF" } ], "setup_commands": [], "validation": { "commands": [ { "description": "Pod 'init-demo' is Running", "command": "kubectl get pod init-demo -o jsonpath='{.status.phase}'", "expected_output": "Running", "match": "exact" }, { "description": "Init container 'setup' completed with exit code 0", "command": "kubectl get pod init-demo -o jsonpath='{.status.initContainerStatuses[0].state.terminated.exitCode}'", "expected_output": "0", "match": "exact" }, { "description": "File /shared/status.txt contains 'ready'", "command": "kubectl exec init-demo -- cat /shared/status.txt 2>/dev/null", "expected_output": "ready", "match": "contains" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete pod init-demo --ignore-not-found --grace-period=0 --force" } ] }, { "id": "restart-policy-mcq", "title": "Pod Restart Policies", "category": "Core Concepts", "difficulty": "Easy", "type": "mcq", "weight": 3, "description": "## Pod Restart Policies\n\nA pod runs a one-time database migration script. If the script **fails** (non-zero exit), the container should be restarted. If it **succeeds** (exit 0), it should **not** be restarted.\n\nWhich `restartPolicy` should be set in the pod spec?", "options": [ { "id": "a", "text": "`Always` — restarts the container regardless of exit code (this is the default)" }, { "id": "b", "text": "`OnFailure` — restarts only if the container exits with a non-zero code" }, { "id": "c", "text": "`Never` — the container is never restarted, even on failure" }, { "id": "d", "text": "`OnCompletion` — restarts the container only after it completes successfully" } ], "correct_option": "b", "explanation": "`OnFailure` is the right policy for batch workloads like migration scripts — Kubernetes restarts the container on any non-zero exit code but leaves it alone after a clean exit (code 0). `Always` (the default for Deployment pods) would keep restarting even after success, creating an infinite restart loop. `Never` means no automatic recovery on failure. `OnCompletion` does not exist in Kubernetes — the three valid values are `Always`, `OnFailure`, and `Never`. Jobs automatically use `OnFailure` by default.", "hints": [ { "title": "restartPolicy values", "body": "Three valid values: `Always` (default for Pods), `OnFailure` (for Jobs/batch), `Never` (run-once, no retry). Check with `kubectl explain pod.spec.restartPolicy`.", "command": "kubectl explain pod.spec.restartPolicy" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "kubectl-output-format-mcq", "title": "kubectl Output Formats", "category": "Core Concepts", "difficulty": "Easy", "type": "mcq", "weight": 2, "description": "## kubectl Output Formats\n\nYou need to retrieve the **full live manifest** of a running deployment named `frontend` in YAML format so you can save it to a file for version control.\n\nWhich command produces the correct output?", "options": [ { "id": "a", "text": "`kubectl describe deployment frontend` — prints a human-readable summary with events" }, { "id": "b", "text": "`kubectl get deployment frontend -o yaml` — prints the full manifest in YAML format" }, { "id": "c", "text": "`kubectl export deployment frontend` — exports a portable manifest (removed in k8s 1.18)" }, { "id": "d", "text": "`kubectl manifest deployment frontend` — a built-in command for generating manifests" } ], "correct_option": "b", "explanation": "`kubectl get -o yaml` (or `--output=yaml`) prints the full live manifest as YAML, including all managed fields. `kubectl describe` gives a human-readable summary with events — not a re-applicable YAML manifest. `kubectl export` was removed in Kubernetes 1.18. `kubectl manifest` does not exist. Use `-o json` for JSON output, or `-o jsonpath='...'` to extract specific fields. To save to a file: `kubectl get deployment frontend -o yaml > frontend.yaml`.", "hints": [ { "title": "Output format flags", "body": "The `-o` / `--output` flag controls format: `-o yaml`, `-o json`, `-o wide` (extra columns), `-o name` (just the resource name), `-o jsonpath='...'` (field extraction).", "command": "kubectl get deployment frontend -o yaml" } ], "setup_commands": [], "default_namespace": "default", "teardown_commands": [] }, { "id": "pod-disruption-budget-task", "title": "Configure a PodDisruptionBudget", "category": "Cluster Administration", "difficulty": "Medium", "type": "task", "weight": 6, "description": "## PodDisruptionBudget\n\nA **PodDisruptionBudget (PDB)** limits the number of pods of a replicated application that are down simultaneously during voluntary disruptions (node drains, cluster upgrades). It is a critical CKA topic.\n\n**Your task:**\n\nA Deployment named `web-app` with **3 replicas** is already running. Create a **PodDisruptionBudget** named `web-pdb` in the `default` namespace that:\n- Applies to pods with the label `app=web-app`\n- Ensures **at least 2 pods** are always available (`minAvailable: 2`)\n\n```bash\n# Verify:\nkubectl get pdb web-pdb\n```", "hints": [ { "title": "PodDisruptionBudget manifest", "body": "Use `spec.minAvailable` (integer or percentage) and `spec.selector.matchLabels` to target the right pods.", "command": "cat </dev/null || true" }, { "command": "kubectl rollout status deployment/web-app --timeout=90s" } ], "validation": { "commands": [ { "description": "PDB 'web-pdb' exists", "command": "kubectl get pdb web-pdb -o jsonpath='{.metadata.name}'", "expected_output": "web-pdb", "match": "exact" }, { "description": "minAvailable is 2", "command": "kubectl get pdb web-pdb -o jsonpath='{.spec.minAvailable}'", "expected_output": "2", "match": "exact" }, { "description": "PDB selector targets app=web-app", "command": "kubectl get pdb web-pdb -o jsonpath='{.spec.selector.matchLabels.app}'", "expected_output": "web-app", "match": "exact" }, { "description": "Deployment 'web-app' has 3 replicas", "command": "kubectl get deployment web-app -o jsonpath='{.spec.replicas}'", "expected_output": "3", "match": "exact" } ] }, "default_namespace": "default", "teardown_commands": [ { "command": "kubectl delete pdb web-pdb --ignore-not-found" }, { "command": "kubectl delete deployment web-app --ignore-not-found" } ] }, { "id": "priority-class-task", "title": "Pod Priority and PriorityClass", "category": "Workloads & Scheduling", "difficulty": "Medium", "type": "task", "weight": 6, "description": "## Pod Priority and PriorityClass\n\n**PriorityClasses** assign a numeric priority to pods. When cluster resources are scarce, the scheduler preempts lower-priority pods to make room for higher-priority ones. This is a CKA exam topic.\n\n**Your task:**\n\n1. Create a **PriorityClass** named `high-priority` with:\n - `value: 1000000`\n - `globalDefault: false`\n - `description: \"High priority workloads\"`\n2. Create a Pod named `critical-pod` using `nginx:alpine` that references the `high-priority` PriorityClass via `spec.priorityClassName`\n\n```bash\n# Verify:\nkubectl get priorityclass high-priority\nkubectl get pod critical-pod -o jsonpath='{.spec.priorityClassName}'\n```", "hints": [ { "title": "Create the PriorityClass", "body": "PriorityClass is a cluster-scoped resource (not namespaced). Higher value = higher priority.", "command": "cat <