Run your Flask app on Kubernetes

In this article we are going to use a very simple Flask application, “dockerise” it and then run it on Kubernetes using minikube.

Part 1 : The basics

The files that we should have at the end of this first part will look like this :

/flask-hello
 app.py
 Dockerfile
 .dockerignore
 requirements.txt
 deployment.yaml
 service.yaml

Flask application

This is the most basic flask app. We just changed the host to listen not only on 127.0.0.1 (localhost) but on all interfaces, and we enable the debug mode.

# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
    return "Hello World!"
if __name__ == '__main__':
    app.run(debug=True,host='0.0.0.0')

You can test this app by running the following command :

python app.py

Result should look like :

╰─ python app.py
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 236-626-748

Now when you browse http://127.0.0.1:5000/ or http://localhost:5000 you should see a blank web page with “Hello World !”

Dockerfile

Great ! Now that we have our (very) basic Flask app we will put in a Docker container. To do so create a Docker file with the following informations :

#Dockerfile for flask_hello
FROM python:3.7
WORKDIR /usr/src/app
COPY . /usr/src/app/
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 5000
ENTRYPOINT ["python"]
CMD ["app.py"]

FROM python:3.7 > use the python image with version tag 3.7
https://hub.docker.com/_/python

WORKDIR : set the current working directory during the build

COPY : copy the content of the local current directory to the destination in the container

RUN : run the pip install comment with the requirement file as an argument (see below)

EXPOSE : as flask use port 5000 we expose this port on the container

ENTRYPOINT : is the command to run when the container starts

CMD : This will be provide a default argument to the entry point

The result will be a basic container using the official python 3.7 image with our flask code, all necessary requirement (Flask), necessary port open and flask server running at start.

Requirements

Requirements

The requirement file contains all necessary library for our code to be run.
I recommend using virtualenv and running the following command to populate the file :

pip freeze > requirement.txt                                                                           

The content of your file should at least contain the following (with newer version as this blog post gets old)

# requirement.txt
Click==7.0
Flask==1.1.0
itsdangerous==1.1.0
Jinja2==2.10.1
MarkupSafe==1.1.1
Werkzeug==0.15.4

Dockerignore

To avoid having unnecessary files copied to your docker container I recommend that you use a dockerignore file (similar to .gitignore file). This will exclude those files during the container build process.

# .dockerignore
.dockerignore
*.md
Dockerfile
*.pyc
Makefile

Because we want to deploy our app to kubernetes we will need to push our image to a docker registry, kubernetes will later pull it when deploying the pods. You can use the registry of your choice, to keep it simple we will use dockerhub. I should therefore have a dockerhub account and configure docker on your local machine to identify using docker login command.

Now we will create the docker image. Replace <yourDockerHubName> with your docker username.

docker build -t <yourDockerHubName>/flask-hello .

-t : use tag, 
docker hub identifier : yourDockerHubName
image name : flask-hello
. : tell docker to look for dockerfile in the current directory

Output example :

╰─ docker build -t climz/flask-hello .
Sending build context to Docker daemon  9.728kB
Step 1/7 : FROM python:3.7
 ---> a4cc999cf2aa
Step 2/7 : WORKDIR /usr/src/app
 ---> Using cache
 ---> cb7ff906ed67
Step 3/7 : COPY . /usr/src/app/
 ---> 2b3760e2a13a
Step 4/7 : RUN pip install --no-cache-dir -r requirements.txt
 ---> Running in 97deac8e0f34
Collecting Flask==1.0.3 (from -r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/9a/74/670ae9737d14114753b8c8fdf2e8bd212a05d3b361ab15b44937dfd40985/Flask-1.0.3-py2.py3-none-any.whl (92kB)
Collecting Jinja2>=2.10 (from Flask==1.0.3->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/1d/e7/fd8b501e7a6dfe492a433deb7b9d833d39ca74916fa8bc63dd1a4947a671/Jinja2-2.10.1-py2.py3-none-any.whl (124kB)
Collecting Werkzeug>=0.14 (from Flask==1.0.3->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/9f/57/92a497e38161ce40606c27a86759c6b92dd34fcdb33f64171ec559257c02/Werkzeug-0.15.4-py2.py3-none-any.whl (327kB)
Collecting click>=5.1 (from Flask==1.0.3->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl (81kB)
Collecting itsdangerous>=0.24 (from Flask==1.0.3->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/76/ae/44b03b253d6fade317f32c24d100b3b35c2239807046a4c953c7b89fa49e/itsdangerous-1.1.0-py2.py3-none-any.whl
Collecting MarkupSafe>=0.23 (from Jinja2>=2.10->Flask==1.0.3->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/98/7b/ff284bd8c80654e471b769062a9b43cc5d03e7a615048d96f4619df8d420/MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl
Installing collected packages: MarkupSafe, Jinja2, Werkzeug, click, itsdangerous, Flask
Successfully installed Flask-1.0.3 Jinja2-2.10.1 MarkupSafe-1.1.1 Werkzeug-0.15.4 click-7.0 itsdangerous-1.1.0
Removing intermediate container 97deac8e0f34
 ---> b3662c206b73
Step 5/7 : EXPOSE 5000
 ---> Running in 8376f2f876ec
Removing intermediate container 8376f2f876ec
 ---> c48fd492bf8c
Step 6/7 : ENTRYPOINT ["python"]
 ---> Running in b9abf653178d
Removing intermediate container b9abf653178d
 ---> 592be6581762
Step 7/7 : CMD ["app.py"]
 ---> Running in 521afc1d4139
Removing intermediate container 521afc1d4139
 ---> cfae4f0824b1
Successfully built cfae4f0824b1
Successfully tagged climz/flask-hello:latest

Because we did not specify any version tag, docker automatically tag the image with version “latest”

We can now test your docker image by creating a container on our local machine :

docker run --name flask-hello -d -p 5000:5000 climz/flask-hello:latest

View your container running

─ docker ps
CONTAINER ID        IMAGE                      COMMAND             CREATED             STATUS              PORTS                    NAMES
992b8ab307f9        climz/flask-hello:latest   "python app.py"     6 seconds ago       Up 4 seconds        0.0.0.0:5000->5000/tcp   flask-hello

You can test it by accessing the Flask app using the following URLs http://127.0.0.1:5000/ or http://localhost:5000

Now that the image is created and test we can push it to the docker hub registry.

docker image push climz/flask-hello

Kubernetes deployment

At this stage we have a basic Flask app and a docker image containing the app pushed to the docker registry.

We will now deploy it on a brand new (and empty) kubernetes cluster. Here I’m using miniukube for test purpose but running on any other k8s cluster would be the same.

We will create two k8s objects :
– a deployment : the deployment is the recommended way to deploy your app on k8s. Under the hood it will create a replica set and the necessary pods.
– a service : it will expose our application.

Deployment definition

# deployment.yaml - Flask hello world deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-hello-deployment
  labels:
    app: flask-hello
spec:
  replicas: 2
  selector:
    matchLabels:
      app: flask-hello
  template:
    metadata:
      labels:
        app: flask-hello
    spec:
      containers:
      - name: flask-hello
        image: climz/flask-hello:latest
        ports:
        - containerPort: 5000

Service definition

# service.yaml Flask hello world service
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: flask-hello
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 5000
  type: NodePort

Note : you can also put both deployment and service in the same file separate by ---

Deploy to kubernetes

kubectl create deployment.yaml
kubectl create service.yaml

or

kubectl create -f .

or

kubectl diff -f .
kubectl apply -f .

To retrieve the IP address you need to use to access your application run the following command :

minikube service my-service --url

The port (NodePort:) on your kubernetes cluster IP is automatically assigned on a range between (30000-32767)

curl http://192.168.99.100:32652/
Hello World! Welcome to the great world of flask.

You should now be able to access the application

To get logs

kubectl logs -f -lapp=flask-hello

Part 2 : Improvements

makefile : coming soon

Jenkins pipeline : coming soon

Production server : coming soon

Leave a Reply

Your email address will not be published. Required fields are marked *