Building and Deploying our own Images

Establish a process to build our own container images and deploy them to the cluster.

Slowly but surely our homegrown cluster becomes usable. We can

  • instantiate services either from our private registry or any internet image registry
  • provide our services with persistent storage that survives a cluster restart
  • expose a service either with a cluster-external ip address or an Ingress Route proxied by Traefik

Today i am going to cover the basics of performing a docker build, pushing the built image to our private registry and finally deploying a service that uses the built image.

If you want to run this example in your own environment clone the github repository

https://github.com/zubeax/simple-quiz.git

Retracing my steps does not require you to wait until Scartaris’s shadow caresses the crater of Snaefells Jökull. But you won’t (for good or worse) make it to the center of the earth either.

Enjoy the trip.

Docker Build

The ‘build.sh’ script in the repository root kicks off a regular docker build from the contents of the repo (this is what the trailing ‘.’ is for). After the build is complete the ‘docker push’ command pushes the image to our private registry.

#File: 'build.sh'
REGISTRY=registry.k3s.kippel.de:5000
IMAGE=/development/flask/simple-quiz
TAG=v0.9

docker build --progress=plain -t ${REGISTRY}${IMAGE}:${TAG} . 
docker push ${REGISTRY}${IMAGE}:${TAG} 

The service is a flask-based python application. It requires python and a python venv environment with the packages from ‘requirements.txt’ installed. Since the build sets out from a plain-vanilla debian image, we have quite a bit of customizing to do.

#File: 'Dockerfile'
FROM debian:latest

LABEL maintainer="axel@kippel.de"

USER root

RUN apt-get update && apt-get -y install build-essential python3 python3-pip python3-dev python3-venv

ADD ./app/ /app

RUN python3 -m venv /app/quizenv
RUN . /app/quizenv/bin/activate && pip3 install -r /app/requirements.txt

#we need a numeric user to be compliant with OCP rules
RUN chown -R 1000480000 /app
USER 1000480000

EXPOSE 8000

CMD cd /app && . /app/quizenv/bin/activate && /app/quizenv/bin/gunicorn --bind=0.0.0.0 --timeout 600 --log-level debug quiz:app

Here is the build log. Nothing special to remark.

#0 building with "default" instance using docker driver

#1 [internal] load .dockerignore
#1 transferring context: 2B 0.0s done
#1 DONE 0.0s

#2 [internal] load build definition from Dockerfile
#2 transferring dockerfile: 659B 0.0s done
#2 DONE 0.0s

#3 [internal] load metadata for docker.io/library/debian:latest
#3 DONE 0.5s

#4 [1/7] FROM docker.io/library/debian:latest@sha256:133a1f2aa9e55d1c93d0ae1aaa7b94fb141265d0ee3ea677175cdb96f5f990e5
#4 CACHED

#5 [internal] load build context
#5 transferring context: 2.93kB 0.1s done
#5 DONE 0.1s

#6 [2/7] RUN apt-get update && apt-get -y install build-essential python3 python3-pip python3-dev python3-venv
#6 0.905 Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
#6 1.009 Get:2 http://deb.debian.org/debian bookworm-updates InRelease [52.1 kB]
#6 1.011 Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
#6 1.293 Get:4 http://deb.debian.org/debian bookworm/main arm64 Packages [8685 kB]
#6 2.530 Get:5 http://deb.debian.org/debian bookworm-updates/main arm64 Packages [6672 B]
#6 2.532 Get:6 http://deb.debian.org/debian-security bookworm-security/main arm64 Packages [124 kB]
#6 5.054 Fetched 9067 kB in 4s (2154 kB/s)
#6 5.054 Reading package lists...
#6 6.883 Reading package lists...
#6 8.566 Building dependency tree...
#6 8.981 Reading state information...
#6 9.948 The following additional packages will be installed:
#6 9.948   binutils binutils-aarch64-linux-gnu binutils-common bzip2 ca-certificates
...<redacted>...
#6 12.91   readline-common rpcsvc-proto xz-utils zlib1g-dev
#6 12.92 The following packages will be upgraded:
#6 12.92   perl-base
#6 13.07 1 upgraded, 156 newly installed, 0 to remove and 5 not upgraded.
#6 13.07 Need to get 113 MB of archives.
#6 13.07 After this operation, 445 MB of additional disk space will be used.
#6 13.07 Get:1 http://deb.debian.org/debian bookworm/main arm64 perl-base arm64 5.36.0-7+deb12u1 [1478 kB]
#6 13.30 Get:2 http://deb.debian.org/debian bookworm/main arm64 perl-modules-5.36 all 5.36.0-7+deb12u1 [2815 kB]
#6 13.70 Get:3 http://deb.debian.org/debian bookworm/main arm64 libgdbm6 arm64 1.23-3 [70.9 kB]
#6 156.5 Setting up python3-venv (3.11.2-1+b1) ...
...<redacted>...
#6 156.7 Processing triggers for ca-certificates (20230311) ...
#6 156.7 Updating certificates in /etc/ssl/certs...
#6 158.4 0 added, 0 removed; done.
#6 158.4 Running hooks in /etc/ca-certificates/update.d...
#6 158.5 done.
#6 DONE 161.7s

#7 [3/7] ADD ./app/ /app
#7 DONE 6.6s

#8 [4/7] RUN python3 -m venv /app/quizenv
#8 DONE 17.7s

#9 [5/7] RUN . /app/quizenv/bin/activate && pip3 install -r /app/requirements.txt
#9 3.698 Collecting Flask>=2.0.0
#9 3.883   Downloading flask-3.0.0-py3-none-any.whl (99 kB)
#9 4.286      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 99.7/99.7 kB 228.0 kB/s eta 0:00:00
...<redacted>...
#9 30.35 Successfully installed Flask-3.0.0 Jinja2-3.1.2 MarkupSafe-2.1.3 Werkzeug-3.0.1 blinker-1.7.0 click-8.1.7 flask_healthz-1.0.1 grpcio-1.60.0 grpcio-tools-1.60.0 gunicorn-21.2.0 itsdangerous-2.1.2 packaging-23.2 protobuf-4.25.1
#9 DONE 32.4s

#10 [7/7] RUN chown -R 1000480000 /app
#10 DONE 11.0s

#11 exporting to image
#11 exporting layers
#11 exporting layers 22.3s done
#11 writing image sha256:35811cdc7c7616c7c825fb8cc9901f2378c95ec0f60ae087aac1f8dcefb25f43
#11 writing image sha256:35811cdc7c7616c7c825fb8cc9901f2378c95ec0f60ae087aac1f8dcefb25f43 0.2s done
#11 naming to registry.k3s.kippel.de:5000/development/flask/simple-quiz:v0.9
#11 naming to registry.k3s.kippel.de:5000/development/flask/simple-quiz:v0.9 0.3s done
#11 DONE 22.8s

The push refers to repository [registry.k3s.kippel.de:5000/development/flask/simple-quiz]
d3347c231307: Pushed 
8882caf5885e: Pushed 
504284a508c8: Pushed 
9aea55af74f3: Pushed 
0cfc443021d9: Layer already exists 
e9d9a56c6bc5: Pushed 
73dca680fc18: Layer already exists 
v0.9: digest: sha256:af6f2e247c87843c2440bdaefd71068ed74266f04f7191372179ca68a58d2669 size: 1792

Installing the service with helm

The ‘install.sh’ script is a wrapper around helm that also supports ‘upgrade’ and ‘delete’ operations. For a simple first-time installation you could just run :

helm install simple-quiz --namespace simple-quiz --create-namespace -f ./helm/values.yaml ./helm

The installation log is brief :

cd ./kubernetes
./install.sh

WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /etc/rancher/k3s/k3s.yaml
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /etc/rancher/k3s/k3s.yaml
NAME: simple-quiz
LAST DEPLOYED: Tue Dec 12 19:03:09 2023
NAMESPACE: simple-quiz
STATUS: deployed
REVISION: 1
TEST SUITE: None

Let’s do a quick sanity check :

# helm -n simple-quiz list
NAME       	NAMESPACE  	REVISION	UPDATED                                	STATUS  	CHART          	APP VERSION
simple-quiz	simple-quiz	1       	2023-12-12 19:03:09.395572272 +0100 CET	deployed	simple-quiz-0.9	v0.9       

Looking good.

Verifying service sanity

If you are interested, list the kubernetes objects installed by helm :

# kubectl -n simple-quiz get all
NAME                             READY   STATUS    RESTARTS   AGE
pod/simple-quiz-7f8cbb56-8gskz   1/1     Running   0          104s

NAME                  TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/simple-quiz   ClusterIP   10.43.216.58   <none>        8000/TCP   104s

NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/simple-quiz   1/1     1            1           104s

NAME                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/simple-quiz-7f8cbb56   1         1         1       104s

# kubectl -n simple-quiz get ingressroute 
NAME          AGE
simple-quiz   2m34s

Let’s peek at the application log from the simple-quiz pod :

# kubectl logs -n simple-quiz pod/simple-quiz-7f8cbb56-8gskz

[2023-12-12 18:03:53 +0000] [8] [DEBUG] Current configuration:
  config: ./gunicorn.conf.py
...<redacted>...
  strip_header_spaces: False
[2023-12-12 18:03:53 +0000] [8] [INFO] Starting gunicorn 21.2.0
[2023-12-12 18:03:53 +0000] [8] [DEBUG] Arbiter booted
[2023-12-12 18:03:53 +0000] [8] [INFO] Listening at: http://0.0.0.0:8000 (8)
[2023-12-12 18:03:53 +0000] [8] [INFO] Using worker: sync
[2023-12-12 18:03:53 +0000] [9] [INFO] Booting worker with pid: 9
[2023-12-12 18:03:53 +0000] [8] [DEBUG] 1 workers

Ok. gunicorn seems to have started the application successfully.

Accessing the application

Our path-based ingress route should give us access via

http://ingress.k3s.kippel.de/simple-quiz/

Let’s try with curl.

# curl -v http://ingress.k3s.kippel.de/simple-quiz/
*   Trying 192.168.100.150:80...
* Connected to ingress.k3s.kippel.de (192.168.100.150) port 80 (#0)
> GET /simple-quiz/ HTTP/1.1
> Host: ingress.k3s.kippel.de
> User-Agent: curl/7.74.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 1114
< Content-Type: text/html; charset=utf-8
< Date: Tue, 12 Dec 2023 18:11:15 GMT
< Server: gunicorn
< 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <title>Quiz - Python</title>
 </head>
 <body>
    <div class="navbar navbar-default">
    <div class="container">
      <div class="navbar-header">
        <div class="navbar-brand"> <a href="/simple-quiz">Simple Quiz</a></div>
      </div>
      <ul class="nav navbar-nav">
        <li><a href="/simple-quiz/questions/add">Create Question</a></li>
        <li><a href="/simple-quiz/client/">Take Test</a></li>
      </ul>
    </div>
    </div>
    <div class="container">
    <h1>Welcome to the Simple Quiz</h1>
    <div class="jumbotron">
      <p>Welcome to the Simple Quiz where you can create a question, take a test or review feedback</p>
    </div>
    <h3 class="col-md-4"> <a href="/simple-quiz/questions/add">Create Question</a></h3>
    <h3 class="col-md-4"> <a href="/simple-quiz/client/">Take Test</a></h3>
    </div>
</body>
* Connection #0 to host ingress.k3s.kippel.de left intact

Works flawlessly. Trying the same in any browser with access to our network presents the splash screen.

Simple Quiz Splashscreen

Simple Quiz Splashscreen

I will leave figuring out the mechanics of the application as an exercise for the reader.

A slightly more challenging task is this :

Use VS Code for remote debugging of the application. That requires you to add an additional exposed debug port to the service. Use either kubectl proxy to temporarily forward this debug port into the network where you are running VS Code or define a MetalLB Load Balancer service that exposes the debug port over a dedicated ip address (i would prefer the latter).

Rebuild the application image to start the application in debug mode :

python3 -m debugpy –listen 0.0.0.0:13487 ./app/run_server.py

Let me know in the comments how you fared.

Conclusion

We covered quite a bit of ground in our journey. In the next stretch i will look into a number of topics :

  • Installing and customizing gitea as a git replacement.
  • Finding and installing an in-cluster docker-build capability.
  • Installing and customizing Jenkins to integrate gitea and docker-build.

Stay tuned !