← Road to Kubestronaut

CHEATSHEET · DEVOPS · LAST-MINUTE REVISION

CKA — The Certified Kubernetes Administrator Cheatsheet.

kubernetes cka certification kubectl exam-prep

Road to Kubestronaut · Certification 3

CKA: Administration

Guide 3 of 5

The CKA is a 2-hour, hands-on, performance-based exam on the current Kubernetes v1.35 curriculum — no multiple choice, just a live cluster and a terminal. 15–20 tasks, weighted by domain, pass mark 66%, one free retake. Five domains: Troubleshooting 30% · Cluster Architecture 25% · Services & Networking 20% · Workloads & Scheduling 15% · Storage 10%. The whole game is speed and correctness at the keyboard — this sheet is command-first. Kubernetes docs (kubernetes.io) are allowed during the exam; bookmark them. Study order and exam-day playbook at the bottom.

0. Exam-day setup (do this in the first 60 seconds)

Every task runs against a specific cluster/context. Two habits save the most points: switch context first, and generate YAML imperatively instead of typing it.

# the alias that pays for itself
alias k=kubectl
export do="--dry-run=client -o yaml"   # generate manifests fast
export now="--force --grace-period=0"  # delete pods immediately
source <(kubectl completion bash) ; complete -o default -F __start_kubectl k

# ALWAYS switch to the cluster the question names, first thing
kubectl config get-contexts
kubectl config use-context <cluster-name>

# vim: set these in ~/.vimrc so YAML indentation doesn't fight you
# set expandtab tabstop=2 shiftwidth=2 number autoindent
context first, every task The single most common avoidable failure: doing a perfect task in the wrong cluster. Re-run kubectl config use-context at the start of every question. Also note the task's namespace — add -n <ns> everywhere or k config set-context --current --namespace=<ns>.

1. kubectl speed & imperative generation

You rarely write YAML from scratch. Generate a skeleton with $do, edit, apply.

# generate manifests (don't create) — then edit the file
k create deploy web --image=nginx --replicas=3 $do > web.yaml
k run pod1 --image=nginx $do > pod.yaml
k create svc clusterip web --tcp=80:80 $do > svc.yaml
k create cm app --from-literal=KEY=val $do > cm.yaml
k create secret generic db --from-literal=pass=s3cr3t $do > sec.yaml
k create job pi --image=perl -- perl -e 'print 1' $do > job.yaml
k create cronjob c --image=busybox --schedule="*/1 * * * *" -- date $do > cj.yaml

# quick imperative actions (no file needed)
k expose deploy web --port=80 --target-port=8080          # make a Service
k scale deploy web --replicas=5
k set image deploy/web nginx=nginx:1.27
k label pod pod1 tier=frontend
k annotate pod pod1 note="exam"
k rollout restart deploy/web

# inspect fast
k get pods -A -o wide                    # all namespaces, node + IP
k get pods --show-labels
k get po -l app=web                      # label selector
k describe pod pod1                       # events at the bottom = gold
k explain pod.spec.containers             # field reference, offline
--dry-run=client -o yaml is your pen You will not have time to hand-write PodSpecs. Generate, redirect to a file, edit the 2–3 fields you need, k apply -f. kubectl explain --recursive is the offline field lookup when you forget a nesting.

2. Cluster Architecture, Installation & Config (25%)

The control plane, in one breath

ComponentJob
kube-apiserverThe front door — every request goes through it; the only thing that talks to etcd.
etcdThe database — all cluster state. Back this up (high-value exam task).
kube-schedulerPicks a node for each unscheduled Pod (filters + scores).
kube-controller-managerReconciliation loops (Deployment→ReplicaSet→Pods, node, endpoints…).
kubeletNode agent — runs containers, reports status. A systemd service, not a pod.
kube-proxyPrograms Service networking (iptables/IPVS) on each node.
CNI pluginPod networking (Calico/Cilium/Flannel). No CNI ⇒ nodes stay NotReady.

On a kubeadm cluster the control-plane components run as static pods — manifests in /etc/kubernetes/manifests/ on the control-plane node. Edit a file there and the kubelet restarts that pod automatically.

kubeadm — create, join, HA

# init a control plane (pick a pod CIDR your CNI expects)
kubeadm init --pod-network-cidr=10.244.0.0/16 --control-plane-endpoint="LB:6443"
mkdir -p ~/.kube && cp -i /etc/kubernetes/admin.conf ~/.kube/config
# then apply a CNI (e.g. calico/cilium manifest) — nodes go Ready after

# join a worker (token printed by init; regenerate if expired)
kubeadm token create --print-join-command
kubeadm join LB:6443 --token <t> --discovery-token-ca-cert-hash sha256:<h>

# join another control plane (HA) — add --control-plane --certificate-key
kubeadm init phase upload-certs --upload-certs   # prints the cert key
HA = stacked etcd + a load balancer A highly-available control plane means ≥3 control-plane nodes (odd number, for etcd quorum) behind a load balancer on :6443, joined with --control-plane. etcd quorum needs a majority — 3 nodes tolerate 1 failure, 5 tolerate 2.

Cluster upgrade (kubeadm) — control plane then workers

# --- on the first control-plane node ---
apt-get update && apt-get install -y kubeadm=1.35.x-*   # upgrade kubeadm first
kubeadm upgrade plan
kubeadm upgrade apply v1.35.x
# then upgrade the node's kubelet + kubectl
kubectl drain <cp-node> --ignore-daemonsets
apt-get install -y kubelet=1.35.x-* kubectl=1.35.x-*
systemctl daemon-reload && systemctl restart kubelet
kubectl uncordon <cp-node>

# --- on each worker (one at a time) ---
kubeadm upgrade node                       # NOT 'upgrade apply' on workers
kubectl drain <worker> --ignore-daemonsets --delete-emptydir-data
# (install kubelet/kubectl, restart kubelet, then)
kubectl uncordon <worker>
order + drain Upgrade kubeadm first, then apply (control plane) / node (workers), then the kubelet. Always drain before and uncordon after. Skipping minor versions (1.33→1.35) is unsupported — go one minor at a time.

etcd backup & restore (high-value — practice cold)

# find the certs (from the etcd static-pod manifest or describe)
export E="--cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  --endpoints=https://127.0.0.1:2379"

# BACKUP
ETCDCTL_API=3 etcdctl $E snapshot save /opt/snap.db
ETCDCTL_API=3 etcdctl $E snapshot status /opt/snap.db -w table

# RESTORE to a new data dir
ETCDCTL_API=3 etcdctl snapshot restore /opt/snap.db \
  --data-dir=/var/lib/etcd-restore
# then point the etcd static pod at the new dir:
#   edit /etc/kubernetes/manifests/etcd.yaml -> hostPath + --data-dir
#   kubelet restarts etcd automatically
restore ≠ save flags snapshot restore does not need the cert flags — only the snapshot + --data-dir. After restore you must repoint the etcd manifest's hostPath volume and the --data-dir arg to the new directory, then wait for the static pod to come back.

RBAC — who can do what

# Role (namespaced) + RoleBinding
k create role dev --verb=get,list,watch --resource=pods -n team
k create rolebinding dev-bind --role=dev --user=jane -n team

# ClusterRole (cluster-wide) + ClusterRoleBinding
k create clusterrole reader --verb=get,list --resource=nodes
k create clusterrolebinding read-nodes --clusterrole=reader --user=jane

# bind a ClusterRole into ONE namespace with a RoleBinding
k create rolebinding x --clusterrole=reader --serviceaccount=team:sa1 -n team

# TEST permissions (do this to verify!)
k auth can-i list pods --as=jane -n team
k auth can-i '*' '*' --as=system:serviceaccount:team:sa1
Role vs ClusterRole scope Role + RoleBinding = one namespace. ClusterRole + ClusterRoleBinding = whole cluster. A ClusterRole bound by a RoleBinding applies only in that binding's namespace — the common way to grant a reusable role per-namespace. Verify with kubectl auth can-i --as=....

CRDs, Operators, Helm & Kustomize

  • CRD — extends the API with a new kind. k get crd, then k get <newkind> -A. An Operator is a CRD + a controller watching it.
  • Helm — package manager. helm repo add & update, helm install <rel> <chart> -n ns, helm upgrade, helm list -A, helm uninstall, helm template (render without installing).
  • Kustomize — template-free overlays. kubectl apply -k ./dir (dir has a kustomization.yaml listing resources + patches). kubectl kustomize ./dir renders it.

3. Workloads & Scheduling (15%)

Deployments & rollouts

k create deploy web --image=nginx:1.26 --replicas=3
k set image deploy/web nginx=nginx:1.27           # trigger a rollout
k rollout status deploy/web
k rollout history deploy/web
k rollout undo deploy/web --to-revision=1          # roll back
k scale deploy/web --replicas=6
k autoscale deploy/web --min=2 --max=10 --cpu-percent=70   # HPA

ConfigMaps & Secrets into Pods

# env from a key, and mount as a volume
spec:
  containers:
  - name: app
    image: nginx
    envFrom: [{ configMapRef: { name: app } }]      # all keys as env
    env:
    - name: PASS
      valueFrom: { secretKeyRef: { name: db, key: pass } }
    volumeMounts: [{ name: cfg, mountPath: /etc/cfg }]
  volumes:
  - name: cfg
    configMap: { name: app }
Secrets are base64, not encrypted k get secret x -o jsonpath='{.data.pass}' | base64 -d to read. Base64 is encoding, not security — encryption-at-rest is a separate EncryptionConfiguration on the apiserver.

Scheduling — where a Pod lands

MechanismWhat it does
nodeSelectorHard match on node labels: nodeSelector: {disk: ssd}.
nodeNameBypass the scheduler entirely — pin to one node by name.
Affinity / anti-affinitySoft/hard rules (required…/preferred…) over node or pod labels.
Taint + TolerationTaint repels pods from a node; a matching toleration lets a pod stay.
Resourcesrequests drive scheduling; limits cap usage (CPU throttled, memory OOMKilled).
# taints (control-plane nodes carry one by default)
k taint node n1 key=val:NoSchedule
k taint node n1 key=val:NoSchedule-        # remove (trailing minus)
# toleration on the pod:
#   tolerations: [{key: key, operator: Equal, value: val, effect: NoSchedule}]

# cordon/drain (also a Troubleshooting skill)
k cordon n1                                 # no new pods
k drain n1 --ignore-daemonsets --delete-emptydir-data
k uncordon n1

Static pods, DaemonSets, Jobs

  • Static pod — managed by the kubelet directly from /etc/kubernetes/manifests/ (or staticPodPath in the kubelet config). Name gets the node suffix. Delete the file to remove it.
  • DaemonSet — one pod per (matching) node; how kube-proxy and CNI agents run.
  • Job (run-to-completion) / CronJob (scheduled). completions, parallelism, backoffLimit.

4. Services & Networking (20%)

Services — the four types

TypeReach
ClusterIP (default)In-cluster virtual IP. The DNS name is svc.ns.svc.cluster.local.
NodePortOpens a port (30000–32767) on every node → routes to the Service.
LoadBalancerCloud LB in front of a NodePort (needs a cloud/MetalLB provider).
ExternalNameCNAME alias to an external DNS name; no proxying.
k expose deploy web --port=80 --target-port=8080          # ClusterIP
k expose deploy web --type=NodePort --port=80
k get ep web              # endpoints = the pod IPs behind it (empty ⇒ selector wrong)
no endpoints ⇒ selector mismatch A Service finds pods by label selector. If k get endpoints <svc> is empty, the Service's selector doesn't match any pod's labels, or the pods aren't Ready. This is the #1 "service not working" cause.

Ingress & Gateway API

Ingress = L7 HTTP routing to Services, via an ingress controller (nginx, etc.). Gateway API is the newer, more expressive successor now on the curriculum — three roles: GatewayClass (the controller), Gateway (the listener/IP), and HTTPRoute (the routing rules, attached to a Gateway).

k get ingress -A ; k describe ingress web
k get gatewayclass ; k get gateway -A ; k get httproute -A
# HTTPRoute: parentRefs -> the Gateway; rules -> matches + backendRefs (Services)

NetworkPolicy — deny-by-default is opt-in

# default-deny all ingress in a namespace, then allow specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: team }
spec:
  podSelector: {}          # all pods
  policyTypes: [Ingress]
  # (no ingress rules = deny all ingress)
---
spec:                       # allow from pods labelled role=api on port 8080
  podSelector: { matchLabels: { app: db } }
  policyTypes: [Ingress]
  ingress:
  - from: [{ podSelector: { matchLabels: { role: api } } }]
    ports: [{ protocol: TCP, port: 8080 }]
policies are additive + need a CNI that enforces them With no NetworkPolicy, all traffic is allowed. The moment one selects a pod, that pod is default-deny for the chosen policyTypes, and rules only add allowances. Flannel doesn't enforce NetworkPolicy; Calico/Cilium do.

CoreDNS & service discovery

# DNS lives in kube-system as a Deployment 'coredns' + Service 'kube-dns'
k get pods -n kube-system -l k8s-app=kube-dns
k get svc kube-dns -n kube-system
# test from a throwaway pod:
k run t --image=busybox:1.28 --rm -it --restart=Never -- nslookup web.team
# pod A record: 10-244-1-5.team.pod.cluster.local ; svc: web.team.svc.cluster.local

5. Storage (10%)

The chain: a PersistentVolume (PV) is a piece of storage; a PersistentVolumeClaim (PVC) is a request that binds to a matching PV; a Pod mounts the PVC. A StorageClass enables dynamic provisioning (create a PV on demand when a PVC asks).

FieldMeaning
accessModesRWO (one node RW), ROX (many nodes RO), RWX (many nodes RW), RWOP (one pod).
reclaimPolicyRetain (keep data after PVC delete), Delete (remove the volume).
storageClassNameEmpty "" = static/no dynamic; a name = dynamic via that class.
volumeBindingModeImmediate vs WaitForFirstConsumer (bind when a pod is scheduled).
k get pv,pvc,sc
k describe pvc data                       # 'Pending' ⇒ no matching PV / no default SC
k get sc                                   # is there a (default) StorageClass?
PVC Pending = no match A PVC stuck Pending means no PV satisfies its size/accessMode/class, or there's no default StorageClass to provision one. Check describe pvc events. A PVC can't shrink; expansion needs allowVolumeExpansion: true on the StorageClass.

6. Troubleshooting (30% — the biggest domain)

Highest weight, so drill it. The method: look at events and logs before touching anything.

# the universal first four commands
k get pods -A -o wide                      # what's not Running/Ready
k describe pod <p>                          # Events section = the cause
k logs <p> [-c <container>] [--previous]    # --previous = last crash
k get events -A --sort-by=.lastTimestamp

# node level
k get nodes ; k describe node <n>          # conditions: MemoryPressure, DiskPressure, Ready
ssh <node>; systemctl status kubelet; journalctl -u kubelet -f
crictl ps -a ; crictl logs <id>            # container runtime, on the node

Pod statuses → cause

StatusUsual cause
PendingUnschedulable — no node fits (resources, taints, nodeSelector), or PVC unbound.
ImagePullBackOffBad image name/tag, private registry without a pull secret.
CrashLoopBackOffContainer starts then exits — check logs --previous; bad command, missing config, failing probe.
OOMKilledExceeded its memory limit — raise the limit or fix the leak.
0/1 ReadyRunning but readiness probe failing — app not serving on the probe path/port yet.
Init:…An init container hasn't finished — check its logs specifically.

NotReady node

# almost always the kubelet or the CNI
ssh <node>
systemctl status kubelet && journalctl -u kubelet --no-pager | tail -40
systemctl restart kubelet
# check: kubelet config (/var/lib/kubelet/config.yaml), certs, CNI plugin present,
# container runtime up (systemctl status containerd)
NotReady = kubelet, CNI, or runtime Node NotReady is nearly always: kubelet down/misconfigured, container runtime down, or no CNI. journalctl -u kubelet tells you which. A fresh cluster with all nodes NotReady = you forgot to install a CNI.

Broken control plane (static pods)

# apiserver down? kubectl won't respond. Inspect static pods on the node:
ssh <cp-node>
ls /etc/kubernetes/manifests/                         # apiserver/etcd/scheduler/ccm
crictl ps -a | grep -E 'apiserver|etcd|scheduler'
crictl logs <id>                                       # why it won't start
# fix the manifest yaml (bad flag, wrong etcd endpoint), kubelet re-creates the pod

Service / DNS not reachable

k get ep <svc>                             # empty ⇒ selector/labels mismatch
k get pods -n kube-system -l k8s-app=kube-dns   # CoreDNS healthy?
k run t --image=busybox:1.28 --rm -it --restart=Never -- \
   sh -c 'nslookup <svc>.<ns>; wget -qO- <svc>.<ns>:<port>'
# also: NetworkPolicy blocking it? kube-proxy running on the node?

7. jsonpath, sorting & scripting shortcuts

# custom columns / jsonpath (exam loves "get X and write it to a file")
k get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}'
k get pods -o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName'
k get pods --sort-by=.metadata.creationTimestamp
k get pods --field-selector status.phase=Running
k top nodes ; k top pods -A                # needs metrics-server
k get po -o yaml | k neat 2>/dev/null || k get po -o yaml   # strip noise if kubectl-neat present

8. What to keep in mind (exam mindset)

  • Switch context first, every task. Then check the namespace. Most lost points are "right answer, wrong cluster/ns."
  • Never hand-write YAML you can generate. $do a skeleton, edit, apply. Speed is the exam.
  • Read events first. describe and logs --previous answer most troubleshooting tasks before you change anything.
  • Verify every task. Re-run a get/auth can-i/nslookup to confirm the change actually worked — partial credit is per-task.
  • Flag and move on. ~6–8 min per task average. If one fights you, note it and return; a 30%-weighted troubleshooting task is worth more than a stuck 4% one.
  • Use the docs. kubernetes.io is allowed — bookmark NetworkPolicy, PV, RBAC, and the kubeadm upgrade page; copy YAML from there and edit.
  • Static pods restart themselves. Edit the manifest in /etc/kubernetes/manifests/; don't kubectl delete and expect it gone.
  • Practice etcd backup/restore cold. It's a near-guaranteed, high-value, all-or-nothing task.

9. Common mistakes

  • Wrong context / namespace — the biggest silent point-loser. Set both at task start.
  • Forgetting --ignore-daemonsets (and often --delete-emptydir-data) on drain — it errors out otherwise.
  • Upgrading kubelet before kubeadm upgrade apply — wrong order; kubeadm first, then kubelet.
  • Passing cert flags to etcdctl snapshot restore — restore only needs the snapshot + --data-dir; and don't forget to repoint the etcd manifest.
  • Service selector ≠ pod labels — empty endpoints. Always k get ep to confirm.
  • Expecting NetworkPolicy to work on Flannel — it won't enforce; needs Calico/Cilium.
  • PVC size/accessMode/class not matching any PV — stays Pending; read describe pvc events.
  • Editing an immutable field (e.g. a Job's selector, a Pod's containers) — delete & recreate instead.
  • Trusting a task is done without verifying — always re-query. Partial, silent failures cost whole tasks.
  • Reading a Secret and forgetting base64 -d — the value in .data is base64-encoded.
  • Deleting a static pod with kubectl — the kubelet re-creates it from the manifest file; remove/edit the file.
  • Not using imperative generation — hand-writing PodSpecs burns the clock you needed for troubleshooting.

10. Study priority order

  1. Troubleshooting (30%) — pod statuses, NotReady nodes, control-plane static pods, service/DNS
  2. etcd backup & restore — drill until automatic
  3. kubeadm upgrade + drain/cordon/uncordon
  4. RBAC (Role/ClusterRole/bindings + auth can-i)
  5. Services & NetworkPolicy (endpoints, selectors, deny-by-default)
  6. Scheduling (taints/tolerations, affinity, nodeSelector, resources)
  7. Storage (PV/PVC/StorageClass binding, accessModes, reclaim)
  8. Deployments & rollouts, ConfigMap/Secret injection
  9. Ingress / Gateway API, CoreDNS
  10. CRDs/Operators, Helm, Kustomize (recognize + basic ops)

Exam: 2 hours, ~15–20 performance-based tasks on Kubernetes v1.35, pass at 66%, one free retake. Lean on imperative kubectl, verify every task, and spend your time where the weight is — troubleshooting and cluster architecture are more than half the marks.

11. Official sources & freshness

This guide was re-checked against the official Linux Foundation exam page and CNCF curriculum on 19 July 2026. The performance environment is currently Kubernetes v1.35; re-check before booking because versions and policies change.

← prev: KCSA next: CKAD →
© cvam — written in plaintext, served warm