Killer Shell - Exam Simulators
Killer Shell - Exam Simulators
Score
Questions and Answers
Preview Questions and Answers
Exam Tips
Each question needs to be solved on a specific instance other than your main candidate@terminal . You'll need to connect to the
correct instance via ssh, the command is provided before each question. To connect to a different instance you always need to
return first to your main terminal by running the exit command, from there you can connect to a different one.
In the real exam each question will be solved on a different instance whereas in the simulator multiple questions will be solved on
same instances.
Use sudo -i to become root on any node in case necessary.
Question 1 | Contexts
You're asked to extract the following information out of kubeconfig file /opt/course/1/kubeconfig on cka9412 :
1. Write all kubeconfig context names into /opt/course/1/contexts , one per line
Answer:
All that's asked for here could be extracted by manually reading the kubeconfig file. But we're going to use kubectl for it.
Step 1
➜ ssh cka9412
➜ candidate@cka9412:~$ k --kubeconfig /opt/course/1/kubeconfig config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
cluster-admin kubernetes admin@internal
cluster-w100 kubernetes account-0027@internal
* cluster-w200 kubernetes account-0028@internal
# cka9412:/opt/course/1/contexts
cluster-admin
cluster-w100
cluster-w200
Step 2
# cka9412:/opt/course/1/current-context
cluster-w200
Step 3
And finally we extract the certificate and write it base64 decoded into the required location:
Instead of using --raw to see the sensitive certificate information, we could also simply open the kubeconfig file in an editor. No
matter how, we copy the whole value of client-certificate-data and base64 decode it:
Or if we like it automated:
# cka9412:/opt/course/1/cert
-----BEGIN CERTIFICATE-----
MIICvDCCAaQCFHYdjSZFKCyUCR1B2naXCg/UjSHLMA0GCSqGSIb3DQEBCwUAMBUx
EzARBgNVBAMTCmt1YmVybmV0ZXMwHhcNMjQxMDI4MTkwOTUwWhcNMjYwMzEyMTkw
OTUwWjAgMR4wHAYDVQQDDBVhY2NvdW50LTAwMjdAaW50ZXJuYWwwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDpUsQDDEW+48AvZmKbKS2wmsZa0gy+kzii
cZDpZg8mgO+50jNnHQ4ICqAjG3fFHmPnbuj0sZGXf+ym0J2dVL9jwGSt5NVoLrjj
TwlBG63+k4jRBxLBv6479iPXXk0giP38TAoS//dtJ+O8isbRMnlb9aI5L2JYxHfN
VL2qrF9arLf1DN+h0havFxn9noJ/iZx/qb/FHgeZqnTf7zR6OouRuWHG523jnTqA
06K+g4k2o6hg3u7JM/cNbHFM7S2qSBDkS266JJtvMtC+cpsmg/eUnDhA21tTa4vg
lbpw6vxnJcwMt4n0KaAeS0j4L3OC869alpy1jvI3ATjFjuckLESLAgMBAAEwDQYJ
KoZIhvcNAQELBQADggEBADQQLGYZoUSrbpgFV69sHvMuoxn2YUt1B5FBmQrxwOli
dem936q2ZLMr34rQ5rC1uTQDraWXa44yHmVZ07tdINkV2voIexHjX91gVC+LirQq
IKGxiok9CKLE7NReF63pp/7BNe7/P6cORh0O2EDM4TgHXLpXrt7tdPEXwvrN1tMQ
z5av9Po5Td4Vf0paODtlahwhIZ6K7ctgVGT1KdQln1qXDb/Vwq3VyYBAJKlmOu9l
bj3nmvc7D99e9p4y4GFCkAlbxv9TD0T4yvYgVFtRTVbGAkmazwUHrfcQnRTVfKoz
SfsYRy6L1RKxjwh74KnhKJz+09JqXpr7MVdQgjh0Rdw=
-----END CERTIFICATE-----
Task completed.
Install the MinIO Operator using Helm in Namespace minio . Then configure and create the Tenant CRD:
2. Install Helm chart minio/operator into the new Namespace. The Helm Release should be called minio-operator
3. Update the Tenant resource in /opt/course/2/minio-tenant.yaml to include enableSFTP: true under features
ℹ️ It is not required for MinIO to run properly. Installing the Helm Chart and the Tenant resource as requested is enough
Answer:
Helm Chart: Kubernetes YAML template-files combined into a single package, Values allow customisation
Helm Release: Installed instance of a Chart
Helm Values: Allow to customise the YAML template-files in a Chart when creating a Release
Operator: Pod that communicates with the Kubernetes API and might work with CRDs
Step 1
➜ ssh cka7968
Now we install the MinIO Helm chart into it and name the release minio-operator :
Because we installed the Helm chart there are now some CRDs available:
Just like we can create a Pod, we can now create a Tenant, MinIOJob or PolicyBinding. We can also list all available fields for the
Tenant CRD like this:
Step 3
We need to update the Yaml in the file which creates a Tenant resource:
apiVersion: minio.min.io/v2
kind: Tenant
metadata:
name: tenant
namespace: minio
labels:
app: minio
spec:
features:
bucketDNS: false
enableSFTP: true # ADD
image: quay.io/minio/minio:latest
pools:
- servers: 1
name: pool-0
volumesPerServer: 0
volumeClaimTemplate:
apiVersion: v1
kind: persistentvolumeclaims
metadata: { }
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Mi
storageClassName: standard
status: { }
requestAutoCert: true
Step 4
In this scenario we installed an operator using Helm and created a CRD with which that operator works. This is a common pattern
in Kubernetes.
There are two Pods named o3db-* in Namespace project-h800 . The Project H800 management asked you to scale these down to
one replica to save resources.
Answer:
➜ ssh cka3962
From their name it looks like these are managed by a StatefulSet. But if we're unsure we could also check for the most common
resources which manage Pods:
Confirmed, we have to work with a StatefulSet. We could also look at the Pod labels to find this out:
➜ candidate@cka3962:~ k -n project-h800 get pod --show-labels | grep o3db
o3db-0 1/1 Running 0 6d19h
app=nginx,apps.kubernetes.io/pod-index=0,controller-revision-hash=o3db-
5fbd4bb9cc,statefulset.kubernetes.io/pod-name=o3db-0
o3db-1 1/1 Running 0 6d19h
app=nginx,apps.kubernetes.io/pod-index=1,controller-revision-hash=o3db-
5fbd4bb9cc,statefulset.kubernetes.io/pod-name=o3db-1
Check all available Pods in the Namespace project-c13 and find the names of those that would probably be terminated first if the
nodes run out of resources (cpu or memory).
Write the Pod names into /opt/course/4/pods-terminated-first.txt .
Answer:
When available cpu or memory resources on the nodes reach their limit, Kubernetes will look for Pods that are using more
resources than they requested. These will be the first candidates for termination. If some Pods containers have no resource
requests/limits set, then by default those are considered to use more than requested. Kubernetes assigns Quality of Service
classes to Pods based on the defined resources and limits.
Hence we should look for Pods without resource requests defined, we can do this with a manual approach:
➜ ssh cka2556
Or we do something like:
We see that the Pods of Deployment c13-3cc-runner-heavy don't have any resource requests specified. Hence our answer would
be:
# /opt/course/4/pods-terminated-first.txt
c13-3cc-runner-heavy-65588d7d6-djtv9map
c13-3cc-runner-heavy-65588d7d6-v8kf5map
c13-3cc-runner-heavy-65588d7d6-wwpb4map
Automatic way
Not necessary and probably too slow for this task, but to automate this process you could use jsonpath:
This lists all Pod names and their requests/limits, hence we see the three Pods without those defined.
Or we look for the Quality of Service classes:
Previously the application api-gateway used some external autoscaler which should now be replaced with a
HorizontalPodAutoscaler (HPA). The application has been deployed to Namespaces api-gateway-staging and api-gateway-prod
like this:
Answer
Kustomize is a standalone tool to manage K8s Yaml files, but it also comes included with kubectl. The common idea is to have a
base set of K8s Yaml and then override or extend it for different overlays, like here done for staging and prod:
➜ ssh cka5774
➜ candidate@cka5774:~$ cd /opt/course/5/api-gateway
➜ candidate@cka5774:/opt/course/5/api-gateway$ ls
base prod staging
Investigate Base
Running kubectl kustomize DIR will build the whole Yaml based on whatever is defined in the kustomization.yaml .
In the case above we did build for the base directory, which produces Yaml that is not expected to be deployed just like that. We
can see for example that all resources contain namespace: NAMESPACE_REPLACE entries which won't be possible to apply because
Namespace names need to be lowercase.
But for debugging it can be useful to build the base Yaml.
Investigate Staging
We can see that all resources now have namespace: api-gateway-staging . Also staging seems to change the ConfigMap value to
horizontal-scaling: "60" . And it adds the additional label env: staging to the Deployment. The rest is taken from base.
# cka5774:/opt/course/5/api-gateway/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: api-gateway.yaml
transformers:
- |-
apiVersion: builtin
kind: NamespaceTransformer
metadata:
name: notImportantHere
namespace: api-gateway-staging
Actually we see that no changes were performed, because everything is already deployed:
➜ candidate@cka5774:/opt/course/5/api-gateway$ k -n api-gateway-staging get deploy,cm
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/api-gateway 1/1 1 1 20m
Investigate Prod
Everything said about staging is also true about prod, there are just different values of resources changed. Hence we should also
see that there are no changes to be applied:
Step 1
We need to remove the ConfigMap from base, staging and prod because staging and prod both reference it as a patch. If we would
only remove it from base we would run into an error when trying to build staging for example:
So we edit files base/api-gateway.yaml , staging/api-gateway.yaml and prod/api-gateway.yaml and remove the ConfigMap.
Afterwards we should get no errors and Yaml without that ConfigMap:
Step 2
We're going to add the requested HPA into the base config file:
# cka5774:/opt/course/5/api-gateway/base/api-gateway.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-gateway
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-gateway
minReplicas: 2
maxReplicas: 4
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-gateway
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
spec:
replicas: 1
selector:
matchLabels:
id: api-gateway
template:
metadata:
labels:
id: api-gateway
spec:
serviceAccountName: api-gateway
containers:
- image: httpd:2-alpine
name: httpd
Notice that we don't specify a Namespace here as done also for the other resources. The Namespace will be set by staging and prod
overlays automatically.
Step 3
In prod the HPA should have max replicas set to 6 so we add this to the prod patch:
# cka5774:/opt/course/5/api-gateway/prod/api-gateway.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-gateway
spec:
maxReplicas: 6
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
labels:
env: prod
With that change we should see that staging will have the HPA with maxReplicas: 4 from base, whereas prod will have
maxReplicas: 6 :
➜ candidate@cka5774:/opt/course/5/api-gateway$ k kustomize staging | grep maxReplicas -B5
kind: HorizontalPodAutoscaler
metadata:
name: api-gateway
namespace: api-gateway-staging
spec:
maxReplicas: 4
Step 4
We notice that the HPA was created as expected, but nothing was done with the ConfigMap that we removed from the Yaml files
earlier. We need to delete the remote ConfigMaps manually, why is explained in more detail at the end of this solution.
Done!
After deleting the ConfigMaps manually we should not see any changes when running a diff. This is because the ConfigMap does
not exist any longer in our Yaml and we already applied all changes. But we might see something like this:
Above we can see that we would change the replicas from 2 to 1. This is because the HPA already set the replicas to the
minReplicas that we defined and it's different than the default replicas: of the Deployment:
This means each time we deploy our Kustomize built Yaml, the replicas that the HPA applied would be overwritten, which is not
cool. It does not matter for the scoring of this question but to prevent this we could simply remove the replicas: setting from
the Deployment in base, staging and prod.
We had to delete the remote ConfigMaps manually. Kustomize won't delete remote resources if they only exist remote. This is
because it does not keep any state and hence doesn't know which remote resources were created by Kustomize or by anything
else.
Helm will remove remote resources if they only exist remote and if they were created by Helm. It can do this because it keeps a
state of all performed changes.
Both approaches have pros and cons:
Kustomize is less complex by not having to manage state, but might need more manual work cleaning up
Helm can keep better track of remote resources, but things can get complex and messy if there is a state error or mismatch.
State changes (Helm actions) at the same time need to be prevented or accounted for.
Create a new PersistentVolume named safari-pv . It should have a capacity of 2Gi, accessMode ReadWriteOnce, hostPath
/Volumes/Data and no storageClassName defined.
Next create a new PersistentVolumeClaim in Namespace project-t230 named safari-pvc . It should request 2Gi storage,
accessMode ReadWriteOnce and should not define a storageClassName. The PVC should bound to the PV correctly.
Finally create a new Deployment safari in Namespace project-t230 which mounts that volume at /tmp/safari-data . The Pods
of that Deployment should be of image httpd:2-alpine .
Answer
➜ ssh cka7968
# cka7968:/home/candidate/6_pv.yaml
kind: PersistentVolume
apiVersion: v1
metadata:
name: safari-pv
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/Volumes/Data"
ℹ️ Using the hostPath volume type presents many security risks, avoid if possible. Be aware that data stored in the
hostPath directory will not be shared across nodes. The data available for a Pod depends on which node the Pod is
scheduled.
# cka7968:/home/candidate/6_pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: safari-pvc
namespace: project-t230
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
Then create:
# cka7968:/home/candidate/6_dep.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: safari
name: safari
namespace: project-t230
spec:
replicas: 1
selector:
matchLabels:
app: safari
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: safari
spec:
volumes: # add
- name: data # add
persistentVolumeClaim: # add
claimName: safari-pvc # add
containers:
- image: httpd:2-alpine
name: container
volumeMounts: # add
- name: data # add
mountPath: /tmp/safari-data # add
The metrics-server has been installed in the cluster. Write two bash scripts which use kubectl :
Answer:
➜ ssh cka5774
➜ candidate@cka5774:~$ k top -h
Display resource (CPU/memory) usage.
The top command allows you to see the resource consumption for nodes or pods.
This command requires Metrics Server to be correctly configured and working on the server.
Available Commands:
node Display resource (CPU/memory) usage of nodes
pod Display resource (CPU/memory) usage of pods
Usage:
kubectl top [flags] [options]
Use "kubectl top <command> --help" for more information about a given command.
Use "kubectl options" for a list of global command-line options (applies to all commands).
We see that the metrics server provides information about resource usage:
We create the first file, ensure to not use aliases but instead the full command names:
# cka5774:/opt/course/7/node.sh
kubectl top node
For the second file we might need to check the docs again:
# cka5774:/opt/course/7/pod.sh
kubectl top pod --containers=true
Your coworker notified you that node cka3962-node1 is running an older Kubernetes version and is not even part of the cluster
yet.
ℹ️ You can connect to the worker node using ssh cka3962-node1 from cka3962
Answer:
➜ ssh cka3962
➜ candidate@cka3962-node1:~$ sudo -i
Above we can see that kubeadm is already installed in the exact needed version, otherwise we would need to install it using apt
install kubeadm=1.32.1-1.1 .
This is usually the proper command to upgrade a worker node. But as mentioned in the question description, this node is not yet
part of the cluster. Hence there is nothing to update. We'll add the node to the cluster later using kubeadm join . For now we can
continue with updating kubelet and kubectl:
Now that we're up to date with kubeadm, kubectl and kubelet we can restart the kubelet:
These errors occur because we still need to run kubeadm join to join the node into the cluster. Let's do this in the next step.
First we log into the controlplane node and generate a new TLS bootstrap token, also printing out the join command:
➜ ssh cka3962
➜ candidate@cka3962:~$ sudo -i
We see the expiration of 23h for our token, we could adjust this by passing the ttl argument.
Next we connect again to cka3962-node1 and simply execute the join command from above:
Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
ℹ️ If you have troubles with kubeadm join you might need to run kubeadm reset before
Finally we check the node status:
There is ServiceAccount secret-reader in Namespace project-swan . Create a Pod of image nginx:1-alpine named api-
contact which uses this ServiceAccount.
Exec into the Pod and use curl to manually query all Secrets from the Kubernetes Api.
Write the result into file /opt/course/9/result.json .
Answer:
https://kubernetes.io/docs/tasks/run-application/access-api-from-pod
You can find information in the K8s Docs by searching for "curl api" for example.
➜ ssh cka9412
# cka9412:/home/candidate/9.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: api-contact
name: api-contact
namespace: project-swan # add
spec:
serviceAccountName: secret-reader # add
containers:
- image: nginx:1-alpine
name: api-contact
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
Create it:
Once in the container we can connect to the K8s Api using curl , it's usually available via the Service named kubernetes in
Namespace default . Because of K8s internal DNS resolution we can use the url kubernetes.default .
ℹ️ Otherwise we can find the K8s Api IP via environment variables inside the Pod, simply run env
So we can try to contact the K8s Api:
➜ / # curl https://kubernetes.default
curl: (60) SSL peer certificate or SSH remote key was not OK
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.
➜ / # curl -k https://kubernetes.default
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
"reason": "Forbidden",
"details": {},
"code": 403
}~ $
➜ / # curl -k https://kubernetes.default/api/v1/secrets
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "secrets is forbidden: User \"system:anonymous\" cannot list resource \"secrets\" in API group
\"\" at the cluster scope",
"reason": "Forbidden",
"details": {
"kind": "secrets"
},
"code": 403
}
The first command fails because of an untrusted certificate, but we can ignore this with -k for this scenario. We explain at the end
how we can add the correct certificate instead of having to use the insecure -k option.
The last command shows 403 forbidden, this is because we are not passing any authorisation information. For the K8s Api we are
connecting as system:anonymous , which should not have permission to perform the query. We want to change this and connect
using the Pod's ServiceAccount named secret-reader .
➜ / # TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
Now we're able to list all Secrets as the Pod's ServiceAccount secret-reader .
For troubleshooting we could also check if the ServiceAccount is actually able to list Secrets:
# cka9412:/opt/course/9/result.json
{
"kind": "SecretList",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "4881"
},
"items": [
{
...
{
"metadata": {
"name": "read-me",
"namespace": "project-swan",
"uid": "f7c9a279-9609-4f9a-aa30-d29e175b7a6c",
"resourceVersion": "3380",
"creationTimestamp": "2024-12-05T15:11:58Z",
"managedFields": [
{
"manager": "kubectl-create",
"operation": "Update",
"apiVersion": "v1",
"time": "2024-12-05T15:11:58Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:data": {
".": {},
"f:token": {}
},
"f:type": {}
}
}
]
},
"data": {
"token": "ZjMyMDEzOTYtZjVkOC00NTg0LWE2ZjEtNmYyZGZkYjM4NzVl"
},
"type": "Opaque"
}
]
}
...
The easiest way would probably be to copy and paste the result manually. But if it's too long or not possible we could also do:
➜ / # curl -k https://kubernetes.default/api/v1/secrets -H "Authorization: Bearer ${TOKEN}" > result.json
➜ / # exit
➜ / # CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Create a new ServiceAccount processor in Namespace project-hamster . Create a Role and RoleBinding, both named processor
as well. These should allow the new SA to only create Secrets and ConfigMaps in that Namespace.
Answer:
Let's talk a little about RBAC resources
A ClusterRole|Role defines a set of permissions and where it is available, in the whole cluster or just a single Namespace.
A ClusterRoleBinding|RoleBinding connects a set of permissions with an account and defines where it is applied, in the whole
cluster or just a single Namespace.
Because of this there are 4 different RBAC combinations and 3 valid ones:
1. Role + RoleBinding (available in single Namespace, applied in single Namespace)
2. ClusterRole + ClusterRoleBinding (available cluster-wide, applied cluster-wide)
3. ClusterRole + RoleBinding (available cluster-wide, applied in single Namespace)
4. Role + ClusterRoleBinding (NOT POSSIBLE: available in single Namespace, applied cluster-wide)
To the solution
➜ ssh cka3962
So we execute:
Now we bind the Role to the ServiceAccount, and for this we can also view examples:
So we create it:
Like this:
➜ candidate@cka3962:~$ k -n project-hamster auth can-i create secret --as system:serviceaccount:project-
hamster:processor
yes
Done.
Use Namespace project-tiger for the following. Create a DaemonSet named ds-important with image httpd:2-alpine and
labels id=ds-important and uuid=18426a0b-5f59-4e10-923f-c0e078e82462 . The Pods it creates should request 10 millicore cpu
and 10 mebibyte memory. The Pods of that DaemonSet should run on all nodes, also controlplanes.
Answer:
As of now we aren't able to create a DaemonSet directly using kubectl , so we create a Deployment and just change it up:
➜ ssh cka2556
Or we could search for a DaemonSet example yaml in the K8s docs and alter it to our needs.
We adjust the yaml to:
# cka2556:/home/candidate/11.yaml
apiVersion: apps/v1
kind: DaemonSet # change from Deployment to Daemonset
metadata:
creationTimestamp: null
labels: # add
id: ds-important # add
uuid: 18426a0b-5f59-4e10-923f-c0e078e82462 # add
name: ds-important
namespace: project-tiger # important
spec:
#replicas: 1 # remove
selector:
matchLabels:
id: ds-important # add
uuid: 18426a0b-5f59-4e10-923f-c0e078e82462 # add
#strategy: {} # remove
template:
metadata:
creationTimestamp: null
labels:
id: ds-important # add
uuid: 18426a0b-5f59-4e10-923f-c0e078e82462 # add
spec:
containers:
- image: httpd:2-alpine
name: ds-important
resources:
requests: # add
cpu: 10m # add
memory: 10Mi # add
tolerations: # add
- effect: NoSchedule # add
key: node-role.kubernetes.io/control-plane # add
#status: {} # remove
It was requested that the DaemonSet runs on all nodes, so we need to specify the toleration for this.
Let's give it a go:
Above we can see one Pod on each node, including the controlplane one.
There should only ever be one Pod of that Deployment running on one worker node, use topologyKey:
kubernetes.io/hostname for this
ℹ️ Because there are two worker nodes and the Deployment has three replicas the result should be that the third Pod
won't be scheduled. In a way this scenario simulates the behaviour of a DaemonSet, but using a Deployment with a fixed
number of replicas
Answer:
There are two possible ways, one using podAntiAffinity and one using topologySpreadConstraint .
PodAntiAffinity
The idea here is that we create a "Inter-pod anti-affinity" which allows us to say a Pod should only be scheduled on a node where
another Pod of a specific label (here the same label) is not already running.
Let's begin by creating the Deployment template:
➜ ssh cka2556
# cka2556:/home/candidate/12.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
id: very-important # change
name: deploy-important
namespace: project-tiger # important
spec:
replicas: 3 # change
selector:
matchLabels:
id: very-important # change
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
id: very-important # change
spec:
containers:
- image: nginx:1-alpine
name: container1 # change
resources: {}
- image: google/pause # add
name: container2 # add
affinity: # add
podAntiAffinity: # add
requiredDuringSchedulingIgnoredDuringExecution: # add
- labelSelector: # add
matchExpressions: # add
- key: id # add
operator: In # add
values: # add
- very-important # add
topologyKey: kubernetes.io/hostname # add
status: {}
Specify a topologyKey, which is a pre-populated Kubernetes label, you can find this by describing a node.
TopologySpreadConstraints
We can achieve the same with topologySpreadConstraints . Best to try out and play with both.
# cka2556:/home/candidate/12.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
id: very-important # change
name: deploy-important
namespace: project-tiger # important
spec:
replicas: 3 # change
selector:
matchLabels:
id: very-important # change
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
id: very-important # change
spec:
containers:
- image: nginx:1-alpine
name: container1 # change
resources: {}
- image: google/pause # add
name: container2 # add
topologySpreadConstraints: # add
- maxSkew: 1 # add
topologyKey: kubernetes.io/hostname # add
whenUnsatisfiable: DoNotSchedule # add
labelSelector: # add
matchLabels: # add
id: very-important # add
status: {}
And running the following we see one Pod on each worker node and one not scheduled.
If we kubectl describe the not scheduled Pod it will show us the reason didn't match pod anti-affinity rules :
Warning FailedScheduling 119s (x2 over 2m1s) default-scheduler 0/3 nodes are available: 1 node(s) had
untolerated taint {node-role.kubernetes.io/control-plane: }, 2 node(s) didn't match pod anti-affinity rules.
preemption: 0/3 nodes are available: 1 Preemption is not helpful for scheduling, 2 No preemption victims
found for incoming pod.
Warning FailedScheduling 20s (x2 over 22s) default-scheduler 0/3 nodes are available: 1 node(s) had
untolerated taint {node-role.kubernetes.io/control-plane: }, 2 node(s) didn't match pod topology spread
constraints. preemption: 0/3 nodes are available: 1 Preemption is not helpful for scheduling, 2 No preemption
victims found for incoming pod.
The team from Project r500 wants to replace their Ingress (networking.k8s.io) with a Gateway Api (gateway.networking.k8s.io)
solution. The old Ingress is available at /opt/course/13/ingress.yaml .
Perform the following in Namespace project-r500 and for the already existing Gateway:
1. Create a new HTTPRoute named traffic-director which replicates the routes from the old Ingress
2. Extend the new HTTPRoute with path /auto which redirects to mobile if the User-Agent is exactly mobile and to desktop
otherwise
The existing Gateway is reachable at http://r500.gateway:30080 which means your implementation should work for these
commands:
curl r500.gateway:30080/desktop
curl r500.gateway:30080/mobile
curl r500.gateway:30080/auto -H "User-Agent: mobile"
curl r500.gateway:30080/auto
Answer:
Comparing for example the older Ingress (networking.k8s.io/v1) and newer HTTPRoute (gateway.networking.k8s.io/v1) CRDs then
they look quite similar in what they offer. They have a different config structure but provide the same idea of functionality.
The magic of the Gateway Api comes more to shine because of further resources (GRPCRoute, TCPRoute) and the architecture
which is designed to be more flexible and extendable. This will provide better integration into existing cloud infrastructure and
providers like GCP or AWS will be able to develop their own Gateway Api implementations.
Investigate CRDs
➜ ssh cka7968
We can see that various CRDs from gateway.networking.k8s.io are available. In this scenario we'll only work directly with HTTPRoute
which we need to create. It will reference the existing Gateway main which references the existing GatewayClass nginx :
We receive a 404 because no routes have been defined yet. We receive this 404 from a Nginx because the Gateway Api
implementation in this scenario has been done via the Nginx Gateway Fabric. But for this scenario it wouldn't matter if another
implementation (Traefik, Envoy, ...) would've been used, because all will work with the same Gateway Api CRDs.
The url r500.gateway:30080 is reachable because of a static entry in /etc/hosts which points to the only node in the cluster.
And on that node, as well as on all others if there would be more, port 30080 is open because of a NodePort Service:
Step 1
Now we'll have a look at the provided Ingress Yaml which we need to convert:
# cka7968:/opt/course/13/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: traffic-director
spec:
ingressClassName: nginx
rules:
- host: r500.gateway
http:
paths:
- backend:
service:
name: web-desktop
port:
number: 80
path: /desktop
pathType: Prefix
- backend:
service:
name: web-mobile
port:
number: 80
path: /mobile
pathType: Prefix
We can see two paths /desktop and /mobile which point to the K8s Services web-desktop and web-mobile . Based on this we
create a HTTPRoute which replicates the behaviour and in which we reference the existing Gateway:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: traffic-director
namespace: project-r500
spec:
parentRefs:
- name: main # use the name of the existing Gateway
hostnames:
- "r500.gateway"
rules:
- matches:
- path:
type: PathPrefix
value: /desktop
backendRefs:
- name: web-desktop
port: 80
- matches:
- path:
type: PathPrefix
value: /mobile
backendRefs:
- name: web-mobile
port: 80
Step 2
Now things get more interesting and we need to add new path /auto which redirects depending on the User-Agent. The User-
Agent is handled as a HTTP header and we only have to check for the exact value, hence we can extend our HTTPRoute like this:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: traffic-director
namespace: project-r500
spec:
parentRefs:
- name: main
hostnames:
- "r500.gateway"
rules:
- matches:
- path:
type: PathPrefix
value: /desktop
backendRefs:
- name: web-desktop
port: 80
- matches:
- path:
type: PathPrefix
value: /mobile
backendRefs:
- name: web-mobile
port: 80
# NEW FROM HERE ON
- matches:
- path:
type: PathPrefix
value: /auto
headers:
- type: Exact
name: user-agent
value: mobile
backendRefs:
- name: web-mobile
port: 80
- matches:
- path:
type: PathPrefix
value: /auto
backendRefs:
- name: web-desktop
port: 80
We added two new rules, the first redirects to mobile conditionally on header value and the second redirects to desktop.
If the question text mentions something like "add one new path /auth" then this doesn't necessarily mean just one entry in the
rules array, it can depend on conditions. We added at first the following rule:
- matches:
- path:
type: PathPrefix
value: /auto
headers:
- type: Exact
name: user-agent
value: mobile
backendRefs:
- name: web-mobile
port: 80
Note that we use - path: and header: , not - path: and - header: . This means both path and header will be connected
AND. So only if the path is /auto AND the header user-agent is mobile we route to mobile.
If we would do the following then these would be connected OR and it would be wrong for this question:
The next rule we added is the one for desktop, at the very end:
- matches:
- path:
type: PathPrefix
value: /auto
backendRefs:
- name: web-desktop
port: 80
In this one we don't have to check any header value again because the question required that "otherwise" traffic should be
redirected to desktop. So this acts as a "catch all" for route /auto .
We need to understand that the order of rules matters. If we would add the desktop rule before the mobile one it wouldn't work
because no requests would ever reach the mobile rule.
Our solution should result in this:
Answer:
➜ ssh cka9412
➜ candidate@cka9412:~$ sudo -i
➜ root@cka9412:~# openssl x509 -noout -text -in /etc/kubernetes/pki/apiserver.crt | grep Validity -A2
Validity
Not Before: Oct 29 14:14:27 2024 GMT
Not After : Oct 29 14:19:27 2025 GMT
# cka9412:/opt/course/14/expiration
Oct 29 14:19:27 2025 GMT
# cka9412:/opt/course/14/kubeadm-renew-certs.sh
kubeadm certs renew apiserver
Question 15 | NetworkPolicy
ℹ️ All Pods in the Namespace run plain Nginx images. This allows simple connectivity tests like: k -n project-snake exec
POD_NAME -- curl POD_IP:PORT
ℹ️ For example, connections from backend-* Pods to vault-* Pods on port 3333 should no longer work
Answer:
➜ ssh cka7968
Now we create the NP by copying and changing an example from the K8s Docs:
# cka7968:/home/candidate/15_np.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: np-backend
namespace: project-snake
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress # policy is only about Egress
egress:
- # first rule
to: # first condition "to"
- podSelector:
matchLabels:
app: db1
ports: # second condition "port"
- protocol: TCP
port: 1111
- # second rule
to: # first condition "to"
- podSelector:
matchLabels:
app: db2
ports: # second condition "port"
- protocol: TCP
port: 2222
The NP above has two rules with two conditions each, it can be read as:
Wrong example
# WRONG
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: np-backend
namespace: project-snake
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
- # first rule
to: # first condition "to"
- podSelector: # first "to" possibility
matchLabels:
app: db1
- podSelector: # second "to" possibility
matchLabels:
app: db2
ports: # second condition "ports"
- protocol: TCP # first "ports" possibility
port: 1111
- protocol: TCP # second "ports" possibility
port: 2222
The NP above has one rule with two conditions and two condition-entries each, it can be read as:
Using this NP it would still be possible for backend-* Pods to connect to db2-* Pods on port 1111 for example which should be
forbidden.
Create NetworkPolicy
And to verify:
Also helpful to use kubectl describe on the NP to see how K8s has interpreted the policy.
1. Make a backup of the existing configuration Yaml and store it at /opt/course/16/coredns_backup.yaml . You should be able
to fast recover from the backup
2. Update the CoreDNS configuration in the cluster so that DNS resolution for SERVICE.NAMESPACE.custom-domain will work
exactly like and in addition to SERVICE.NAMESPACE.cluster.local
Test your configuration for example from a Pod with busybox:1 image. These commands should result in an IP address:
nslookup kubernetes.default.svc.cluster.local
nslookup kubernetes.default.svc.custom-domain
Answer:
➜ ssh cka5774
Step 1
CoreDNS uses a ConfigMap by default when installed via Kubeadm. Creating a backup is always a good idea before performing
sensitive changes:
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30 {
disable success cluster.local
disable denial cluster.local
}
loop
reload
loadbalance
}
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
...
Step 2
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes custom-domain cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30 {
disable success cluster.local
disable denial cluster.local
}
loop
reload
loadbalance
}
kind: ConfigMap
metadata:
creationTimestamp: "2024-12-26T20:35:11Z"
name: coredns
namespace: kube-system
resourceVersion: "262"
uid: c76d208f-1bc8-4c0f-a8e8-a8bfa440870e
Note that we added custom-domain in the same line where cluster.local is already defined.
Now we need to restart the Deployment:
➜ candidate@cka5774:~$ k -n kube-system rollout restart deploy coredns
deployment.apps/coredns restarted
We should see both Pods restarted and running without errors, this is only the case if there are no syntax errors in the CoreDNS
config.
To test the updated configuration we create a Pod, image busybox:1 contains nslookup already:
➜ / # nslookup kubernetes.default.svc.custom-domain
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: kubernetes.default.svc.custom-domain
Address: 10.96.0.1
➜ / # nslookup kubernetes.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: kubernetes.default.svc.cluster.local
Address: 10.96.0.1
This Service is often used from Pods that need to communicate with the K8s Api, like operators.
In Namespace project-tiger create a Pod named tigers-reunite of image httpd:2-alpine with labels pod=container and
container=pod . Find out on which node the Pod is scheduled. Ssh into that node and find the containerd container belonging to
that Pod.
Using command crictl :
ℹ️ You can connect to a worker node using ssh cka2556-node1 or ssh cka2556-node2 from cka2556
Answer:
ℹ️ In this environment crictl can be used for container management. In the real exam this could also be docker . Both
commands can be used with the same arguments.
Here it's cka2556-node1 so we ssh into that node and and check the container info:
➜ candidate@cka2556-node1:~$ sudo -i
Step 1
# cka2556:/opt/course/17/pod-container.txt
ba62e5d465ff0 io.containerd.runc.v2
Step 2
Here we run crictl logs on the worker node and copy the content manually, that works if it's not a lot of logs. Otherwise we
could write the logs into a file on cka2556-node1 and download the file via scp from cka2556 .
The file should look like this:
# cka2556:/opt/course/17/pod-container.log
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 10.44.0.37. Set
the 'ServerName' directive globally to suppress this message
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 10.44.0.37. Set
the 'ServerName' directive globally to suppress this message
[Mon Sep 13 13:32:18.555280 2021] [mpm_event:notice] [pid 1:tid 139929534545224] AH00489: Apache/2.4.41
(Unix) configured -- resuming normal operations
[Mon Sep 13 13:32:18.555610 2021] [core:notice] [pid 1:tid 139929534545224] AH00094: Command line: 'httpd -D
FOREGROUND'
This is a preview of the CKA Simulator content. The full CKA Simulator is available in two versions: A and B. Each version contains at
least 17 different questions. These preview questions are in addition to the provided ones and can also be solved in the interactive
environment.
The cluster admin asked you to find out the following information about etcd running on cka9412 :
Answer:
Find out etcd information
➜ candidate@cka9412:~$ sudo -i
We see it's running as a Pod, more specific a static Pod. So we check for the default kubelet directory for static manifests:
So we look at the yaml and the parameters with which etcd is started:
# cka9412:/etc/kubernetes/manifests/etcd.yaml
apiVersion: v1
kind: Pod
metadata:
annotations:
kubeadm.kubernetes.io/etcd.advertise-client-urls: https://192.168.100.21:2379
creationTimestamp: null
labels:
component: etcd
tier: control-plane
name: etcd
namespace: kube-system
spec:
containers:
- command:
- etcd
- --advertise-client-urls=https://192.168.100.21:2379
- --cert-file=/etc/kubernetes/pki/etcd/server.crt # server certificate
- --client-cert-auth=true # enabled
- --data-dir=/var/lib/etcd
- --experimental-initial-corrupt-check=true
- --experimental-watch-progress-notify-interval=5s
- --initial-advertise-peer-urls=https://192.168.100.21:2380
- --initial-cluster=cka9412=https://192.168.100.21:2380
- --key-file=/etc/kubernetes/pki/etcd/server.key # server private key
- --listen-client-urls=https://127.0.0.1:2379,https://192.168.100.21:2379
- --listen-metrics-urls=http://127.0.0.1:2381
- --listen-peer-urls=https://192.168.100.21:2380
- --name=cka9412
- --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt
- --peer-client-cert-auth=true
- --peer-key-file=/etc/kubernetes/pki/etcd/peer.key
- --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
- --snapshot-count=10000
- --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
image: registry.k8s.io/etcd:3.5.15-0
imagePullPolicy: IfNotPresent
...
We see that client authentication is enabled and also the requested path to the server private key, now let's find out the expiration
of the server certificate:
➜ root@cka9412:~# openssl x509 -noout -text -in /etc/kubernetes/pki/etcd/server.crt | grep Validity -A2
Validity
Not Before: Oct 29 14:14:27 2024 GMT
Not After : Oct 29 14:19:27 2025 GMT
There we have it. Let's write the information into the requested file:
# /opt/course/p1/etcd-info.txt
Server private key location: /etc/kubernetes/pki/etcd/server.key
Server certificate expiration date: Oct 29 14:19:27 2025 GMT
Is client certificate authentication enabled: yes
You're asked to confirm that kube-proxy is running correctly. For this perform the following in Namespace project-hamster :
2. Create Service p2-service which exposes the Pod internally in the cluster on port 3000->80
3. Write the iptables rules of node cka2556 belonging the created Service p2-service into file /opt/course/p2/iptables.txt
4. Delete the Service and confirm that the iptables rules are gone again
Answer:
➜ candidate@cka2556:~$ k -n project-hamster expose pod p2-pod --name p2-service --port 3000 --target-port 80
We should see that Pods and Services are connected, hence the Service should have Endpoints.
The idea here is to find the kube-proxy container and check its logs:
➜ candidate@cka2556:~$ sudo -i
This could be repeated on each controlplane and worker node where the result should be the same.
Great. Now let's write these logs into the requested file:
Delete the Service and confirm the iptables rules are gone::
➜ root@cka2556:~#
Kubernetes Services are implemented using iptables rules (with default config) on all nodes. Every time a Service has been altered,
created, deleted or Endpoints of a Service have changed, the kube-apiserver contacts every node's kube-proxy to update the
iptables rules according to the current state.
2. Expose it on port 80 as a ClusterIP Service named check-ip-service . Remember/output the IP of that Service
ℹ️ The second Service should get an IP address from the new CIDR range
Answer:
➜ ssh cka9412
➜ candidate@cka9412:~$ sudo -i
# cka9412:/etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
component: kube-apiserver
tier: control-plane
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --advertise-address=192.168.100.21
...
- --service-account-key-file=/etc/kubernetes/pki/sa.pub
- --service-cluster-ip-range=11.96.0.0/12 # change
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
...
# /etc/kubernetes/manifests/kube-controller-manager.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
component: kube-controller-manager
tier: control-plane
name: kube-controller-manager
namespace: kube-system
spec:
containers:
- command:
- kube-controller-manager
- --allocate-node-cidrs=true
- --authentication-kubeconfig=/etc/kubernetes/controller-manager.conf
- --authorization-kubeconfig=/etc/kubernetes/controller-manager.conf
- --bind-address=127.0.0.1
- --client-ca-file=/etc/kubernetes/pki/ca.crt
- --cluster-cidr=10.244.0.0/16
- --cluster-name=kubernetes
- --cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt
- --cluster-signing-key-file=/etc/kubernetes/pki/ca.key
- --controllers=*,bootstrapsigner,tokencleaner
- --kubeconfig=/etc/kubernetes/controller-manager.conf
- --leader-elect=true
- --node-cidr-mask-size=24
- --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
- --root-ca-file=/etc/kubernetes/pki/ca.crt
- --service-account-private-key-file=/etc/kubernetes/pki/sa.key
- --service-cluster-ip-range=11.96.0.0/12 # change
- --use-service-account-credentials=true
There we go, the new Service got an IP of the updated range assigned. We also see that both Services have our Pod as endpoint.
Knowledge
Study all topics as proposed in the curriculum till you feel comfortable with all.
General
Study all topics as proposed in the curriculum till you feel comfortable with all
Do 1 or 2 test session with this CKA Simulator. Understand the solutions and maybe try out other ways to achieve the same
thing.
Setup your aliases, be fast and breath kubectl
The majority of tasks in the CKA will also be around creating Kubernetes resources, like it's tested in the CKAD. So preparing a
bit for the CKAD can't hurt.
Learn and Study the in-browser scenarios on https://killercoda.com/killer-shell-cka (and maybe for CKAD https://killercoda.co
m/killer-shell-ckad)
Imagine and create your own scenarios to solve
Components
Understanding Kubernetes components and being able to fix and investigate clusters: https://kubernetes.io/docs/tasks/debu
g-application-cluster/debug-cluster
Know advanced scheduling: https://kubernetes.io/docs/concepts/scheduling/kube-scheduler
When you have to fix a component (like kubelet) in one cluster, just check how it's setup on another node in the same or even
another cluster. You can copy config files over etc
If you like you can look at Kubernetes The Hard Way once. But it's NOT necessary to do, the CKA is not that complex. But
KTHW helps understanding the concepts
You should install your own cluster using kubeadm (one controlplane, one worker) in a VM or using a cloud provider and
investigate the components
Know how to use Kubeadm to for example add nodes to a cluster
Know how to create an Ingress resources
Know how to snapshot/restore ETCD from another machine
CKA Exam Info
Read the Curriculum
https://github.com/cncf/curriculum
Read the Handbook
https://docs.linuxfoundation.org/tc-docs/certification/lf-handbook2
Kubernetes documentation
Get familiar with the Kubernetes documentation and be able to use the search. Allowed resources are:
https://kubernetes.io/docs
https://kubernetes.io/blog
https://helm.sh/docs
PSI Bridge
Starting with PSI Bridge:
The exam will now be taken using the PSI Secure Browser, which can be downloaded using the newest versions of Microsoft
Edge, Safari, Chrome, or Firefox
Multiple monitors will no longer be permitted
Use of personal bookmarks will no longer be permitted
Terminal Handling
Bash Aliases
In the real exam, each question has to be solved on a different instance to which you connect via ssh. This means it's not advised
to configure bash aliases because they wouldn't be available on the instances accessed by ssh.
Be fast
Use the history command to reuse already entered commands or use even faster history search through Ctrl r .
If a command takes some time to execute, like sometimes kubectl delete pod x . You can put a task in the background using
Ctrl z and pull it back into foreground running command fg .
You can delete pods fast with:
k delete pod x --grace-period 0 --force
Vim
Be great with vim.
Settings
In case you face a situation where vim is not configured properly and you face for example issues with pasting copied content you
should be able to configure via ~/.vimrc or by entering manually in vim settings mode:
set tabstop=2
set expandtab
set shiftwidth=2
About
FAQ
Support
Store
Pricing
Impressum
Datenschutz
AGB
CONTENT
CKS
CKA
CKAD
LFCS
LFCT
LINKS
Killercoda
Kim Wuestkamp