CKA Mock Exam — Complete Questions & Solutions¶
Exam Rules Reminder
- Use
sudoto edit files in/etc/kubernetes/manifests/ - Use
sudofor 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
- Argo CD Setup
- Multi-Container Pod (Co-located Containers)
- MariaDB with Persistent Storage
- Resource Distribution Across Pods
- Horizontal Pod Autoscaler (HPA)
- Custom Resource Definitions (CRDs)
- PriorityClass and Deployment Update
- CNI Installation (Calico / Tigera Operator)
- Install cri-dockerd
- Storage Setup (SC, PV, PVC)
- Ingress Configuration
- Migrate from Ingress to Gateway API
- Network Policy
- Troubleshoot Broken Cluster
- Expose Pod via NodePort
- 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
¶
#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:
- Install Argo CD in namespace
argocd, the installation yaml file at https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml - Add the Git repository
https://github.com/example-org/app-config.gitto Argo CD - Generate a Kubernetes manifest template for an application named
myappfrom that repo, without installing CRDs, targeting namespacemyapp-prod - 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:
- Update the existing Deployment
app-loggerto add a sidecar container namedlog-reader - Configure a shared
emptyDirvolume namedshared-logsbetween both containers - The main container
appwrites logs to/var/log/app/app.log - The sidecar
log-readerrunstail -f /var/log/app/app.log - 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 directorybriefly at startup. This is normal — ignore it unless the pod is inCrashLoopBackOff.
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:
- Create a PVC that binds to
mariadb-pv - Update the Deployment to use the PVC with mount path
/var/lib/mysql - 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:
- Update Deployment
web-appto run 3 replicas - Set resource requests so all 3 pods can be scheduled
- 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 nodefirst. 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:
- List all CRDs related to
cert-managerand save to/opt/artifacts/cert-manager-crds.txt - Explain the
spec.subjectfield 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:
- Create a PriorityClass with value =
1000000 - 1=999999 - Update Deployment
critical-appto use this PriorityClass - 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:
- Install Calico via Tigera Operator
- Update the cluster CIDR to match value from
kube-controller-manager - 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:
- Install
cri-dockerdfrom.debpackage at/opt/packages/cri-dockerd.deb - Enable IP forwarding via sysctl
- 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:
- Create a StorageClass with
WaitForFirstConsumerbinding mode - Create a PersistentVolume
- Create a PersistentVolumeClaim — observe it stays Pending
- Create a Pod using the PVC
- 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:
- Create a Gateway with TLS on port 443
- Create an HTTPRoute with the same routing rules
- 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:
- Find the incorrect etcd endpoint in the API server manifest
- Correct it using the IP/port from the etcd manifest
- 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). Port2380= etcd peer port (etcd-to-etcd replication only). Do NOT use2380for--etcd-servers.
Question 15: Expose Pod via NodePort¶
Context: Cluster k8s-cluster-1, namespace default.
Task:
- Create a Pod named
web-podrunningnginx - Expose it via a NodePort Service on node port
30080 - 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:
- Update the ConfigMap to support both TLS 1.2 and TLS 1.3
- Restart the Deployment to pick up the change
- 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.