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/debug is present
  • Pod should restart if the file does not exist, so not add --restart=Never this 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.
  • readiness and liveness will be delayed for 30 secs, ls for 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>
  1. Container no ready up to the 30 secs.
  2. After it, after the first sleep command, file gets created and then pod is ready by the readinessProbe.
  3. 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.
  4. 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 create httpd05.
  • And k create -f <(sed -e 's/nginx/redis/g' -e 's/80/6379/g' nginx05.yml) to create redis05.

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 nginx05 access redis05.
  • Only httpd05 access nginx05.
  • httpd05 is open to the world for access, like a landing page for a user.
  • No other ingress is allowed to both nginx05 and redis05.

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

Updated: