How To Run Flask REST API with minikube Kubernetes Cluster in Virtualbox

By Adam McQuistan in Python  04/12/2021 Comment

Introduction

In this How To article I demonstrate running a simple Flask Python REST API service on a local minikube Kubernetes cluster using the VirtualBox Driver. Recently I began learning Kubernetes and landed on using minikube for local development and experimentation. I started by spinning up a few Pods, Services, and Deployments using publically available Docker images to get a feel for the kubectl Kubernetes CLI and things were going swell.

Before long I naturally wanted get my own Python code running in my minikube Kubernetes cluster so, I wrote a simple Python Flask Hello World REST API app, built a Docker image, assembled a Kubernetes deployment.yaml manifest and used kubectl to launch the deployment. Then I got my first Kubernetes slap in the face ... well turns out minikube is actually only able to pull container images from it's local docker environment or public/private docker registries. Then I learned by I must inject my Docker image into the minikube cluster and before being able to run it. I will work through this problem in this article.

Getting a Simple Flask REST API Containerized

To start I create a simple Dockerized Flask REST API composed of the following directory structure.

$ tree .
.
├── Dockerfile
├── app.py
└── requirements.txt

The single file Flask source code, app.py, contains the following Hello World index route.

# app.py

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def hello_world():
    return jsonify({
      'greeting': 'Hello World!' 
    })


if __name__ == '__main__':
    app.run(host='0.0.0.0')

The requirements.txt file lists the Flask library.

# requirements.txt

flask

And the Docker file builds an image for this app using the Python3.8 Slim Buster Image as show below.

# Dockerfile

FROM python:3.8-slim-buster

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

I build the custom Flask App Docker image.

docker build -f Dockerfile -t flask-rest-hello-world:latest .

For a sanity check I run the app locally mapping my host port 8000 to container port 5000 like so.

docker run -p 8000:5000 flask-rest-hello-world

Then I hit http://localhost:8000 url with the HTTPie HTTP Client like to verify that the app does in fact run.

$ http --json http://localhost:8000
HTTP/1.0 200 OK
Content-Length: 28
Content-Type: application/json
Date: Mon, 12 Apr 2021 05:12:14 GMT
Server: Werkzeug/1.0.1 Python/3.8.9

{
    "greeting": "Hello World!"
}

At this point I can just CTRL+C in the terminal running my Flask REST API Docker app and move on to creating a Kubernetes Deployment and Service.

Creating a Kubernetes Deployment and Service YAML Templates

In order to deploy my containerized Flask REST API I need to define a Deployment that maps the container to a set of pods along with a Service that establishes networking.

The Deployment is specified in a deployment.yaml file is as follows.

# deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-rest-hello-world-deploy
  labels:
    type: restapi
spec:
  selector: 
    matchLabels:
      app: flask-rest-hello-world
  replicas: 3
  template:
    metadata:
      name: flask-rest-hello-world-tmpl
      labels:
        app: flask-rest-hello-world
    spec:
      containers:
        - name: flask-rest-hello-world
          image: flask-rest-hello-world:latest
          ports:
            - containerPort: 5000

And the Service is specified in a service.yaml file is like so.

# service.yaml

apiVersion: v1
kind: Service
metadata:
  name: flask-rest-hello-world-svc
spec:
  type: LoadBalancer
  selector:
    app: flask-rest-hello-world
  ports:
    - protocol: "TCP"
      port: 8000
      targetPort: 5000

Trying and Failing to Deploy Flask REST API to Local Minikube

Often times I find it most beneficial when I'm Googling around trying to solve a problem if I can spot a tutorial or forum post demonstrating my exact problem. It is a pretty good indicator I'm on the right track to finding my solution so, I've decided to include this section that shows what doesn't work before I get to the real solution.

First I create the deployment within minikube using the following.

$ kubectl create --filename deployment.yaml 
deployment.apps/flask-rest-hello-world-deploy created

Following that I create a Service using the service.yaml file.

$ kubectl create --filename service.yaml 
service/flask-rest-hello-world-svc created

Great! Both commands replied back with created so, let me peek at the components that got spun up using kubectl like so.

$ kubectl get all                          
NAME                                                READY   STATUS              RESTARTS   AGE
pod/flask-rest-hello-world-deploy-7f85bdcc8-2wggq   0/1     ErrImageNeverPull   0          4s
pod/flask-rest-hello-world-deploy-7f85bdcc8-2xzgp   0/1     ErrImageNeverPull   0          4s
pod/flask-rest-hello-world-deploy-7f85bdcc8-jvkqb   0/1     ErrImageNeverPull   0          4s

NAME                                 TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
service/flask-rest-hello-world-svc   LoadBalancer   10.96.50.189   <pending>     8000:30953/TCP   14m
service/kubernetes                   ClusterIP      10.96.0.1      <none>        443/TCP          16d

NAME                                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/flask-rest-hello-world-deploy   0/3     3            0           4s

NAME                                                      DESIRED   CURRENT   READY   AGE
replicaset.apps/flask-rest-hello-world-deploy-7f85bdcc8   3         3         0       4s

Hmm ... looks like none of my pods have were able to be successfully provisioned and are in error states. This is because the kubernetes cluster running inside the VirtualBox minikube cluster has no idea of the flask-rest-hello-world image I built on the local host machine.

Loading the Docker Image into Minikube

In order to get the locally built image of my Dockerized Flask REST API app into my minikube Kubernetes cluster I can use the minikube cli as follows.

minikube image load flask-rest-hello-world:latest

Now if I query the Kubernetes cluster resources I see that the pods are running.

$ kubectl get all
NAME                                                READY   STATUS    RESTARTS   AGE
pod/flask-rest-hello-world-deploy-7f85bdcc8-2wggq   1/1     Running   0          8m6s
pod/flask-rest-hello-world-deploy-7f85bdcc8-2xzgp   1/1     Running   0          8m6s
pod/flask-rest-hello-world-deploy-7f85bdcc8-jvkqb   1/1     Running   0          8m6s

NAME                                 TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
service/flask-rest-hello-world-svc   LoadBalancer   10.96.50.189   <pending>     8000:30953/TCP   22m
service/kubernetes                   ClusterIP      10.96.0.1      <none>        443/TCP          16d

NAME                                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/flask-rest-hello-world-deploy   3/3     3            3           8m6s

NAME                                                      DESIRED   CURRENT   READY   AGE
replicaset.apps/flask-rest-hello-world-deploy-7f85bdcc8   3         3         3       8m6s

Next time I just need to remember to use the above minikube command to load my custom local Docker images into minikube before utilizing any Pods that reference it and I'll be fine.

Testing the Flask REST API Running In Kubernetes Minikube

At this point I can generate a network tunnel from my host machine to the minikube cluster to route traffic through by issuing the minikube tunnble command letting it run continuously in it's own terminal.

$ minikube tunnel
Password:
Status:
        machine: minikube
        pid: 35371
        route: 10.96.0.0/12 -> 192.168.99.100
        minikube: Running
        services: [flask-rest-hello-world-svc]
    errors: 
                minikube: no errors
                router: no errors
                loadbalancer emulator: no errors

Then in another terminal I use the following command to determine the external IP the REST API app's network service.

$ kubectl get services
NAME                         TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)          AGE
flask-rest-hello-world-svc   LoadBalancer   10.96.50.189   10.96.50.189   8000:30953/TCP   32m
kubernetes                   ClusterIP      10.96.0.1      <none>         443/TCP          16d

Now If I hit that external IP and mapped port with HTTPie I get a friendly Hello World! greeting from my Flask REST API running inside the minikube cluster.

$ http --json http://10.96.50.189:8000
HTTP/1.0 200 OK
Content-Length: 28
Content-Type: application/json
Date: Mon, 12 Apr 2021 06:16:12 GMT
Server: Werkzeug/1.0.1 Python/3.8.9

{
    "greeting": "Hello World!"
}

Cleanup

At this point I've completed my POC and can now clean up the unneeded resources in the minikube cluster.

I start with deleting the deployment

$ kubectl delete deployment flask-rest-hello-world-deploy
deployment.apps "flask-rest-hello-world-deploy" deleted

Then the service.

$ kubectl delete service flask-rest-hello-world-svc
service "flask-rest-hello-world-svc" deleted

Conclusion

In this How To article I demonstrated how to build a Dockerized Flask REST API, load the subsequent image into a locally running minikube Kubernetes cluster, then deploy it with using a Deployment and Service resource in Kubernetes.

As always, thanks for reading and please do not hesitate to critique or comment below.

Share with friends and colleagues

[[ likes ]] likes

Community favorites for Python

theCodingInterface