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.
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.
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
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.
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.
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!"
}
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
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.