4031 lines
183 KiB
JSON
4031 lines
183 KiB
JSON
[
|
|
{
|
|
"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 <name>` 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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: config-reader\nspec:\n containers:\n - name: reader\n image: busybox:1.36\n command: [\"sleep\", \"3600\"]\n volumeMounts:\n - name: config-vol\n mountPath: /etc/app-config\n volumes:\n - name: config-vol\n configMap:\n name: app-config\nEOF"
|
|
},
|
|
{
|
|
"title": "Apply and verify",
|
|
"body": "Apply the manifest and confirm files are visible inside the container.",
|
|
"command": "kubectl apply -f /tmp/config-reader.yaml && kubectl exec config-reader -- ls /etc/app-config"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"description": "Verifies ConfigMap exists with correct keys and the pod mounts it at the right path.",
|
|
"commands": [
|
|
{
|
|
"description": "ConfigMap 'app-config' exists",
|
|
"command": "kubectl get configmap app-config -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "app-config",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "ConfigMap has key 'environment=production'",
|
|
"command": "kubectl get configmap app-config -o jsonpath='{.data.environment}'",
|
|
"expected_output": "production",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'config-reader' is Running",
|
|
"command": "kubectl get pod config-reader -o jsonpath='{.status.phase}'",
|
|
"expected_output": "Running",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "ConfigMap is mounted at /etc/app-config",
|
|
"command": "kubectl exec config-reader -- ls /etc/app-config",
|
|
"expected_output": "environment",
|
|
"match": "contains"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete configmap app-config --ignore-not-found"
|
|
},
|
|
{
|
|
"command": "kubectl delete pod config-reader --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "rbac-role",
|
|
"title": "RBAC: Create Role and RoleBinding",
|
|
"category": "Security",
|
|
"difficulty": "Hard",
|
|
"type": "task",
|
|
"weight": 9,
|
|
"description": "## Restrict Namespace Access with RBAC\n\nA CI/CD service account needs read-only access to pods and deployments in the `staging` namespace.\n\n**Your tasks:**\n1. Create namespace `staging`\n2. Create a **ServiceAccount** named `ci-reader` in the `staging` namespace\n3. Create a **Role** named `read-workloads` in `staging` that allows `get`, `list`, `watch` on `pods` and `deployments`\n4. Create a **RoleBinding** named `ci-reader-binding` that binds `read-workloads` to the `ci-reader` ServiceAccount\n5. Verify the ServiceAccount **can** list pods but **cannot** create them",
|
|
"hints": [
|
|
{
|
|
"title": "Create namespace and service account",
|
|
"body": "Create these first before the role, as the RoleBinding references both.",
|
|
"command": "kubectl create namespace staging\nkubectl create serviceaccount ci-reader -n staging"
|
|
},
|
|
{
|
|
"title": "Create the Role",
|
|
"body": "Use `kubectl create role` with multiple `--verb` and `--resource` flags.",
|
|
"command": "kubectl create role read-workloads \\\n --verb=get,list,watch \\\n --resource=pods,deployments \\\n -n staging"
|
|
},
|
|
{
|
|
"title": "Bind the Role",
|
|
"body": "RoleBinding ties a Role to a subject. ServiceAccount subjects need namespace-qualified names.",
|
|
"command": "kubectl create rolebinding ci-reader-binding \\\n --role=read-workloads \\\n --serviceaccount=staging:ci-reader \\\n -n staging"
|
|
},
|
|
{
|
|
"title": "Verify permissions with auth can-i",
|
|
"body": "Use `--as` to impersonate the service account and test its permissions.",
|
|
"command": "kubectl auth can-i list pods --as=system:serviceaccount:staging:ci-reader -n staging\nkubectl auth can-i create pods --as=system:serviceaccount:staging:ci-reader -n staging"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"description": "Validates the full RBAC chain: namespace, SA, role, rolebinding, and effective permissions.",
|
|
"commands": [
|
|
{
|
|
"description": "Namespace 'staging' exists",
|
|
"command": "kubectl get namespace staging -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "staging",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "ServiceAccount 'ci-reader' exists in staging",
|
|
"command": "kubectl get serviceaccount ci-reader -n staging -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "ci-reader",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Role 'read-workloads' exists in staging",
|
|
"command": "kubectl get role read-workloads -n staging -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "read-workloads",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "RoleBinding 'ci-reader-binding' exists",
|
|
"command": "kubectl get rolebinding ci-reader-binding -n staging -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "ci-reader-binding",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "ci-reader CAN list pods",
|
|
"command": "kubectl auth can-i list pods --as=system:serviceaccount:staging:ci-reader -n staging",
|
|
"expected_output": "yes",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "ci-reader CANNOT create pods",
|
|
"command": "kubectl auth can-i create pods --as=system:serviceaccount:staging:ci-reader -n staging",
|
|
"expected_output": "no",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete namespace staging --ignore-not-found --wait=false"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "broken-deployment",
|
|
"title": "Debug a Failing Deployment",
|
|
"category": "Troubleshooting",
|
|
"difficulty": "Medium",
|
|
"type": "task",
|
|
"weight": 8,
|
|
"description": "## Fix the Broken Deployment\n\nA deployment has been pre-created in the `debug` namespace but its pods are not running.\n\n**Your task:** Identify the problem and fix it so that the deployment has **all 3 pods in Ready state**.\n\n> 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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: local-pvc\nspec:\n accessModes: [ReadWriteOnce]\n storageClassName: local-path\n resources:\n requests:\n storage: 200Mi\nEOF"
|
|
},
|
|
{
|
|
"title": "Create the Pod with PVC mount",
|
|
"body": "Reference the PVC by name under volumes and mount it in the container.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: storage-pod\nspec:\n containers:\n - name: app\n image: nginx:1.25\n volumeMounts:\n - name: data\n mountPath: /data\n volumes:\n - name: data\n persistentVolumeClaim:\n claimName: local-pvc\nEOF"
|
|
},
|
|
{
|
|
"title": "Write file into the pod",
|
|
"body": "Use kubectl exec to write a file into the mounted volume.",
|
|
"command": "kubectl exec storage-pod -- sh -c 'echo hello-k8s > /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 <<EOF | kubectl apply -f -\napiVersion: networking.k8s.io/v1\nkind: NetworkPolicy\nmetadata:\n name: db-isolate\n namespace: netpol\nspec:\n podSelector:\n matchLabels:\n app: database\n policyTypes:\n - Ingress\n ingress:\n - from:\n - podSelector:\n matchLabels:\n role: backend\nEOF"
|
|
},
|
|
{
|
|
"title": "Verify the policy",
|
|
"body": "List NetworkPolicies to confirm it was created.",
|
|
"command": "kubectl get networkpolicy db-isolate -n netpol -o yaml"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl create namespace netpol"
|
|
},
|
|
{
|
|
"command": "kubectl run database --image=nginx:1.25 --labels=app=database -n netpol"
|
|
},
|
|
{
|
|
"command": "kubectl run backend --image=busybox:1.36 --labels=role=backend -n netpol --command -- sleep 3600"
|
|
},
|
|
{
|
|
"command": "kubectl run other --image=busybox:1.36 --labels=role=other -n netpol --command -- sleep 3600"
|
|
}
|
|
],
|
|
"validation": {
|
|
"description": "Verifies the NetworkPolicy exists and correctly targets the database pod.",
|
|
"commands": [
|
|
{
|
|
"description": "NetworkPolicy 'db-isolate' exists",
|
|
"command": "kubectl get networkpolicy db-isolate -n netpol -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "db-isolate",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Policy targets pods with label app=database",
|
|
"command": "kubectl get networkpolicy db-isolate -n netpol -o jsonpath='{.spec.podSelector.matchLabels.app}'",
|
|
"expected_output": "database",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Policy has Ingress policyType",
|
|
"command": "kubectl get networkpolicy db-isolate -n netpol -o jsonpath='{.spec.policyTypes[0]}'",
|
|
"expected_output": "Ingress",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Ingress allows from role=backend",
|
|
"command": "kubectl get networkpolicy db-isolate -n netpol -o jsonpath='{.spec.ingress[0].from[0].podSelector.matchLabels.role}'",
|
|
"expected_output": "backend",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "netpol",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete namespace netpol --ignore-not-found --wait=false"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "services-mcq",
|
|
"title": "Kubernetes Service Types",
|
|
"category": "Networking",
|
|
"difficulty": "Easy",
|
|
"type": "mcq",
|
|
"weight": 4,
|
|
"description": "## Choose the Right Service Type\n\nYou have a microservice running inside the cluster that needs to be reachable **from the internet** (external traffic). The cluster is hosted on a cloud provider (AWS/GCP/Azure).\n\nWhich **Service type** should you use?",
|
|
"options": [
|
|
{
|
|
"id": "a",
|
|
"text": "ClusterIP — the default service type, only reachable within the cluster"
|
|
},
|
|
{
|
|
"id": "b",
|
|
"text": "NodePort — opens a port on every node, accessible from outside if the node IP is reachable"
|
|
},
|
|
{
|
|
"id": "c",
|
|
"text": "LoadBalancer — provisions a cloud load balancer and assigns an external IP automatically"
|
|
},
|
|
{
|
|
"id": "d",
|
|
"text": "ExternalName — maps the service to a DNS name, not suitable for external ingress"
|
|
}
|
|
],
|
|
"correct_option": "c",
|
|
"explanation": "`LoadBalancer` is the right choice for production external traffic on cloud-hosted clusters. It provisions a cloud provider load balancer (like an AWS ALB or GCP L4 LB) and assigns an external IP. `NodePort` works in theory but requires knowing node IPs and is not production-grade. `ClusterIP` is internal-only. `ExternalName` is for mapping services to external DNS names.",
|
|
"hints": [
|
|
{
|
|
"title": "Recall Service types",
|
|
"body": "There are 4 service types: ClusterIP (internal), NodePort (node-level), LoadBalancer (cloud LB), ExternalName (DNS alias). Each builds on the previous in terms of exposure level.",
|
|
"command": "kubectl explain service.spec.type"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"default_namespace": "default",
|
|
"teardown_commands": []
|
|
},
|
|
{
|
|
"id": "namespaces-basics",
|
|
"title": "Working with Namespaces",
|
|
"category": "Core Concepts",
|
|
"difficulty": "Easy",
|
|
"type": "task",
|
|
"weight": 4,
|
|
"description": "## Working with Namespaces\n\nNamespaces provide a mechanism for isolating groups of resources within a single cluster.\n\n**Your task:**\n\n1. Create a namespace called `team-alpha`\n2. Deploy a `nginx:alpine` Deployment named `web` with **2 replicas** inside the `team-alpha` namespace\n\n**Verify your work:**\n```bash\nkubectl get namespace team-alpha\nkubectl get deployment web -n team-alpha\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Create a namespace",
|
|
"body": "Use `kubectl create namespace <name>` 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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: limited-pod\nspec:\n containers:\n - name: app\n image: nginx:alpine\n resources:\n requests:\n cpu: \"100m\"\n memory: \"64Mi\"\n limits:\n cpu: \"200m\"\n memory: \"128Mi\"\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Pod 'limited-pod' exists",
|
|
"command": "kubectl get pod limited-pod -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "limited-pod",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "CPU request is 100m",
|
|
"command": "kubectl get pod limited-pod -o jsonpath='{.spec.containers[0].resources.requests.cpu}'",
|
|
"expected_output": "100m",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Memory limit is 128Mi",
|
|
"command": "kubectl get pod limited-pod -o jsonpath='{.spec.containers[0].resources.limits.memory}'",
|
|
"expected_output": "128Mi",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "CPU limit is 200m",
|
|
"command": "kubectl get pod limited-pod -o jsonpath='{.spec.containers[0].resources.limits.cpu}'",
|
|
"expected_output": "200m",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod limited-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "secrets-basics",
|
|
"title": "Creating and Using Secrets",
|
|
"category": "Configuration",
|
|
"difficulty": "Easy",
|
|
"type": "task",
|
|
"weight": 5,
|
|
"description": "## Creating and Using Secrets\n\nSecrets let you store sensitive data (passwords, tokens, keys) separately from your pod specs.\n\n**Your task:**\n\n1. Create a Secret named `app-secret` with the key `api-key` and value `supersecret`\n2. Create a Pod named `secret-reader` using `busybox:1.36` that:\n - Mounts the secret key `api-key` as an environment variable named `API_KEY`\n - Runs: `sleep 3600`\n\n**Verify:**\n```bash\nkubectl exec secret-reader -- env | grep API_KEY\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Create the Secret",
|
|
"body": "Use `kubectl create secret generic` with `--from-literal`.",
|
|
"command": "kubectl create secret generic app-secret --from-literal=api-key=supersecret"
|
|
},
|
|
{
|
|
"title": "Reference secret in a Pod env var",
|
|
"body": "Use `env[].valueFrom.secretKeyRef` in the container spec to map a secret key to an env var.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: secret-reader\nspec:\n containers:\n - name: reader\n image: busybox:1.36\n command: [\"sleep\",\"3600\"]\n env:\n - name: API_KEY\n valueFrom:\n secretKeyRef:\n name: app-secret\n key: api-key\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Secret 'app-secret' exists",
|
|
"command": "kubectl get secret app-secret -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "app-secret",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'secret-reader' is Running",
|
|
"command": "kubectl get pod secret-reader -o jsonpath='{.status.phase}'",
|
|
"expected_output": "Running",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod references the secret via env var",
|
|
"command": "kubectl get pod secret-reader -o jsonpath='{.spec.containers[0].env[0].valueFrom.secretKeyRef.name}'",
|
|
"expected_output": "app-secret",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete secret app-secret --ignore-not-found"
|
|
},
|
|
{
|
|
"command": "kubectl delete pod secret-reader --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "liveness-probe-task",
|
|
"title": "Configure a Liveness Probe",
|
|
"category": "Workloads",
|
|
"difficulty": "Medium",
|
|
"type": "task",
|
|
"weight": 6,
|
|
"description": "## Configure a Liveness Probe\n\nKubernetes uses **liveness probes** to know when to restart a container. If a container's liveness probe fails repeatedly, the kubelet kills the container and the pod's restart policy takes effect.\n\n**Your task:**\n\nCreate a Deployment named `probed-app` with:\n- Image: `nginx:alpine`\n- 1 replica\n- A **liveness probe** that:\n - Checks `HTTP GET /` on port `80`\n - `initialDelaySeconds: 5`\n - `periodSeconds: 10`",
|
|
"hints": [
|
|
{
|
|
"title": "Liveness probe structure",
|
|
"body": "Add `livenessProbe` under `spec.containers[]`. Use `httpGet` with `path` and `port` fields.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: probed-app\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: probed-app\n template:\n metadata:\n labels:\n app: probed-app\n spec:\n containers:\n - name: app\n image: nginx:alpine\n ports:\n - containerPort: 80\n livenessProbe:\n httpGet:\n path: /\n port: 80\n initialDelaySeconds: 5\n periodSeconds: 10\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Deployment 'probed-app' exists",
|
|
"command": "kubectl get deployment probed-app -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "probed-app",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Liveness probe uses HTTP GET on port 80",
|
|
"command": "kubectl get deployment probed-app -o jsonpath='{.spec.template.spec.containers[0].livenessProbe.httpGet.port}'",
|
|
"expected_output": "80",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "initialDelaySeconds is 5",
|
|
"command": "kubectl get deployment probed-app -o jsonpath='{.spec.template.spec.containers[0].livenessProbe.initialDelaySeconds}'",
|
|
"expected_output": "5",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "periodSeconds is 10",
|
|
"command": "kubectl get deployment probed-app -o jsonpath='{.spec.template.spec.containers[0].livenessProbe.periodSeconds}'",
|
|
"expected_output": "10",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete deployment probed-app --ignore-not-found"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "jobs-cronjobs-mcq",
|
|
"title": "Jobs vs CronJobs",
|
|
"category": "Workloads",
|
|
"difficulty": "Easy",
|
|
"type": "mcq",
|
|
"weight": 3,
|
|
"description": "## Jobs vs CronJobs\n\nA data-processing team needs to run a database backup script **every night at midnight**. Which Kubernetes resource should they use, and why?",
|
|
"options": [
|
|
{
|
|
"id": "a",
|
|
"text": "A `Job` — because Jobs run a task until it completes successfully, including on a nightly schedule"
|
|
},
|
|
{
|
|
"id": "b",
|
|
"text": "A `CronJob` — because it creates Jobs on a time-based schedule (cron syntax) and is designed for recurring tasks"
|
|
},
|
|
{
|
|
"id": "c",
|
|
"text": "A `Deployment` with `restartPolicy: OnFailure` — deployments keep tasks running on schedule"
|
|
},
|
|
{
|
|
"id": "d",
|
|
"text": "A `DaemonSet` — because it ensures exactly one backup process per node nightly"
|
|
}
|
|
],
|
|
"correct_option": "b",
|
|
"explanation": "A **CronJob** is the right resource for recurring scheduled tasks. It uses standard cron syntax (e.g. `0 0 * * *` for midnight daily) and creates a new `Job` object at each scheduled time. A plain `Job` runs once to completion — you would need external scheduling to run it nightly. Deployments are for long-running services, and DaemonSets run pods on every node.",
|
|
"hints": [
|
|
{
|
|
"title": "CronJob syntax",
|
|
"body": "CronJobs use standard cron format: `minute hour day-of-month month day-of-week`. For midnight daily: `0 0 * * *`.",
|
|
"command": "kubectl explain cronjob.spec.schedule"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"default_namespace": "default",
|
|
"teardown_commands": []
|
|
},
|
|
{
|
|
"id": "rolling-update-task",
|
|
"title": "Perform a Rolling Update",
|
|
"category": "Workloads",
|
|
"difficulty": "Easy",
|
|
"type": "task",
|
|
"weight": 5,
|
|
"description": "## Perform a Rolling Update\n\nA Deployment named `app-v1` is running `nginx:1.24`. Your team needs to upgrade it to `nginx:1.25`.\n\n**Your task:**\n\nUpdate the image of the `app-v1` deployment to `nginx:1.25` using a rolling update strategy. Ensure all replicas are running the new image.\n\n```bash\n# Hint: kubectl set image can update an image in-place\nkubectl set image deployment/<name> <container>=<image>\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Set a new image on a Deployment",
|
|
"body": "Use `kubectl set image deployment/<name> <container-name>=<new-image>` 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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: env-pod\nspec:\n containers:\n - name: app\n image: busybox:1.36\n command: [\"sleep\",\"3600\"]\n envFrom:\n - configMapRef:\n name: app-env\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "ConfigMap 'app-env' has LOG_LEVEL=debug",
|
|
"command": "kubectl get configmap app-env -o jsonpath='{.data.LOG_LEVEL}'",
|
|
"expected_output": "debug",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "ConfigMap 'app-env' has APP_PORT=8080",
|
|
"command": "kubectl get configmap app-env -o jsonpath='{.data.APP_PORT}'",
|
|
"expected_output": "8080",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'env-pod' references app-env via envFrom",
|
|
"command": "kubectl get pod env-pod -o jsonpath='{.spec.containers[0].envFrom[0].configMapRef.name}'",
|
|
"expected_output": "app-env",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'env-pod' is Running",
|
|
"command": "kubectl get pod env-pod -o jsonpath='{.status.phase}'",
|
|
"expected_output": "Running",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete configmap app-env --ignore-not-found"
|
|
},
|
|
{
|
|
"command": "kubectl delete pod env-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "daemonset-mcq",
|
|
"title": "Understanding DaemonSets",
|
|
"category": "Workloads",
|
|
"difficulty": "Easy",
|
|
"type": "mcq",
|
|
"weight": 3,
|
|
"description": "## Understanding DaemonSets\n\nYour team wants to deploy a **log collector agent** that must run on **every node** in the cluster, including any nodes added in the future.\n\nWhich Kubernetes resource is the best fit?",
|
|
"options": [
|
|
{
|
|
"id": "a",
|
|
"text": "A `Deployment` with `replicas` set to the number of nodes"
|
|
},
|
|
{
|
|
"id": "b",
|
|
"text": "A `StatefulSet` with `replicas` matching the node count"
|
|
},
|
|
{
|
|
"id": "c",
|
|
"text": "A `DaemonSet`, which automatically places one pod per node and adapts as nodes join or leave"
|
|
},
|
|
{
|
|
"id": "d",
|
|
"text": "A `CronJob` that runs `kubectl create pod` on each node every minute"
|
|
}
|
|
],
|
|
"correct_option": "c",
|
|
"explanation": "A **DaemonSet** guarantees that exactly one copy of a pod runs on every (or selected) node. As nodes are added to the cluster, the DaemonSet controller automatically schedules the pod on them; when nodes are removed, the pods are garbage-collected. Classic use cases: log shippers (Fluentd, Filebeat), monitoring agents (Prometheus Node Exporter), and CNI plugins. A Deployment with fixed replicas does NOT guarantee one-pod-per-node coverage.",
|
|
"hints": [
|
|
{
|
|
"title": "When to use a DaemonSet",
|
|
"body": "DaemonSets are ideal for cluster-level infrastructure: log collection, metrics agents, node monitoring, and CNI/CSI plugins that need to run on every node.",
|
|
"command": "kubectl explain daemonset.spec"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"default_namespace": "default",
|
|
"teardown_commands": []
|
|
},
|
|
{
|
|
"id": "scale-deployment",
|
|
"title": "Scaling a Deployment",
|
|
"category": "Workloads",
|
|
"difficulty": "Easy",
|
|
"type": "task",
|
|
"weight": 4,
|
|
"description": "## Scaling a Deployment\n\nA deployment named `webapp` is currently running with **1 replica**. Traffic has increased and you need to scale it up.\n\n**Your task:**\n\nScale the `webapp` deployment to **4 replicas** and verify all pods become Ready.\n\n```bash\n# You can use the imperative command\nkubectl scale deployment webapp --replicas=4\n# Or edit the manifest\nkubectl edit deployment webapp\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Scale imperatively",
|
|
"body": "`kubectl scale` is the quickest way to change replica count without editing a YAML file.",
|
|
"command": "kubectl scale deployment webapp --replicas=4"
|
|
},
|
|
{
|
|
"title": "Verify the scale",
|
|
"body": "Check READY column in `kubectl get deployment webapp` — it should show 4/4.",
|
|
"command": "kubectl get deployment webapp"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl create deployment webapp --image=nginx:alpine --replicas=1"
|
|
},
|
|
{
|
|
"command": "kubectl rollout status deployment/webapp --timeout=60s"
|
|
}
|
|
],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Deployment 'webapp' has 4 replicas specified",
|
|
"command": "kubectl get deployment webapp -o jsonpath='{.spec.replicas}'",
|
|
"expected_output": "4",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Deployment 'webapp' has 4 ready replicas",
|
|
"command": "kubectl get deployment webapp -o jsonpath='{.status.readyReplicas}' 2>/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 <container-name>` flag: `kubectl logs <pod-name> -c <container-name>`. 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 <node-name> 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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: ssd-pod\nspec:\n nodeSelector:\n disk: ssd\n containers:\n - name: app\n image: nginx:alpine\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Node 'k8s-lab' has label disk=ssd",
|
|
"command": "kubectl get node k8s-lab -o jsonpath='{.metadata.labels.disk}'",
|
|
"expected_output": "ssd",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'ssd-pod' has nodeSelector disk=ssd",
|
|
"command": "kubectl get pod ssd-pod -o jsonpath='{.spec.nodeSelector.disk}'",
|
|
"expected_output": "ssd",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'ssd-pod' is Running",
|
|
"command": "kubectl get pod ssd-pod -o jsonpath='{.status.phase}'",
|
|
"expected_output": "Running",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl label node k8s-lab disk- 2>/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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: gpu-pod\nspec:\n tolerations:\n - key: \"env\"\n operator: \"Equal\"\n value: \"gpu\"\n effect: \"NoSchedule\"\n containers:\n - name: app\n image: nginx:alpine\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl taint nodes k8s-lab env=gpu:NoSchedule --overwrite=true 2>/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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: ResourceQuota\nmetadata:\n name: compute-quota\n namespace: team-quota\nspec:\n hard:\n pods: \"5\"\n requests.cpu: \"1\"\n requests.memory: \"500Mi\"\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl create namespace team-quota 2>/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/<name>` 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 <<EOF | kubectl apply -f -\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: ready-app\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: ready-app\n template:\n metadata:\n labels:\n app: ready-app\n spec:\n containers:\n - name: app\n image: nginx:alpine\n readinessProbe:\n httpGet:\n path: /\n port: 80\n initialDelaySeconds: 3\n periodSeconds: 5\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Deployment 'ready-app' exists",
|
|
"command": "kubectl get deployment ready-app -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "ready-app",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Readiness probe path is /",
|
|
"command": "kubectl get deployment ready-app -o jsonpath='{.spec.template.spec.containers[0].readinessProbe.httpGet.path}'",
|
|
"expected_output": "/",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Readiness probe initialDelaySeconds is 3",
|
|
"command": "kubectl get deployment ready-app -o jsonpath='{.spec.template.spec.containers[0].readinessProbe.initialDelaySeconds}'",
|
|
"expected_output": "3",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete deployment ready-app --ignore-not-found"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "nodeport-task",
|
|
"title": "Expose a Deployment via NodePort",
|
|
"category": "Services & Networking",
|
|
"difficulty": "Easy",
|
|
"type": "task",
|
|
"weight": 5,
|
|
"description": "## Expose a Deployment via NodePort\n\nA **NodePort** service exposes an application on a static port on every node, making it accessible outside the cluster without a cloud load balancer.\n\n**Your task:**\n\nA deployment `frontend` is already running. Expose it as a **NodePort** service named `frontend-np` on:\n- Service port `80`\n- NodePort `30080`\n- Target port `80`",
|
|
"hints": [
|
|
{
|
|
"title": "Create NodePort service",
|
|
"body": "Use `kubectl expose` or apply a Service manifest with `type: NodePort` and the `nodePort` field.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Service\nmetadata:\n name: frontend-np\nspec:\n type: NodePort\n selector:\n app: frontend\n ports:\n - port: 80\n targetPort: 80\n nodePort: 30080\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl create deployment frontend --image=nginx:alpine --replicas=1 2>/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 `<service>.<namespace>.svc.<cluster-domain>`. 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-name>.<namespace>.svc.cluster.local`. You can verify with: `kubectl exec <pod> -- nslookup <service-name>`.",
|
|
"command": "kubectl exec -it <pod-name> -- 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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: shared-data\nspec:\n containers:\n - name: writer\n image: busybox:1.36\n command: [\"sh\",\"-c\",\"echo hello-kube > /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 `busybox:1.36` (command: `sleep 3600`) with the following security settings:\n- `runAsUser: 1000`\n- `runAsNonRoot: true`\n- `allowPrivilegeEscalation: false`\n\n```bash\n# Verify after creation:\nkubectl get pod secure-pod -o jsonpath='{.spec.containers[0].securityContext}'\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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: secure-pod\nspec:\n containers:\n - name: app\n image: busybox:1.36\n command: [\"sleep\", \"3600\"]\n securityContext:\n runAsUser: 1000\n runAsNonRoot: true\n allowPrivilegeEscalation: false\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Pod 'secure-pod' exists",
|
|
"command": "kubectl get pod secure-pod -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "secure-pod",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "runAsUser is 1000",
|
|
"command": "kubectl get pod secure-pod -o jsonpath='{.spec.containers[0].securityContext.runAsUser}'",
|
|
"expected_output": "1000",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "runAsNonRoot is true",
|
|
"command": "kubectl get pod secure-pod -o jsonpath='{.spec.containers[0].securityContext.runAsNonRoot}'",
|
|
"expected_output": "true",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "allowPrivilegeEscalation is false",
|
|
"command": "kubectl get pod secure-pod -o jsonpath='{.spec.containers[0].securityContext.allowPrivilegeEscalation}'",
|
|
"expected_output": "false",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod secure-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "crashloop-fix",
|
|
"title": "Fix a CrashLoopBackOff Pod",
|
|
"category": "Troubleshooting",
|
|
"difficulty": "Medium",
|
|
"type": "task",
|
|
"weight": 7,
|
|
"description": "## Fix a CrashLoopBackOff\n\nA deployment `crash-app` is in `CrashLoopBackOff` because it was deployed with a broken command.\n\n**Your task:**\n\nFix the `crash-app` deployment so all pods run successfully. The container should run `nginx -g 'daemon off;'` (the default nginx command). Update the deployment to remove the broken command override.\n\n```bash\n# Check what's wrong:\nkubectl describe deployment crash-app\nkubectl logs -l app=crash-app\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Remove the broken command",
|
|
"body": "Use `kubectl patch` or `kubectl edit deployment crash-app`. Remove the `command` override so nginx uses its default entrypoint.",
|
|
"command": "kubectl patch deployment crash-app --type=json -p='[{\"op\":\"remove\",\"path\":\"/spec/template/spec/containers/0/command\"}]'"
|
|
},
|
|
{
|
|
"title": "Alternative: kubectl set",
|
|
"body": "You can also edit the deployment directly and delete the command field.",
|
|
"command": "kubectl edit deployment crash-app"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl create deployment crash-app --image=nginx:alpine --replicas=1 2>/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 - <<EOF\napiVersion: v1\nkind: Service\nmetadata:\n name: my-svc\nspec:\n selector:\n app: wrong-label\n ports:\n - port: 80\n targetPort: 80\nEOF"
|
|
}
|
|
],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Service 'my-svc' selector is app=backend",
|
|
"command": "kubectl get service my-svc -o jsonpath='{.spec.selector.app}'",
|
|
"expected_output": "backend",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Endpoints exist for my-svc",
|
|
"command": "kubectl get endpoints my-svc -o jsonpath='{.subsets[0].addresses[0].ip}' 2>/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 <<EOF | kubectl apply -f -\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: web-ingress\nspec:\n rules:\n - host: app.lab.local\n http:\n paths:\n - path: /\n pathType: Prefix\n backend:\n service:\n name: web-svc\n port:\n number: 80\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl create deployment web-app --image=nginx:alpine --replicas=1 2>/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 <<EOF | kubectl apply -f -\napiVersion: networking.k8s.io/v1\nkind: NetworkPolicy\nmetadata:\n name: restrict-egress\n namespace: default\nspec:\n podSelector:\n matchLabels:\n role: isolated\n policyTypes:\n - Egress\n egress:\n - to:\n - podSelector:\n matchLabels:\n role: allowed\n ports:\n - protocol: TCP\n port: 80\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "NetworkPolicy 'restrict-egress' exists",
|
|
"command": "kubectl get networkpolicy restrict-egress -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "restrict-egress",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Policy applies to role=isolated pods",
|
|
"command": "kubectl get networkpolicy restrict-egress -o jsonpath='{.spec.podSelector.matchLabels.role}'",
|
|
"expected_output": "isolated",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Policy type includes Egress",
|
|
"command": "kubectl get networkpolicy restrict-egress -o jsonpath='{.spec.policyTypes[*]}'",
|
|
"expected_output": "Egress",
|
|
"match": "contains"
|
|
},
|
|
{
|
|
"description": "Egress allowed to role=allowed pods",
|
|
"command": "kubectl get networkpolicy restrict-egress -o jsonpath='{.spec.egress[0].to[0].podSelector.matchLabels.role}'",
|
|
"expected_output": "allowed",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete networkpolicy restrict-egress --ignore-not-found"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "limitrange-task",
|
|
"title": "LimitRange for Default Container Limits",
|
|
"category": "Workloads & Scheduling",
|
|
"difficulty": "Medium",
|
|
"type": "task",
|
|
"weight": 6,
|
|
"description": "## LimitRange\n\nA **LimitRange** sets default resource requests/limits for containers in a namespace. Any container created without explicit limits will automatically get the LimitRange defaults.\n\n**Your task:**\n\nCreate a namespace `bounded-ns` and a LimitRange named `default-limits` that sets:\n- Default CPU limit: `200m`\n- Default CPU request: `100m`\n- Default Memory limit: `256Mi`\n- Default Memory request: `128Mi`",
|
|
"hints": [
|
|
{
|
|
"title": "LimitRange manifest",
|
|
"body": "Use `type: Container` and the `default`/`defaultRequest` fields inside `spec.limits[]`.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: LimitRange\nmetadata:\n name: default-limits\n namespace: bounded-ns\nspec:\n limits:\n - type: Container\n default:\n cpu: \"200m\"\n memory: \"256Mi\"\n defaultRequest:\n cpu: \"100m\"\n memory: \"128Mi\"\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl create namespace bounded-ns 2>/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 <<EOF | kubectl apply -f -\napiVersion: batch/v1\nkind: Job\nmetadata:\n name: pi-job\nspec:\n completions: 1\n template:\n spec:\n restartPolicy: Never\n containers:\n - name: pi\n image: perl:5.34\n command: [\"perl\",\"-Mbignum=bpi\",\"-wle\",\"print bpi(100)\"]\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Job 'pi-job' exists",
|
|
"command": "kubectl get job pi-job -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "pi-job",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Job completions is 1",
|
|
"command": "kubectl get job pi-job -o jsonpath='{.spec.completions}'",
|
|
"expected_output": "1",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Job restartPolicy is Never",
|
|
"command": "kubectl get job pi-job -o jsonpath='{.spec.template.spec.restartPolicy}'",
|
|
"expected_output": "Never",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete job pi-job --ignore-not-found"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "pod-affinity-mcq",
|
|
"title": "Pod Affinity vs Anti-Affinity",
|
|
"category": "Workloads & Scheduling",
|
|
"difficulty": "Medium",
|
|
"type": "mcq",
|
|
"weight": 4,
|
|
"description": "## Pod Affinity vs Anti-Affinity\n\nYou want to ensure that **cache** pods are always scheduled on the **same node** as the **app** pods they serve (to reduce latency). Which scheduling rule should you use in the cache pod spec?",
|
|
"options": [
|
|
{
|
|
"id": "a",
|
|
"text": "`spec.affinity.podAntiAffinity` with `requiredDuringSchedulingIgnoredDuringExecution` matching `app=app-pod`"
|
|
},
|
|
{
|
|
"id": "b",
|
|
"text": "`spec.affinity.podAffinity` with `requiredDuringSchedulingIgnoredDuringExecution` matching `app=app-pod`"
|
|
},
|
|
{
|
|
"id": "c",
|
|
"text": "`spec.affinity.nodeAffinity` with a `matchExpressions` for the app pod's node label"
|
|
},
|
|
{
|
|
"id": "d",
|
|
"text": "`spec.tolerations` with `key=app-pod` and `effect=NoSchedule`"
|
|
}
|
|
],
|
|
"correct_option": "b",
|
|
"explanation": "**Pod Affinity** (`spec.affinity.podAffinity`) attracts pods to nodes where matching pods are already running. Use `requiredDuringSchedulingIgnoredDuringExecution` for a hard constraint (cache pod MUST co-locate with app pod) vs `preferredDuringSchedulingIgnoredDuringExecution` for a soft preference. **Pod Anti-Affinity** does the opposite — spreads pods apart. Tolerations are unrelated to co-location and work with taints.",
|
|
"hints": [
|
|
{
|
|
"title": "Affinity types",
|
|
"body": "podAffinity = attract to same node. podAntiAffinity = repel from same node. nodeAffinity = attract to nodes with specific labels. The `topologyKey: kubernetes.io/hostname` means 'same node'.",
|
|
"command": "kubectl explain pod.spec.affinity.podAffinity"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"default_namespace": "default",
|
|
"teardown_commands": []
|
|
},
|
|
{
|
|
"id": "pvc-dynamic-task",
|
|
"title": "Dynamic PVC Provisioning",
|
|
"category": "Storage",
|
|
"difficulty": "Medium",
|
|
"type": "task",
|
|
"weight": 6,
|
|
"description": "## Dynamic PVC Provisioning\n\nDynamic provisioning automatically creates a PersistentVolume when a PVC is created, using a StorageClass. k3s ships with a built-in `local-path` StorageClass.\n\n**Your task:**\n\n1. Create a **PersistentVolumeClaim** named `app-data` that requests:\n - Storage: `1Gi`\n - Access mode: `ReadWriteOnce`\n - StorageClass: `local-path`\n2. Create a Pod named `data-pod` using `nginx:alpine` that mounts `app-data` at `/data`\n\n```bash\nkubectl get storageclass # verify local-path exists\n```",
|
|
"hints": [
|
|
{
|
|
"title": "PVC manifest",
|
|
"body": "Set `storageClassName: local-path` (k3s default). Dynamic provisioning creates the PV automatically.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: app-data\nspec:\n accessModes: [ReadWriteOnce]\n storageClassName: local-path\n resources:\n requests:\n storage: 1Gi\nEOF"
|
|
},
|
|
{
|
|
"title": "Mount PVC in a Pod",
|
|
"body": "Reference the PVC by name in `spec.volumes[].persistentVolumeClaim.claimName`.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: data-pod\nspec:\n containers:\n - name: app\n image: nginx:alpine\n volumeMounts:\n - name: storage\n mountPath: /data\n volumes:\n - name: storage\n persistentVolumeClaim:\n claimName: app-data\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "PVC 'app-data' exists",
|
|
"command": "kubectl get pvc app-data -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "app-data",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "PVC requests 1Gi storage",
|
|
"command": "kubectl get pvc app-data -o jsonpath='{.spec.resources.requests.storage}'",
|
|
"expected_output": "1Gi",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'data-pod' mounts the PVC",
|
|
"command": "kubectl get pod data-pod -o jsonpath='{.spec.volumes[0].persistentVolumeClaim.claimName}'",
|
|
"expected_output": "app-data",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'data-pod' is Running",
|
|
"command": "kubectl get pod data-pod -o jsonpath='{.status.phase}'",
|
|
"expected_output": "Running",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pvc app-data --ignore-not-found"
|
|
},
|
|
{
|
|
"command": "kubectl delete pod data-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "serviceaccount-pod",
|
|
"title": "ServiceAccount for Pod API Access",
|
|
"category": "Cluster Administration",
|
|
"difficulty": "Medium",
|
|
"type": "task",
|
|
"weight": 7,
|
|
"description": "## ServiceAccount for Pod API Access\n\nServiceAccounts provide an identity for pods that need to interact with the Kubernetes API (e.g., operators, CI runners, custom controllers).\n\n**Your task:**\n\n1. Create a ServiceAccount named `api-reader` in the `default` namespace\n2. Create a Pod named `api-pod` using `nginx:alpine` that uses the `api-reader` ServiceAccount\n\n```bash\n# Verify:\nkubectl get pod api-pod -o jsonpath='{.spec.serviceAccountName}'\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Create a ServiceAccount",
|
|
"body": "ServiceAccounts are namespace-scoped resources. Use `kubectl create serviceaccount`.",
|
|
"command": "kubectl create serviceaccount api-reader"
|
|
},
|
|
{
|
|
"title": "Assign ServiceAccount to a Pod",
|
|
"body": "Set `spec.serviceAccountName` in the pod spec.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: api-pod\nspec:\n serviceAccountName: api-reader\n containers:\n - name: app\n image: nginx:alpine\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "ServiceAccount 'api-reader' exists",
|
|
"command": "kubectl get serviceaccount api-reader -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "api-reader",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'api-pod' uses api-reader ServiceAccount",
|
|
"command": "kubectl get pod api-pod -o jsonpath='{.spec.serviceAccountName}'",
|
|
"expected_output": "api-reader",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'api-pod' is Running",
|
|
"command": "kubectl get pod api-pod -o jsonpath='{.status.phase}'",
|
|
"expected_output": "Running",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete serviceaccount api-reader --ignore-not-found"
|
|
},
|
|
{
|
|
"command": "kubectl delete pod api-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cluster-upgrade-mcq",
|
|
"title": "Cluster Upgrade Order",
|
|
"category": "Cluster Administration",
|
|
"difficulty": "Hard",
|
|
"type": "mcq",
|
|
"weight": 5,
|
|
"description": "## Cluster Upgrade Order\n\nYou need to upgrade a kubeadm-managed cluster from v1.28 to v1.29. What is the **correct order** of operations?",
|
|
"options": [
|
|
{
|
|
"id": "a",
|
|
"text": "1. Upgrade worker nodes → 2. Upgrade control plane → 3. Upgrade kubeadm → 4. Update kubelet/kubectl"
|
|
},
|
|
{
|
|
"id": "b",
|
|
"text": "1. Upgrade kubeadm on control plane → 2. Run `kubeadm upgrade apply` → 3. Upgrade kubelet/kubectl on control plane → 4. Drain and upgrade worker nodes one by one"
|
|
},
|
|
{
|
|
"id": "c",
|
|
"text": "1. `kubectl drain` all nodes → 2. Upgrade all nodes simultaneously → 3. `kubectl uncordon` all nodes"
|
|
},
|
|
{
|
|
"id": "d",
|
|
"text": "1. Upgrade etcd first → 2. Upgrade API server → 3. Upgrade scheduler and controller-manager → 4. Upgrade kubelet"
|
|
}
|
|
],
|
|
"correct_option": "b",
|
|
"explanation": "The correct kubeadm upgrade order is: **1)** Upgrade `kubeadm` on the control plane node → **2)** Run `kubeadm upgrade apply v1.29.x` (upgrades control-plane components) → **3)** Upgrade `kubelet` and `kubectl` on the control plane, then `systemctl daemon-reload && systemctl restart kubelet` → **4)** For each worker: `kubectl drain`, upgrade `kubeadm`/`kubelet`/`kubectl`, run `kubeadm upgrade node`, restart kubelet, `kubectl uncordon`. Never upgrade workers before the control plane.",
|
|
"hints": [
|
|
{
|
|
"title": "kubeadm upgrade steps",
|
|
"body": "Key commands: `kubeadm upgrade plan` (preview), `kubeadm upgrade apply v1.29.x` (apply), `kubectl drain <node>` (before worker upgrade), `kubectl uncordon <node>` (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 <pod> --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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: web-sidecar\nspec:\n containers:\n - name: app\n image: nginx:alpine\n - name: sidecar\n image: busybox:1.36\n command: [\"sleep\", \"3600\"]\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Pod 'web-sidecar' exists",
|
|
"command": "kubectl get pod web-sidecar -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "web-sidecar",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod has 2 containers",
|
|
"command": "kubectl get pod web-sidecar -o jsonpath='{.spec.containers[*].name}' | wc -w | tr -d ' '",
|
|
"expected_output": "2",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod is Running",
|
|
"command": "kubectl get pod web-sidecar -o jsonpath='{.status.phase}'",
|
|
"expected_output": "Running",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod web-sidecar --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "expose-service-basics",
|
|
"title": "Exposing an Application",
|
|
"category": "Services & Networking",
|
|
"difficulty": "Easy",
|
|
"type": "task",
|
|
"weight": 4,
|
|
"description": "## Exposing an Application\n\nA `redis` deployment is running, but other pods cannot reliably connect to it because pod IPs change when they restart.\n\n**Your task:**\n\nCreate a **ClusterIP** Service named `redis-svc` to expose the `redis` deployment on port `6379`.\n\n```bash\n# Verify your service:\nkubectl get svc redis-svc\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Use kubectl expose",
|
|
"body": "The imperative `expose` command is the fastest way to create a service for a deployment.",
|
|
"command": "kubectl expose deployment redis --name=redis-svc --port=6379 --target-port=6379"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl create deployment redis --image=redis:alpine"
|
|
}
|
|
],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Service 'redis-svc' exists",
|
|
"command": "kubectl get svc redis-svc -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "redis-svc",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Service targets port 6379",
|
|
"command": "kubectl get svc redis-svc -o jsonpath='{.spec.ports[0].port}'",
|
|
"expected_output": "6379",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Service selector matches deployment",
|
|
"command": "kubectl get svc redis-svc -o jsonpath='{.spec.selector.app}'",
|
|
"expected_output": "redis",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete deployment redis --ignore-not-found"
|
|
},
|
|
{
|
|
"command": "kubectl delete svc redis-svc --ignore-not-found"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "pod-labeling-basics",
|
|
"title": "Labelling Resources",
|
|
"category": "Core Concepts",
|
|
"difficulty": "Easy",
|
|
"type": "task",
|
|
"weight": 3,
|
|
"description": "## Labelling Resources\n\nLabels are key-value pairs attached to objects, used to organize and select subsets of objects. A pod named `app-worker` is currently running, but it's missing a required label.\n\n**Your task:**\n\nAdd the label `tier=frontend` to the existing pod `app-worker`.\n\n```bash\n# Verify labels\nkubectl get pod app-worker --show-labels\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Use kubectl label",
|
|
"body": "You can label resources imperatively without editing the YAML.",
|
|
"command": "kubectl label pod app-worker tier=frontend"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl run app-worker --image=nginx:alpine"
|
|
}
|
|
],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Pod has label tier=frontend",
|
|
"command": "kubectl get pod app-worker -o jsonpath='{.metadata.labels.tier}'",
|
|
"expected_output": "frontend",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod app-worker --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "extract-logs-basics",
|
|
"title": "Extract Container Logs",
|
|
"category": "Troubleshooting",
|
|
"difficulty": "Easy",
|
|
"type": "task",
|
|
"weight": 4,
|
|
"description": "## Extract Pod Logs\n\nA pod named `log-generator` is running and emitting log lines every second.\n\n**Your task:**\n\nUse `kubectl logs` to inspect the logs and confirm that the message `ERROR: Database connection failed` appears in the output.\n\n> 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 <source> <pod>:<dest>`",
|
|
"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 eyJzdGF0dXMiOiAib2sifQo= | base64 -d > /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"
|
|
},
|
|
{
|
|
"command": "rm -f /tmp/config.json"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"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 <<EOF | kubectl apply -f -\napiVersion: networking.k8s.io/v1\nkind: NetworkPolicy\nmetadata:\n name: default-deny-all\n namespace: default\nspec:\n podSelector: {}\n policyTypes:\n - Ingress\n - Egress\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "NetworkPolicy default-deny-all exists",
|
|
"command": "kubectl get networkpolicy default-deny-all -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "default-deny-all",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Policy applies to all pods (empty podSelector)",
|
|
"command": "kubectl get networkpolicy default-deny-all -o jsonpath='{.spec.podSelector.matchLabels}'",
|
|
"expected_output": "",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete networkpolicy default-deny-all --ignore-not-found"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-pod-security-context",
|
|
"title": "Pod Security Context",
|
|
"category": "Workload Security",
|
|
"difficulty": "Medium",
|
|
"type": "task",
|
|
"weight": 5,
|
|
"description": "## Restricting Container Privileges\n\nContainers run as root by default, which is a major security risk.\n\n**Your task:**\n\nCreate a Pod named `secure-pod` using the `busybox:1.36` image (command: `sleep 3600`). Configure its security context so that it runs as user ID `1000`, runs as group ID `3000`, and sets `allowPrivilegeEscalation: false` at the container level.\n\n```bash\n# Check the security context of your pod\nkubectl get pod secure-pod -o yaml\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Security Context Fields",
|
|
"body": "You need `securityContext` at the Pod level for `runAsUser` and `runAsGroup`, and at the Container level for `allowPrivilegeEscalation`.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: secure-pod\nspec:\n securityContext:\n runAsUser: 1000\n runAsGroup: 3000\n containers:\n - name: app\n image: busybox:1.36\n command: [\"sleep\", \"3600\"]\n securityContext:\n allowPrivilegeEscalation: false\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Pod secure-pod runs as user 1000",
|
|
"command": "kubectl get pod secure-pod -o jsonpath='{.spec.securityContext.runAsUser}'",
|
|
"expected_output": "1000",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "allowPrivilegeEscalation is false",
|
|
"command": "kubectl get pod secure-pod -o jsonpath='{.spec.containers[0].securityContext.allowPrivilegeEscalation}'",
|
|
"expected_output": "false",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod secure-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-rbac-least-privilege",
|
|
"title": "RBAC Least Privilege",
|
|
"category": "Cluster Security",
|
|
"difficulty": "Medium",
|
|
"type": "task",
|
|
"weight": 4,
|
|
"description": "## Role-Based Access Control\n\nService Accounts should only be granted the permissions they explicitly need.\n\n**Your task:**\n\nCreate a Role named `pod-reader` in the `default` namespace that only allows the verbs `get`, `list`, and `watch` on the `pods` resource. Then, bind this Role to a ServiceAccount named `read-only-sa` using a RoleBinding named `read-only-binding`.\n\n```bash\n# Verify your RBAC setup:\nkubectl auth can-i list pods --as=system:serviceaccount:default:read-only-sa\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Imperative commands",
|
|
"body": "You can create all 3 resources using imperative commands: create serviceaccount, create role, create rolebinding.",
|
|
"command": "kubectl create sa read-only-sa && kubectl create role pod-reader --verb=get,list,watch --resource=pods && kubectl create rolebinding read-only-binding --role=pod-reader --serviceaccount=default:read-only-sa"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "ServiceAccount read-only-sa exists",
|
|
"command": "kubectl get sa read-only-sa -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "read-only-sa",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Role allows list pods",
|
|
"command": "kubectl auth can-i list pods --as=system:serviceaccount:default:read-only-sa",
|
|
"expected_output": "yes",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Role denies delete pods",
|
|
"command": "kubectl auth can-i delete pods --as=system:serviceaccount:default:read-only-sa",
|
|
"expected_output": "no",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete sa read-only-sa --ignore-not-found"
|
|
},
|
|
{
|
|
"command": "kubectl delete role pod-reader --ignore-not-found"
|
|
},
|
|
{
|
|
"command": "kubectl delete rolebinding read-only-binding --ignore-not-found"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-seccomp-profile",
|
|
"title": "Seccomp Profiles",
|
|
"category": "Workload Security",
|
|
"difficulty": "Hard",
|
|
"type": "task",
|
|
"weight": 6,
|
|
"description": "## Secure Computing Mode (Seccomp)\n\nSeccomp restricts the system calls that a container can make to the host kernel.\n\n**Your task:**\n\nCreate a Pod named `seccomp-pod` using the `busybox:1.36` image (command: `sleep 3600`). Configure the pod so its seccomp profile type is set to `RuntimeDefault`.\n\n```bash\n# Tip: Review PodSecurityContext documentation\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Set seccompProfile",
|
|
"body": "Add `seccompProfile: { type: RuntimeDefault }` to the pod's securityContext.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: seccomp-pod\nspec:\n securityContext:\n seccompProfile:\n type: RuntimeDefault\n containers:\n - name: main\n image: busybox:1.36\n command: [\"sleep\", \"3600\"]\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "seccompProfile type is RuntimeDefault",
|
|
"command": "kubectl get pod seccomp-pod -o jsonpath='{.spec.securityContext.seccompProfile.type}'",
|
|
"expected_output": "RuntimeDefault",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod seccomp-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-immutable-secret",
|
|
"title": "Immutable Secrets",
|
|
"category": "Cluster Security",
|
|
"difficulty": "Easy",
|
|
"type": "task",
|
|
"weight": 3,
|
|
"description": "## Immutable Resources\n\nMarking a Secret or ConfigMap as immutable protects it from accidental or malicious modifications, and also improves the performance of the kube-apiserver by significantly decreasing load.\n\n**Your task:**\n\nCreate a Secret named `db-creds` with the key `password` and value `super-secret`. Make this secret **immutable**.\n\n```bash\n# Verify immutability:\nkubectl get secret db-creds -o yaml\n```",
|
|
"hints": [
|
|
{
|
|
"title": "immutable: true",
|
|
"body": "Set `immutable: true` at the root level of the Secret manifest.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Secret\nmetadata:\n name: db-creds\nimmutable: true\nstringData:\n password: super-secret\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Secret db-creds exists and is immutable",
|
|
"command": "kubectl get secret db-creds -o jsonpath='{.immutable}'",
|
|
"expected_output": "true",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete secret db-creds --ignore-not-found"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-apparmor-profile",
|
|
"title": "AppArmor Profiles",
|
|
"category": "Workload Security",
|
|
"difficulty": "Hard",
|
|
"type": "task",
|
|
"weight": 5,
|
|
"description": "## AppArmor Profiles\n\nAppArmor is a Linux kernel security module that restricts programs' capabilities. In Kubernetes 1.30+, it is supported natively in the securityContext.\n\n**Your task:**\n\nCreate a Pod named `apparmor-pod` using the `nginx:alpine` image. Configure its Pod-level `securityContext` to use the `RuntimeDefault` AppArmor profile.\n\n```bash\n# Verify the profile is set:\nkubectl get pod apparmor-pod -o yaml | grep appArmor\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Set AppArmor Profile",
|
|
"body": "Add `appArmorProfile: { type: RuntimeDefault }` under `spec.securityContext`.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: apparmor-pod\nspec:\n securityContext:\n appArmorProfile:\n type: RuntimeDefault\n containers:\n - name: app\n image: nginx:alpine\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "appArmorProfile type is RuntimeDefault",
|
|
"command": "kubectl get pod apparmor-pod -o jsonpath='{.spec.securityContext.appArmorProfile.type}'",
|
|
"expected_output": "RuntimeDefault",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod apparmor-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-automount-token",
|
|
"title": "Disable ServiceAccount Token",
|
|
"category": "Workload Security",
|
|
"difficulty": "Easy",
|
|
"type": "task",
|
|
"weight": 3,
|
|
"description": "## Service Account Tokens\n\nBy default, Kubernetes automatically mounts a ServiceAccount API token into every Pod, which can be a significant security risk if the Pod is compromised.\n\n**Your task:**\n\nCreate a Pod named `no-token-pod` using the `alpine` image (with command `sleep 3600`). Explicitly disable the automatic mounting of the ServiceAccount token for this pod.\n\n```bash\n# Verify token is not mounted:\nkubectl get pod no-token-pod -o yaml | grep automount\n```",
|
|
"hints": [
|
|
{
|
|
"title": "automountServiceAccountToken",
|
|
"body": "Set `automountServiceAccountToken: false` in the pod's spec.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: no-token-pod\nspec:\n automountServiceAccountToken: false\n containers:\n - name: app\n image: alpine\n command: [\"sleep\", \"3600\"]\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "automountServiceAccountToken is false",
|
|
"command": "kubectl get pod no-token-pod -o jsonpath='{.spec.automountServiceAccountToken}'",
|
|
"expected_output": "false",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod no-token-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-network-policy-metadata",
|
|
"title": "Block Cloud Metadata",
|
|
"category": "Network Security",
|
|
"difficulty": "Hard",
|
|
"type": "task",
|
|
"weight": 6,
|
|
"description": "## Restricting Cloud Metadata Access\n\nCloud providers expose sensitive instance metadata at `169.254.169.254`. An attacker exploiting an SSRF vulnerability can use this to steal cloud credentials.\n\n**Your task:**\n\nCreate a NetworkPolicy named `deny-metadata` in the `default` namespace that applies to all pods. It should explicitly **deny** all Egress traffic to the IP block `169.254.169.254/32`, while **allowing** all other Egress traffic.\n\n```bash\n# Tip: Use an ipBlock exception (except).\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Except IPBlock",
|
|
"body": "Create an Egress rule that allows all traffic (`0.0.0.0/0`) EXCEPT `169.254.169.254/32`.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: networking.k8s.io/v1\nkind: NetworkPolicy\nmetadata:\n name: deny-metadata\n namespace: default\nspec:\n podSelector: {}\n policyTypes:\n - Egress\n egress:\n - to:\n - ipBlock:\n cidr: 0.0.0.0/0\n except:\n - 169.254.169.254/32\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "NetworkPolicy deny-metadata exists",
|
|
"command": "kubectl get networkpolicy deny-metadata -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "deny-metadata",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Policy applies to all pods",
|
|
"command": "kubectl get networkpolicy deny-metadata -o jsonpath='{.spec.podSelector.matchLabels}'",
|
|
"expected_output": "",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Policy blocks metadata server",
|
|
"command": "kubectl get networkpolicy deny-metadata -o jsonpath='{.spec.egress[0].to[0].ipBlock.except[0]}' 2>/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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: readonly-pod\nspec:\n containers:\n - name: app\n image: busybox:1.36\n command: [\"sleep\", \"3600\"]\n securityContext:\n readOnlyRootFilesystem: true\n volumeMounts:\n - name: tmp-vol\n mountPath: /tmp\n volumes:\n - name: tmp-vol\n emptyDir: {}\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Pod 'readonly-pod' exists",
|
|
"command": "kubectl get pod readonly-pod -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "readonly-pod",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "readOnlyRootFilesystem is true",
|
|
"command": "kubectl get pod readonly-pod -o jsonpath='{.spec.containers[0].securityContext.readOnlyRootFilesystem}'",
|
|
"expected_output": "true",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'readonly-pod' is Running",
|
|
"command": "kubectl get pod readonly-pod -o jsonpath='{.status.phase}'",
|
|
"expected_output": "Running",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod readonly-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-drop-capabilities",
|
|
"title": "Drop Linux Capabilities",
|
|
"category": "Workload Security",
|
|
"difficulty": "Medium",
|
|
"type": "task",
|
|
"weight": 5,
|
|
"description": "## Drop Linux Capabilities\n\nBy default, containers are granted a set of Linux capabilities. Dropping ALL capabilities and adding back only what is explicitly needed follows the principle of least privilege and is a CKS exam requirement.\n\n**Your task:**\n\nCreate a Pod named `nocaps-pod` using `nginx:alpine` with the following capability configuration in the container `securityContext`:\n- Drop **ALL** capabilities\n- Add back only **`NET_BIND_SERVICE`** (required by nginx to bind to port 80)\n\n```bash\n# Verify:\nkubectl get pod nocaps-pod -o jsonpath='{.spec.containers[0].securityContext.capabilities}'\n```",
|
|
"hints": [
|
|
{
|
|
"title": "capabilities drop and add",
|
|
"body": "Under `spec.containers[].securityContext.capabilities`, use `drop: [\"ALL\"]` and `add: [\"NET_BIND_SERVICE\"]`.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: nocaps-pod\nspec:\n containers:\n - name: app\n image: nginx:alpine\n securityContext:\n capabilities:\n drop:\n - ALL\n add:\n - NET_BIND_SERVICE\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Pod 'nocaps-pod' exists",
|
|
"command": "kubectl get pod nocaps-pod -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "nocaps-pod",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "ALL capabilities are dropped",
|
|
"command": "kubectl get pod nocaps-pod -o jsonpath='{.spec.containers[0].securityContext.capabilities.drop[0]}'",
|
|
"expected_output": "ALL",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "NET_BIND_SERVICE is added back",
|
|
"command": "kubectl get pod nocaps-pod -o jsonpath='{.spec.containers[0].securityContext.capabilities.add[0]}'",
|
|
"expected_output": "NET_BIND_SERVICE",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod nocaps-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-tls-ingress",
|
|
"title": "Secure Ingress with TLS",
|
|
"category": "Network Security",
|
|
"difficulty": "Hard",
|
|
"type": "task",
|
|
"weight": 7,
|
|
"description": "## Secure Ingress with TLS\n\nExposing applications over HTTPS requires a TLS certificate stored as a Kubernetes Secret of type `kubernetes.io/tls`. The Ingress controller uses this secret to terminate TLS connections.\n\n**Your task:**\n\n1. A self-signed TLS certificate has been pre-generated at `/tmp/tls.crt` and `/tmp/tls.key`.\n2. A deployment `tls-app` and service `tls-svc` are already running.\n3. Create a **TLS Secret** named `app-tls` from the cert files.\n4. Create an **Ingress** named `tls-ingress` that:\n - Routes traffic for host `secure.lab.local` to service `tls-svc` on port `80`\n - Terminates TLS using the secret `app-tls`",
|
|
"hints": [
|
|
{
|
|
"title": "Create the TLS Secret",
|
|
"body": "Use `kubectl create secret tls` with `--cert` and `--key` flags pointing to the pre-generated files.",
|
|
"command": "kubectl create secret tls app-tls --cert=/tmp/tls.crt --key=/tmp/tls.key"
|
|
},
|
|
{
|
|
"title": "Create the TLS Ingress",
|
|
"body": "Add a `spec.tls[]` block referencing the secret name alongside `spec.rules[]`.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: tls-ingress\nspec:\n tls:\n - hosts:\n - secure.lab.local\n secretName: app-tls\n rules:\n - host: secure.lab.local\n http:\n paths:\n - path: /\n pathType: Prefix\n backend:\n service:\n name: tls-svc\n port:\n number: 80\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/tls.key -out /tmp/tls.crt -subj '/CN=secure.lab.local' 2>/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: `[email protected]`\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 [email protected]"
|
|
},
|
|
{
|
|
"title": "Reference imagePullSecrets in a Pod",
|
|
"body": "Add `spec.imagePullSecrets[].name` to the pod spec with the name of your secret.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: private-pod\nspec:\n imagePullSecrets:\n - name: registry-creds\n containers:\n - name: app\n image: nginx:alpine\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Secret 'registry-creds' has docker-registry type",
|
|
"command": "kubectl get secret registry-creds -o jsonpath='{.type}'",
|
|
"expected_output": "kubernetes.io/dockerconfigjson",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'private-pod' references registry-creds as imagePullSecret",
|
|
"command": "kubectl get pod private-pod -o jsonpath='{.spec.imagePullSecrets[0].name}'",
|
|
"expected_output": "registry-creds",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod private-pod --ignore-not-found --grace-period=0 --force"
|
|
},
|
|
{
|
|
"command": "kubectl delete secret registry-creds --ignore-not-found"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-mcq-sandboxing",
|
|
"title": "Container Runtime Sandboxing",
|
|
"category": "System Hardening",
|
|
"difficulty": "Hard",
|
|
"type": "mcq",
|
|
"weight": 3,
|
|
"description": "## Container Runtime Sandboxing\n\nA security team wants to run untrusted workloads with stronger isolation than the standard `runc` runtime — specifically, they want each container's system calls to be intercepted by a user-space kernel rather than reaching the host kernel directly.\n\nWhich solution achieves this, and how is it configured in Kubernetes?",
|
|
"options": [
|
|
{
|
|
"id": "a",
|
|
"text": "Seccomp with `RuntimeDefault` — restricts which syscalls are allowed via a BPF filter, but syscalls still reach the host kernel"
|
|
},
|
|
{
|
|
"id": "b",
|
|
"text": "AppArmor with a `deny` profile — enforces path-based MAC policies but does not intercept syscalls"
|
|
},
|
|
{
|
|
"id": "c",
|
|
"text": "gVisor (`runsc`) via a `RuntimeClass` — intercepts all syscalls in user-space with its own kernel implementation, isolating the host kernel from the container"
|
|
},
|
|
{
|
|
"id": "d",
|
|
"text": "A `PodDisruptionBudget` — prevents pod disruptions and provides workload isolation"
|
|
}
|
|
],
|
|
"correct_option": "c",
|
|
"explanation": "**gVisor** (`runsc`) is a user-space kernel from Google that intercepts all container system calls before they reach the host kernel, providing strong isolation. In Kubernetes it is configured via a **RuntimeClass** resource and referenced in a pod with `spec.runtimeClassName`. Kata Containers is a similar alternative using lightweight VMs. Seccomp filters syscalls but they still hit the host kernel. AppArmor enforces file-access policies, not syscall interception.",
|
|
"hints": [
|
|
{
|
|
"title": "RuntimeClass and runtimeClassName",
|
|
"body": "Create a `RuntimeClass` that maps a name to the container runtime handler (e.g., `handler: runsc`). Reference it in a pod with `spec.runtimeClassName: <name>`.",
|
|
"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 <<EOF | kubectl apply -f -\napiVersion: node.k8s.io/v1\nkind: RuntimeClass\nmetadata:\n name: gvisor\nhandler: runsc\nEOF"
|
|
},
|
|
{
|
|
"title": "Reference the RuntimeClass in a Pod",
|
|
"body": "Set `spec.runtimeClassName: gvisor` in the pod spec.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: sandbox-pod\nspec:\n runtimeClassName: gvisor\n containers:\n - name: app\n image: nginx:alpine\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "RuntimeClass 'gvisor' exists with handler runsc",
|
|
"command": "kubectl get runtimeclass gvisor -o jsonpath='{.handler}'",
|
|
"expected_output": "runsc",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'sandbox-pod' references RuntimeClass gvisor",
|
|
"command": "kubectl get pod sandbox-pod -o jsonpath='{.spec.runtimeClassName}'",
|
|
"expected_output": "gvisor",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod sandbox-pod --ignore-not-found --grace-period=0 --force"
|
|
},
|
|
{
|
|
"command": "kubectl delete runtimeclass gvisor --ignore-not-found"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-audit-policy",
|
|
"title": "Write a Kubernetes Audit Policy",
|
|
"category": "Cluster Setup",
|
|
"difficulty": "Hard",
|
|
"type": "task",
|
|
"weight": 7,
|
|
"description": "## Kubernetes Audit Policy\n\nAudit logging lets you track who did what on the cluster. The kube-apiserver reads an **audit policy file** to decide which events to log and at which verbosity level.\n\n**Your task:**\n\nWrite an audit policy file at `/etc/kubernetes/audit-policy.yaml` with the following rules (in order):\n\n1. Log all operations on `secrets` in any namespace at level **`RequestResponse`**.\n2. Log read operations (`get`, `list`, `watch`) on `pods` at level **`Metadata`**.\n3. **Drop** (level `None`) all events for the `system:masters` group.\n4. Log everything else at level **`Metadata`** as a catch-all.\n\n```bash\n# Verify the policy file exists and contains the key fields:\ncat /etc/kubernetes/audit-policy.yaml\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Audit policy structure",
|
|
"body": "An audit policy is a YAML file with `apiVersion: audit.k8s.io/v1`, `kind: Policy`, and a `rules:` list. Order matters — first matching rule wins.",
|
|
"command": "cat <<EOF > /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 <<EOF | kubectl apply -f -\napiVersion: networking.k8s.io/v1\nkind: NetworkPolicy\nmetadata:\n name: allow-backend-only\n namespace: frontend\nspec:\n podSelector: {}\n policyTypes:\n - Egress\n egress:\n - to:\n - namespaceSelector:\n matchLabels:\n kubernetes.io/metadata.name: backend\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl create namespace frontend --dry-run=client -o yaml | kubectl apply -f -"
|
|
},
|
|
{
|
|
"command": "kubectl create namespace backend --dry-run=client -o yaml | kubectl apply -f -"
|
|
}
|
|
],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "NetworkPolicy 'allow-backend-only' exists in frontend namespace",
|
|
"command": "kubectl get networkpolicy allow-backend-only -n frontend -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "allow-backend-only",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Policy type is Egress only",
|
|
"command": "kubectl get networkpolicy allow-backend-only -n frontend -o jsonpath='{.spec.policyTypes[0]}'",
|
|
"expected_output": "Egress",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Policy uses namespaceSelector targeting backend namespace",
|
|
"command": "kubectl get networkpolicy allow-backend-only -n frontend -o jsonpath='{.spec.egress[0].to[0].namespaceSelector.matchLabels.kubernetes\\.io/metadata\\.name}'",
|
|
"expected_output": "backend",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "frontend",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete networkpolicy allow-backend-only -n frontend --ignore-not-found"
|
|
},
|
|
{
|
|
"command": "kubectl delete namespace frontend --ignore-not-found --wait=false"
|
|
},
|
|
{
|
|
"command": "kubectl delete namespace backend --ignore-not-found --wait=false"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "cks-non-root-enforce",
|
|
"title": "Enforce Non-Root Container",
|
|
"category": "Workload Security",
|
|
"difficulty": "Medium",
|
|
"type": "task",
|
|
"weight": 5,
|
|
"description": "## Enforce Non-Root Execution\n\nRunning containers as root is one of the most common security misconfigurations. Kubernetes provides `runAsNonRoot` and `runAsUser` to enforce this at the pod level.\n\n**Your task:**\n\nCreate a Pod named `nonroot-pod` in the `default` namespace using the `busybox:1.36` image (command: `sleep 3600`) with the following security configuration:\n\n- Pod-level `securityContext`:\n - `runAsNonRoot: true`\n - `runAsUser: 10001`\n - `runAsGroup: 10001`\n- Container-level `securityContext`:\n - `allowPrivilegeEscalation: false`\n - `readOnlyRootFilesystem: true`\n- An `emptyDir` volume mounted at `/tmp` so the process has writable scratch space.\n\n```bash\n# Verify:\nkubectl get pod nonroot-pod -o jsonpath='{.spec.securityContext}'\n```",
|
|
"hints": [
|
|
{
|
|
"title": "Combine Pod and Container securityContext",
|
|
"body": "Put runAsNonRoot, runAsUser, and runAsGroup in the pod-level securityContext. Put allowPrivilegeEscalation and readOnlyRootFilesystem in the container-level securityContext.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: nonroot-pod\nspec:\n securityContext:\n runAsNonRoot: true\n runAsUser: 10001\n runAsGroup: 10001\n containers:\n - name: app\n image: busybox:1.36\n command: [\"sleep\", \"3600\"]\n securityContext:\n allowPrivilegeEscalation: false\n readOnlyRootFilesystem: true\n volumeMounts:\n - name: tmp-dir\n mountPath: /tmp\n volumes:\n - name: tmp-dir\n emptyDir: {}\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "Pod 'nonroot-pod' exists",
|
|
"command": "kubectl get pod nonroot-pod -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "nonroot-pod",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "runAsNonRoot is true",
|
|
"command": "kubectl get pod nonroot-pod -o jsonpath='{.spec.securityContext.runAsNonRoot}'",
|
|
"expected_output": "true",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "runAsUser is 10001",
|
|
"command": "kubectl get pod nonroot-pod -o jsonpath='{.spec.securityContext.runAsUser}'",
|
|
"expected_output": "10001",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "allowPrivilegeEscalation is false",
|
|
"command": "kubectl get pod nonroot-pod -o jsonpath='{.spec.containers[0].securityContext.allowPrivilegeEscalation}'",
|
|
"expected_output": "false",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "readOnlyRootFilesystem is true",
|
|
"command": "kubectl get pod nonroot-pod -o jsonpath='{.spec.containers[0].securityContext.readOnlyRootFilesystem}'",
|
|
"expected_output": "true",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod nonroot-pod --ignore-not-found --grace-period=0 --force"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "annotate-resource-basics",
|
|
"title": "Annotating Kubernetes Resources",
|
|
"category": "Core Concepts",
|
|
"difficulty": "Easy",
|
|
"type": "task",
|
|
"weight": 3,
|
|
"description": "## Annotating Kubernetes Resources\n\nAnnotations attach arbitrary non-identifying metadata to Kubernetes objects — such as contact info, tool versions, or documentation URLs. Unlike labels, annotations **cannot** be used as selectors.\n\n**Your task:**\n\nA Deployment named `myapp` is running in the `default` namespace. Add the following annotations to it:\n- `[email protected]`\n- `reviewed=true`\n\n```bash\n# Verify:\nkubectl get deployment myapp -o jsonpath='{.metadata.annotations}'\n```",
|
|
"hints": [
|
|
{
|
|
"title": "kubectl annotate",
|
|
"body": "Use `kubectl annotate` to imperatively add or overwrite annotations on any resource.",
|
|
"command": "kubectl annotate deployment myapp [email protected] reviewed=true"
|
|
},
|
|
{
|
|
"title": "Overwrite an existing annotation",
|
|
"body": "If the annotation key already exists, pass `--overwrite` to update it.",
|
|
"command": "kubectl annotate deployment myapp [email protected] reviewed=true --overwrite"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl create deployment myapp --image=nginx:alpine --replicas=1 2>/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 [email protected]",
|
|
"command": "kubectl get deployment myapp -o jsonpath='{.metadata.annotations.contact}'",
|
|
"expected_output": "[email protected]",
|
|
"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 <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: init-demo\nspec:\n initContainers:\n - name: setup\n image: busybox:1.36\n command: [\"sh\", \"-c\", \"echo ready > /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 <resource> -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 <<EOF | kubectl apply -f -\napiVersion: policy/v1\nkind: PodDisruptionBudget\nmetadata:\n name: web-pdb\nspec:\n minAvailable: 2\n selector:\n matchLabels:\n app: web-app\nEOF"
|
|
},
|
|
{
|
|
"title": "Check the PDB status",
|
|
"body": "After creating the PDB, check `kubectl get pdb web-pdb` — the ALLOWED DISRUPTIONS column shows how many pods can be disrupted at once.",
|
|
"command": "kubectl get pdb web-pdb -o wide"
|
|
}
|
|
],
|
|
"setup_commands": [
|
|
{
|
|
"command": "kubectl create deployment web-app --image=nginx:alpine --replicas=3 2>/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 <<EOF | kubectl apply -f -\napiVersion: scheduling.k8s.io/v1\nkind: PriorityClass\nmetadata:\n name: high-priority\nvalue: 1000000\ndescription: \"High priority workloads\"\nEOF"
|
|
},
|
|
{
|
|
"title": "Reference PriorityClass in a Pod",
|
|
"body": "Set `spec.priorityClassName` in the pod spec to the name of your PriorityClass.",
|
|
"command": "cat <<EOF | kubectl apply -f -\napiVersion: v1\nkind: Pod\nmetadata:\n name: critical-pod\nspec:\n priorityClassName: high-priority\n containers:\n - name: app\n image: nginx:alpine\nEOF"
|
|
}
|
|
],
|
|
"setup_commands": [],
|
|
"validation": {
|
|
"commands": [
|
|
{
|
|
"description": "PriorityClass 'high-priority' exists",
|
|
"command": "kubectl get priorityclass high-priority -o jsonpath='{.metadata.name}'",
|
|
"expected_output": "high-priority",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "PriorityClass value is 1000000",
|
|
"command": "kubectl get priorityclass high-priority -o jsonpath='{.value}'",
|
|
"expected_output": "1000000",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'critical-pod' uses high-priority PriorityClass",
|
|
"command": "kubectl get pod critical-pod -o jsonpath='{.spec.priorityClassName}'",
|
|
"expected_output": "high-priority",
|
|
"match": "exact"
|
|
},
|
|
{
|
|
"description": "Pod 'critical-pod' is Running",
|
|
"command": "kubectl get pod critical-pod -o jsonpath='{.status.phase}'",
|
|
"expected_output": "Running",
|
|
"match": "exact"
|
|
}
|
|
]
|
|
},
|
|
"default_namespace": "default",
|
|
"teardown_commands": [
|
|
{
|
|
"command": "kubectl delete pod critical-pod --ignore-not-found --grace-period=0 --force"
|
|
},
|
|
{
|
|
"command": "kubectl delete priorityclass high-priority --ignore-not-found"
|
|
}
|
|
]
|
|
}
|
|
] |