Skip to content

CKA Mock Exam — Complete Questions & Solutions

Exam Rules Reminder

  • Use sudo to edit files in /etc/kubernetes/manifests/
  • Use sudo for kubelet restart and system-level debugging
  • Use documentation links provided in each question
  • SSH into the correct node as specified in the question
  • Always verify the correct namespace before applying changes

Question Topic

  1. Argo CD Setup
  2. Multi-Container Pod (Co-located Containers)
  3. MariaDB with Persistent Storage
  4. Resource Distribution Across Pods
  5. Horizontal Pod Autoscaler (HPA)
  6. Custom Resource Definitions (CRDs)
  7. PriorityClass and Deployment Update
  8. CNI Installation (Calico / Tigera Operator)
  9. Install cri-dockerd
  10. Storage Setup (SC, PV, PVC)
  11. Ingress Configuration
  12. Migrate from Ingress to Gateway API
  13. Network Policy
  14. Troubleshoot Broken Cluster
  15. Expose Pod via NodePort
  16. TLS Configuration Update

Kubernetes to practice the exam task

#Install kind if does not exit
brew install kind

#create kubernetes cluster
kind create cluster --name <cluster-name>

#cleanup
kind delete cluster --name <cluster-name>

#Install limactl
brew install lima
limactl --version
limactl start --name=cka-node template://ubuntu-lts
limactl shell cka-node
limactl stop cka-node
limactl delete cka-node
limactl list

Question 1: Argo CD Setup

Context: You are working on cluster k8s-cluster-1.

Task:

  1. Install Argo CD in namespace argocd, the installation yaml file at https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
  2. Add the Git repository https://github.com/example-org/app-config.git to Argo CD
  3. Generate a Kubernetes manifest template for an application named myapp from that repo, without installing CRDs, targeting namespace myapp-prod
  4. Save the generated output to argocd-myapp.yaml

Solution

# create cluster if it does not exist
kind create cluster --name k8s-cluster-1


# Install Argo CD
kubectl create namespace argocd

# Install argocd
kubectl create -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Wait for pods to be ready
kubectl wait --for=condition=Ready pod --all -n argocd --timeout=120s

# Generate manifest template WITHOUT installing CRDs
cat <<EOF > /opt/artifacts/argocd-myapp.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/pkhamdee/gitops-example.git
    targetRevision: HEAD
    path: apps/guestbook/overlays/dev
  destination:
    server: https://kubernetes.default.svc
    namespace: myapp-prod
  syncPolicy:
    syncOptions:
    - CreateNamespace=true
EOF

kubectl apply -n argocd -f argocd-myapp.yaml --dry-run=client 

# Verify output saved
cat argocd-myapp.yaml

Extra argocd cli (option)

# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d && echo

# Port-forward to login
kubectl port-forward svc/argocd-server -n argocd 8080:443 &

# Login via CLI
argocd login localhost:8080 \
  --username admin \
  --password <password-from-above> \
  --insecure

argocd app create myapp \
  --repo https://github.com/pkhamdee/gitops-example.git \
  --path apps/guestbook/overlays/dev \
  --dest-server https://kubernetes.default.svc \
  --sync-option CreateNamespace=true \
  --dest-namespace myapp-prod 

argocd app delete myapp
argocd app get myapp
argocd app sync myapp

Question 2: Multi-Container Pod (Sidecar)

Context: Cluster k8s-cluster-1, namespace default.

Task:

  1. Update the existing Deployment app-logger to add a sidecar container named log-reader
  2. Configure a shared emptyDir volume named shared-logs between both containers
  3. The main container app writes logs to /var/log/app/app.log
  4. The sidecar log-reader runs tail -f /var/log/app/app.log
  5. Verify logs from the sidecar container

Solution

# Prerequisite
# Create app-logger resource
kubectl apply -f -<<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-logger
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app-logger
  template:
    metadata:
      labels:
        app: app-logger
    spec:
      containers:
      - name: app
        image: busybox
        command: ["/bin/sh", "-c"]
        args:
        - while true; do echo "$(date) - log entry" >> /var/log/app/app.log; sleep 5; done
        volumeMounts:
        - name: shared-logs
          mountPath: /var/log/app
      volumes:
      - name: shared-logs
        emptyDir: {}
EOF        

# Now practice the exam fix:

# Export existing deployment
kubectl get deployment app-logger -o yaml > /tmp/app-logger.yaml

# Edit `/tmp/app-logger.yaml`:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-logger
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app-logger
  template:
    metadata:
      labels:
        app: app-logger
    spec:
      containers:
      - name: app
        image: busybox
        command: ["/bin/sh", "-c"]
        args:
        - while true; do echo "$(date) - log entry" >> /var/log/app/app.log; sleep 5; done
        volumeMounts:
        - name: shared-logs
          mountPath: /var/log/app

      # ADD THIS SIDECAR:
      - name: log-reader
        image: busybox
        command: ["/bin/sh", "-c"]
        args:
        - tail -f /var/log/app/app.log
        volumeMounts:
        - name: shared-logs
          mountPath: /var/log/app

      volumes:
      - name: shared-logs
        emptyDir: {}

#Apply
kubectl apply -f /tmp/app-logger.yaml

#Verify pod is running
kubectl get pods -l app=app-logger

#Check sidecar logs
kubectl logs <pod-name> -c log-reader

#Confirm logs are flowing
kubectl logs <pod-name> -c log-reader --follow

#cleanup
kubectl delete deploy app-logger

Trick note: The sidecar may show tail: can't open '/var/log/app/app.log': No such file or directory briefly at startup. This is normal — ignore it unless the pod is in CrashLoopBackOff.


Question 3: MariaDB with Persistent Storage

Context: Cluster k8s-cluster-1, namespace database.

Given: - PV name: mariadb-pv - StorageClass: local-path or any exist - Deployment YAML at /opt/manifests/mariadb-deployment.yaml

Task:

  1. Create a PVC that binds to mariadb-pv
  2. Update the Deployment to use the PVC with mount path /var/lib/mysql
  3. Verify the application is running

Solution

# Prerequisite

# Create mariadb-pv PV pointing to the VM path:
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mariadb-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: "local-path"
  hostPath:
    path: /tmp/mariadb-data
EOF

# Create mariadb deployment
kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
  name: database
---
apiVersion: v1
kind: Secret
metadata:
  name: mariadb-secret
  namespace: database
type: Opaque
stringData:
  MYSQL_ROOT_PASSWORD: "rootpassword"
  MYSQL_DATABASE: "appdb"
  MYSQL_USER: "appuser"
  MYSQL_PASSWORD: "apppassword"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mariadb
  namespace: database
  labels:
    app: mariadb
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mariadb
  template:
    metadata:
      labels:
        app: mariadb
    spec:
      containers:
      - name: mariadb
        image: mariadb:10.11
        ports:
        - containerPort: 3306
        envFrom:
        - secretRef:
            name: mariadb-secret
        # TODO: add volumeMounts here
---
apiVersion: v1
kind: Service
metadata:
  name: mariadb
  namespace: database
spec:
  type: ClusterIP
  selector:
    app: mariadb
  ports:
  - protocol: TCP
    port: 3306
    targetPort: 3306
EOF

# Now practice the exam fix:

# Inspect the existing PV to match spec
kubectl get pv mariadb-pv -o yaml

# Create VPC `/tmp/mariadb-pvc.yaml`:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mariadb-pvc
  namespace: database
spec:
  storageClassName: "local-path"
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  volumeName: mariadb-pv

kubectl apply -f /tmp/mariadb-pvc.yaml
kubectl get pvc -n database   # should show Bound

# Now practice the exam fix:

# In the Deployment spec, add volume and volumeMount:
spec:
  template:
    spec:
      containers:
      - name: mariadb
        volumeMounts:
        - name: mariadb-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mariadb-storage
        persistentVolumeClaim:
          claimName: mariadb-pvc

kubectl apply -f /opt/manifests/mariadb-deployment.yaml
kubectl get pods -n database # STATUS=Running
kubectl get pvc -n database  # STATUS=Bound
kubectl logs -n database -l app=mariadb --tail=5

Question 4: Resource Distribution Across Pods

Context: Cluster k8s-cluster-1, namespace production.

Task:

  1. Update Deployment web-app to run 3 replicas
  2. Set resource requests so all 3 pods can be scheduled
  3. Divide node allocatable CPU and memory equally across 3 replicas

Solution

# Prerequisite

# Must check total and free to get cpu/mem for the question
kubectl describe node lima-rancher-desktop | grep -A8 "Allocatable:"
kubectl describe node lima-rancher-desktop 2>/dev/null | grep -A8 "Allocated resources"

# put memory and mem to below yaml
kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
  name: production
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: web-app
        image: nginx:1.25
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: "700m"      # intentionally high — 3x700m=2100m exceeds node available
            memory: "1400Mi" # intentionally high — 3x1400Mi=4200Mi exceeds node available
          limits:
            cpu: "700m"
            memory: "1400Mi"
EOF

# Now practice the exam fix:

# Check node allocatable resources
kubectl get nodes
kubectl describe node <node-name> | grep -A8 "Allocatable:"
# Example output:
#   cpu:     2
#   memory:  3800Mi

# Check what's already requested on the node
kubectl describe node <node-name> | grep -A8 "Allocated resources"

# Calculate per-pod request
#Allocatable:   2000m CPU    3916Mi memory
#Allocated:     1650m CPU    3004Mi memory
#               ─────────    ─────────────
#Free:           350m CPU     912Mi memory

#The web-app deployment (1 replica × 700m) is inside that 1650m. So when you change to 3 replicas #you need to account for swapping out that 700m and replacing with 3×new requests:

#System pods only (excluding web-app): 1650m - 700m = 950m CPU
#Available for web-app total:          2000m - 950m = 1050m CPU

#Per pod (3 replicas):  1050m / 3 = 350m  → use 300m (leave headroom)

#System pods memory (excluding web-app): 3004Mi - 1400Mi = 1604Mi
#Available for web-app total:            3916Mi - 1604Mi = 2312Mi

#Per pod (3 replicas):  2312Mi / 3 = 770Mi → use 700Mi (leave headroom)

# Update deployment
kubectl edit deployment web-app -n production

spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: web-app
        resources:
          requests:
            cpu: "300m"       # adjust based on actual availability
            memory: "700Mi"   # adjust based on actual availability

# restart deployment after edit
kubectl rollout restart deploy web-app -n production

# Verify all 3 pods scheduled and running
kubectl get pods -n production -l app=web-app

# If pods are Pending, diagnose:
kubectl describe pod <pending-pod> -n production | grep -A5 Events
# Reduce requests and retry

Key: Always run kubectl describe node first. Do NOT use fixed percentages — base it on actual remaining allocatable capacity.


Question 5: Horizontal Pod Autoscaler (HPA)

Context: Cluster k8s-cluster-1, namespace default.

Task: Update the existing HPA web-hpa to add a stabilization window of 30 seconds for scale-down behavior.


Solution

# Prerequisite

# Deploy web-app
kubectl apply -f -<<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: web-app
        image: nginx:1.25
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
EOF

#Create HPA
kubectl apply -f -<<EOF
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
EOF

# Now practice the exam fix:

# Inspect existing HPA
kubectl get hpa web-hpa -o yaml

# Edit HPA
kubectl edit hpa web-hpa

Add `behavior` section:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
  behavior:                        # ADD THIS BLOCK
    scaleDown:
      stabilizationWindowSeconds: 30

# Verify
kubectl get hpa web-hpa -o yaml | grep -A5 stabilization

Question 6: Custom Resource Definitions (CRDs)

Context: Cluster k8s-cluster-1.

Task:

  1. List all CRDs related to cert-manager and save to /opt/artifacts/cert-manager-crds.txt
  2. Explain the spec.subject field in the Certificate resource and save to /opt/artifacts/cert-manager-subject.txt

Solution

# Prerequisite
# Deploy CRD if does not exist
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml

# Now practice the exam fix:

# List cert-manager CRDs
kubectl get crds | grep cert-manager.io > /opt/artifacts/cert-manager-crds.txt

# Verify
cat /opt/artifacts/cert-manager-crds.txt
# Expected entries:
# certificates.cert-manager.io
# certificaterequests.cert-manager.io
# clusterissuers.cert-manager.io
# issuers.cert-manager.io
# orders.acme.cert-manager.io
# challenges.acme.cert-manager.io

# Explain spec.subject
kubectl explain certificate.spec.subject > /opt/artifacts/cert-manager-subject.txt

# Verify
cat /opt/artifacts/cert-manager-subject.txt


#Expected content of `cert-manager-subject.txt`:

#spec.subject defines the X.509 Subject for the Certificate. It allows
#specifying fields such as:
#  organizations       - list of organizations (O field)
#  countries           - list of countries (C field)
#  organizationalUnits - list of OUs
#  localities          - list of localities (L field)
#  provinces           - list of provinces (ST field)
#  streetAddresses     - list of street addresses
#  postalCodes         - list of postal codes
#  serialNumber        - serial number of the certificate subject

Question 7: PriorityClass and Deployment Update

Context: Cluster k8s-cluster-1, namespace default.

Task:

  1. Create a PriorityClass with value = 1000000 - 1 = 999999
  2. Update Deployment critical-app to use this PriorityClass
  3. Use kubectl patch only for the deployment update

Solution

# Prerequisite

# Deploy critical-app resource
kubectl apply -f -<<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: critical-app
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: critical-app
  template:
    metadata:
      labels:
        app: critical-app
    spec:
      containers:
      - name: critical-app
        image: nginx:1.25
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
EOF   

# Now practice the exam fix:

# Create PriorityClass
cat <<EOF | kubectl apply -f -
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 999999
globalDefault: false
description: "High priority class, one below 1000000"
EOF

# Patch Deployment using kubectl patch ONLY
kubectl patch deployment critical-app \
  --type=merge \
  -p '{"spec":{"template":{"spec":{"priorityClassName":"high-priority"}}}}'

# Verify
kubectl get deployment critical-app \
  -o jsonpath='{.spec.template.spec.priorityClassName}'

kubectl get pods -l app=critical-app \
  -o jsonpath='{.items[0].spec.priorityClassName}'

Question 8: CNI Installation (Calico / Tigera Operator)

Context: Cluster k8s-cluster-1 — control plane node. CNI not yet installed.

Reference (provided in exam): https://docs.tigera.io/calico/latest/getting-started/kubernetes/self-managed-onprem/onpremises

Task:

  1. Install Calico via Tigera Operator
  2. Update the cluster CIDR to match value from kube-controller-manager
  3. Apply configuration and restart kubelet

Solution

# Prerequisite
# Install kind (if not already):

brew install kind

# Create a kind cluster with no CNI:

cat <<EOF | kind create cluster --name cni-practice --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  disableDefaultCNI: true
  podSubnet: "192.168.0.0/16"   # Calico expects this CIDR
EOF

# Switch kubectl context to the new cluster:
kubectl config use-context kind-cni-practice

# Verify nodes are NotReady (no CNI yet — this is the exam starting state)
kubectl get nodes
# NAME                        STATUS     ROLES
# cni-practice-control-plane  NotReady   control-plane

# Now practice the exam fix:

# Check cluster CIDR (as the exam instructs)
kubectl get pod kube-controller-manager-cni-practice-control-plane \
  -n kube-system -o yaml | grep cluster-cidr

# Expected: --cluster-cidr=192.168.0.0/16

# Install Tigera Operator
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml

# Download custom resources
curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/custom-resources.yaml

# Verify CIDR matches (192.168.0.0/16 already matches kind config)
grep cidr custom-resources.yaml

# Apply
kubectl create -f custom-resources.yaml

# Watch rollout
watch kubectl get pods -n calico-system

# Simulate kubelet restart (inside kind node):
# SSH into the kind node
docker exec -it cni-practice-control-plane bash

# Inside the node:
systemctl restart kubelet
exit

# Verify:
kubectl get nodes
# NAME                        STATUS   ROLES
# cni-practice-control-plane  Ready    control-plane  ← now Ready

# Cleanup after practice:
kind delete cluster --name cni-practice

Question 9: Install cri-dockerd

Context: Worker node node01.

Reference (provided in exam): https://kubernetes.io/docs/setup/production-environment/container-runtimes/

Task:

  1. Install cri-dockerd from .deb package at /opt/packages/cri-dockerd.deb
  2. Enable IP forwarding via sysctl
  3. Verify configuration

Solution

# Prerequisite

# Create an Ubuntu VM with Lima:
# Verify limactl is available
limactl --version

# Start Ubuntu VM (uses HVF — Apple's native hypervisor, no QEMU issues)
limactl start --name=cka-node template://ubuntu-lts

# Shell into it
limactl shell cka-node

# Now practice the exam fix:

# Inside Lima Ubuntu VM
sudo apt-get update && sudo apt-get install -y docker.io

# Download cri-dockerd for arm64 (Apple Silicon)
curl -s https://api.github.com/repos/Mirantis/cri-dockerd/releases/latest \
  | grep "browser_download_url.*arm64.deb\"" \
  | cut -d '"' -f 4

# copy the URL and wget it

wget <URL-from-above> -O /tmp/cri-dockerd.deb
sudo dpkg -i /tmp/cri-dockerd.deb

sudo systemctl daemon-reload
sudo systemctl enable --now cri-docker.socket cri-docker.service

sudo tee /etc/sysctl.d/99-kubernetes-cri.conf <<EOF
net.ipv4.ip_forward = 1
EOF

sudo sysctl --system
sysctl net.ipv4.ip_forward

# Cleanup after practice:
exit
limactl stop cka-node
limactl delete cka-node

Question 10: Storage Setup (SC, PV, PVC)

Context: Cluster k8s-cluster-1, namespace storage-demo.

Task:

  1. Create a StorageClass with WaitForFirstConsumer binding mode
  2. Create a PersistentVolume
  3. Create a PersistentVolumeClaim — observe it stays Pending
  4. Create a Pod using the PVC
  5. Verify PVC becomes Bound once Pod is scheduled

Solution

# Prerequisite

# Create StorageClass, PV, and PVC
cat <<EOF | kubectl apply -f -
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer   # PVC stays Pending until Pod scheduled

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /mnt/data

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-pvc
  namespace: storage-demo
spec:
  storageClassName: local-storage
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
EOF

# PVC is Pending at this point (expected)
kubectl get pvc -n storage-demo   # STATUS: Pending


# Now practice the exam fix:

# Create Pod to trigger binding
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: storage-pod
  namespace: storage-demo
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - mountPath: /data
      name: storage
  volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: local-pvc
EOF

# Verify PVC is now Bound
kubectl get pvc -n storage-demo   # STATUS: Bound
kubectl get pod storage-pod -n storage-demo

Question 11: Ingress Configuration

Context: Cluster k8s-cluster-1, namespace web.

Given: - IngressClass: nginx (exists in cluster) - Backend service: echo-service, port 8080

Task: Create an Ingress resource: - Host: example.com - Path: /echo - Backend: echo-service:8080


Solution

# Prerequisite

# Install nginx if does not exist
# Install nginx ingress controller:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.1/deploy/static/provider/cloud/deploy.yaml

# Wait for it to be ready
kubectl wait --namespace ingress-nginx \
  --for=condition=Ready pod \
  --selector=app.kubernetes.io/component=controller \
  --timeout=120s

# Verify IngressClass exists
kubectl get ingressclass
# NAME    CONTROLLER
# nginx   k8s.io/ingress-nginx

# Apply prerequisite resources:
kubectl apply -f -<<EOF
apiVersion: v1
kind: Namespace
metadata:
  name: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-deployment
  namespace: web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: echo
  template:
    metadata:
      labels:
        app: echo
    spec:
      containers:
      - name: echo
        image: nginx:1.25
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: echo-service
  namespace: web
spec:
  type: ClusterIP
  selector:
    app: echo
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 80
EOF


# Now practice the exam task (create the Ingress):

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: echo-ingress
  namespace: web
spec:
  ingressClassName: nginx
  rules:
  - host: example.com
    http:
      paths:
      - path: /echo
        pathType: Prefix
        backend:
          service:
            name: echo-service
            port:
              number: 8080
EOF

# Verify:
kubectl get ingress -n web

# Test connectivity
INGRESS_IP=$(kubectl get svc ingress-nginx-controller \
  -n ingress-nginx \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

curl -H "Host: example.com" http://${INGRESS_IP}/echo

Question 12: Migrate from Ingress to Gateway API

Context: Cluster k8s-cluster-1, namespace web.

Given: - Existing Ingress: echo-ingress (same rules as Q11) - GatewayClass: nginx exists in cluster - TLS secret: tls-secret in namespace web

Task:

  1. Create a Gateway with TLS on port 443
  2. Create an HTTPRoute with the same routing rules
  3. Delete the existing Ingress

Solution

# Prerequisite

# Enable Gateway API in Traefik:
# Edit Traefik deployment to enable Gateway API provider
kubectl edit deployment traefik -n kube-system
# Add --providers.kubernetesgateway=true to the args:

#containers:
#- args:
#  - --providers.kubernetesgateway=true   # ADD THIS LINE
#  - --global.checknewversion
#  - --global.sendanonymoususage
# ... rest of existing args

#Create GatewayClass for Traefik:

cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: traefik
spec:
  controllerName: traefik.io/gateway-controller
EOF

kubectl get gatewayclass
# NAME      CONTROLLER
# traefik   traefik.io/gateway-controller

#Create TLS secret:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /tmp/tls.key \
  -out /tmp/tls.crt \
  -subj "/CN=example.com/O=example"

kubectl create secret tls tls-secret \
  --cert=/tmp/tls.crt \
  --key=/tmp/tls.key \
  -n web

# Verify prerequisites:

kubectl get gatewayclass        # traefik
kubectl get secret tls-secret -n web
kubectl get ingress echo-ingress -n web   # existing ingress to migrate


#Now practice the exam task using traefik as the gatewayClassName:

cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: echo-gateway
  namespace: web
spec:
  gatewayClassName: traefik      # use traefik instead of nginx
  listeners:
  - name: https
    protocol: HTTPS
    port: 443
    hostname: "example.com"
    tls:
      mode: Terminate
      certificateRefs:
      - name: tls-secret
        kind: Secret
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: echo-route
  namespace: web
spec:
  parentRefs:
  - name: echo-gateway
    namespace: web
  hostnames:
  - "example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /echo
    backendRefs:
    - name: echo-service
      port: 8080
EOF

# Delete old Ingress
kubectl delete ingress echo-ingress -n web

# Verify
kubectl get gateway,httproute -n web
kubectl get ingress -n web   # should be empty

Question 13: Network Policy

Context: Cluster k8s-cluster-1, namespace app.

Given: - Frontend pods: label role=frontend - Backend pods: label role=backend

Task: Apply a NetworkPolicy so only frontend pods can access backend pods on port 8080.


Solution

# Create kind cluster with Calico (proper enforcement test):

cat <<EOF | kind create cluster --name netpol-practice --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  disableDefaultCNI: true
  podSubnet: "192.168.0.0/16"
EOF

kubectl config use-context kind-netpol-practice

# Install Calico
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml
curl -sO https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/custom-resources.yaml
kubectl create -f custom-resources.yaml

# Wait for Calico
kubectl wait --for=condition=Ready pod --all -n calico-system --timeout=120s

# Prerequisite YAML (apply on either cluster):
kubectl apply -f -<<EOF
apiVersion: v1
kind: Namespace
metadata:
  name: app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
      role: backend
  template:
    metadata:
      labels:
        app: backend
        role: backend
    spec:
      containers:
      - name: backend
        image: nginx:1.25
        ports:
        - containerPort: 8080
        command: ["/bin/sh", "-c"]
        args:
        - nginx -g 'daemon off;' & sleep 1 &&
          sed -i 's/listen       80/listen       8080/g' /etc/nginx/conf.d/default.conf &&
          nginx -s reload && wait
---
apiVersion: v1
kind: Service
metadata:
  name: backend-svc
  namespace: app
spec:
  selector:
    role: backend
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
      role: frontend
  template:
    metadata:
      labels:
        app: frontend
        role: frontend
    spec:
      containers:
      - name: frontend
        image: curlimages/curl:8.5.0
        command: ["sleep", "infinity"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: other-app
  namespace: app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: other-app
  template:
    metadata:
      labels:
        app: other-app
    spec:
      containers:
      - name: other-app
        image: curlimages/curl:8.5.0
        command: ["sleep", "infinity"]
EOF

#Verify prerequisites:

kubectl get pods -n app --show-labels
# should show: role=frontend, role=backend, and other-app (no role label)

kubectl get svc -n app

#Test BEFORE applying NetworkPolicy (all should succeed):

FRONTEND=$(kubectl get pod -n app -l role=frontend -o jsonpath='{.items[0].metadata.name}')
OTHER=$(kubectl get pod -n app -l app=other-app -o jsonpath='{.items[0].metadata.name}')

kubectl exec $FRONTEND -n app -- curl -s --max-time 3 http://backend-svc:8080
# 200 OK ✓

kubectl exec $OTHER -n app -- curl -s --max-time 3 http://backend-svc:8080
# 200 OK ✓


#Now practice the exam task

#Apply the NetworkPolicy

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: app
spec:
  podSelector:
    matchLabels:
      role: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 8080
EOF

#Test AFTER applying NetworkPolicy:

kubectl exec $FRONTEND -n app -- curl -s --max-time 3 http://backend-svc:8080
# 200 OK ✓  (frontend allowed)

kubectl exec $OTHER -n app -- curl -s --max-time 3 http://backend-svc:8080
# timeout ✗  (other-app blocked) — only works on Calico, not Flannel

Question 14: Troubleshoot Broken Cluster

Context: Cluster k8s-cluster-1 — control plane not responding.

Symptom: kubectl get nodes times out or API server is down.

Task:

  1. Find the incorrect etcd endpoint in the API server manifest
  2. Correct it using the IP/port from the etcd manifest
  3. Verify cluster is healthy and pods are scheduling again

Solution

# Prerequisite

# Create kind cluster:
kind create cluster --name broken-cluster

kubectl config use-context kind-broken-cluster

# Verify healthy first
kubectl get nodes
kubectl get pods -A

# Break the cluster (simulate the exam problem):
# SSH into control plane node
docker exec -it broken-cluster-control-plane bash

#Inside the node:

# Change etcd port from 2379 (correct) to 2380 (wrong)
sed -i 's|--etcd-servers=https://127.0.0.1:2379|--etcd-servers=https://127.0.0.1:2380|' \
  /etc/kubernetes/manifests/kube-apiserver.yaml

# Verify the bad change
grep etcd-servers /etc/kubernetes/manifests/kube-apiserver.yaml
# --etcd-servers=https://127.0.0.1:2380  ← broken

exit

# Confirm cluster is broken:
# Wait ~15 seconds for kubelet to restart the static pod
sleep 15

kubectl get nodes
# Error from server: etcdserver: request timed out  ← cluster broken ✓


# Now practice the exam fix:

# SSH into control plane
docker exec -it broken-cluster-control-plane bash

# Check etcd manifest for correct port
grep "listen-client-urls" /etc/kubernetes/manifests/etcd.yaml
# --listen-client-urls=https://127.0.0.1:2379  ← correct port is 2379

# Check apiserver manifest for wrong port
grep "etcd-servers" /etc/kubernetes/manifests/kube-apiserver.yaml
# --etcd-servers=https://127.0.0.1:2380  ← wrong!

# Fix it
sed -i 's|--etcd-servers=https://127.0.0.1:2380|--etcd-servers=https://127.0.0.1:2379|' \
  /etc/kubernetes/manifests/kube-apiserver.yaml

exit

# Wait for API server to recover (~30 seconds)
sleep 30

# Verify
kubectl get nodes       # Ready
kubectl get pods -A     # all Running
kubectl get componentstatuses


#Cleanup:
kind delete cluster --name broken-cluster

Key: Port 2379 = etcd client port (used by API server). Port 2380 = etcd peer port (etcd-to-etcd replication only). Do NOT use 2380 for --etcd-servers.


Question 15: Expose Pod via NodePort

Context: Cluster k8s-cluster-1, namespace default.

Task:

  1. Create a Pod named web-pod running nginx
  2. Expose it via a NodePort Service on node port 30080
  3. Verify access via NodePort

Solution

# Create Pod
kubectl run web-pod \
  --image=nginx \
  --labels="app=web-pod" \
  --port=80

# Verify pod is running
kubectl get pod web-pod

# Expose via NodePort Service
kubectl expose pod web-app --type=NodePort --port=80 --target-port=80 --dry-run=client -o yaml

# Add node port
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  labels:
    run: web-app
  name: web-app
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
    nodePort: 30080    # External NodePort
  selector:
    run: web-app
  type: NodePort
EOF

# Verify
kubectl get svc web-pod-svc
kubectl get nodes -o wide   # get node IP

# Test access
curl http://<NODE-IP>:30080

Question 16: TLS Configuration Update

Context: Cluster k8s-cluster-1, namespace tls-app.

Given: - ConfigMap tls-config currently has only TLS 1.3 configured - Deployment name: tls-app - Test URL: https://tls-app.example.com

Task:

  1. Update the ConfigMap to support both TLS 1.2 and TLS 1.3
  2. Restart the Deployment to pick up the change
  3. Verify TLS 1.2 connectivity using the given URL

Solution

# Prerequisite

# Generate TLS certificate:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /tmp/tls.key \
  -out /tmp/tls.crt \
  -subj "/CN=tls-app.example.com/O=example"

# Create secret from generated cert (replaces the placeholder above)
kubectl create namespace tls-app
kubectl create secret tls tls-app-cert \
  --cert=/tmp/tls.crt \
  --key=/tmp/tls.key \
  -n tls-app

# Apply prerequisite resources:

kubectl apply -f -<<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  name: tls-config
  namespace: tls-app
data:
  nginx.conf: |
    server {
        listen 443 ssl;
        server_name tls-app.example.com;

        ssl_certificate     /etc/nginx/ssl/tls.crt;
        ssl_certificate_key /etc/nginx/ssl/tls.key;
        ssl_protocols       TLSv1.3;

        location / {
            return 200 'TLS App Running\n';
            add_header Content-Type text/plain;
        }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tls-app
  namespace: tls-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tls-app
  template:
    metadata:
      labels:
        app: tls-app
    spec:
      containers:
      - name: tls-app
        image: nginx:1.25
        ports:
        - containerPort: 443
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx/conf.d/default.conf
          subPath: nginx.conf
        - name: tls-cert
          mountPath: /etc/nginx/ssl
          readOnly: true
      volumes:
      - name: nginx-config
        configMap:
          name: tls-config
      - name: tls-cert
        secret:
          secretName: tls-app-cert
---
apiVersion: v1
kind: Service
metadata:
  name: tls-app
  namespace: tls-app
spec:
  type: NodePort
  selector:
    app: tls-app
  ports:
  - protocol: TCP
    port: 443
    targetPort: 443
    nodePort: 31443
EOF

# Verify pod is running
kubectl get pods -n tls-app

#Confirm TLS 1.3 works but TLS 1.2 is blocked (initial broken state):
kubectl port-forward svc/tls-app -n tls-app 8443:443 &

# TLS 1.3 should work
curl --tlsv1.3 -k https://localhost:8443
# TLS App Running ✓

# TLS 1.2 should FAIL (only 1.3 configured)
curl --tlsv1.2 --tls-max 1.2 -k https://localhost:8443
# curl: (35) error:0A00042E:SSL routines::tlsv1 alert protocol version ✗

#Now practice the exam fix:

# Edit ConfigMap — add TLSv1.2
kubectl edit configmap tls-config -n tls-app
# Change: ssl_protocols TLSv1.3;
# To:     ssl_protocols TLSv1.2 TLSv1.3;

# Restart deployment to pick up change
kubectl rollout restart deployment tls-app -n tls-app
kubectl rollout status deployment tls-app -n tls-app

# Re-forward after restart
kill %1
kubectl port-forward svc/tls-app -n tls-app 8443:443 &

# Verify TLS 1.2 now works
curl --tlsv1.2 --tls-max 1.2 -k https://localhost:8443
# TLS App Running ✓

# Verify TLS 1.3 still works
curl --tlsv1.3 -k https://localhost:8443
# TLS App Running ✓

Quick Reference Card

# Topic Key Command / Concept
1 Argo CD Setup argocd app create --dry-run -o yaml
2 Sidecar Container emptyDir shared volume + tail -f
3 PVC Bound to PV Match storageClassName + set volumeName
4 Resource Distribution kubectl describe node → divide allocatable by replicas
5 HPA Stabilization behavior.scaleDown.stabilizationWindowSeconds: 30
6 CRDs — cert-manager kubectl get crds \| grep cert-manager.io
7 PriorityClass Patch kubectl patch --type=merge only
8 CNI Calico Match CIDR from kube-controller-manager.yaml, restart kubelet
9 cri-dockerd Install dpkg -i + sysctl net.ipv4.ip_forward = 1
10 SC/PV/PVC WaitForFirstConsumer → PVC Pending until Pod created
11 Ingress Use ingressClassName field
12 Gateway API Gateway + HTTPRoute, then delete old Ingress
13 Network Policy Apply podSelector to backend; from.podSelector for frontend
14 Broken Cluster etcd client port = 2379, not 2380 in apiserver manifest
15 NodePort Service Set nodePort: 30080 in service spec
16 TLS Update TLSv1.2 TLSv1.3 in ConfigMap + kubectl rollout restart

General Exam Tips

# Always check your namespace first
kubectl config get-contexts
kubectl config set-context --current --namespace=<target-ns>

# Verify you are on the correct cluster
kubectl config current-context

# SSH to nodes for node-level tasks
ssh node01
sudo -i   # escalate if needed

# Static pod manifests location
ls /etc/kubernetes/manifests/

# Kubelet restart (after manifest changes)
sudo systemctl restart kubelet
sudo systemctl status kubelet

Discussion

Have thoughts on this post? Share them below — questions, corrections, or your own experience are all welcome.