CKA study materials
CKA study resources

CKA (CNCF) common mistakes

CKA · Understand the traps and how to avoid them.

Mistake 1

Starts creating or editing resources immediately without confirming which cluster and namespace kubectl is currently pointed at, and without pinning them for the session.

The exam environment supplies a kubeconfig containing several contexts (and tasks frequently name a non-default namespace). The default namespace is default, so manifests apply cleanly and look successful while the grader reads a different namespace and finds nothing.

What to do instead

Before the first task, run kubectl config get-contexts and kubectl config use-context <ctx>. Then set the working namespace once with kubectl config set-context --current --namespace=<ns> and re-check it whenever a new task names a namespace, or pass -n <ns> on every command. Confirm placement with kubectl get <resource> -n <ns> right after applying.

Mistake 2

Restores an etcd snapshot but leaves the restore half-finished: the new data directory is created on disk yet the etcd static pod manifest still points at the old hostPath, or etcd is not restarted/verified afterwards.

etcdctl snapshot restore only writes a new data directory. The running etcd is a static pod whose volumes, --data-dir, and --initial-cluster flags live in /etc/kubernetes/manifests/etcd.yaml; unless that file is edited to mount the restored directory, the API server keeps serving the old state and the cluster looks unchanged.

What to do instead

Work in this order: take a fresh copy of /etc/kubernetes/manifests/etcd.yaml and, if possible, a new etcdctl snapshot save; run ETCDCTL_API=3 etcdctl snapshot restore <file> --data-dir=/var/lib/etcd-restore; edit the etcd manifest's hostPath volume and --data-dir (also fix --initial-cluster if the restore node differs) to the restored path; wait for the container to reappear, then verify with kubectl get nodes and by listing a known object. Do not delete the original data directory until the cluster is healthy.

Mistake 3

Creates a Role (or ClusterRole) for a task but never creates the matching RoleBinding/ClusterRoleBinding, binds it to the wrong subject kind or namespace, or uses the wrong apiGroups for the resources, and never proves the result works.

A Role grants nothing on its own, and RBAC is silent about mistakes: kubectl apply succeeds, the objects exist, and the permissions simply do not take effect. A ServiceAccount subject also needs the correct namespace field (omitted, it defaults to the binding's namespace), and core resources require apiGroups: [""] while Deployments require "apps".

What to do instead

Always create the pair, then test as the subject itself: kubectl auth can-i list pods --as=system:serviceaccount:<ns>:<sa> -n <ns> (and a negative check for something that should be denied). Read the apiGroups from kubectl api-resources | grep <kind> rather than guessing, and confirm pod-level permissions with kubectl exec from a pod running as that ServiceAccount when the task allows it.

Mistake 4

Hand-writes every manifest from a blank file, guessing apiVersion, field names, and indentation instead of generating a skeleton with an imperative command and editing it.

Under time pressure this produces avoidable errors - wrong apiVersion for the cluster version, misnested spec.containers, tabs instead of spaces - and each rejected apply costs a full debug cycle. Candidates also forget flags like --port, --target-port, --type, and --replicas, then hand-add them incorrectly.

What to do instead

Generate, then edit: kubectl create deployment web --image=nginx --replicas=3 --dry-run=client -o yaml > web.yaml, kubectl create service clusterip web --tcp=80:80 --dry-run=client -o yaml, kubectl run tmp --image=busybox --dry-run=client -o yaml -- sleep 3600. Validate the field names with kubectl explain <kind>.spec.<field> (add --recursive for trees) and apply with kubectl apply -f or kubectl create -f.

Mistake 5

A Pod stays Pending and the candidate adds scheduling fields by trial and error - adding a toleration but no matching nodeSelector (or the reverse), or misspelling a label key - without first reading the scheduler's message.

Pending is the scheduler saying no node satisfied every constraint at once. The exact reason is already in the event (0/3 nodes are available: 1 node(s) had untolerated taint ... 2 node(s) didn't match Pod's node affinity/selector), but candidates skip describe and patch blindly, so each edit addresses only half of the problem.

What to do instead

Run kubectl describe pod <pod> | tail -20 and read the FailedScheduling message first; then inventory the nodes with kubectl get nodes --show-labels and kubectl describe node <node> | grep -i taint. Satisfy every constraint together (toleration for the taint and the nodeSelector/affinity for the label) and re-check that the Pod reaches Running on the intended node.

Mistake 6

Creates a Service and moves on without checking that it actually has endpoints, typically because the selector does not match the Pod labels or `targetPort` does not match the container's listening port.

A Service with no backing endpoints is created successfully and looks fine in get svc, so there is no error to react to. The mismatch is invisible unless you look at EndpointSlices or the Pod labels directly, and targetPort defaults to port, which is a frequent silent mismatch.

What to do instead

After creating the Service, run kubectl get endpointslices -n <ns> -l kubernetes.io/service-name=<svc> (or get endpoints) and confirm the Pod IPs and correct port are listed. Cross-check with kubectl get pods --show-labels against kubectl get svc <svc> -o yaml for selector and targetPort, and finish with a real request: kubectl run t --rm -it --image=busybox:1.28 --restart=Never -- wget -qO- <svc>:<port>.

Mistake 7

Writes a NetworkPolicy that does not express the intended isolation - placing `namespaceSelector` and `podSelector` in the same peer when OR was meant, omitting an egress rule for DNS, or forgetting that an empty `podSelector: {}` selects every Pod in the namespace as the policy target.

Peers combine with OR, but the selectors inside one peer combine with AND, so namespaceSelector plus podSelector in a single peer allows only Pods matching both. Once any policy selects a Pod, all traffic not explicitly allowed - including DNS on port 53 - is denied, which makes name-based connections fail while IP-based ones work.

What to do instead

Separate peers in the from/to list when OR is intended. When the policy has Egress, add a peer allowing UDP/TCP 53 to the cluster DNS namespace, or verify with kubectl get svc -n kube-system kube-dns. Test both directions with throwaway Pods labelled to match the policy, and confirm the negative case (a non-matching Pod must fail) so the isolation is actually proven.

Mistake 8

Submits a PVC that remains Pending and then guesses at the cause instead of reconciling the requested storage characteristics with what the cluster offers.

Binding fails on a mismatch between storageClassName (including the empty-string versus unset distinction), requested accessModes, requested size versus PV capacity, or volumeMode; if no default StorageClass exists, an unset class never binds. kubectl get pvc shows Pending but not the reason.

What to do instead

Read kubectl describe pvc <pvc> | tail -15 for the provisioner event. Compare against kubectl get storageclass (look for (default)) and kubectl get pv -o wide for capacity, access modes, and the PV's nodeAffinity. Then align the manifest - set storageClassName to an existing class, request an accessMode the provisioner supports, and re-apply (deleting the stuck PVC if it is still unbound). Confirm the Pod mounting it starts and that kubectl exec can read/write the mount.

Mistake 9

Diagnoses a broken node or control-plane component from the wrong layer - reading container logs of unrelated system Pods, or expecting `kubectl` to reveal the fault - instead of going onto the affected node to inspect kubelet, the container runtime, and the static pod manifests.

When kubelet is down or misconfigured, the node goes NotReady and nothing on it is reported to the API server, so kubectl get pods and kubectl logs show stale or missing data. The actual cause lives in systemctl status kubelet, journalctl -u kubelet, /var/lib/kubelet/config.yaml, or /etc/kubernetes/manifests.

What to do instead

Establish the layer first: kubectl get nodes and kubectl get pods -n kube-system -o wide to see what is missing and from which node. Then SSH to that node and check systemctl status kubelet and journalctl -u kubelet -n 50 --no-pager, validate the kubelet config (YAML syntax, staticPodPath, clientCAFile paths), and use crictl ps -a / crictl logs for runtime-level failures. Fix the file, systemctl daemon-reload && systemctl restart kubelet, and re-check node status from the API server.

Mistake 10

Stops as soon as the apply succeeds and moves to the next task, without confirming the end state the task actually asked for (Pod Running and Ready, volume mounted, Service reachable, permissions effective).

Kubernetes accepts manifests asynchronously - a Deployment with a bad image tag, an unschedulable pod, or a fix applied to a file that was never re-applied all produce a clean kubectl apply output. Editing a running Pod's fields that are immutable (image is fine, but adding a volume, changing resources on some paths, or altering a Deployment's selector) also silently fails or requires recreation.

What to do instead

End every task with an explicit verification pass: kubectl get <resource> -n <ns> -o wide for STATUS/READY and node placement, kubectl describe for recent events, kubectl logs when the container starts, and a functional test (curl/wget to the Service, kubectl exec ... cat on the mounted volume, kubectl auth can-i for RBAC). If a change is rejected as immutable, delete and recreate the object from the corrected manifest rather than leaving the old one in place.