Contents

Chapter 1

Chapter 1: Cluster Architecture, Installation & Configuration

Exam weight: 25% — The largest single domain. You must be able to install a cluster with kubeadm, manage control plane components, perform upgrades, and understand the architecture.


Task 1: Initialize a Cluster with kubeadm

Objective: Bootstrap a new Kubernetes cluster from scratch using kubeadm.

Task Statement

You have a fresh Ubuntu 22.04 VM with container runtime and kubeadm already installed. Initialize a Kubernetes cluster on this node with the following requirements:

  • Pod network CIDR: 10.244.0.0/16
  • Service CIDR: 10.96.0.0/12
  • Kubernetes version: v1.30.0

After initialization, configure kubectl for the current user and install a CNI plugin.

Solution

bash
# Step 1: Initialize the control plane
sudo kubeadm init \
  --pod-network-cidr=10.244.0.0/16 \
  --service-cidr=10.96.0.0/12 \
  --kubernetes-version=v1.30.0

# Step 2: Configure kubectl for the current user
# (kubeadm prints these instructions after init)
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

# Step 3: Install a CNI plugin (Calico in this example)
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml

# Alternative: Flannel CNI
# kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

Verification

bash
# All nodes should be Ready (may take 1-2 minutes after CNI install)
kubectl get nodes
# NAME         STATUS   ROLES           AGE   VERSION
# control-01   Ready    control-plane   5m    v1.30.0

# Control plane pods should all be Running
kubectl get pods -n kube-system
# coredns pods, calico/flannel pods, etcd, api-server, scheduler, controller-manager

# Cluster info
kubectl cluster-info

Key Takeaways

  • kubeadm init generates a join token that expires in 24 hours. Save it or regenerate with kubeadm token create --print-join-command.
  • The --pod-network-cidr must match your CNI plugin's expected range (Flannel defaults to 10.244.0.0/16).
  • Without a CNI plugin, nodes remain NotReady and CoreDNS pods stay Pending.

Task 2: Join a Worker Node to the Cluster

Objective: Add a worker node to an existing cluster.

Task Statement

A second VM is available at IP 192.168.1.11. Join it to the cluster initialized in Task 1. After joining, verify the node is Ready and can schedule pods.

Solution

bash
# On the CONTROL PLANE node — get the join command
kubeadm token create --print-join-command
# Output example:
# kubeadm join 192.168.1.10:6443 --token abcdef.0123456789abcdef \
#   --discovery-token-ca-cert-hash sha256:1234567890abcdef...

# On the WORKER NODE — run the join command (with sudo)
sudo kubeadm join 192.168.1.10:6443 \
  --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

# If the token has expired, create a new one on the control plane:
kubeadm token create --print-join-command

Verification

bash
# On the control plane
kubectl get nodes -o wide
# Should show two nodes, both Ready

# Verify the worker can run pods
kubectl run test-worker --image=nginx --restart=Never \
  --overrides='{"spec":{"nodeName":"worker-01"}}'
kubectl get pod test-worker -o wide
# Should be Running on worker-01

kubectl delete pod test-worker --force --grace-period=0

Key Takeaways

  • Tokens expire after 24 hours by default. You can create longer-lived tokens with --ttl 48h or non-expiring with --ttl 0 (not recommended for production).
  • The --discovery-token-ca-cert-hash ensures the worker connects to the legitimate API server (prevents man-in-the-middle attacks).
  • If you lose the hash: openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'

Task 3: Manage Control Plane Components

Objective: Understand and inspect the static pod manifests that run the control plane.

Task Statement

Without using kubectl, find the configuration for the kube-apiserver. Identify the following:

1. Which port the API server listens on

2. Whether audit logging is enabled

3. The etcd endpoint the API server connects to

Solution

bash
# Control plane components run as static pods managed by kubelet.
# Their manifests live in /etc/kubernetes/manifests/

ls /etc/kubernetes/manifests/
# etcd.yaml  kube-apiserver.yaml  kube-controller-manager.yaml  kube-scheduler.yaml

# Read the API server manifest
cat /etc/kubernetes/manifests/kube-apiserver.yaml

# Find the secure port (look for --secure-port)
grep -- '--secure-port' /etc/kubernetes/manifests/kube-apiserver.yaml
# --secure-port=6443

# Check for audit logging
grep -- '--audit' /etc/kubernetes/manifests/kube-apiserver.yaml
# If no output, audit logging is NOT enabled

# Find the etcd endpoint
grep -- '--etcd-servers' /etc/kubernetes/manifests/kube-apiserver.yaml
# --etcd-servers=https://127.0.0.1:2379

Verification

bash
# Verify the API server is listening on the expected port
sudo ss -tlnp | grep 6443
# Should show kube-apiserver listening

# Verify etcd is running
sudo ss -tlnp | grep 2379
# Should show etcd listening

# Alternative: check via kubectl
kubectl get pod kube-apiserver-control-01 -n kube-system -o yaml | \
  grep -A5 'containers:'

Key Takeaways

  • Static pod manifests in /etc/kubernetes/manifests/ are watched by kubelet. Any changes are applied automatically (kubelet restarts the pod).
  • If the API server won't start after editing its manifest, check journalctl -u kubelet and crictl ps -a for error details.
  • On the exam, you may need to modify these files directly — practice editing them and watching the pod restart.

Task 4: Upgrade a Cluster with kubeadm

Objective: Perform a minor version upgrade of the control plane and worker nodes.

Task Statement

Your cluster is running Kubernetes v1.29.0. Upgrade the control plane node to v1.30.0, then upgrade all worker nodes. The upgrade must be done with zero downtime for running workloads.

Solution

bash
# ====== CONTROL PLANE UPGRADE ======

# Step 1: Upgrade kubeadm on the control plane
sudo apt-mark unhold kubeadm
sudo apt-get update
sudo apt-get install -y kubeadm=1.30.0-*
sudo apt-mark hold kubeadm

# Step 2: Verify the upgrade plan
sudo kubeadm upgrade plan
# Shows available versions and what will be upgraded

# Step 3: Apply the upgrade
sudo kubeadm upgrade apply v1.30.0
# Wait for "SUCCESS! Your cluster was upgraded to v1.30.0"

# Step 4: Upgrade kubelet and kubectl on the control plane
sudo apt-mark unhold kubelet kubectl
sudo apt-get install -y kubelet=1.30.0-* kubectl=1.30.0-*
sudo apt-mark hold kubelet kubectl
sudo systemctl daemon-reload
sudo systemctl restart kubelet

# ====== WORKER NODE UPGRADE (repeat for each worker) ======

# Step 5: Drain the worker node (from control plane)
kubectl drain worker-01 --ignore-daemonsets --delete-emptydir-data

# Step 6: On the WORKER NODE — upgrade kubeadm, then kubelet
sudo apt-mark unhold kubeadm kubelet kubectl
sudo apt-get update
sudo apt-get install -y kubeadm=1.30.0-* kubelet=1.30.0-* kubectl=1.30.0-*
sudo apt-mark hold kubeadm kubelet kubectl

# Step 7: On the WORKER NODE — upgrade the node config
sudo kubeadm upgrade node

# Step 8: Restart kubelet on the worker
sudo systemctl daemon-reload
sudo systemctl restart kubelet

# Step 9: Uncordon the worker (from control plane)
kubectl uncordon worker-01

Verification

bash
# All nodes should show v1.30.0
kubectl get nodes
# NAME         STATUS   ROLES           AGE   VERSION
# control-01   Ready    control-plane   30d   v1.30.0
# worker-01    Ready    <none>          30d   v1.30.0

# Verify all system pods are running
kubectl get pods -n kube-system

# Verify workloads were rescheduled
kubectl get pods -A -o wide

Key Takeaways

  • Always upgrade control plane first, then workers.
  • Only upgrade one minor version at a time (v1.29 → v1.30, never v1.28 → v1.30).
  • kubectl drain evicts pods gracefully. Use --ignore-daemonsets because DaemonSet pods cannot be drained.
  • kubectl uncordon re-enables scheduling after the upgrade.
  • This is a high-value exam task. Practice it until it's second nature.

Task 5: Manage kubeconfig Files

Objective: Work with multiple kubeconfig files and contexts.

Task Statement

Create a new kubeconfig entry for a user named dev-user that uses a client certificate for authentication. Set up a context called dev-context that uses dev-user with the development namespace. Switch to this context.

Solution

bash
# Step 1: Create a context with an existing user and namespace
kubectl config set-credentials dev-user \
  --client-certificate=/path/to/dev-user.crt \
  --client-key=/path/to/dev-user.key

# Step 2: Set a context
kubectl config set-context dev-context \
  --cluster=kubernetes \
  --user=dev-user \
  --namespace=development

# Step 3: Switch to the new context
kubectl config use-context dev-context

# Step 4: Verify
kubectl config current-context
# dev-context

Verification

bash
# View all contexts
kubectl config get-contexts
# CURRENT   NAME          CLUSTER       AUTHINFO    NAMESPACE
# *         dev-context   kubernetes    dev-user    development

# View full kubeconfig
kubectl config view

# Switch back to the admin context
kubectl config use-context kubernetes-admin@kubernetes

Key Takeaways

  • The exam will ask you to switch contexts at the start of each task: kubectl config use-context .
  • kubeconfig files can be merged by setting KUBECONFIG=/path/to/config1:/path/to/config2.
  • The default kubeconfig location is ~/.kube/config.

Task 6: Manage Multiple Schedulers

Objective: Deploy a custom scheduler alongside the default scheduler.

Task Statement

Deploy a second scheduler named custom-scheduler in the kube-system namespace. Then create a pod that uses this custom scheduler.

Solution

bash
# Step 1: Copy the default scheduler manifest
sudo cp /etc/kubernetes/manifests/kube-scheduler.yaml /tmp/custom-scheduler.yaml

# Step 2: Edit the custom scheduler config
# Key changes:
# - Change the pod name to custom-scheduler
# - Add --scheduler-name=custom-scheduler
# - Change the --leader-elect to false (or use --leader-elect-resource-name to avoid conflicts)
# - Change the port to avoid conflicts (--secure-port=10260)

# Step 3: Apply as a regular pod (not static pod)
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: custom-scheduler
  namespace: kube-system
  labels:
    component: custom-scheduler
spec:
  containers:
  - name: kube-scheduler
    image: registry.k8s.io/kube-scheduler:v1.30.0
    command:
    - kube-scheduler
    - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
    - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
    - --kubeconfig=/etc/kubernetes/scheduler.conf
    - --scheduler-name=custom-scheduler
    - --leader-elect=false
    - --secure-port=10260
    volumeMounts:
    - name: kubeconfig
      mountPath: /etc/kubernetes/scheduler.conf
      readOnly: true
  volumes:
  - name: kubeconfig
    hostPath:
      path: /etc/kubernetes/scheduler.conf
      type: File
EOF

# Step 4: Create a pod that uses the custom scheduler
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: custom-scheduled-pod
spec:
  schedulerName: custom-scheduler
  containers:
  - name: nginx
    image: nginx
EOF

Verification

bash
# Check the custom scheduler is running
kubectl get pod custom-scheduler -n kube-system

# Check the pod was scheduled by the custom scheduler
kubectl get events --field-selector reason=Scheduled | grep custom-scheduled-pod
# Should show "Successfully assigned ... by custom-scheduler"

kubectl get pod custom-scheduled-pod -o wide
# Should be Running on a node

Key Takeaways

  • Multiple schedulers can coexist. Pods use spec.schedulerName to choose which scheduler handles them.
  • Set --leader-elect=false for the custom scheduler (or use a different --leader-elect-resource-name).
  • If schedulerName doesn't match any running scheduler, the pod stays Pending.

Task 7: Configure the API Server for Audit Logging

Objective: Enable audit logging on the API server to track who did what.

Task Statement

Enable audit logging on the kube-apiserver with the following requirements:

  • Log all requests at the Metadata level
  • Log RequestResponse level for pods and deployments
  • Write audit logs to /var/log/kubernetes/audit.log
  • Limit the log file to 100MB with 5 retained backups

Solution

bash
# Step 1: Create the audit policy file
sudo mkdir -p /etc/kubernetes/audit
sudo tee /etc/kubernetes/audit/policy.yaml <<'EOF'
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  # Log pod and deployment changes at RequestResponse level
  - level: RequestResponse
    resources:
    - group: ""
      resources: ["pods"]
    - group: "apps"
      resources: ["deployments"]

  # Log everything else at Metadata level
  - level: Metadata
    omitStages:
    - RequestReceived
EOF

# Step 2: Create the log directory
sudo mkdir -p /var/log/kubernetes

# Step 3: Edit the API server manifest to add audit flags
# Add these arguments to /etc/kubernetes/manifests/kube-apiserver.yaml:
#   - --audit-policy-file=/etc/kubernetes/audit/policy.yaml
#   - --audit-log-path=/var/log/kubernetes/audit.log
#   - --audit-log-maxsize=100
#   - --audit-log-maxbackup=5
#
# Add these volume mounts:
#   volumeMounts:
#   - name: audit-policy
#     mountPath: /etc/kubernetes/audit/policy.yaml
#     readOnly: true
#   - name: audit-log
#     mountPath: /var/log/kubernetes
#
#   volumes:
#   - name: audit-policy
#     hostPath:
#       path: /etc/kubernetes/audit/policy.yaml
#       type: File
#   - name: audit-log
#     hostPath:
#       path: /var/log/kubernetes
#       type: DirectoryOrCreate

Verification

bash
# Wait for the API server to restart (kubelet watches the manifest)
kubectl get pods -n kube-system | grep apiserver

# Create a test pod to generate an audit event
kubectl run audit-test --image=nginx --restart=Never

# Check the audit log
sudo tail -5 /var/log/kubernetes/audit.log | python3 -m json.tool

# Look for the pod creation event
sudo grep '"audit-test"' /var/log/kubernetes/audit.log | head -1 | python3 -m json.tool

kubectl delete pod audit-test --force --grace-period=0

Key Takeaways

  • Audit policy levels: None, Metadata, Request, RequestResponse (most verbose).
  • The API server static pod must have volume mounts for both the policy file and the log directory.
  • If the API server doesn't restart, check journalctl -u kubelet for errors.

Task 8: Understand and Inspect Cluster Certificates

Objective: View certificate details and expiration dates for cluster components.

Task Statement

List all certificates used by the Kubernetes cluster. Identify which certificates expire within the next 90 days and how to renew them.

Solution

bash
# Method 1: Use kubeadm to check all certificate expirations
sudo kubeadm certs check-expiration
# CERTIFICATE                EXPIRES                  RESIDUAL TIME
# admin.conf                 Jun 16, 2027 10:00 UTC   364d
# apiserver                  Jun 16, 2027 10:00 UTC   364d
# apiserver-etcd-client      Jun 16, 2027 10:00 UTC   364d
# ...

# Method 2: Manually inspect a specific certificate
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text -noout | \
  grep -A2 'Validity'
# Not Before: Jun 16 10:00:00 2026 GMT
# Not After : Jun 16 10:00:00 2027 GMT

# Check the Subject and SANs (Subject Alternative Names)
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text -noout | \
  grep -A1 'Subject:'
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text -noout | \
  grep -A5 'Subject Alternative Name'

# Renew all certificates
sudo kubeadm certs renew all
# Then restart control plane components:
# (kubelet watches static pod manifests and restarts them)

# Renew a specific certificate
sudo kubeadm certs renew apiserver

Verification

bash
# After renewal, verify new expiration dates
sudo kubeadm certs check-expiration

# Verify the API server is still functional
kubectl get nodes
kubectl get pods -n kube-system

Key Takeaways

  • Kubeadm-generated certificates expire after 1 year by default.
  • kubeadm certs renew all renews all certificates but you must restart the control plane pods (or the entire node).
  • The CA certificate has a 10-year validity by default — it does NOT get renewed by kubeadm certs renew.
  • On the exam, you may need to identify an expired certificate causing API server failures — check with openssl x509 -in -text -noout.
Chapter 2

Chapter 2: Workloads & Scheduling

Exam weight: 15% — You must create, update, and manage Deployments, DaemonSets, StatefulSets, Jobs, and CronJobs. You also need to understand pod scheduling with taints, tolerations, node affinity, and resource limits.


Task 1: Create and Scale a Deployment

Objective: Create a Deployment with specific replicas, update its image, and manage rollout history.

Task Statement

1. Create a Deployment named web-frontend in the production namespace with 4 replicas of nginx:1.24.

2. Update the image to nginx:1.25 and record the change.

3. Check the rollout status and history.

4. Roll back to the previous version.

Solution

bash
# Create the namespace
kubectl create namespace production

# Step 1: Create the deployment (imperative + dry-run for quick generation)
kubectl create deployment web-frontend \
  --image=nginx:1.24 \
  --replicas=4 \
  --namespace=production

# Step 2: Update the image with a recorded change
kubectl set image deployment/web-frontend nginx=nginx:1.25 \
  -n production

# Alternatively, edit the deployment directly:
# kubectl edit deployment web-frontend -n production

# Step 3: Check rollout status
kubectl rollout status deployment/web-frontend -n production
# deployment "web-frontend" successfully rolled out

# View rollout history
kubectl rollout history deployment/web-frontend -n production
# REVISION  CHANGE-CAUSE
# 1         <none>
# 2         <none>

# Step 4: Roll back
kubectl rollout undo deployment/web-frontend -n production

# Roll back to a specific revision:
# kubectl rollout undo deployment/web-frontend --to-revision=1 -n production

Verification

bash
# Check current image
kubectl get deployment web-frontend -n production -o jsonpath='{.spec.template.spec.containers[0].image}'
# nginx:1.24  (rolled back)

# Check all pods are running
kubectl get pods -n production -l app=web-frontend
# Should show 4 pods all Running

Key Takeaways

  • kubectl set image is the fastest way to update container images during the exam.
  • kubectl rollout undo goes back one revision by default. Use --to-revision=N for a specific one.
  • Deployment strategy defaults to RollingUpdate with maxUnavailable=25% and maxSurge=25%.

Task 2: Create a DaemonSet

Objective: Deploy a pod on every node in the cluster.

Task Statement

Create a DaemonSet named log-collector in the kube-system namespace that runs busybox:1.36 on every node. The container should run the command sh -c "tail -f /var/log/syslog" and mount the host's /var/log directory.

Solution

bash
# There's no "kubectl create daemonset" imperative command.
# Generate a deployment YAML and convert it:
kubectl create deployment log-collector --image=busybox:1.36 \
  --dry-run=client -o yaml > /tmp/ds.yaml

# Edit to convert Deployment to DaemonSet (or just apply directly):
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-collector
  namespace: kube-system
  labels:
    app: log-collector
spec:
  selector:
    matchLabels:
      app: log-collector
  template:
    metadata:
      labels:
        app: log-collector
    spec:
      containers:
      - name: log-collector
        image: busybox:1.36
        command: ["sh", "-c", "tail -f /var/log/syslog"]
        volumeMounts:
        - name: host-log
          mountPath: /var/log
          readOnly: true
      volumes:
      - name: host-log
        hostPath:
          path: /var/log
          type: Directory
      # Tolerate control-plane taint so it runs on ALL nodes
      tolerations:
      - key: node-role.kubernetes.io/control-plane
        operator: Exists
        effect: NoSchedule
EOF

Verification

bash
# One pod per node (including control plane if toleration is set)
kubectl get daemonset log-collector -n kube-system
# DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE
# 3         3         3       3             3

kubectl get pods -n kube-system -l app=log-collector -o wide
# Should show one pod per node

# Check logs from one pod
kubectl logs -n kube-system -l app=log-collector --tail=5

Key Takeaways

  • DaemonSets don't have replicas — they automatically run one pod per matching node.
  • Add tolerations for control-plane nodes if you want the DaemonSet to run everywhere.
  • Common exam DaemonSet use cases: logging agents, monitoring agents, network plugins.
  • No imperative kubectl create daemonset exists — know how to write the YAML.

Task 3: Create a StatefulSet

Objective: Deploy a stateful workload with persistent storage and ordered pod management.

Task Statement

Create a StatefulSet named database in the default namespace with 3 replicas of nginx:1.25. Each pod should have a PersistentVolumeClaim requesting 1Gi of storage. Use a headless Service named database-svc.

Solution

bash
# Step 1: Create the headless Service (required for StatefulSet)
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: database-svc
  labels:
    app: database
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None       # Headless service
  selector:
    app: database
EOF

# Step 2: Create the StatefulSet
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: database
spec:
  serviceName: "database-svc"
  replicas: 3
  selector:
    matchLabels:
      app: database
  template:
    metadata:
      labels:
        app: database
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi
EOF

Verification

bash
# Pods are created in order: database-0, database-1, database-2
kubectl get pods -l app=database -w
# database-0   1/1     Running   0          30s
# database-1   1/1     Running   0          20s
# database-2   1/1     Running   0          10s

# Each pod gets its own PVC
kubectl get pvc
# data-database-0   Bound    pv-xxx   1Gi   RWO
# data-database-1   Bound    pv-yyy   1Gi   RWO
# data-database-2   Bound    pv-zzz   1Gi   RWO

# DNS records for individual pods (via headless service)
# database-0.database-svc.default.svc.cluster.local
# database-1.database-svc.default.svc.cluster.local
# database-2.database-svc.default.svc.cluster.local

# Test DNS resolution from within the cluster
kubectl run dns-test --image=busybox:1.36 --restart=Never --rm -it -- \
  nslookup database-svc.default.svc.cluster.local

Key Takeaways

  • StatefulSet pods get stable, predictable names (name-0, name-1, ...) and stable network identities via headless Services.
  • Each pod gets its own PVC that persists even if the pod is deleted.
  • Pods are created in order (0, 1, 2) and terminated in reverse order (2, 1, 0).
  • StatefulSets require a serviceName field pointing to a headless Service.

Task 4: Use Taints and Tolerations

Objective: Control pod scheduling using node taints and pod tolerations.

Task Statement

1. Taint node worker-01 so that only pods with a specific toleration can be scheduled there.

2. Create a pod that tolerates the taint.

3. Create another pod without the toleration and verify it's NOT scheduled on worker-01.

Solution

bash
# Step 1: Taint the node
kubectl taint nodes worker-01 environment=dedicated:NoSchedule

# Step 2: Create a pod WITH the toleration
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: tolerant-pod
spec:
  containers:
  - name: nginx
    image: nginx
  tolerations:
  - key: "environment"
    operator: "Equal"
    value: "dedicated"
    effect: "NoSchedule"
EOF

# Step 3: Create a pod WITHOUT the toleration
kubectl run intolerant-pod --image=nginx --restart=Never

# Step 4: Check scheduling
kubectl get pods -o wide
# tolerant-pod can be on any node (including worker-01)
# intolerant-pod will NOT be on worker-01

Verification

bash
# Check the taint on the node
kubectl describe node worker-01 | grep Taints
# Taints: environment=dedicated:NoSchedule

# Check pod placements
kubectl get pods -o wide
# intolerant-pod should be on worker-02 or worker-03, NOT worker-01

# Clean up: remove the taint
kubectl taint nodes worker-01 environment=dedicated:NoSchedule-
# The trailing dash (-) removes the taint

Key Takeaways

  • Taints go on nodes; tolerations go on pods.
  • NoSchedule: New pods won't be scheduled. Existing pods stay.
  • NoExecute: New pods won't be scheduled AND existing pods without toleration are evicted.
  • PreferNoSchedule: Scheduler tries to avoid the node but will use it if necessary.
  • Use - suffix to remove a taint: kubectl taint nodes key=value:effect-

Task 5: Use Node Affinity

Objective: Schedule pods on nodes matching specific labels.

Task Statement

1. Label node worker-02 with disk=ssd.

2. Create a Deployment that uses requiredDuringSchedulingIgnoredDuringExecution node affinity to only run on nodes with disk=ssd.

3. Create another Deployment that uses preferredDuringSchedulingIgnoredDuringExecution to prefer SSD nodes.

Solution

bash
# Step 1: Label the node
kubectl label nodes worker-02 disk=ssd

# Step 2: Hard affinity (required)
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ssd-required
spec:
  replicas: 3
  selector:
    matchLabels:
      app: ssd-required
  template:
    metadata:
      labels:
        app: ssd-required
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: disk
                operator: In
                values:
                - ssd
      containers:
      - name: nginx
        image: nginx
EOF

# Step 3: Soft affinity (preferred)
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ssd-preferred
spec:
  replicas: 3
  selector:
    matchLabels:
      app: ssd-preferred
  template:
    metadata:
      labels:
        app: ssd-preferred
    spec:
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 80
            preference:
              matchExpressions:
              - key: disk
                operator: In
                values:
                - ssd
      containers:
      - name: nginx
        image: nginx
EOF

Verification

bash
# "required" pods should ALL be on worker-02 (or Pending if it can't fit)
kubectl get pods -l app=ssd-required -o wide

# "preferred" pods should MOSTLY be on worker-02 but can go elsewhere
kubectl get pods -l app=ssd-preferred -o wide

# Remove the label to see the effect on new scheduling
# kubectl label nodes worker-02 disk-

Key Takeaways

  • required = hard constraint (pod stays Pending if no matching node exists).
  • preferred = soft constraint (scheduler tries but can use other nodes).
  • Weight (1–100) determines how much the scheduler favors matching nodes.
  • operator: In (match any value in list), NotIn, Exists, DoesNotExist, Gt, Lt.
  • nodeSelector is the simpler alternative: spec.nodeSelector: {disk: ssd}.

Task 6: Resource Requests and Limits

Objective: Set CPU and memory boundaries for pods and observe the scheduler's behavior.

Task Statement

Create a pod named resource-demo that requests 100m CPU and 128Mi memory, with limits of 200m CPU and 256Mi memory. Then create a second pod that requests more resources than available and observe it remaining Pending.

Solution

bash
# Pod with manageable resource requests
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: resource-demo
spec:
  containers:
  - name: app
    image: nginx
    resources:
      requests:
        cpu: "100m"
        memory: "128Mi"
      limits:
        cpu: "200m"
        memory: "256Mi"
EOF

# Pod that requests too many resources (will stay Pending)
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: resource-hog
spec:
  containers:
  - name: app
    image: nginx
    resources:
      requests:
        cpu: "100"
        memory: "200Gi"
EOF

Verification

bash
# resource-demo should be Running
kubectl get pod resource-demo

# resource-hog should be Pending
kubectl get pod resource-hog
kubectl describe pod resource-hog | grep -A3 Events
# Warning  FailedScheduling  Insufficient cpu / Insufficient memory

# Check node allocatable resources
kubectl describe nodes | grep -A5 "Allocated resources"

# Clean up
kubectl delete pod resource-demo resource-hog --force --grace-period=0

Key Takeaways

  • Requests = minimum guaranteed resources. The scheduler uses requests to find a suitable node.
  • Limits = maximum resources the container can use. Exceeding memory limits → OOMKilled. Exceeding CPU limits → throttling.
  • 1 CPU = 1000 millicores (1000m). 100m = 10% of one CPU core.
  • Memory units: Ki (kibibytes), Mi (mebibytes), Gi (gibibytes).
  • If no requests are set, the pod can be scheduled on any node but may be evicted under memory pressure.

Task 7: Create Jobs and CronJobs

Objective: Run one-time and scheduled batch workloads.

Task Statement

1. Create a Job named data-export that runs busybox and executes echo "Export complete". It should retry up to 3 times on failure and complete 5 parallel tasks.

2. Create a CronJob that runs the same task every 15 minutes.

Solution

bash
# Step 1: Create the Job
kubectl apply -f - <<'EOF'
apiVersion: batch/v1
kind: Job
metadata:
  name: data-export
spec:
  completions: 5          # Total number of successful completions needed
  parallelism: 5          # Run all 5 in parallel
  backoffLimit: 3          # Max retries per pod
  activeDeadlineSeconds: 120  # Timeout after 2 minutes
  template:
    spec:
      containers:
      - name: exporter
        image: busybox:1.36
        command: ["sh", "-c", "echo 'Export complete for batch item'; sleep 5"]
      restartPolicy: Never
EOF

# Step 2: Create the CronJob
kubectl apply -f - <<'EOF'
apiVersion: batch/v1
kind: CronJob
metadata:
  name: scheduled-export
spec:
  schedule: "*/15 * * * *"     # Every 15 minutes
  concurrencyPolicy: Forbid     # Don't start new job if previous is still running
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: exporter
            image: busybox:1.36
            command: ["sh", "-c", "echo 'Scheduled export at $(date)'; sleep 5"]
          restartPolicy: OnFailure
EOF

Verification

bash
# Check Job status
kubectl get jobs
kubectl describe job data-export

# Watch pods complete
kubectl get pods -l job-name=data-export -w

# Check CronJob
kubectl get cronjobs
kubectl describe cronjob scheduled-export

# Manually trigger the CronJob (useful for testing)
kubectl create job test-run --from=cronjob/scheduled-export
kubectl get pods -l job-name=test-run
kubectl logs -l job-name=test-run

Key Takeaways

  • completions = total successful pods needed. parallelism = how many run at once.
  • backoffLimit = max retries before the Job is marked as Failed.
  • restartPolicy must be Never or OnFailure for Jobs (not Always).
  • CronJob concurrencyPolicy: Allow (default), Forbid, Replace.
  • CronJob schedule uses standard cron syntax: minute hour day month weekday.

Task 8: Use Pod Topology Spread Constraints

Objective: Distribute pods evenly across nodes or zones.

Task Statement

Create a Deployment with 6 replicas that spreads pods evenly across available nodes, with a maximum skew of 1 pod.

Solution

bash
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spread-demo
spec:
  replicas: 6
  selector:
    matchLabels:
      app: spread-demo
  template:
    metadata:
      labels:
        app: spread-demo
    spec:
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: spread-demo
      containers:
      - name: nginx
        image: nginx
EOF

Verification

bash
# Check distribution across nodes
kubectl get pods -l app=spread-demo -o wide
# With 3 worker nodes and 6 replicas, expect 2 pods per node

# If using 2 nodes, expect 3 per node (maxSkew=1 allows at most 1 difference)
kubectl get pods -l app=spread-demo \
  -o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName' | sort -k2

Key Takeaways

  • topologyKey: kubernetes.io/hostname = spread across nodes. Use topology.kubernetes.io/zone for zone spreading.
  • maxSkew: 1 = the difference in pod count between any two topology domains can be at most 1.
  • whenUnsatisfiable: DoNotSchedule = strict enforcement (pods go Pending). ScheduleAnyway = best effort.
Chapter 3
🔒 Available in full product

Chapter 3: Services & Networking

Chapter 4
🔒 Available in full product

Chapter 4: Storage

Chapter 5
🔒 Available in full product

Chapter 5: Troubleshooting

Chapter 6
🔒 Available in full product

Chapter 6: Security & RBAC

Chapter 7
🔒 Available in full product

Chapter 7: etcd Backup & Restore

You’ve reached the end of the free preview

Get the full Kubernetes CKA Lab Workbook and unlock everything.

All Chapters

Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.

Full Tool Suite

Access all interactive tools with complete data, all workload profiles, and the full scenario library.

Source Files

Downloadable source code, configuration files, and working examples from every chapter.

Lifetime Updates

Free updates for life. Every new chapter, tool, and improvement included.

Buy Now — $79 →
📦 Free sample included — download another copy for the full product.
Kubernetes CKA Lab Workbook v1.0.0 — Free Preview