CKA — Kubernetes Administrator
CKA Mock Exam B
This is the second set
This is round B, and there is not a single task that overlaps with round A. The domain allocation is matched exactly to A, but the competencies asked are all different. Where A asked about a namespace-scoped Role, this one asks about a cluster-scoped ClusterRole and service account tokens, and where A asked about taints and tolerations, this one asks about spreading by zone and priority. Solving a problem you have already solved is not practice, so after you finish A, time yourself and take this set once more.
The real CKA has you solve 15–20 tasks in 120 minutes, and you pass if you exceed 66%. This set is also matched to 17 tasks, 120 minutes, and a 66% passing score. It uses partial credit, so you do not need to get everything right. Passing 12 of the 17 counts as complete.
Do not look at the hints; work through to the end first. The real exam has no hints. It is better for your score to skip a task you are stuck on and come back to it if time remains. Use the hints and answers for review after you have finished the exam once.
Things that cost you time if you first learn them at the real exam site
- The exam is a remote desktop, and for each task you work by running
sshto the designated host. Nested ssh is not supported. When you finish a task, be sure to return to where you started withexit. - The
kalias and bash autocompletion are already set up. The advice to create an alias as soon as you start does not fit the current environment. This lab Pod is set up the same way. yq,helm,etcdctl,openssl,curl, andmanare also already installed.- Terminal copy is
Ctrl+Shift+C, and paste isCtrl+Shift+V. Ctrl+Wcloses the browser tab. UseCtrl+Alt+Wto delete a word.- The INSERT key is blocked, so in vim you enter insert mode with
i. - Each question has a different weight, and several paths to the correct answer are allowed.
Domain allocation
| Domain | Actual weight | Tasks in this set |
|---|---|---|
| Cluster Architecture, Installation and Configuration | 25% | Tasks 1–4 |
| Workloads and Scheduling | 15% | Tasks 5–7 |
| Services and Networking | 20% | Tasks 8–10 |
| Storage | 10% | Tasks 11–12 |
| Troubleshooting | 30% | Tasks 13–17 |
Tasks 13 through 17
In the real exam, the broken resources are prepared in advance. This environment has no such mechanism, so the broken manifest is included in each task's instructions. Apply it as it is first, then diagnose the symptom and fix it. Even if you skip applying it and create the right thing from the start, grading passes, but then it does not serve as diagnostic practice. For tasks 13 through 16 the namespace broken must be created in advance, and for task 17 the namespace audit, for the manifest to go in.
Tasks that create files
Task 3 leaves files under /root/exam. The directory is not created in advance, so create it first. The grader does not trust the values written in the files as they are; it recomputes the values from the cluster and the snapshot and compares them.
What differs in this environment
The cluster inside the Pod is a single-user cluster brought up by kwokctl. The control plane is real, so manifests, RBAC, scheduling, quotas, CRDs, PV binding, and drains all actually work. However, workload containers do not run, so you cannot use kubectl exec, logs, or port-forward. The tasks do not require them either. The internet is also blocked, so you cannot pull new images or download from a chart repository.
Before you start, check with kubectl get nodes that the 3 nodes are Ready. If they are not yet, the cluster is still coming up (it takes about 2 minutes).
Grading
When you press the check button of each task, the grader recomputes the values from the live cluster and compares them. It looks not at what you wrote in a file but at what state the cluster is in. Several paths to the correct answer are allowed.
A cluster-scoped read-only account
Create the namespace infra and a service account auditor. Pods that use this service account must not have a token mounted automatically.
The ClusterRole infra-reader allows get, list, and watch on nodes and persistentvolumes in the core group and storageclasses in the storage.k8s.io group. There must be no other permissions. Bind it to that service account with the ClusterRoleBinding infra-reader.
You turn off the automatic token mount with the service account's automountServiceAccountToken. A mistake that widens permissions does not show up if you check only what is allowed, so use kubectl auth can-i ... --as=system:serviceaccount:infra:auditor to also ask about the verbs that must not work. You ask about cluster-scoped resources without a namespace.
Requesting and approving a user certificate
A new team member dev wants to get a client certificate to connect to the cluster. Create a certificate request with CN dev and submit it as the CertificateSigningRequest dev. The signerName is kubernetes.io/kube-apiserver-client, the usages must include client auth, and after submitting, finish approving it.
Then, in the namespace dev-team, create the Role dev and RoleBinding dev so that that user can get, list, and watch Pods. There must be no other permissions.
You create the request with openssl, and in spec.request you put the PEM as base64 on one line (base64 -w0). Approval is kubectl certificate approve. The subject kind of the binding is User, not ServiceAccount.
An etcd snapshot and its revision number
Create the namespace ops, and in it put a ConfigMap pre-snapshot with the data stage=before.
Then save a snapshot of this cluster's etcd to /root/exam/etcd-snapshot.db, and write only the revision number contained in that snapshot, as one line of digits, to /root/exam/etcd-revision.txt.
The etcd of this cluster accepts connections at 127.0.0.1:2379 without authentication. Taking a snapshot needs a live server (etcdctl snapshot save), while reading the metadata of the saved file needs only the file (etcdutl snapshot status). The revision number is in that metadata, so extract it rather than copying it by eye.
Installing a release from a Helm chart
In the namespace platform, install the Helm release web.
Create the chart new and put it at /root/exam/charts/web. The Deployment that comes up from the install must have 3 replicas and the image nginx:1.27, and must carry the label app.kubernetes.io/instance: web.
The default chart that helm create makes has an image repository of nginx and an empty tag, so it uses the chart's appVersion. You can change the replicas and tag by editing the values file or by overriding them with --set at install time. There is no internet, so you cannot pull from a chart repository.
A workload that scales up and down by CPU utilization
In the namespace shop, create the Deployment cart. The image is nginx:1.27, there are 2 replicas, and the container's requests.cpu is 200m.
Create a HorizontalPodAutoscaler cart that targets that Deployment. Minimum 2 and maximum 10, with a target of 70% average CPU utilization, and a stabilization window of 300 seconds for scale-down.
A Utilization target can be computed only if the Pod has requests.cpu. Without a request, the HPA stays at unknown forever. The stabilization window is under behavior.scaleDown, and that is a field that exists only in autoscaling/v2.
Spread evenly by zone
In the namespace shop, create the Deployment feed. The image is nginx:1.27, there are 6 replicas, and the Pod label is app=feed.
The Pods must be spread evenly based on the nodes' topology.kubernetes.io/zone label. The difference in count between zones must not exceed 1, and if the condition cannot be satisfied, the Pod is not scheduled.
topologySpreadConstraints takes four things. What to divide by (topologyKey), how much skew is allowed (maxSkew), what to do when it cannot be kept (whenUnsatisfiable), and which Pods are counted together (labelSelector). If you leave out the last one, what gets counted changes.
Priority and preemption policy
Create two PriorityClasses.
high-priority: value 100000, can displace lower-priority Pods, and its description must not be empty.low-priority: value 100, never displaces Pods lower than itself.
Neither may be the default class. Then, in the namespace shop, create the Deployment stream to run nginx:1.27 with 2 replicas, and have it use high-priority.
'Can displace' and 'never displaces' are separated by preemptionPolicy. Whether it is the default class is globalDefault, and if you turn it on, even Pods that do not name a class receive that priority. A Pod that is already running does not pick up a priority change made later.
A NodePort Service and session affinity
In the namespace edge, create the Deployment portal. The image is nginx:1.27, with 3 replicas and container port 8080.
The Service portal is a NodePort that accepts port 80 and forwards to 8080, with the node port fixed at 30080. The same client must go to the same Pod, and the retention time is 3600 seconds. Send traffic that arrives from outside the node only to Pods on that node.
'The same client to the same Pod' is sessionAffinity, and the retention time is separate under sessionAffinityConfig. 'Only to Pods on that node' is externalTrafficPolicy. If the selector is wrong, the object is created fine and only the endpoints are empty, so check it yourself.
Locking down outgoing traffic
For all Pods in the namespace edge, create the NetworkPolicy default-deny-egress that blocks outgoing traffic by default.
Then, with the NetworkPolicy portal-egress, for only the app=portal Pods, allow just two things. One is port 53 (both UDP and TCP) going to the namespace kube-system, and the other is TCP 5432 going to 10.40.0.0/16. However, exclude 10.40.9.0/24 of that range.
If name resolution is blocked, the rest of the allowances are useless. DNS must be open for both UDP and TCP. You write an address range with ipBlock and carve a hole in it with the except inside. If you write the items of the to list separately, it is OR.
Building a way in with the Gateway API
Create the GatewayClass labhub. The controllerName is labhub.io/gateway.
In the namespace edge, create the Gateway edge-gw that accepts HTTP on port 80 with a listener named http, and lets only routes in the same namespace attach. Then, with the HTTPRoute portal, for the host portal.example.com, send / to port 80 of the Service portal (the Service created in task 8). Match the path with PathPrefix.
The Gateway API is not a default Kubernetes resource but a separate standard that comes in as CRDs. This cluster has only the CRDs and no implementation, so the status is not filled in. An HTTPRoute declares by itself which Gateway it attaches to with parentRefs, and you write the backend in backendRefs by name and port.
A local volume tied to a node
Create the StorageClass local-node. The provisioner is kubernetes.io/no-provisioner and the reclaimPolicy is Retain.
The PV pv-local-1 is 5Gi, ReadWriteOnce, local-node, and is a local volume, not a hostPath, with path /mnt/local1, usable only on the node lab-node-2. In the namespace data, have the PVC records — 5Gi, ReadWriteOnce, local-node — bind to that PV.
Then, in the same namespace, create the Deployment archiver to run nginx:1.27 with 1 replica and mount that PVC at /records. Do not specify a node in the Pod spec. Let the volume decide the placement.
A local volume is not created at all without nodeAffinity. And once a Pod is bound to that volume, the scheduler applies the volume's node constraint to the Pod as it is. That is why the Pod goes to that node even though you do not use a nodeSelector.
A volume attached separately to each StatefulSet Pod
In the namespace data, create a headless Service db. The port is 5432 and it has no cluster IP.
Then create the StatefulSet db. It uses that Service, the image is nginx:1.27, and there are 3 replicas. Each Pod must get its own 1Gi, ReadWriteOnce volume, the name of that volume claim template is data, the storage class is db-static, and the mount path is /var/lib/db.
This cluster has no dynamic provisioner, so you must prepare the volumes in advance. All three Pods must be Running.
The PVC name that volumeClaimTemplates creates is 'volume claim template name-StatefulSet name-ordinal'. If that PVC cannot bind, the Pod stops at ordinal 0 and the next Pod is not created at all. Create in advance as many PVs of the same class as there are Pods.
A workload that cannot sit on any node
First create the namespace broken and apply the manifest below as it is.
apiVersion: apps/v1
kind: Deployment
metadata: {name: ingest, namespace: broken}
spec:
replicas: 2
selector: {matchLabels: {app: ingest}}
template:
metadata: {labels: {app: ingest}}
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- {key: disktype, operator: In, values: ["ssd"]}
containers:
- name: ingest
image: nginx:1.27
The Pods cannot leave Pending. The requirement that this workload must run on a node with a fast disk is correct, so leave the Pod spec as it is and fix the cluster side so that both Pods are Running.
Why the scheduler refused is left as a sentence in the Pod's PodScheduled condition. A required condition is not 'nice to have' but 'it will not sit without it.' What a node has is expressed by labels.
A workload that cannot get a volume
First apply the manifest below as it is.
apiVersion: v1
kind: PersistentVolume
metadata: {name: pv-archive-old}
spec:
capacity: {storage: 5Gi}
accessModes: ["ReadWriteOnce"]
persistentVolumeReclaimPolicy: Retain
storageClassName: standard
hostPath: {path: /mnt/archive-old}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: archive, namespace: broken}
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: slow
resources: {requests: {storage: 8Gi}}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: vault, namespace: broken}
spec:
replicas: 1
selector: {matchLabels: {app: vault}}
template:
metadata: {labels: {app: vault}}
spec:
volumes:
- name: archive
persistentVolumeClaim: {claimName: archive}
containers:
- name: vault
image: nginx:1.27
volumeMounts:
- {name: archive, mountPath: /archive}
The Pods cannot leave Pending. The request of the PVC archive (8Gi, slow) is a correct requirement, so leave it as it is and get the Pods to Running.
If the PVC is Pending, the Pod cannot sit either. For a binding to hold on a static volume, the class name, access mode, and capacity must all match, and in the existing volume two of those are off. This cluster has no dynamic provisioner.
A workload for which no Pods are created at all
First apply the manifest below as it is.
apiVersion: apps/v1
kind: Deployment
metadata: {name: courier, namespace: broken}
spec:
replicas: 2
selector: {matchLabels: {app: courier}}
template:
metadata: {labels: {app: courier}}
spec:
serviceAccountName: courier
containers:
- name: courier
image: nginx:1.27
The Deployment was created, but no Pods appear at all. This workload must run under a dedicated identity, so leave the reference as it is and get the 2 Pods to Running.
When Pods are not created at all, you have to look not at the Pod but at the party that was trying to create it. The sentence that admission rejected is left as is in the status conditions of kubectl describe rs -n broken. Service accounts exist separately in each namespace.
A selector that cannot be changed
First apply the manifest below as it is.
apiVersion: apps/v1
kind: Deployment
metadata: {name: payments, namespace: broken}
spec:
replicas: 3
selector: {matchLabels: {app: payment}}
template:
metadata: {labels: {app: payment}}
spec:
containers:
- name: payments
image: nginx:1.27
In this team's convention, this workload's label must be app=payments, but it went out wrongly in the singular form. Keep the Deployment name payments as it is, and change the selector and the Pod label to app=payments. There are 3 replicas, and no Pods with the old label may remain.
If you try to fix it and apply, the apiserver rejects it. Read which field the rejection message is talking about. That field is fixed at creation and cannot be changed later, so to change the value while keeping the name, there is only one way.
A binding to which permissions do not attach
First create the namespace audit and apply the manifest below as it is.
apiVersion: v1
kind: ServiceAccount
metadata: {name: reporter, namespace: audit}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: reader, namespace: audit}
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: {name: reader, namespace: audit}
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: pod-reader}
subjects:
- {kind: User, name: reporter}
Even though the Role was created, the service account reporter cannot read Pods. Do not delete the service account and the Role reader; make that service account able to get, list, and watch Pods inside the namespace audit. There must be no other permissions anywhere.
There are two wrong places. One is the role name the binding points to, and the other is the kind of the subject. The name of the service account is reporter, but its authentication name is different from that. And a roleRef cannot be changed after it is created. You check with kubectl auth can-i --as=system:serviceaccount:audit:reporter.