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.
Objective: Bootstrap a new Kubernetes cluster from scratch using kubeadm.
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:
10.244.0.0/1610.96.0.0/12After initialization, configure kubectl for the current user and install a CNI plugin.
# 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# 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-infokubeadm init generates a join token that expires in 24 hours. Save it or regenerate with kubeadm token create --print-join-command.--pod-network-cidr must match your CNI plugin's expected range (Flannel defaults to 10.244.0.0/16).NotReady and CoreDNS pods stay Pending.Objective: Add a worker node to an existing cluster.
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.
# 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# 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--ttl 48h or non-expiring with --ttl 0 (not recommended for production).--discovery-token-ca-cert-hash ensures the worker connects to the legitimate API server (prevents man-in-the-middle attacks).openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'Objective: Understand and inspect the static pod manifests that run the control plane.
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
# 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# 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:'/etc/kubernetes/manifests/ are watched by kubelet. Any changes are applied automatically (kubelet restarts the pod).journalctl -u kubelet and crictl ps -a for error details.Objective: Perform a minor version upgrade of the control plane and worker nodes.
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.
# ====== 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# 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 widekubectl drain evicts pods gracefully. Use --ignore-daemonsets because DaemonSet pods cannot be drained.kubectl uncordon re-enables scheduling after the upgrade.Objective: Work with multiple kubeconfig files and contexts.
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.
# 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# 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@kuberneteskubectl config use-context .KUBECONFIG=/path/to/config1:/path/to/config2.~/.kube/config.Objective: Deploy a custom scheduler alongside the default scheduler.
Deploy a second scheduler named custom-scheduler in the kube-system namespace. Then create a pod that uses this custom scheduler.
# 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# 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 nodespec.schedulerName to choose which scheduler handles them.--leader-elect=false for the custom scheduler (or use a different --leader-elect-resource-name).schedulerName doesn't match any running scheduler, the pod stays Pending.Objective: Enable audit logging on the API server to track who did what.
Enable audit logging on the kube-apiserver with the following requirements:
Metadata levelRequestResponse level for pods and deployments/var/log/kubernetes/audit.log# 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# 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=0None, Metadata, Request, RequestResponse (most verbose).journalctl -u kubelet for errors.Objective: View certificate details and expiration dates for cluster components.
List all certificates used by the Kubernetes cluster. Identify which certificates expire within the next 90 days and how to renew them.
# 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# 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-systemkubeadm certs renew all renews all certificates but you must restart the control plane pods (or the entire node).kubeadm certs renew.openssl x509 -in -text -noout .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.
Objective: Create a Deployment with specific replicas, update its image, and manage rollout history.
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.
# 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# 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 Runningkubectl 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.RollingUpdate with maxUnavailable=25% and maxSurge=25%.Objective: Deploy a pod on every node in the cluster.
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.
# 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# 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=5replicas — they automatically run one pod per matching node.tolerations for control-plane nodes if you want the DaemonSet to run everywhere.kubectl create daemonset exists — know how to write the YAML.Objective: Deploy a stateful workload with persistent storage and ordered pod management.
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.
# 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# 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.localserviceName field pointing to a headless Service.Objective: Control pod scheduling using node taints and pod tolerations.
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.
# 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# 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 taintNoSchedule: 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.- suffix to remove a taint: kubectl taint nodes key=value:effect- Objective: Schedule pods on nodes matching specific labels.
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.
# 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# "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-required = hard constraint (pod stays Pending if no matching node exists).preferred = soft constraint (scheduler tries but can use other nodes).operator: In (match any value in list), NotIn, Exists, DoesNotExist, Gt, Lt.nodeSelector is the simpler alternative: spec.nodeSelector: {disk: ssd}.Objective: Set CPU and memory boundaries for pods and observe the scheduler's behavior.
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.
# 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# 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=01 CPU = 1000 millicores (1000m). 100m = 10% of one CPU core.Ki (kibibytes), Mi (mebibytes), Gi (gibibytes).Objective: Run one-time and scheduled batch workloads.
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.
# 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# 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-runcompletions = 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).concurrencyPolicy: Allow (default), Forbid, Replace.minute hour day month weekday.Objective: Distribute pods evenly across nodes or zones.
Create a Deployment with 6 replicas that spreads pods evenly across available nodes, with a maximum skew of 1 pod.
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# 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 -k2topologyKey: 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.Get the full Kubernetes CKA Lab Workbook and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.