Putting just the exercise plus solutions here on this page separated from the main questions page.
Questions / Solutions
1. Create a new pod with the nginx image.
k run nginx --image=nginx --restart=Never
Check if the pod is running:
k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 8s 10.244.1.151 minikube-m02 <none> <none>
Clean up:
k delete po nginx --force=true
2. Create a new pod with the name redis and with the image redis:1.99.
Full question text:
2. https://fernhtls.github.io/devops/2026/02/01/ckad-simulator-solutions.html#question-1)
1. Identify the problem with the pod.
2. Rectify the problem with the pod and wait until the pod is ready and healthy.
k run redis --image=redis:1.99 --restart=Never
check the pod and you can see the error straight away:
k get po
NAME READY STATUS RESTARTS AGE
redis 0/1 ErrImagePull 0 3m17s
So error is that the image is incorrect, so tag 1.99 does not exist probably.
Removing the tag will fix the issue.
k delete po redis --force=true; k run redis --image=redis --restart=Never
Check if the pod is running now:
k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
redis 1/1 Running 0 10s 10.244.1.120 minikube-m02 <none> <none>
You can check the version of the image probably on investigating deeper and tag can be used as well to bring the pod up.
k exec -ti redis -- /bin/sh -c bash
And run the redis-cli command as below:
root@redis:/data# redis-cli INFO server | grep "redis_version"
redis_version:8.4.0
root@redis:/data#
So the image could be for example 8.4.0.
k delete po redis --force=true; k run redis --image=redis:8.4.0 --restart=Never
Check the image again:
k get po redis -o json | jq '.spec.containers[].image'
Should show now 8.4.0.
Clean up with:
k delete po redis --force=true
3. Create a new replicaset rs-1 using image nginx with labels tier:frontend and selectors as tier:frontend. Create 3 replicas for the same. Scale the above replicaset to 5 replicas.
Full question text:
3. Create a new replicaset rs-1 using image nginx with labels tier:frontend and selectors as tier:frontend. Create 3 replicas for the same. Scale the above replicaset to 5 replicas.
1. Update the image of the above replicaset to use nginx:alpine image.
2. Scale down the replicaset to 2 replicas.
You can create a deployment yaml and adapt with some of the initial requirements and then just adapt:
- The resource kind.
- Add the label and the selector on rs metadata level, on template.pod level and the selectors as well.
- Remove the
strategy: {}as it’s only valid for deployments.
k create deploy rs-1 --image=nginx --replicas=3 --dry-run=client -o yaml > rs-1.yml
So the content of rs-1 is the following:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: rs-1
name: rs-1
spec:
replicas: 3
selector:
matchLabels:
app: rs-1
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: rs-1
spec:
containers:
- image: nginx
name: nginx
resources: {}
status: {}
Adapting the yml for a replica-set we have the following:
PS: you can do the replacements, like for the labels with the following when editing it using vi / vim: %s/app: rs-1/tier: frontend/g.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
creationTimestamp: null
labels:
tier: frontend
name: rs-1
spec:
replicas: 3
selector:
matchLabels:
tier: frontend
template:
metadata:
creationTimestamp: null
labels:
tier: frontend
spec:
containers:
- image: nginx
name: nginx
resources: {}
status: {}
And then the rs can be created with the following:
k apply -f rs-1.yml
k get rs -o wide
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
rs-1 3 3 3 4m15s nginx nginx tier=frontend
Next we can scale up the replica-set to 5 replicas without editing the yml file:
k scale rs rs-1 --replicas=5
k get rs -o wide
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
rs-1 5 5 5 6m33s nginx nginx tier=frontend
In order to change the image we can edit the resource with:
k edit rs rs-1
k get rs -o wide
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
rs-1 5 5 5 12m nginx nginx:alpine tier=frontend
k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
rs-1-4xwx2 1/1 Running 0 13m 10.244.0.153 minikube <none> <none>
rs-1-679nh 1/1 Running 0 7m29s 10.244.1.169 minikube-m02 <none> <none>
rs-1-dw8bt 1/1 Running 0 13m 10.244.1.213 minikube-m02 <none> <none>
rs-1-jjtqr 1/1 Running 0 13m 10.244.1.79 minikube-m02 <none> <none>
rs-1-r4xh2 1/1 Running 0 7m29s 10.244.0.182 minikube <none> <none>
Scale down the replicas to 2 replicas:
k scale rs rs-1 --replicas=2
k get rs -o wide
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
rs-1 2 2 2 15m nginx nginx:alpine tier=frontend
k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
rs-1-4xwx2 1/1 Running 0 15m 10.244.0.153 minikube <none> <none>
rs-1-jjtqr 1/1 Running 0 15m 10.244.1.79 minikube-m02 <none> <none>
Clean up the rs:
k delete rs rs-1 --force=true
4. Create a new deployment deploy-1 using image busybox with 3 replicas and command sleep 3600.
Full question text:
4. Create a new deployment deploy-1 using image busybox with 3 replicas and command sleep 3600.
1. Update the image of the above deployment with image ubuntu.
We can create with imperative cmds the deployment, but you can inspect it as well first:
k create deploy deploy-1 --image=busybox --replicas=3 --dry-run=client -o yaml -- sleep 3600
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: deploy-1
name: deploy-1
spec:
replicas: 3
selector:
matchLabels:
app: deploy-1
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: deploy-1
spec:
containers:
- command:
- sleep
- "3600"
image: busybox
name: busybox
resources: {}
status: {}
And then removing the dry-run and yaml output we can run the following:
k create deploy deploy-1 --image=busybox --replicas=3 -- sleep 3600
And then on inspecting the pods:
k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
deploy-1-79bdf44cfc-9t92f 1/1 Running 0 12s 10.244.1.142 minikube-m02 <none> <none>
deploy-1-79bdf44cfc-djqz2 1/1 Running 0 12s 10.244.0.24 minikube <none> <none>
deploy-1-79bdf44cfc-w6k9f 1/1 Running 0 12s 10.244.1.111 minikube-m02 <none> <none>
We can now set the image with the following command:
k set image deploy deploy-1 busybox=ubuntu
As the sleep commad does not need ay adaptation we will see the following when inspecting the pods and deployment:
k get deploy deploy-1 -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deploy-1 3/3 3 3 5m45s busybox ubuntu app=deploy-1
You see some pods being terminated, but new pods being brought up. In this case the default strategy is rollingUpdate, and you can inspect that looking at the deployment yaml:
k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
deploy-1-79bdf44cfc-9t92f 1/1 Terminating 0 5m51s 10.244.1.142 minikube-m02 <none> <none>
deploy-1-79bdf44cfc-djqz2 1/1 Terminating 0 5m51s 10.244.0.24 minikube <none> <none>
deploy-1-79bdf44cfc-w6k9f 1/1 Terminating 0 5m51s 10.244.1.111 minikube-m02 <none> <none>
deploy-1-8557c548f8-bzzpv 1/1 Running 0 24s 10.244.1.80 minikube-m02 <none> <none>
deploy-1-8557c548f8-g5qc9 1/1 Running 0 13s 10.244.1.73 minikube-m02 <none> <none>
deploy-1-8557c548f8-gtvbw 1/1 Running 0 18s 10.244.0.186 minikube <none> <none>
k get deployments.apps deploy-1 -o yaml | grep -A5 strategy
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
So in this case the strategy here means:
- pods are update gradually, not all at once.
- maxsurge 25%: max number of extra pods on rollingupdates, so in our case here 1, so we can have up to 4 pods running.
- maxunavailable 25%: so at least 2 pods must remain running / available.
- so the update will bring up one pod, see if it’s on running state and terminate an old pod version, and keep on going until the update is done.
Clean up:
k delete deploy deploy-1 --force=true
5. Create a namespace finance.
Full question text:
5. Create a namespace finance.
1. Create a pod redis01 using image redis in the finance namespace.
Create the namespace:
k create ns finance
Deploy the pod in the namespace:
k -n finance run redis01 --image=redis
k -n finance get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
redis01 1/1 Running 0 7s 10.244.1.134 minikube-m02 <none> <none>
I’m just passing the -n argument to run the commands but you could have switch the contexts here, but for the test I would not use it, but would just use the -n, unless requested, as you might end up not switching back to the next namespace and creating new resources on the wrong one.
k config set-context --current --namespace=finance
k config get-contexts $(k config current-context)
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* minikube minikube minikube finance
k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
redis01 1/1 Running 0 4m13s 10.244.1.134 minikube-m02 <none> <none>
Clean up the namespace and all objects within:
k delete ns finance --force=true
And return the context to the default namespace if needed:
k config set-context --current --namespace=default
6. Deploy a redis02 pod using the redis:alpine image with the labels set to tier=db.
Full question text:
6. Deploy a redis02 pod using the redis:alpine image with the labels set to tier=db.
1. Create a service redis-service to expose the redis02 application within the cluster on port 6379.
Let’s just check the yaml first for the pod creation:
k run redis02 --image=redis --labels="tier=db" --restart=Never --dry-run=client -o yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
tier: db
name: redis01
spec:
containers:
- image: redis
name: redis01
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
It seems to be ok, so let’s create:
k run redis02 --image=redis --labels="tier=db" --restart=Never
Now exposing the pod within the cluster with a ClusterIP service on port 6379:
k expose pod redis02 --port=6379 --target-port=6379 --type="ClusterIP"
Check the service:
k get svc redis02 -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
redis02 ClusterIP 10.104.8.85 <none> 6379/TCP 26s tier=db
k describe svc redis02
k describe svc redis02
Name: redis02
Namespace: default
Labels: tier=db
Annotations: <none>
Selector: tier=db
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.104.8.85
IPs: 10.104.8.85
Port: <unset> 6379/TCP
TargetPort: 6379/TCP
Endpoints: 10.244.1.41:6379
Session Affinity: None
Internal Traffic Policy: Cluster
Events: <none>
We could try and test the connection, but during the test it might be tested or the status might be showed in some cases:
k run redis-cli --rm -it --image=redis:alpine -- redis-cli -h redis02
It might be that you have to press a few times enter, on my test the terminal took a while to show, but you can see that the client connected to the service:
If you don't see a command prompt, try pressing enter.
redis02:6379>
redis02:6379> INFO server
# Server
redis_version:8.4.0
redis_git_sha1:00000000
redis_git_dirty:1
redis_build_id:bdcb6d52387ca71d
redis_mode:standalone
os:Linux 6.12.13-200.fc41.aarch64 aarch64
arch_bits:64
monotonic_clock:POSIX clock_gettime
multiplexing_api:epoll
atomicvar_api:c11-builtin
gcc_version:12.2.0
process_id:1
process_supervised:no
run_id:e0d516a75f183ea089a97bd15102451c6cd58e6f
tcp_port:6379
server_time_usec:1770027604976812
uptime_in_seconds:761
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:8419924
executable:/data/redis-server
config_file:
io_threads_active:0
listener0:name=tcp,bind=*,bind=-::*,port=6379
redis02:6379> CLIENT LIST
id=11 addr=10.244.1.69:54548 laddr=10.244.1.41:6379 fd=24 name= age=194 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=26 qbuf-free=20448 argv-mem=10 multi-mem=0 rbs=1024 rbp=0 obl=0 oll=0 omem=0 tot-mem=22554 events=r cmd=client|list user=default redir=-1 resp=2 lib-name= lib-ver= io-thread=0 tot-net-in=79 tot-net-out=322232 tot-cmds=2
redis02:6379>
So the ip returned on running CLIENT LIST, shows laddr=10.244.1.41:6379, and as we can see on the service that endpoint ip is indeed 10.244.1.41:6379.
So the full dns kubernetes resolution for this service is redis02.default.svc.cluster.local.
kubectl run redis-cli --rm -it --image=redis:alpine -- redis-cli -h redis02.default.svc.cluster.local
If you don't see a command prompt, try pressing enter.
redis02.default.svc.cluster.local:6379>
redis02.default.svc.cluster.local:6379> CLIENT LIST
id=12 addr=10.244.1.172:58756 laddr=10.244.1.41:6379 fd=24 name= age=28 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=26 qbuf-free=20448 argv-mem=10 multi-mem=0 rbs=1024 rbp=0 obl=0 oll=0 omem=0 tot-mem=22554 events=r cmd=client|list user=default redir=-1 resp=2 lib-name= lib-ver= io-thread=0 tot-net-in=53 tot-net-out=321594 tot-cmds=1
redis02.default.svc.cluster.local:6379>
Clean up:
k delete svc redis02; k delete po redis02 --force=true
7. Create a new pod called custom-nginx using the nginx image and expose it on container port 80, but service should expose 8080.
Creating the pod and exposing the pod on a container-port, and creating the service:
k run custom-nginx --image=nginx --port=80 --restart=Never
k expose pod custom-nginx --port=8080 --target-port=80 --type="ClusterIP"
k get all
NAME READY STATUS RESTARTS AGE
pod/custom-nginx 1/1 Running 0 49s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/custom-nginx ClusterIP 10.96.75.134 <none> 8080/TCP 6s
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 15h
k describe svc custom-nginx
Name: custom-nginx
Namespace: default
Labels: run=custom-nginx
Annotations: <none>
Selector: run=custom-nginx
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.75.134
IPs: 10.96.75.134
Port: <unset> 8080/TCP
TargetPort: 80/TCP
Endpoints: 10.244.1.101:80
Session Affinity: None
Internal Traffic Policy: Cluster
Events: <none>
We can test the service resolution as well:
k run netshoot --rm -it --image=nicolaka/netshoot -- bash
On the terminal of the netshoot, run a curl command to test if the svc is reached.
k run netshoot --rm -it --image=nicolaka/netshoot -- bash
If you don't see a command prompt, try pressing enter.
netshoot:~# curl -v http://custom-nginx:8080
* Host custom-nginx:8080 was resolved.
* IPv6: (none)
* IPv4: 10.96.75.134
* Trying 10.96.75.134:8080...
* Established connection to custom-nginx (10.96.75.134 port 8080) from 10.244.1.79 port 42136
* using HTTP/1.x
> GET / HTTP/1.1
> Host: custom-nginx:8080
> User-Agent: curl/8.18.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx/1.29.4
< Date: Mon, 02 Feb 2026 11:28:38 GMT
< Content-Type: text/html
< Content-Length: 615
< Last-Modified: Tue, 09 Dec 2025 18:28:10 GMT
< Connection: keep-alive
< ETag: "69386a3a-267"
< Accept-Ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
* Connection #0 to host custom-nginx:8080 left intact
netshoot:~#
Clean up:
k delete svc custom-nginx; k delete po custom-nginx --force=true
8. Create a pod called httpd using the image httpd:alpine in the default namespace. Next, create a service of type ClusterIP by the same name (httpd). The target port for the service should be 80.
Pod creation:
k run httpd --image=httpd:alpine --restart=Never
Service creation with expose:
k expose pod httpd --port=80 --target-port=80 --type="ClusterIP"
Check if it was created:
k get all -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/httpd 1/1 Running 0 101s 10.244.1.137 minikube-m02 <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/httpd ClusterIP 10.107.31.39 <none> 80/TCP 4s run=httpd
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 18h <none>
Check id it’s reachable:
k run netshoot --rm -ti --image=nicolaka/netshoot -- bash
Run the curl command to reach it, the result should be similar to the one below:
k run netshoot --rm -it --image=nicolaka/netshoot -- bash
If you don't see a command prompt, try pressing enter.
netshoot:~# curl -v http://httpd
* Host httpd:80 was resolved.
* IPv6: (none)
* IPv4: 10.107.31.39
* Trying 10.107.31.39:80...
* Established connection to httpd (10.107.31.39 port 80) from 10.244.1.109 port 58292
* using HTTP/1.x
> GET / HTTP/1.1
> Host: httpd
> User-Agent: curl/8.18.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: Mon, 02 Feb 2026 14:36:50 GMT
< Server: Apache/2.4.66 (Unix)
< Last-Modified: Fri, 07 Nov 2025 08:23:08 GMT
< ETag: "bf-642fce432f300"
< Accept-Ranges: bytes
< Content-Length: 191
< Content-Type: text/html
<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>It works! Apache httpd</title>
</head>
<body>
<p>It works!</p>
</body>
</html>
* Connection #0 to host httpd:80 left intact
netshoot:~#
Clean up:
k delete svc httpd; k delete po httpd --force=true
9. Create a pod with the ubuntu image to run a container to sleep for 5000 seconds.
Create the pod:
k run ubuntu --image=ubuntu --restart=Never -- sleep 5000
Check if the pod was created and you can try get into iteractive mode oon it’s bash terminal:
k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ubuntu 1/1 Running 0 25s 10.244.1.226 minikube-m02 <none> <none>
k exec -ti ubuntu -- bash
k exec -ti ubuntu -- bash
root@ubuntu:/# ps -ef | grep sleep
root 1 0 0 14:49 ? 00:00:00 sleep 5000
root 28 19 0 14:51 pts/1 00:00:00 grep --color=auto sleep
root@ubuntu:/#
Clean up:
k delete po ubuntu --force=true
10. Create a new ConfigMap webapp-color. Set key-value pairs as COLOR=blue.
Let’s check the yaml we can generate with teh cm create first:
k create cm webapp-color --from-literal="COLOR=blue" --dry-run=client -o yaml
apiVersion: v1
data:
COLOR: blue
kind: ConfigMap
metadata:
creationTimestamp: null
name: webapp-color
If the config is ok we can then just create it:
k create cm webapp-color --from-literal="COLOR=blue"
k describe cm webapp-color
Name: webapp-color
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
COLOR:
-
blue
BinaryData
====
Events: <none>
PS: Do not remove now the config map as it might be used on later exercises.
11. Create a deployment nginx01 using nginx image, which has environment variable CREATED_BY with value octocat.
Full question text:
11. Create a deployment nginx01 using nginx image, which has environment variable CREATED_BY with value octocat.
1. Verify the environment variable in the containers of the above deployment.
k create deploy nginx01 --image=nginx --dry-run=client -o yaml
Let’s add it the env part of the resources then:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: nginx01
name: nginx01
spec:
replicas: 1
selector:
matchLabels:
app: nginx01
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: nginx01
spec:
containers:
- image: nginx
name: nginx
env:
- name: CREATED_BY
value: octocat
resources: {}
status: {}
Checking the env var at the pod:
k exec -ti nginx01-646558f5df-2q79f -- bash
root@nginx01-646558f5df-2q79f:/# env | grep -i created
CREATED_BY=octocat
root@nginx01-646558f5df-2q79f:/#
Clean up:
k delete deploy nginx01 --force=true
12. Create a new config map db-config with key-value pairs as:
Full question text:
12. Create a new config map db-config with key-value pairs as:
1. db-host = "localhost.example.com"
2. db-port = 3306
3. db-name = "kubernetes-objects"
4. Mount the config map db-config in the deployment ubuntu01 with image ubuntu which has 2 replicas.
5. check if the mount works and check the config file values.
Create the configmap:
k create cm db-config \
--from-literal="db-host=localhost.example.com" \
--from-literal="db-port=3306" \
--from-literal="db-name=kubernetes-objects"
k describe cm db-config
Name: db-config
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
db-host:
----
localhost.example.com
db-name:
----
kubernetes-objects
db-port:
----
3306
BinaryData
====
Events: <none>
Create the deploy yaml and edit to add the volume:
k create deploy ubuntu01 --image=ubuntu --replicas=2 --dry-run=client -o yaml > ubuntu01.yml
Edit the yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: ubuntu01
name: ubuntu01
spec:
replicas: 2
selector:
matchLabels:
app: ubuntu01
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: ubuntu01
spec:
containers:
- command:
- sleep
- "5000"
image: ubuntu
name: ubuntu
resources: {}
volumeMounts:
- name: db-config
mountPath: "/db-config"
readOnly: true
volumes:
- name: db-config
configMap:
name: db-config
status: {}
Create the deplyment:
k apply -f ubuntu01.yml
Check the file mounted in the pod:
k get po
NAME READY STATUS RESTARTS AGE
ubuntu01-78bcbb6554-bmzzm 1/1 Running 0 35s
ubuntu01-78bcbb6554-v2s4p 1/1 Running 0 33s
k exec -ti ubuntu01-78bcbb6554-bmzzm -- bash
root@ubuntu01-78bcbb6554-bmzzm:/#
root@ubuntu01-78bcbb6554-bmzzm:/# cat /db-config/
..2026_02_03_09_58_48.446555486/ ..data/ db-host db-name db-port
root@ubuntu01-78bcbb6554-bmzzm:/# cat /db-config/*
localhost.example.comkubernetes-objects3306root@ubuntu01-78bcbb6554-bmzzm:/#
root@ubuntu01-78bcbb6554-bmzzm:/#
root@ubuntu01-78bcbb6554-bmzzm:/# cat /db-config/db-host
localhost.example.comroot@ubuntu01-78bcbb6554-bmzzm:/#
root@ubuntu01-78bcbb6554-bmzzm:/#
root@ubuntu01-78bcbb6554-bmzzm:/# cat /db-config/db-name
kubernetes-objectsroot@ubuntu01-78bcbb6554-bmzzm:/#
root@ubuntu01-78bcbb6554-bmzzm:/#
root@ubuntu01-78bcbb6554-bmzzm:/# cat /db-config/db-port
3306root@ubuntu01-78bcbb6554-bmzzm:/#
root@ubuntu01-78bcbb6554-bmzzm:/#
root@ubuntu01-78bcbb6554-bmzzm:/#
exit
Clean up:
k delete deploy unbuntu01 force=true;
**Ps: Keep db-config as it’s going to be used on the exercise below
13. Create a deployment named multi-con-01, which has two containers …
Full question text:
13. Create a deployment named multi-con-01, which has two containers, one container uses nginx image and has port 80 exposed. Mount the config map db-config as environment variable. Create the second container in the same deployment with image ubuntu, keep the container alive using the command sleep 5000 and mount the config map in db-config with different keys where the new keys are mapped as:
1. db-host --> DB_URL
2. db-port --> DB_PORT
3. db-name --> DB_NAME
Create the deployment yaml:
k create deploy multi-con-01 --image=nginx --port=80 --dry-run=client -o yaml > multi-con-01.yaml
Edit the yaml with both containers and all mount for all configs:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: multi-con-01
name: multi-con-01
spec:
replicas: 1
selector:
matchLabels:
app: multi-con-01
template:
metadata:
creationTimestamp: null
labels:
app: multi-con-01
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: db-config
- image: ubuntu
name: ubuntu
args:
- sleep
- "5000"
env:
- name: DB_URL
valueFrom:
configMapKeyRef:
name: db-config
key: db-host
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: db-config
key: db-port
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: db-config
key: db-name
Check the pod and deployment:
k get all -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/multi-con-01-6755f5746f-wc5ch 2/2 Running 0 3m17s 10.244.1.222 minikube-m02 <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d <none>
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/multi-con-01 1/1 1 1 3m18s nginx,ubuntu nginx,ubuntu app=multi-con-01
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
replicaset.apps/multi-con-01-6755f5746f 1 1 1 3m17s nginx,ubuntu nginx,ubuntu app=multi-con-01,pod-template-hash=6755f5746f
Both containers are ready and working. Check the env vars:
k exec -ti multi-con-01-6755f5746f-wc5ch -c nginx -- bash
root@multi-con-01-6755f5746f-wc5ch:/# env | grep -i db
db-name=kubernetes-objects
db-host=localhost.example.com
db-port=3306
root@multi-con-01-6755f5746f-wc5ch:/#
k exec -ti multi-con-01-6755f5746f-wc5ch -c ubuntu -- bash
root@multi-con-01-6755f5746f-wc5ch:/# env | grep -i db
DB_PORT=3306
DB_URL=localhost.example.com
DB_NAME=kubernetes-objects
root@multi-con-01-6755f5746f-wc5ch:/#
Clean up:
k delete deploy multi-con-01 --force=true
14. Create an opaque secret named db-creds with following key value pairs:
Full question text:
14. Create an opaque secret named db-creds with following key value pairs:
1. db_user=root
2. db_password=pass404
3. db_role=admin
k create secret generic db-creds \
--from-literal="db_user=root" \
--from-literal="db_password=pass404" \
--from-literal="db_role=admin"
k get secret
k get secret
NAME TYPE DATA AGE
db-creds Opaque 3 7s
```bash
k describe secret
k describe secret
Name: db-creds
Namespace: default
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
db_role: 5 bytes
db_user: 4 bytes
db_password: 7 bytes
k get secret db-creds -o yaml
k get secret db-creds -o yaml
apiVersion: v1
data:
db_password: cGFzczQwNA==
db_role: YWRtaW4=
db_user: cm9vdA==
kind: Secret
metadata:
creationTimestamp: "2026-02-06T12:48:40Z"
name: db-creds
namespace: default
resourceVersion: "452025"
uid: f3249f22-2b34-41fa-bb6e-321b86aaf1a1
type: Opaque
PS: not a good practice to create secrets using –from-literal on automation or anywhere, as it stays on logs and on all yamls, and pure BASE64 is not encryoption too. Best practice is to encrypt secrets, decrypt where your automation worker run, and / or use a vault that can integrate easily with your platfornm.
Dont remobe it now, as the secret will be used on the next exercise.
15. Mount the secret db-creds in the deployment multi-con-01 [created in (23)] as environment variables on both containers with the same keys as mentioned in the secret.
Use the yaml above, from question 14, for the multi-con-01 deployment, edit and add the secret mounted as env vars with the keys as they’re already in the secret:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: multi-con-01
name: multi-con-01
spec:
replicas: 1
selector:
matchLabels:
app: multi-con-01
template:
metadata:
creationTimestamp: null
labels:
app: multi-con-01
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: db-config
- secretRef:
name: db-creds
- image: ubuntu
name: ubuntu
args:
- sleep
- "5000"
envFrom:
- secretRef:
name: db-creds
env:
- name: DB_URL
valueFrom:
configMapKeyRef:
name: db-config
key: db-host
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: db-config
key: db-port
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: db-config
key: db-name
Check the ubuntu container for the new env vars:
k exec -ti multi-con-01-dcb5466f6-mvl5d -c ubuntu -- bash
k exec -ti multi-con-01-dcb5466f6-mvl5d -c ubuntu -- bash
root@multi-con-01-dcb5466f6-mvl5d:/# env | grep -i db
db_password=pass404
db_user=root
DB_PORT=3306
DB_URL=localhost.example.com
db_role=admin
DB_NAME=kubernetes-objects
root@multi-con-01-dcb5466f6-mvl5d:/#
Clean up:
k delete deploy multi-con-01 --force=true; k delete secret db-creds
16. Create a pod ubuntu02 with image ubuntu and run the command sleep 3400 as user 1001.
k run ubuntu02 --image=ubuntu --dry-run=client -o yaml -- sleep 3400 > ubuntu02.yml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: ubuntu02
name: ubuntu02
spec:
securityContext:
runAsUser: 1001
containers:
- args:
- sleep
- "3400"
image: ubuntu
name: ubuntu02
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
Check the user runing te pod / container:
k exec -ti ubuntu02 -- bash
k exec -ti ubuntu02 -- bash
I have no name!@ubuntu02:/$ whoami
whoami: cannot find name for user ID 1001
I have no name!@ubuntu02:/$
Clean up:
k delete po ubuntu02 --force=true
17. Create a pod ubuntu03 with image ubuntu and add capabilities for SYS_TIME to each container. Run the command date -s ‘01JAN 2020 10:00:00’ to verify if it is successful.
Create the initla yaml with:
k run ubuntu03 --image=ubuntu --dry-run=client -o yaml -- sleep 5000 > ubuntu03.yml
Edit to add the securityContext:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: ubuntu03
name: ubuntu03
spec:
containers:
- args:
- sleep
- "5000"
image: ubuntu
name: ubuntu03
resources: {}
securityContext:
capabilities:
add: ["SYS_TIME"]
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
Testing the command for chaging the contianer date:
k exec -ti ubuntu03 -- bash
root@ubuntu03:/# date
Fri Feb 6 14:07:38 UTC 2026
root@ubuntu03:/#
root@ubuntu03:/# date -s '01JAN 2020 10:00:00'
Wed Jan 1 10:00:00 UTC 2020
root@ubuntu03:/#
root@ubuntu03:/# date
Wed Jan 1 10:00:02 UTC 2020
root@ubuntu03:/#
Clean up:
k delete po ubuntu03 --force=true
18. Taint one of the worker nodes in your cluster with the following property, mode=maintenance:NoSchedule. Create a pod nginx02 with image nginx:alpine and ensure the pod runs on the tainted node as above. Hint: You might want to use tolerations and nodeselectors both in case you have multiple worker nodes.
My minikube setup has two nodes as we can see here:
k get nodes
k get nodes
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane 22m v1.32.0
minikube-m02 Ready <none> 21m v1.32.0
I’ll taint the node minikube-m02:
k taint node minikube-m02 mode=maintenance:NoSchedule
Taints don’t show on get, so let me inspect the yaml of the node, and use jq to bring only the taints:
k get nodes minikube-m02 -o json | jq '.spec.taints'
[
{
"effect": "NoSchedule",
"key": "mode",
"value": "maintenance"
}
]
[
{
"effect": "NoSchedule",
"key": "mode",
"value": "maintenance"
}
]
And or jut using grep on top of the yaml (sometimes it’s quite hard to get the right json path):
k get nodes minikube-m02 -o yaml | grep -i -A5 "taint"
k get nodes minikube-m02 -o yaml | grep -i -A5 "taint"
taints:
- effect: NoSchedule
key: mode
value: maintenance
status:
addresses:
Generating a pod with toleration to the taint, so it can be scheduled on that node:
k run nginx02 --image=nginx:alpine --restart=Never --dry-run=client -o yaml > nginx02.yml
Edit the yaml adding nodeSelector and tolerations:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx02
name: nginx02
spec:
nodeSelector:
kubernetes.io/hostname: minikube-m02
tolerations:
- key: "mode"
operator: "Equal"
value: "maintenance"
effect: "NoSchedule"
containers:
- image: nginx:alpine
name: nginx02
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Never
status: {}
Ps: adding node selector to force the pod to be schedule on that node.*
Apply the yaml and check if the node is up an running on the node:
k apply -f nginx02.yml
k apply -f nginx02.yml
pod/nginx02 created
➜ fernhtls.github.io git:(main) ✗ k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx02 1/1 Running 0 8s 10.244.1.206 minikube-m02 <none> <none>
If we remove the toleration and try to create the pod again:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx02
name: nginx02
spec:
nodeSelector:
kubernetes.io/hostname: minikube-m02
containers:
- image: nginx:alpine
name: nginx02
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Never
status: {}
k delete po nginx02 --force=true; k create -f nginx02.yml
k delete po nginx02 --force=true; k create -f nginx02.yml
k delete po nginx02 --force=true; k create -f nginx02.yml
Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
pod "nginx02" force deleted
pod/nginx02 created
➜ fernhtls.github.io git:(main) ✗
➜ fernhtls.github.io git:(main) ✗ k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx02 0/1 Pending 0 7s <none> <none> <none> <none>
It’s Pending, so if we describe the pod:
➜ fernhtls.github.io git:(main) ✗ k describe po nginx02
Name: nginx02
Namespace: default
Priority: 0
Service Account: default
Node: <none>
Labels: run=nginx02
Annotations: <none>
Status: Pending
IP:
IPs: <none>
Containers:
nginx02:
Image: nginx:alpine
Port: <none>
Host Port: <none>
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-g6b7n (ro)
Conditions:
Type Status
PodScheduled False
Volumes:
kube-api-access-g6b7n:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
QoS Class: BestEffort
Node-Selectors: kubernetes.io/hostname=minikube-m02
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 3m7s default-scheduler 0/2 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, 1 node(s) had untolerated taint {mode: maintenance}. preemption: 0/2 nodes are available: 2 Preemption is not helpful for scheduling.
It’s clear that the pod was not scheduled as it does not have the toleration for the node.
You can check the events and use the selector filter to find events for the pod as well:
k get events -n default --field-selector involvedObject.name=nginx02
k get events -n default --field-selector involvedObject.name=nginx02
LAST SEEN TYPE REASON OBJECT MESSAGE
25m Normal Scheduled pod/nginx02 Successfully assigned default/nginx02 to minikube-m02
25m Normal Pulling pod/nginx02 Pulling image "nginx:alpine"
24m Normal Pulled pod/nginx02 Successfully pulled image "nginx:alpine" in 4.934s (4.934s including waiting). Image size: 25811077 bytes.
24m Normal Created pod/nginx02 Created container: nginx02
24m Normal Started pod/nginx02 Started container nginx02
18m Normal Killing pod/nginx02 Stopping container nginx02
13m Warning FailedScheduling pod/nginx02 0/2 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, 1 node(s) had untolerated taint {mode: maintenance}. preemption: 0/2 nodes are available: 2 Preemption is not helpful for scheduling.
2m52s Warning FailedScheduling pod/nginx02 0/2 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, 1 node(s) had untolerated taint {mode: maintenance}. preemption: 0/2 nodes are available: 2 Preemption is not helpful for scheduling.
Clean up (delete the pod and remove the taint):
k delete po nginx02 --force=true; k taint node minikube-m02 mode=maintenance:NoSchedule-
19. Create a pod box0 with image busybox and command sleep 30; touch /tmp/debug; sleep 30; rm /tmp/debug. The pod should not be ready until the file /tmp/debug is available in the pod. If the file doesn’t exist, the pod must restart.
Let’s create the intial yaml for the question:
k run box0 --image=busybox --dry-run=client -o yaml -- /bin/sh -c "sleep 30; touch /tmp/debug; sleep 300; rm -f /tmp/debug" > box0.yml
With the yaml lets now add the readiness and liveness probes for:
- Pod should be ready only when
/tmp/debugis present - Pod should restart if the file does not exist, so not add
--restart=Neverthis time. - I have increased the second sleep command as it’s nice to have some time to check whats going on in the pod, but feel free to decrease it.
readinessandlivenesswill be delayed for 30 secs,lsfor the file, test every 5 secs and have a 3 failure threshold.
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: box0
name: box0
spec:
containers:
- args:
- /bin/sh
- -c
- sleep 30; touch /tmp/debug; sleep 300; rm -f /tmp/debug
image: busybox
name: box0
resources: {}
readinessProbe:
exec:
command:
- ls
- /tmp/debug
initialDelaySeconds: 30
periodSeconds: 5
livenessProbe:
exec:
command:
- ls
- /tmp/debug
initialDelaySeconds: 30
periodSeconds: 5
dnsPolicy: ClusterFirst
restartPolicy: Always
Let’s check the pod states throught the time where the pod is running, and check if it restarts:
k get po -o wide -w
k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
box0 0/1 Running 0 2s 10.244.1.96 minikube-m02 <none> <none>
box0 1/1 Running 0 33s 10.244.1.96 minikube-m02 <none> <none>
box0 0/1 Completed 0 5m32s 10.244.1.96 minikube-m02 <none> <none>
box0 0/1 Running 1 (4s ago) 5m35s 10.244.1.96 minikube-m02 <none> <none>
box0 1/1 Running 1 (35s ago) 6m6s 10.244.1.96 minikube-m02 <none> <none>
- Container no ready up to the 30 secs.
- After it, after the first sleep command, file gets created and then pod is ready by the readinessProbe.
- After 5 mins the file is removed from the container, so livenessProbe will fail for 3 times, so 15 secs and then the pod is restarted.
- Container will continue to cicle on those states as it restarts, file gets created and then removed.
On describing the pod you can see some stats for the readiness and liveness probes:
k describe po box0 | grep -i ness
k describe po box0 | grep -i ness
Liveness: exec [ls /tmp/debug] delay=30s timeout=1s period=5s #success=1 #failure=3
Readiness: exec [ls /tmp/debug] delay=30s timeout=1s period=5s #success=1 #failure=3
PS: no successThreshold was set, so default one was pushed there.
Clean up:
k delete po box0 --force=true
20. Create a pod nginx03 with image nginx, make sure the server is up and running every 10 seconds. The nginx server ideally takes 30 seconds to warm up so ensure that there are no false negatives when reporting the health of the pod. Note: Nginx runs on port 80.
Creating the initial yaml for the pod:
k run nginx03 --image=nginx --port=80 --dry-run=client -o yaml > nginx03.yml
Liveness and Readiness reqquirements for the pod:
- Make sure it’s up every 10 secs.
- Only make it ready to receive requests after 30 secs, as “nginx takes 30 secs to start”.
- Use the port 80 to check both startup and liveness with.
In pratical terms both the readiness or startup probe could be use to check the slow start / warm on this particular case,
but one advantage of the startup is that it won’t be executing anymore after start is done, handling over directly to liveness top
continue inspecting.
Main differences between both:
- startup:
- slow startup / warm up for apps.
- runs until it succeds and does not run anymore.
- use it for k8s to not panic or handover to ready probes or liveness probes.
- see it more as “pod / container has to do all of this to really be done with startup, otherwise don’t start”.
- readiness:
- allows traffic to the pod if it succeds, so it adds / removes from service endpoints depending on the state.
- continuasly run through the pod / container life.
- good for checking if the app is really ready to serve requests, like if the db behind it is health as well and reachable.
- see it as “pod / app might be slow because db is slow as well, keep on trying but don’t serve traffic here”.
- liveness:
- checks if the pod / container is still working properly.
- different than readiness it restarts the pod if it fails, so makes the pod marked as unhealthy and it needs to restart.
- see it more as “pod / container app beyond repair, restart it”.
Usually a good practice is to have different end points for each check, as they have different concepts and effects.
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx03
name: nginx03
spec:
containers:
- image: nginx
name: nginx03
ports:
- containerPort: 80
resources: {}
startupProbe:
tcpSocket:
port: 80
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 2
livenessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
Creating the pod:
k apply -f nginx03.yml
On monitoring the change of the pod states we see the following:
k get po -o wide -w
k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx03 0/1 Running 0 4s 10.244.1.125 minikube-m02 <none> <none>
nginx03 1/1 Running 0 33s 10.244.1.125 minikube-m02 <none> <none>
We can see that the pod was not ready until 30 secs later, so waiting for the warm from nginx.
We can describe the pod as well to check the numbers for both liveness and startup:
k describe po nginx03 | egrep -i 'startup|ness'
Liveness: tcp-socket :80 delay=30s timeout=1s period=10s #success=1 #failure=3
Startup: tcp-socket :80 delay=30s timeout=1s period=10s #success=1 #failure=3
Clean up:
k delete deploy deploy01 --force=true
21. This is a multistage problem: ….
Full question text:
21. This is a multistage problem:
1. Create a deployment deploy01 with image nginx with port 80 exposed.
2. Update the deployment with image nginx:dev and make sure you record the execution of this action.
3. If the deployment in step b is unsuccessful, rollback the deployment to the previous state by setting any fields explicitly.
4. If the nginx deployment is in a healthy state, scale the deployment to run 3 replicas, also record this execution for audit-keeping.
5. Display the history of deployment deploy-01 and store it in file /tmp/21.txt
k create deploy deploy01 --image=nginx --port=80
k get deploy -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deploy01 1/1 1 1 17s nginx nginx app=deploy01
Set the new image:
k set image deploy deploy01 nginx=nginx:dev
If we monitor the deploy we can see the following now:
k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
deploy01-7bdbd55dbc-5t8cg 0/1 ImagePullBackOff 0 19s 10.244.1.53 minikube-m02 <none> <none>
deploy01-7c6784dc48-w5rmb 1/1 Running 0 2m27s 10.244.1.99 minikube-m02 <none> <none>
deploy01-7bdbd55dbc-5t8cg 0/1 ErrImagePull 0 28s 10.244.1.53 minikube-m02 <none> <none>
deploy01-7bdbd55dbc-5t8cg 0/1 ImagePullBackOff 0 43s 10.244.1.53 minikube-m02 <none> <none>
deploy01-7bdbd55dbc-5t8cg 0/1 ErrImagePull 0 55s 10.244.1.53 minikube-m02 <none> <none>
As we can see the image is not valid, so we need to fix it now, making the rollback of the deploy. Let’s check first the rollout states of the deployment:
k rollout history deploy deploy01
k rollout status deploy deploy01
k rollout history deploy deploy01
deployment.apps/deploy01
REVISION CHANGE-CAUSE
1 <none>
2 <none>
➜ fernhtls.github.io git:(main) ✗ k rollout status deploy deploy01
Waiting for deployment "deploy01" rollout to finish: 1 old replicas are pending termination...
We can see that the new change for the image is not rolled out, so no finishing and we know that’s because of the incorrect image tag. Let’s rollback the deployment:
k rollout undo deploy deploy01
After the undo / rollbck we can inspect the history, status and pods again:
k rollout history deploy deploy01
deployment.apps/deploy01
REVISION CHANGE-CAUSE
2 <none>
3 <none>
k rollout status deploy deploy01
deployment "deploy01" successfully rolled out
k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
deploy01-7c6784dc48-w5rmb 1/1 Running 0 9m59s 10.244.1.99 minikube-m02 <none> <none>
As the deployment is now on a health state, so let’s scale the replicas.
k scale deploy deploy01 --replicas=3
That does not create a new revision though, but the deployment is health and running 3 replicas:
k get deployments.apps deploy01 -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deploy01 3/3 3 3 74m nginx nginx app=deploy01
We can see pods on a health status now, and that the rollout has created a new revision 3 for the rollback / undo.
k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
deploy01-7c6784dc48-4pdxp 1/1 Running 0 80s 10.244.1.56 minikube-m02 <none> <none>
deploy01-7c6784dc48-cp42w 1/1 Running 0 80s 10.244.0.101 minikube <none> <none>
deploy01-7c6784dc48-w5rmb 1/1 Running 0 74m 10.244.1.99 minikube-m02 <none> <none>
k rollout history deployment deploy01
deployment.apps/deploy01
REVISION CHANGE-CAUSE
2 <none>
3 <none>
Let’s save the rollout history to the file /tmp/21.txt:
k rollout history deploy deploy01 > /tmp/21.txt
k rollout history deploy deploy01 > /tmp/21.txt
➜ fernhtls.github.io git:(main) ✗ cat /tmp/21.txt
deployment.apps/deploy01
REVISION CHANGE-CAUSE
2 <none>
3 <none>
We can see pods on a health status now, and that the rollout has created a new revision 3 for the rollback / undo.
22. Create a job that will roll a dice using image kodekloud/throw-dice. The image generates a random number between 1 and 6 and exits. It is a success only if it is a 6 else it is a failure. Check how many times it takes the job to trigger to get a successful outcome.
k create job dice --image=kodekloud/throw-dice
Let’s check the yaml of the job:
k get job dice -o yaml
apiVersion: batch/v1
kind: Job
metadata:
creationTimestamp: "2026-02-07T14:51:56Z"
generation: 1
labels:
batch.kubernetes.io/controller-uid: 24e0ead0-317b-4efb-9aa5-91d8100a6154
batch.kubernetes.io/job-name: dice
controller-uid: 24e0ead0-317b-4efb-9aa5-91d8100a6154
job-name: dice
name: dice
namespace: default
resourceVersion: "102175"
uid: 24e0ead0-317b-4efb-9aa5-91d8100a6154
spec:
backoffLimit: 6
completionMode: NonIndexed
completions: 1
manualSelector: false
parallelism: 1
podReplacementPolicy: TerminatingOrFailed
selector:
matchLabels:
batch.kubernetes.io/controller-uid: 24e0ead0-317b-4efb-9aa5-91d8100a6154
suspend: false
template:
metadata:
creationTimestamp: null
labels:
batch.kubernetes.io/controller-uid: 24e0ead0-317b-4efb-9aa5-91d8100a6154
batch.kubernetes.io/job-name: dice
controller-uid: 24e0ead0-317b-4efb-9aa5-91d8100a6154
job-name: dice
spec:
containers:
- image: kodekloud/throw-dice
imagePullPolicy: Always
name: dice
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Never
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
status:
failed: 5
ready: 0
startTime: "2026-02-07T14:51:56Z"
terminating: 0
uncountedTerminatedPods: {}
Chances of getting a 6 are 1/6, so not great so the job will keep on running for a while until it gets a success, but it can be quick as well of course, and our completions are just set to 1.
If you don’t get success the default backofflimit might apply, 6 in this case. So on my test the job just could not complete.
k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
dice-8bnhh 0/1 Error 0 9m32s 10.244.1.197 minikube-m02 <none> <none>
dice-8g77q 0/1 Error 0 8m57s 10.244.1.50 minikube-m02 <none> <none>
dice-gxcq8 0/1 Error 0 8m16s 10.244.1.5 minikube-m02 <none> <none>
dice-jrs4w 0/1 Error 0 4m14s 10.244.1.242 minikube-m02 <none> <none>
dice-pg9jj 0/1 Error 0 6m55s 10.244.1.119 minikube-m02 <none> <none>
dice-zmqfc 0/1 Error 0 9m18s 10.244.1.116 minikube-m02 <none> <none>
k get job -o wide -w
NAME STATUS COMPLETIONS DURATION AGE CONTAINERS IMAGES SELECTOR
dice Running 0/1 9m57s 9m57s dice kodekloud/throw-dice batch.kubernetes.io/controller-uid=24e0ead0-317b-4efb-9aa5-91d8100a6154
k describe job dice
...
Backoff Limit: 6
Start Time: Sat, 07 Feb 2026 15:51:56 +0100
Pods Statuses: 0 Active (0 Ready) / 0 Succeeded / 6 Failed
Clean up the job:
k delete job dice --force=true
23. Create a job with the above image to generate 8 successful outcomes. Also, ensure that the job will attempt no more than 15 attempts and see if the job is successful. You can add an element of 3 parallel executions if required.
k create job dice --image=kodekloud/throw-dice --dry-run=client -o yaml > dice.yml
Now let’s edit the yaml and add completions, parallelism and backoffLmit:
apiVersion: batch/v1
kind: Job
metadata:
creationTimestamp: null
name: dice
spec:
parallelism: 3
backoffLimit: 15
completions: 8
template:
metadata:
creationTimestamp: null
spec:
containers:
- image: kodekloud/throw-dice
name: dice
resources: {}
restartPolicy: Never
status: {}
This time we’ve got to complete the job, even though the completions were higher now:
k get job -o wide -w
NAME STATUS COMPLETIONS DURATION AGE CONTAINERS IMAGES SELECTOR
dice Complete 8/8 77s 83s dice kodekloud/throw-dice batch.kubernetes.io/controller-uid=44bd4343-291b-44b1-92a6-d3fd64315b13
Inspecting the pods we can see the ones with error and the completed ones:
k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
dice-45jz4 0/1 Error 0 2m9s 10.244.1.180 minikube-m02 <none> <none>
dice-685n7 0/1 Error 0 2m27s 10.244.1.149 minikube-m02 <none> <none>
dice-8bvxh 0/1 Completed 0 2m14s 10.244.1.81 minikube-m02 <none> <none>
dice-8gbcb 0/1 Error 0 2m19s 10.244.1.98 minikube-m02 <none> <none>
dice-8jl86 0/1 Completed 0 107s 10.244.1.226 minikube-m02 <none> <none>
dice-bbd4b 0/1 Error 0 2m10s 10.244.1.131 minikube-m02 <none> <none>
dice-dzm68 0/1 Error 0 95s 10.244.1.216 minikube-m02 <none> <none>
dice-f6r2v 0/1 Error 0 2m19s 10.244.1.44 minikube-m02 <none> <none>
dice-f79tv 0/1 Completed 0 2m27s 10.244.1.86 minikube-m02 <none> <none>
dice-ffmcw 0/1 Error 0 2m27s 10.244.1.71 minikube-m02 <none> <none>
dice-fjcfm 0/1 Completed 0 2m19s 10.244.1.15 minikube-m02 <none> <none>
dice-g67fg 0/1 Completed 0 74s 10.244.1.95 minikube-m02 <none> <none>
dice-hltds 0/1 Completed 0 2m14s 10.244.1.212 minikube-m02 <none> <none>
dice-hv2vj 0/1 Completed 0 2m10s 10.244.1.118 minikube-m02 <none> <none>
dice-q4wjj 0/1 Completed 0 2m23s 10.244.1.113 minikube-m02 <none> <none>
dice-szj6x 0/1 Error 0 107s 10.244.1.206 minikube-m02 <none> <none>
The backoffLimit applies to errors or retries, it’s not an overall attemp limit.
So in our case there the 15 backoffLimit means, that the job will retry 15 times until it gets a completions.
On our previous case you could see that we didn’t get a single completion and we exhausted our 6 default retries.
Clean up:
k delete job dice --force=true
24. Create a scheduled job such that it runs every minute. Use the image kodekloud/throw-dice to check the outcome.
k create cronjopb throw-dice --image=kodekloud/throw-dice --schedule="* * * * *"
k get cronjob
k get cronjob
NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE
throw-dice * * * * * <none> False 0 <none> 15s
Clean up:
k delete cronjob throw-dice --force=true
25. Create a Persistent Volume deploy-history which makes the storage available on each worker nodes at /tmp/deployment. Make sure it has the default provisioner of your cluster assigned. Label the Persistent Volume with audit: true, tier: middleware. The persistent volume must have a capacity of 2 GB.
As there’s no imperative command to create a pv, and the cert allows it you can check the yaml on how to create ir here. So after editing the yaml here’s the pv yaml:
touch deploy-history-pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: deploy-history
labels:
audit: "true"
tier: "middletier"
spec:
capacity:
storage: 2Gi
storageClassName: standard
accessModes:
- ReadWriteOnce
hostPath:
path: /tmp/deployment
type: "DirectoryOrCreate"
Create with:
k create -f deploy-history-pv.yml
Check the pv after creation:
k get pv -o wide
k get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE VOLUMEMODE
deploy-history 2Gi RWO Retain Available standard <unset> 80s Filesystem
Don’t remove the pv as it’s going to be used on a next question.
26. Create a Persistent Volume persistent-data which uses a storage class name persistent, label persistent=”true” and is of type hostPath which stores the data on /tmp/persistent on worker nodes. The persistent volume must have a capacity of 2 GB.
Again lets create a new file, or just copy the previous exercise file as it’s mainly the same yaml definition, and we just need to change a few small things.
cp deploy-history-pv.yml persistent-data.pv
After that we can edit the file with the whole new definition here for this question:
apiVersion: v1
kind: PersistentVolume
metadata:
name: persistent-data
labels:
persistent: "true"
spec:
capacity:
storage: 2Gi
storageClassName: persistent
accessModes:
- ReadWriteOnce
hostPath:
path: /tmp/peristent
type: "DirectoryOrCreate"
Again we can create and then check the pv:
k create -f persistent-data.yml
k get pv -o wide
k get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE VOLUMEMODE
deploy-history 2Gi RWO Retain Available standard <unset> 17m Filesystem
persistent-data 2Gi RWO Retain Available persistent <unset> 8s Filesystem
Again do not remove the pv here, as it will be used further on.
27. Create a Persistent Volume my-personal-data which uses a storage class name persistent and is of type hostPath which stores the data on /tmp/pii on worker nodes. The persistent volume must have a capacity of 2 GB. Since this will store personal data, make sure you can identify the persistent volume using the labels security: high, type: pii, audit: true.
We can again copy from the first yaml file we create for the deploy-history pv and do further changes:
cp deploy-history-pv.tml my-personal-data.yml
And then we can edit the yaml wit the requirements above:
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-personal-data
labels:
security: high
type: pii
audit: "true"
spec:
capacity:
storage: 2Gi
storageClassName: persistent
accessModes:
- ReadWriteOnce
hostPath:
path: /tmp/pii
type: "DirectoryOrCreate"
Create and check the creation of it:
k create -f my-personal-data.yml
k get pv -o wide
k get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE VOLUMEMODE
deploy-history 2Gi RWO Retain Available standard <unset> 29m Filesystem
my-personal-data 2Gi RWO Retain Available persistent <unset> 4s Filesystem
persistent-data 2Gi RWO Retain Available persistent <unset> 11m Filesystem
28. Create a Persistent Volume Claim to bind to persistent volume which uses storage class persistent and requests 2 GB capacity.
We can’t again create persistent volume claims with the imperative commands, so let’s check the yaml syntax here.
touch my-persistent-data-claim.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-persistent-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: persistent
selector:
matchLabels:
persistent: "true"
Create and check if the claim has bounded to the right volume:
k create -f my-personal-data-claim.yml
k get pvc -o wide --show-labels
k get pvc -o wide --show-labels
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE VOLUMEMODE LABELS
my-persistent-data Bound persistent-data 2Gi RWO persistent <unset> 15s Filesystem <none>
29. Create a Persistent Volume Claim to bind to a PV which ensures that the personal and private data is secure. Ensure that this PVC will bind to a PV of type pii.
Let’s copy the pvc yaml from the previous quetions, just generating a new file called personal-data-claim.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: personal-data-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: persistent
selector:
matchLabels:
security: high
type: "pii"
audit: "true"
k create -f personal-data-claim.yml
Let’s check the claims again:
k get pvc -o wide --show-labels
k get pvc -o wide --show-labels
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE VOLUMEMODE LABELS
my-persistent-data Bound persistent-data 2Gi RWO persistent <unset> 137m Filesystem <none>
personal-data-claim Bound my-personal-data 2Gi RWO persistent <unset> 3s Filesystem <none>
30. Create a PVC such that it will bind to a PV that is qualified for middleware applications.
Let’s copy again one of the pvc yaml resouces to create this new one:
cp personal-data-claim.yml middleware-data-claim.yml
Let’s then edit the yaml to create the new pvc:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: middleware-data-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: standard
selector:
matchLabels:
audit: "true"
tier: middleware
Let’s create the new pvc:
k create -f middleware-data-claim.yml
k get pvc -o wide --show-labels
k get pvc -o wide --show-labels
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE VOLUMEMODE LABELS
middleware-data-claim Bound pvc-04611d4b-8904-4ea0-9ebb-8b2f56403622 2Gi RWO standard <unset> 11s Filesystem <none>
my-persistent-data Bound persistent-data 2Gi RWO persistent <unset> 145m Filesystem <none>
personal-data-claim Bound my-personal-data 2Gi RWO persistent <unset> 7m44s Filesystem <none>
31. Create a deployment deploy02 with image busybox, mount one of the PVC from above such that it will store logs in the /tmp/deployment directory. The containers of the deployment must:
Full question text:
31. Create a deployment deploy02 with image busybox, mount one of the PVC from above such that it will store logs in the /tmp/deployment directory. The containers of the deployment must:
1. Create a file in /tmp/deployment with the same name as the pod name with .txt extension.
2. Run the command i=0; while true; do echo "$i $(date)"; sleep 60; i=$((i+1)); done >> /tmp/deployment/${POD_NAME}.txt done to the log file. Hint: You can use init-containers, environment variables to achieve this.
3. Ensure there are 5 replicas of the deployment.
4. Ensure you can see the log files created and populated with data in /tmp/deployment directory on the scheduled worker nodes.
Create first the deployment with imperative command:
k create deploy deploy02 --image=busybox --replicas=5 --dry-run=client -o yaml > deploy02.yml
Now I’ll be adding the following to the yaml:
- the command :
i=0; while true; do echo "$i $(date)"; sleep 60; i=$((i+1)); done >> /tmp/deployment/${POD_NAME}.txt - the env var POD_NAME: getting it from k8s fields metadata.name
- the mount using the middleware-data-claim pvc
- and as the pv / pvc was created with RWO (ReadWriteOnce), we have to make that the mount goes to a single node, as it can’t be mounted on two nodes at the same time.
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: deploy02
name: deploy02
spec:
replicas: 5
selector:
matchLabels:
app: deploy02
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: deploy02
spec:
containers:
- command:
- /bin/sh
- -c
- i=0; while true; do echo "$i $(date)"; sleep 60; i=$((i+1)); done >> /tmp/deployment/${POD_NAME}.txt
image: busybox
name: busybox
resources: {}
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- mountPath: "/tmp/deployment"
name: middleware-pvc
volumes:
- name: middleware-pvc
persistentVolumeClaim:
claimName: middleware-data-claim
status: {}
The PV / PVC is created with RWO (ReadWriteOnce) and hostpath, what allows several pods to be mounting the same PVC. Each node host will have it’s own directory created by the storage-provisioner or driver, making as the files for each pod are spread through nodes. We can see as well that not all files for all pods will show when inspecting one pod or the other.
Let’s then create the deployment and check after all the pods and the node on podman.
k create -f deploy02.yml
k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
deploy02-7c557485bb-6lqnt 1/1 Running 0 7s 10.244.1.177 minikube-m02 <none> <none>
deploy02-7c557485bb-7sfnh 1/1 Running 0 7s 10.244.0.229 minikube <none> <none>
deploy02-7c557485bb-ccxp8 1/1 Running 0 7s 10.244.0.204 minikube <none> <none>
deploy02-7c557485bb-txnb5 1/1 Running 0 7s 10.244.1.90 minikube-m02 <none> <none>
deploy02-7c557485bb-ww7qv 1/1 Running 0 7s 10.244.1.71 minikube-m02 <none> <none>
We can now see all pods running, let’s now check the files, and for that we can do an interactive shell to one of the pods for each node.
k exec -ti deploy02-7c557485bb-ww7qv -- sh
And here we can see all files for all pods running on minikube-m02 node:
k exec -ti deploy02-7c557485bb-ww7qv -- sh
/ # ls -ltr /tmp/deployment/
total 12
-rw-r--r-- 1 root root 93 Feb 11 11:02 deploy02-7c557485bb-ww7qv.txt
-rw-r--r-- 1 root root 93 Feb 11 11:02 deploy02-7c557485bb-6lqnt.txt
-rw-r--r-- 1 root root 93 Feb 11 11:02 deploy02-7c557485bb-txnb5.txt
/ # tail -f /tmp/deployment/deploy02-7c557485bb-*.txt
==> /tmp/deployment/deploy02-7c557485bb-6lqnt.txt <==
0 Wed Feb 11 11:00:35 UTC 2026
1 Wed Feb 11 11:01:35 UTC 2026
2 Wed Feb 11 11:02:35 UTC 2026
==> /tmp/deployment/deploy02-7c557485bb-txnb5.txt <==
0 Wed Feb 11 11:00:38 UTC 2026
1 Wed Feb 11 11:01:38 UTC 2026
2 Wed Feb 11 11:02:38 UTC 2026
==> /tmp/deployment/deploy02-7c557485bb-ww7qv.txt <==
0 Wed Feb 11 11:00:34 UTC 2026
1 Wed Feb 11 11:01:34 UTC 2026
2 Wed Feb 11 11:02:34 UTC 2026
3 Wed Feb 11 11:03:34 UTC 2026
==> /tmp/deployment/deploy02-7c557485bb-6lqnt.txt <==
3 Wed Feb 11 11:03:35 UTC 2026
Let’s now check some pod running on minikube node:
k exec -ti deploy02-7c557485bb-ccxp8 -- sh
/ # ls -ltr /tmp/deployment/
total 8
-rw-r--r-- 1 root root 155 Feb 11 11:04 deploy02-7c557485bb-7sfnh.txt
-rw-r--r-- 1 root root 155 Feb 11 11:04 deploy02-7c557485bb-ccxp8.txt
/ # tail -f /tmp/deployment/deploy02-7c557485bb-*.txt
==> /tmp/deployment/deploy02-7c557485bb-7sfnh.txt <==
0 Wed Feb 11 11:00:34 UTC 2026
1 Wed Feb 11 11:01:34 UTC 2026
2 Wed Feb 11 11:02:34 UTC 2026
3 Wed Feb 11 11:03:34 UTC 2026
4 Wed Feb 11 11:04:34 UTC 2026
==> /tmp/deployment/deploy02-7c557485bb-ccxp8.txt <==
0 Wed Feb 11 11:00:35 UTC 2026
1 Wed Feb 11 11:01:35 UTC 2026
2 Wed Feb 11 11:02:35 UTC 2026
3 Wed Feb 11 11:03:35 UTC 2026
4 Wed Feb 11 11:04:35 UTC 2026
==> /tmp/deployment/deploy02-7c557485bb-7sfnh.txt <==
5 Wed Feb 11 11:05:34 UTC 2026
==> /tmp/deployment/deploy02-7c557485bb-ccxp8.txt <==
5 Wed Feb 11 11:05:35 UTC 2026
We can see only 2 files, as just 2 nodes are running on this same node.
We can check as well the host container running in podman:
podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d786d3251f22 gcr.io/k8s-minikube/kicbase:v0.0.46 4 days ago Up 4 days 127.0.0.1:37833->22/tcp, 127.0.0.1:46179->2376/tcp, 127.0.0.1:37471->5000/tcp, 127.0.0.1:36927->8443/tcp, 127.0.0.1:35437->32443/tcp minikube
8351c9f257d5 gcr.io/k8s-minikube/kicbase:v0.0.46 4 days ago Up 4 days 127.0.0.1:40011->22/tcp, 127.0.0.1:39107->2376/tcp, 127.0.0.1:35633->5000/tcp, 127.0.0.1:34539->8443/tcp, 127.0.0.1:46243->32443/tcp minikube-m02
➜ fernhtls.github.io git:(main) ✗ podman exec -ti 8351c9f257d5 bash
root@minikube-m02:/# ls -tlr /tmp/hostpath-provisioner/default/middleware-data-claim/*
-rw-r--r--. 1 root root 217 Feb 11 11:06 /tmp/hostpath-provisioner/default/middleware-data-claim/deploy02-7c557485bb-ww7qv.txt
-rw-r--r--. 1 root root 217 Feb 11 11:06 /tmp/hostpath-provisioner/default/middleware-data-claim/deploy02-7c557485bb-6lqnt.txt
-rw-r--r--. 1 root root 217 Feb 11 11:06 /tmp/hostpath-provisioner/default/middleware-data-claim/deploy02-7c557485bb-txnb5.txt
root@minikube-m02:/# tail -f /tmp/hostpath-provisioner/default/middleware-data-claim/*
==> /tmp/hostpath-provisioner/default/middleware-data-claim/deploy02-7c557485bb-6lqnt.txt <==
0 Wed Feb 11 11:00:35 UTC 2026
1 Wed Feb 11 11:01:35 UTC 2026
2 Wed Feb 11 11:02:35 UTC 2026
3 Wed Feb 11 11:03:35 UTC 2026
4 Wed Feb 11 11:04:35 UTC 2026
5 Wed Feb 11 11:05:35 UTC 2026
6 Wed Feb 11 11:06:35 UTC 2026
==> /tmp/hostpath-provisioner/default/middleware-data-claim/deploy02-7c557485bb-txnb5.txt <==
0 Wed Feb 11 11:00:38 UTC 2026
1 Wed Feb 11 11:01:38 UTC 2026
2 Wed Feb 11 11:02:38 UTC 2026
3 Wed Feb 11 11:03:38 UTC 2026
4 Wed Feb 11 11:04:38 UTC 2026
5 Wed Feb 11 11:05:38 UTC 2026
6 Wed Feb 11 11:06:38 UTC 2026
==> /tmp/hostpath-provisioner/default/middleware-data-claim/deploy02-7c557485bb-ww7qv.txt <==
0 Wed Feb 11 11:00:34 UTC 2026
1 Wed Feb 11 11:01:34 UTC 2026
2 Wed Feb 11 11:02:34 UTC 2026
3 Wed Feb 11 11:03:34 UTC 2026
4 Wed Feb 11 11:04:34 UTC 2026
5 Wed Feb 11 11:05:35 UTC 2026
6 Wed Feb 11 11:06:35 UTC 2026
7 Wed Feb 11 11:07:35 UTC 2026
==> /tmp/hostpath-provisioner/default/middleware-data-claim/deploy02-7c557485bb-6lqnt.txt <==
7 Wed Feb 11 11:07:35 UTC 2026
==> /tmp/hostpath-provisioner/default/middleware-data-claim/deploy02-7c557485bb-txnb5.txt <==
7 Wed Feb 11 11:07:38 UTC 2026
Clean up:
k delete deploy deploy02
PS: I’m not cleaning up the PV / PVCs here, but you might want to remove it if you don’t want to inspect anymore the files generated or any other aspects of it.
32. Create two deployments i.e. nginx04 and redis04 using images nginx and redis respectively with 3 replicas. Your goal is to ensure that:
Full question text:
32. Create two deployments i.e. nginx04 and redis04 using images nginx and redis respectively with 3 replicas. Your goal is to ensure that:
1. each worker node must not have 2 or more nginx04 pods.
2. each worker node that has a nginx04 pod must have a redis04 pod scheduled on the same node.
Hint: Use pod affinity and anti-affinity to ensure the above scenario.
Let’s create first the initial nginx04 and redis04 deployment yamls:
k create deploy nginx04 --image=nginx --replicas=3 --dry-run=client -o yaml > nginx04.yml
k create deploy redis04 --image=redis --replicas=3 --dry-run=client -o yaml > redis04.yml
So now we have to apply the requirements and they are topology spread constraints for the nginx04 pod, and pod affinity requirement between redis and nginx. Let’s edit both and add all the affinity clauses.
So just for the exercise it and to see the pods getting distribute accordingly I’ve recreated my minikube env with 3 nodes:
minikube start --container-runtime=containerd --cni=cilium --nodes=3 --memory=1.8G --driver=podman
1.8Gb is the minimal required my minikube, but as well my podman env is limited to 6GB max.
Feel free to try the same yaml deployment but maybe instead of using DoNotSchedule use
So for the nginx04 deploy we have to add the topologySpreadConstraints:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: nginx04
name: nginx04
spec:
replicas: 3
selector:
matchLabels:
app: nginx04
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: nginx04
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: nginx04
containers:
- image: nginx
name: nginx
resources: {}
status: {}
Let’s already create the deployment and check if the pods are actually spread through the nodes, one each:
k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx04-74dc6f8d9-2nsv6 1/1 Running 0 25m 10.244.2.234 minikube-m03 <none> <none>
nginx04-74dc6f8d9-fw2pd 1/1 Running 0 25m 10.244.0.18 minikube-m02 <none> <none>
nginx04-74dc6f8d9-nq2rx 1/1 Running 0 25m 10.244.1.135 minikube <none> <none>
Now let’s edit the current yaml for the redis deployment and let’s add the podAfinnity:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: redis04
name: redis04
spec:
replicas: 3
selector:
matchLabels:
app: redis04
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: redis04
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nginx04
topologyKey: kubernetes.io/hostname
containers:
- image: redis
name: redis
resources: {}
status: {}
And we can see that the redis deployment is spread through all nodes just like the nginx:
k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx04-74dc6f8d9-2nsv6 1/1 Running 0 37m 10.244.2.234 minikube-m03 <none> <none>
nginx04-74dc6f8d9-fw2pd 1/1 Running 0 37m 10.244.0.18 minikube-m02 <none> <none>
nginx04-74dc6f8d9-nq2rx 1/1 Running 0 37m 10.244.1.135 minikube <none> <none>
redis04-79cb9f89b7-dd7qg 1/1 Running 0 12s 10.244.0.191 minikube-m02 <none> <none>
redis04-79cb9f89b7-dhk47 1/1 Running 0 12s 10.244.2.149 minikube-m03 <none> <none>
redis04-79cb9f89b7-x62vz 1/1 Running 0 12s 10.244.1.197 minikube <none> <none>
When the schedule is successful for everything we don’t have any events or any details within the pod describe.
In case there was any problems we would see somethiig on scheduler events and messages similar to: 0/5 nodes are available: 3 node(s) didn't match pod topology spread constraints.
Those files are now not epheremal anymore, so even if the deployment is removed and pods are not running, the file is consistent on nodes. PVs and PVCs are quite important for applications that need consistent disk writes, like DBs.
Clean up:
k delete deploy nginx04 redis04
33. Create 3 pods nginx05, redis05, and httpd05 with images nginx, redis, and httpd respectively. Attach a sidecar debug container of image docker.io/subfuzion/netcat to each of them, or any other image that you can test the connectivy. The security team has enforced the application team to limit the communication between unnecessary pods. Ensure that only the following communication is allowed between pods:
Full question text:
33. Create 3 pods nginx05, redis05, and httpd05 with images nginx, redis, and httpd respectively. Attach a sidecar debug container of image `registry.k8s.io/e2e-test-images/jessie-dnsutils:1.3` to each of them, or any other image that you can test the connectivy. The security team has enforced the application team to limit the communication between unnecessary pods. Ensure that only the following communication is allowed between pods:
1. Allow:
1. nginx05 <--> redis05
2. nginx05 <--> httpd05
2. Deny:
1. httpd05 <-/-> redis05
2. redis05 <-/-> rest of the world
3. nginx05 <-/-> the rest of the world
3. Consider the following ports for the pods as:
1. nginx05 : 80
2. httpd05 : 80
3. redis05 : 6379
4. Identify a possible information flow in this system. Write the information flow in the format End-User::Pod1::Pod2::Pod3.
Let’s create the first pod yaml, and copy it over as all pods need to have the sider car container with busybox only, and let’s put a infinity sleep on it, as it’s there mainly to only test the connectivity between the pods. The ssecond step will be to create the network policy with the ingress / egress between the pods / services.
k run nginx05 --image=nginx --port=80 --restart=Never --dry-run=client -o yaml > nginx05.yml
We need to then edit the yaml and add the sidecar container with netcat, and this the yaml:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx05
name: nginx05
spec:
containers:
- image: nginx
name: nginx05
ports:
- containerPort: 80
- name: connec-debug
image: docker.io/subfuzion/netcat
command:
- sleep
- "10000"
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Never
status: {}
Let’s then just create the other pods by using sed and process substistutioni by running:
k create -f <(sed 's/nginx/httpd/g' nginx05.yml)to createhttpd05.- And
k create -f <(sed -e 's/nginx/redis/g' -e 's/80/6379/g' nginx05.yml)to createredis05.
With that we don’t need to generate any files nor nothing else, as mainly we don’t have to push any different arguments for the pods as well.
But if you need to generate each file for some reason, like not being able to do process substituion, just run the following to generate the file: sed 's/nginx/httpd/g' nginx05.yml > httpd05.yml.
We can see that I was able to create all podsd directly with the k create command above:
k create -f <(sed 's/nginx/httpd/g' nginx05.yml)
pod/httpd05 created
k create -f <(sed -e 's/nginx/redis/g' -e 's/80/6379/g' nginx05.yml)
pod/redis05 created
Let me expose all pods through services, to actually be able to reach the pods through service / k8s dns resolution.
k expose pod nginx05 --port=80 --target-port=80 --type='ClusterIP'
k expose pod httpd05 --port=80 --target-port=80 --type='ClusterIP'
k expose pod redis05 --port=6379 --target-port=6379 --type='ClusterIP'
Let’s check if can get to the terminal on connec-debug first:
k exec -ti nginx05 -c connec-debug -- sh
We can run netcat commands to check the connectivy, still without any network policy in place:
nc -w <timeout> -zv <svc name/host> <port>
So on checking the connectivity from nginx05 to the other pods we can see that it’s working:
/ # nc -w 5 -zv redis05 6379
Connection to redis05 6379 port [tcp/*] succeeded!
/ # nc -w 5 -zv httpd05 80
Connection to httpd05 80 port [tcp/http] succeeded!
/ #
For now as there’s no network policy all connections should work. As we can check the rsources during the test, we can get the network policy resource from here.
As we can see by the design of the appm, the flow is USER --> HTTPD05 --> NGINX05 --> REDIS05.
To achieve this and apply all the rules above, I’m creating 3 ingresses network policies.
Each file will be nginx-net-policy.yml and redis05-net-policy.yml.
We have to be careful when applying restricted egress rules, as we might end up blocking to k8s internal like the dns resolution.
So in order here are all 3 network policies.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: nginx05-net-policy
namespace: default
spec:
podSelector:
matchLabels:
run: nginx05
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
run: httpd05
ports:
- protocol: TCP
port: 80
Allows ingress from httpd05 only to nginx05.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: redis05-net-policy
spec:
podSelector:
matchLabels:
run: redis05
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
run: nginx05
ports:
- protocol: TCP
port: 6379
**Allows ingress from nginx05 only to redis05.
So by these policies:
- Only
nginx05accessredis05. - Only
httpd05accessnginx05. httpd05is open to the world for access, like a landing page for a user.- No other ingress is allowed to both
nginx05andredis05.
Let’s test the proper access now following the order above for the flow of information.
So from nginx05 we can connect to redis05 and httpd05:
k exec -ti nginx05 -c connec-debug -- sh
/ # ping -c 2 httpd05
PING httpd05 (10.107.172.172): 56 data bytes
64 bytes from 10.107.172.172: seq=0 ttl=61 time=0.717 ms
64 bytes from 10.107.172.172: seq=1 ttl=61 time=3.751 ms
--- httpd05 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.717/2.234/3.751 ms
/ # ping -c 2 redis05
PING redis05 (10.96.124.16): 56 data bytes
64 bytes from 10.96.124.16: seq=0 ttl=61 time=0.656 ms
64 bytes from 10.96.124.16: seq=1 ttl=61 time=1.789 ms
--- redis05 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.656/1.222/1.789 ms
/ # nc -w 5 -zv httpd05 80
Connection to httpd05 80 port [tcp/http] succeeded!
/ # nc -w 5 -zv redis05 6379
Connection to redis05 6379 port [tcp/*] succeeded!
The flow from nginx05 seems to work fine, let’s see if we can from a different pod if we can connect to nginx05:
k run test-conn -ti --rm --image=docker.io/subfuzion/netcat -- -w 5 -zv nginx05 80
If you don't see a command prompt, try pressing enter.
nc: connect to nginx05 port 80 (tcp) timed out: Operation in progress
Session ended, resume using 'kubectl attach test-conn -c test-conn -i -t' command when the pod is running
pod "test-conn" deleted
Connection from the So we can’t access externally the nginx05 pod.
Let’s check with the same method the access to redis05.
k run test-conn -ti --rm --image=docker.io/subfuzion/netcat -- -w 5 -zv redis05 6379
If you don't see a command prompt, try pressing enter.
nc: connect to redis05 port 6379 (tcp) timed out: Operation in progress
Session ended, resume using 'kubectl attach test-conn -c test-conn -i -t' command when the pod is running
pod "test-conn" deleted
Let’s check if we can access httpd05:
k run test-conn -ti --rm --image=docker.io/subfuzion/netcat -- -w 5 -zv httpd05 80
If you don't see a command prompt, try pressing enter.
warning: couldn't attach to pod/test-conn, falling back to streaming logs: Internal error occurred: unable to upgrade connection: container test-conn not found in pod test-conn_default
Connection to httpd05 80 port [tcp/http] succeeded!
pod "test-conn" deleted
Connection to the external world is only possible to httpd05.
Clean up:
k delete po nginx05 httpd05 redis05
k delete networkpolicies nginx05-net-policy redis05-net-policy
k delete svc nginx05 httpd05 redis05
34. A developer started a pod calculus with image busybox which performs the calculation i=0; while true; do a=$(( i*i+i )); echo $a; i=$(( i+1 )); done. But later realized that this is not a reliable way to start a calculation-intensive pod. The developer decided to use a replicaset instead. But with the current pod running it would ruin all the calculations done so far. Can you help the developer to ensure that a replicaset can be created and the existing pod can become a part of this replicaset without any downtime. Store the manifest file of the replica set in /opt/34_rs.yaml and at the beginning add a comment describing in one line what strategy was implemented to achieve this.
Let’s first create the pod, but I’ll add a sleep within the while commmand, otherwise the calculation will go too fast and we might have some issues.
k run calc --image=busybox --restart=Never --dry-run=client -o yaml -- "/bin/sh" "-c" "i=0; while true; do a=\$(( i*i+i )); echo \$a; i=\$(( i+1 )); sleep 30; done"
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: calc
name: calc
spec:
containers:
- args:
- /bin/sh
- -c
- i=0; while true; do a=$(( i*i+i )); echo $a; i=$(( i+1 )); sleep 30; done
image: busybox
name: calc
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Never
status: {}
By checking the yaml we can see that it has generated a po that should work but let’s test it first, so let’s remove the dry-run and yaml generation.
k run calc --image=busybox --restart=Never -- "/bin/sh" "-c" "i=0; while true; do a=\$(( i*i+i )); echo \$a; i=\$(( i+1 )); sleep 30; done"
k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
calc 1/1 Running 0 6s 10.244.1.127 minikube-m02 <none> <none>
k logs -f calc
0
2
Now let’s use an imperative command to create a deploy, and then change / adapt it to be a replicaset, and use the label run=calc as the selector for the replicaset.
This will make the current pod part of it, and when we scale up we can see that the new replicas are going to the replicas plus the current pod.
k create deploy calc-dist --image=busybox --dry-run=client --replicas=3 -o yaml -- "/bin/sh" "-c" "i=0; while true; do a=\$(( i*i+i )); echo \$a; i=\$(( i+1 )); sleep 30; done" > 34_rs.yaml
Let’s adapt now the deploy resource to a replicaset, by removing the strategy field and changing it to a replicaset.
In order to add the current pod to this replicaset pool, let’s use the same run=calc label in the selector, and as well in the pod template labels.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
creationTimestamp: null
labels:
app: calc
name: calc-dist
spec:
replicas: 3
selector:
matchLabels:
run: calc
template:
metadata:
creationTimestamp: null
labels:
run: calc
spec:
containers:
- command:
- /bin/sh
- -c
- i=0; while true; do a=$(( i*i+i )); echo $a; i=$(( i+1 )); sleep 30; done
image: busybox
name: busybox
k create -f 34_rs.yml
We can see that the replicaset shows 3 replicas, and one of them is the pod started earlier:
k get rs -o wide --show-labels
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR LABELS
calc-dist 3 3 3 12s busybox busybox run=calc app=calc
k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
calc 1/1 Running 0 12m 10.244.1.127 minikube-m02 <none> <none>
calc-dist-jvnnw 1/1 Running 0 20s 10.244.0.25 minikube <none> <none>
calc-dist-ld7kr 1/1 Running 0 20s 10.244.1.251 minikube-m02 <none> <none>
And we can see in the logs that the first calc continue to run, and with it’s own calculations:
➜ fernhtls.github.io git:(main) ✗ k logs -f calc
0
2
6
12
20
30
42
56
72
90
110
132
156
182
210
240
272
306
342
380
420
462
506
552
600
650
702
756
^C
➜ fernhtls.github.io git:(main) ✗ k logs -f calc-dist-jvnnw
0
2
6
12
^C
➜ fernhtls.github.io git:(main) ✗
➜ fernhtls.github.io git:(main) ✗
➜ fernhtls.github.io git:(main) ✗ k logs -f calc-dist-ld7kr
0
2
6
12
^C
➜ fernhtls.github.io git:(main) ✗
This is an example that would not actually need any parallelism, the calculation used in the example, but let’s think that each process brought up could be processing a file,
so each pod is a way a worker process processing several files at once and in parallel. Of course you still need to build some sort of coordinator / management process to control
and dispatch files that can be processed and make it that each pod gets a single file for example, in that case you have mechanism like celery within python that could be used to achieve it.
Clean up:
k delete rs calc-dist --force=true
35. Create a deployment deploy03 that uses image nginx:v2. The container name must be nginx. Check if the deployment is successful. Now delete the deployment. Get the events associated with the deployment deploy03 and pods created by the deployment and store them in a file located at 35_events.txt.
Let’s create the deployment first, but let’s first check if the command will generate the right spec:
k create deploy deploy03 --image-nginx:v2 --dry-run=client -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: deploy03
name: deploy03
spec:
replicas: 1
selector:
matchLabels:
app: deploy03
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: deploy03
spec:
containers:
- image: nginx:v2
name: nginx
resources: {}
status: {}
Seems all correct, so let’s now just create the deploy:
k create deploy deploy03 --image=nginx:v2
We can see that the deployment is not working correctly, and we can suspect already why (image):
k get deploy -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deploy03 0/1 1 0 7s nginx nginx:v2 app=deploy03
Let’s get the events related to the deployment. We can check it all with a describe command:
k describe deploy deploy03
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 2m49s deployment-controller Scaled up replica set deploy03-548b549b65 from 0 to 1
But that does not tell us what’s the actual error with the pods anyway. So let’s describe the pods:
k describe po deploy03-548b549b65-5j8tx
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 4m6s default-scheduler Successfully assigned default/deploy03-548b549b65-5j8tx to minikube-m02
Normal Pulling 59s (x5 over 4m5s) kubelet Pulling image "nginx:v2"
Warning Failed 58s (x5 over 4m4s) kubelet Failed to pull image "nginx:v2": rpc error: code = NotFound desc = failed to pull and unpack image "docker.io/library/nginx:v2": failed to resolve reference "docker.io/library/nginx:v2": docker.io/library/nginx:v2: not found
Warning Failed 58s (x5 over 4m4s) kubelet Error: ErrImagePull
Normal BackOff 4s (x15 over 4m3s) kubelet Back-off pulling image "nginx:v2"
Warning Failed 4s (x15 over 4m3s) kubelet Error: ImagePullBackOff
Now we can see at the pod events that we have an error on pulling the image.
Let’s do it now just pulling info from get events:
k get events -o wide
As I don’t have any other resources running at this namespace the events are mainly only with the deploy03 events and it’s own pods:
k get events -o wide
LAST SEEN TYPE REASON OBJECT SUBOBJECT SOURCE MESSAGE FIRST SEEN COUNT NAME
5m49s Normal Scheduled pod/deploy03-548b549b65-5j8tx default-scheduler Successfully assigned default/deploy03-548b549b65-5j8tx to minikube-m02 5m49s 1 deploy03-548b549b65-5j8tx.1895ee369ceb6566
2m42s Normal Pulling pod/deploy03-548b549b65-5j8tx spec.containers{nginx} kubelet, minikube-m02 Pulling image "nginx:v2" 5m48s 5 deploy03-548b549b65-5j8tx.1895ee36e17e76eb
2m41s Warning Failed pod/deploy03-548b549b65-5j8tx spec.containers{nginx} kubelet, minikube-m02 Failed to pull image "nginx:v2": rpc error: code = NotFound desc = failed to pull and unpack image "docker.io/library/nginx:v2": failed to resolve reference "docker.io/library/nginx:v2": docker.io/library/nginx:v2: not found 5m47s 5 deploy03-548b549b65-5j8tx.1895ee370aa1253b
2m41s Warning Failed pod/deploy03-548b549b65-5j8tx spec.containers{nginx} kubelet, minikube-m02 Error: ErrImagePull 5m47s 5 deploy03-548b549b65-5j8tx.1895ee370aa971e9
28s Normal BackOff pod/deploy03-548b549b65-5j8tx spec.containers{nginx} kubelet, minikube-m02 Back-off pulling image "nginx:v2" 5m46s 21 deploy03-548b549b65-5j8tx.1895ee372d9df1b2
40s Warning Failed pod/deploy03-548b549b65-5j8tx spec.containers{nginx} kubelet, minikube-m02 Error: ImagePullBackOff 5m46s 20 deploy03-548b549b65-5j8tx.1895ee372d9e2d1b
5m49s Normal SuccessfulCreate replicaset/deploy03-548b549b65 replicaset-controller Created pod: deploy03-548b549b65-5j8tx 5m49s 1 deploy03-548b549b65.1895ee369bc1dd18
5m49s Normal ScalingReplicaSet deployment/deploy03 deployment-controller Scaled up replica set deploy03-548b549b65 from 0 to 1 5m49s 1 deploy03.1895ee369a87b498
Let’s delete the deployment and see if we can still get the events through get events.
k delete deploy deploy03
We could anyway filter the results of the events in the command by adding the following for example:
➜ fernhtls.github.io git:(main) ✗ kubectl get events --field-selector involvedObject.name=deploy03-548b549b65-5j8tx
LAST SEEN TYPE REASON OBJECT MESSAGE
24m Normal Scheduled pod/deploy03-548b549b65-5j8tx Successfully assigned default/deploy03-548b549b65-5j8tx to minikube-m02
20m Normal Pulling pod/deploy03-548b549b65-5j8tx Pulling image "nginx:v2"
20m Warning Failed pod/deploy03-548b549b65-5j8tx Failed to pull image "nginx:v2": rpc error: code = NotFound desc = failed to pull and unpack image "docker.io/library/nginx:v2": failed to resolve reference "docker.io/library/nginx:v2": docker.io/library/nginx:v2: not found
20m Warning Failed pod/deploy03-548b549b65-5j8tx Error: ErrImagePull
8m56s Normal BackOff pod/deploy03-548b549b65-5j8tx Back-off pulling image "nginx:v2"
8m56s Warning Failed pod/deploy03-548b549b65-5j8tx Error: ImagePullBackOff
➜ fernhtls.github.io git:(main) ✗ k get po
No resources found in default namespace.
Even without any filtering you will still see these events on your namespace if you just run get events, so that brings all events happening / happened.
As you can see I could still see the events for the pod, even after deletion, so those events are still kept on etcd, but they do have some sort of expiration anyway.
Let’s check what’s the expiration with:
k -n kube-system describe po kube-apiserver-minikube | grep "event-ttl"
In my case with minikube nothing is set, but the default value should be 1 hour or 3600 seconds.
Check here, and search for event-ttl.
36. Create a configmap devconfig that has configuration data as follows:
Full question text:
36. Create a configmap devconfig that has configuration data as follows:
sdk.cfg:
path: /etc/sdk/
version: v2
auth: token
language:
golang: 1.15
Let’s first create the files with the config we need to push as the configmap:
cat << EOF >> sdk.cfg
path: /etc/sdk/
version: v2
auth: token
EOF
echo "golang: 1.15" > language
Let’s now create our configmap from this file:
k create cm devconfig --from-file=./sdk.cfg --from-file=./language --dry-run=client -o yaml
On inspecting the yaml generated we do have the following:
apiVersion: v1
data:
language: |
golang: 1.15
sdk.cfg: |
path: /etc/sdk/
version: v2
auth: token
kind: ConfigMap
metadata:
creationTimestamp: null
name: devconfig
It seems to be all good, so let’s create it with:
k create cm devconfig --from-file=./sdk.cfg --from-file=./language
k describe cm devconfig
Name: devconfig
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
language:
----
golang: 1.15
sdk.cfg:
----
path: /etc/sdk/
version: v2
auth: token
BinaryData
====
Events: <none>
We used a file to create the cm, we could have done with just imperative, but maybe it’s a bit easier and cleaner to have the file to be using the --from-file option to create the cm anyway.
k create cm devconfig2 --from-file=<(cat << EOF
path: /etc/sdk/
version: v2
auth: token
EOF
) \
--from-file=<(cat << EOF
golang: 1.15
EOF
)
So the cm gets created but with the process subsituion file descriptor name, what’s not very nice in the end.
k describe cm devconfig2
Name: devconfig2
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
11:
----
path: /etc/sdk/
version: v2
auth: token
12:
----
golang: 1.15
BinaryData
====
Events: <none>
Content wise it seems all correct, but if this cm gets mounted without changing the file / key itself, we might end up with a broken app for example.
So the keys are not actually the file names as we created before, so let’s not progress with this option.
Maybe another option to explore here would be the --from-env-file, what creates all keys on an env file within the configmap, but it’s not like the example above.
Clean up:
k delete cm devconfig2
37. Create a pod devbox with image busybox such that it projects the contents in devconfig. Project sdk.cfg on path /var/dev-sdk/config/sdk.cfg and language on path /var/dev-sdk/lang/golang/version/value.txt.
Let’s first create the pod yaml, and then inject the configmap mounts there per key.
k run devbox --image=busybox --dry-run=client -o yaml -- sleep "infinity" > 37.yml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: devbox
name: devbox
spec:
containers:
- args:
- sleep
- infinity
image: busybox
name: devbox
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
Let’s add to the yaml the mounts for the configmaps:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: devbox
name: devbox
spec:
containers:
- args:
- sleep
- infinity
image: busybox
name: devbox
volumeMounts:
- name: devconfig
mountPath: /var/dev-sdk/config/sdk.cfg
subPath: sdk.cfg
- name: devconfig
mountPath: /var/dev-sdk/lang/golang/version/value.txt
subPath: language
dnsPolicy: ClusterFirst
restartPolicy: Always
volumes:
- name: devconfig
configMap:
name: devconfig
Let’s create now our pod from the 37.yml file:
k create -f 37.yml
If we describe the pod and check the mounts we see the following:
Mounts:
/var/dev-sdk/config/sdk.cfg from devconfig (rw,path="sdk.cfg")
/var/dev-sdk/lang/golang/version/value.txt from devconfig (rw,path="language")
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-gsfnq (ro)
Let’s check the files within the pod now to check the content:
k exec -ti devbox -- cat /var/dev-sdk/config/sdk.cfg
path: /etc/sdk/
version: v2
auth: token
k exec -ti devbox -- cat /var/dev-sdk/lang/golang/version/value.txt
golang: 1.15
We can see that the file were mounted correctly, on the right location and with the right content from the cm keys.
Clean up:
k delete po devbox --force=true
38. create a pod pod01 with image busybox such that it requests for 100m of cpu and 50mi of memory and the resources allocated are expandable to a limit of 200m of cpu and 250 mi of memory. try to create a pod using imperative command only.
Let’s first create the simple pod yaml and then add the resources part with limits and requests.
k run pod01 --image=busybox --dry-run=client -o yaml -- sleep "infinity" > pod01.yml
Let’s adapt the yaml and add all resource clauses needed:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: pod01
name: pod01
spec:
containers:
- args:
- sleep
- infinity
image: busybox
name: pod01
resources:
limits:
cpu: "0.2"
memory: "250Mi"
requests:
cpu: "0.1"
memory: "50Mi"
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
On describing the pod we can see the following defined on resources:
Limits:
cpu: 200m
memory: 250Mi
Requests:
cpu: 100m
memory: 50Mi
Clean up:
k delete po pod01 --force=true
39. A stateful-set problem:
Full question text:
39. A stateful-set problem:
1. Create a stateful set for a middleware application named middleware that uses image nginx. Ensure that the stateful set has 3 replicas and has a persistent storage such that it uses PVC with a storage of 1 Gi mounted on worker nodes host path at /tmp/middleware/storage. Mount the storage on /var/www/html. Create a init-container in the stateful set with image busybox to create an index.html at /var/www/html. The contents of the index.html can be generated using the command echo "From middleware application" > /var/www/html/index.html. Expose the stateful set through a headless service middleware-svc.
2. Create a temporary pod with image busybox to check if you can access nginx service. Try accessing the service with the headless service and pod names (not ips).
Let’s start with creating the deployment, and change / adapt it to a statefulset:
k create deploy middleware --image=nginx --replicas=3 --dry-run=client -o yaml > 39.yml
Let me edit the yaml to change it to a StatefulSet first and just test, we will add the PV / PVC / SERVICE later on.
apiVersion: apps/v1
kind: StatefulSet
metadata:
creationTimestamp: null
labels:
app: middleware
name: middleware
spec:
replicas: 3
selector:
matchLabels:
app: middleware
template:
metadata:
creationTimestamp: null
labels:
app: middleware
spec:
containers:
- image: nginx
name: nginx
Let’s create the PV and PVC, and then attach to the statefulset. I’m adding the pv and pvc at the file 39_pv.yml.
apiVersion: v1
kind: PersistentVolume
metadata:
name: middleware-pv
spec:
storageClassName: standard
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /tmp/middleware/storage
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: middleware-pvc
spec:
storageClassName: standard
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Let’s check the PV and PVC created and check if it’s all bounded:
k get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
middleware-pv 1Gi RWO Retain Bound default/middleware-pvc standard <unset> 3s
k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
middleware-pvc Bound middleware-pv 1Gi RWO standard <unset> 9s
So now what we are missing at the full statefulset is the volume, and the init cointainer that will write the file index.html.
I’m adding as well the serviceName to the statefulset spec, but the service itself will be created later on.
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app: middleware
name: middleware
spec:
serviceName: middleware-svc
replicas: 3
selector:
matchLabels:
app: middleware
template:
metadata:
labels:
app: middleware
spec:
initContainers:
- image: busybox
name: genindexhtml
args:
- /bin/sh
- -c
- echo "<div>THIS IS THE MIDDLEWARE APP</div>" > /var/www/html/index.html
volumeMounts:
- name: middleware-storage
mountPath: /var/www/html
containers:
- image: nginx
name: nginx
volumeMounts:
- name: middleware-storage
mountPath: /var/www/html
containerPort:
port: 80
volumes:
- name: middleware-storage
persistentVolumeClaim:
claimName: middleware-pvc
I can confirm with the following command that the file has the right content:
k exec -ti middleware-0 -c nginx -- cat /var/www/html/index.html
<div>THIS IS THE MIDDLEWARE APP</div>
So let’s now create a new file 39_svc.yml, using an imperative command, and then modify the service to point to the right selector:
k create svc clusterip middleware-svc --clusterip="None" --tcp=80:80 --dry-run=client -o yaml > 39_svc.yml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: middleware-svc
name: middleware-svc
spec:
clusterIP: None
ports:
- name: 80-80
port: 80
protocol: TCP
targetPort: 80
selector:
app: middleware
status:
loadBalancer: {}
An on describing the service we can see the 3 endpoints for all 3 pods:
k describe svc middleware-svc
Name: middleware-svc
Namespace: default
Labels: app=middleware-svc
Annotations: <none>
Selector: app=middleware
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: None
IPs: None
Port: 80-80 80/TCP
TargetPort: 80/TCP
Endpoints: 10.244.1.115:80,10.244.0.233:80,10.244.1.173:80
Session Affinity: None
Internal Traffic Policy: Cluster
Events: <none>
k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
middleware-0 1/1 Running 0 5m42s 10.244.1.115 minikube-m02 <none> <none>
middleware-1 1/1 Running 0 5m38s 10.244.0.233 minikube <none> <none>
middleware-2 1/1 Running 0 5m35s 10.244.1.173 minikube-m02 <none> <none>
We need a headless service for statefulsets, because now we can reach each pod / node using it’s own DNS like: middleware-1, middleware-2 and middleware-3.
But lets use the full DNS name resolution for it like middleware-0.middleware-svc.default.svc.cluster.local, and the same for all pods.
We could use as well middleware-0.middleware-svc, as we will be running the debug pod on the same namespace, but let’s how the full nanme resolution anyway, as it works from pods from anywhere in the cluster.
Let’s check it now with our temp busybox pod for checking:
k run -it --rm debug --image=busybox --restart=Never -- sh
If you don't see a command prompt, try pressing enter.
/ #
/ # nslookup middleware-0.middleware-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: middleware-0.middleware-svc.default.svc.cluster.local
Address: 10.244.1.115
/ # nslookup middleware-1.middleware-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: middleware-1.middleware-svc.default.svc.cluster.local
Address: 10.244.0.233
/ # nslookup middleware-2.middleware-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: middleware-2.middleware-svc.default.svc.cluster.local
Address: 10.244.1.173
We can see the correct pod IPs being resolved.
Clean up:
k delete svc middleware-svc
k delete statefulset middleware --force=true
k delete pvc middleware-pvc
k delete pv middleware-pv
40. Create a deployment stressor with image polinux/stress. Execute the deployment with command stress and arguments --cpu 6 --timeout 180. Let’s add some limits as well to the pod like: cpu 0.2ms. Enable autoscaling for this deployment with a cpu limit of 75%. Get the autoscale management object and store it in 40_autoscale.yaml file.
Let’s create our deployment yaml first fot the deployment:
k create deploy stressor --image=polinux/stress --dry-run=client -o yaml -- stress --cpu 6 --timeout 180 > stressor.yml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: stressor
name: stressor
spec:
replicas: 1
selector:
matchLabels:
app: stressor
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: stressor
spec:
containers:
- command:
- stress
- --cpu
- "6"
- --timeout
- "180"
image: polinux/stress
name: stress
resources:
limits:
cpu: 0.2
status: {}
Let’s create the deploy now:
k create -f stressor.yml
No autoscale still setup, but let’s include the HPA now.
Let’s check the documentation here.
One extra step to put in place is to enabled the metrics API, otherwise there’s no cpu / memory utilization collection on k8s.
On minikube it’s a matter of enabling the addon, but if you’re using a different setup, check the k8s docs on how to deploy the metrics server.
minikube addons enable metrics-server
We can do imperative commands to generate the HPA resouce:
k autoscale deployment stressor --cpu-percent=40 --min=1 --max=8 --dry-run=client -o yaml > 40_autoscale.yml
Let’s check the yaml for the HPA:
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
creationTimestamp: null
name: stressor
spec:
maxReplicas: 8
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: stressor
targetCPUUtilizationPercentage: 40
status:
currentReplicas: 0
desiredReplicas: 0
After some time of execution you will probably see some events like:
3m36s Normal ScalingReplicaSet deployment/stressor Scaled up replica set stressor-bfbc56b5 from 0 to 1
102s Normal ScalingReplicaSet deployment/stressor Scaled up replica set stressor-768d8dc64b from 0 to 1
99s Normal ScalingReplicaSet deployment/stressor Scaled down replica set stressor-bfbc56b5 from 1 to 0
66s Normal SuccessfulRescale horizontalpodautoscaler/stressor New size: 3; reason: cpu resource utilization (percentage of request) above target
66s Normal ScalingReplicaSet deployment/stressor Scaled up replica set stressor-768d8dc64b from 1 to 3
And the pods scaling up:
k get po -o wide -w
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
stressor-768d8dc64b-6dm4x 1/1 Running 0 25s 10.244.0.59 minikube <none> <none>
stressor-768d8dc64b-6q6c5 1/1 Running 0 2m46s 10.244.1.152 minikube-m02 <none> <none>
stressor-768d8dc64b-7b2ll 1/1 Running 0 25s 10.244.1.52 minikube-m02 <none> <none>
stressor-768d8dc64b-7z54l 1/1 Running 0 9s 10.244.0.155 minikube <none> <none>
stressor-768d8dc64b-88nhz 1/1 Running 0 25s 10.244.0.39 minikube <none> <none>
stressor-768d8dc64b-fwvcg 1/1 Running 0 2m10s 10.244.0.130 minikube <none> <none>
stressor-768d8dc64b-njq54 1/1 Running 0 2m10s 10.244.1.195 minikube-m02 <none> <none>
stressor-768d8dc64b-wqxxk 1/1 Running 0 10s 10.244.1.240 minikube-m02 <none> <none>
Let’s check the pod resources:
k top po
NAME CPU(cores) MEMORY(bytes)
stressor-768d8dc64b-6dm4x 201m 7Mi
stressor-768d8dc64b-6q6c5 202m 7Mi
stressor-768d8dc64b-7b2ll 201m 7Mi
stressor-768d8dc64b-7z54l 200m 7Mi
stressor-768d8dc64b-88nhz 201m 7Mi
stressor-768d8dc64b-fwvcg 200m 7Mi
stressor-768d8dc64b-njq54 201m 7Mi
stressor-768d8dc64b-wqxxk 201m 7Mi
We can see all pods reaching the limit of the CPU resouces and then getting autoscaled.
If we inspect the hpa we see the following:
k describe horizontalpodautoscalers.autoscaling stressor
Name: stressor
Namespace: default
Labels: <none>
Annotations: <none>
CreationTimestamp: Sat, 28 Feb 2026 17:11:34 +0100
Reference: Deployment/stressor
Metrics: ( current / target )
resource cpu on pods (as a percentage of request): 100% (200m) / 40%
Min replicas: 1
Max replicas: 8
Deployment pods: 8 current / 8 desired
Conditions:
Type Status Reason Message
---- ------ ------ -------
AbleToScale True ReadyForNewScale recommended size matches current size
ScalingActive True ValidMetricFound the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)
ScalingLimited True TooManyReplicas the desired replica count is more than the maximum replica count
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulRescale 4m30s horizontal-pod-autoscaler New size: 3; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 2m45s horizontal-pod-autoscaler New size: 6; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 2m30s horizontal-pod-autoscaler New size: 8; reason: cpu resource utilization (percentage of request) above target
So if it reaches 40% of the max, it increases the replicas up to 8 max replicas.
Clean up both resources:
k delete hpa stressor; k delete deploy stressor --force=true