Scaling Python Microservices with Kubernetes

We wrote in depth about setting up microservices in one of our previous posts. In this post we are going to talk about building a simple microservice, containerizing it with docker, and scaling those containers using Kubernetes.

image

Building a Microservice using Flask and Redis

In this post lets get started with writing a Microservice with Flask. The microservice has a single point of entry that allows the user to create a resource with a PUT request, corresponding to a URL of their choice, the GET request serves the request. The stale items are deleted after a year.

The crux of the code that will help us build this microservice is as follows:

db = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    db.incr('count')
    return 'Count is %s.' % db.get('count')

@app.route('/<path:path>', methods = ['PUT', 'GET'])
def home(path):

    if (request.method == 'PUT'):
       event = request.json
       event['last_updated'] = int(time.time())
       event['ttl'] = ttl
       db.delete(path) #remove old keys
       db.hmset(path, event)
       db.expire(path, ttl)
       return flask.jsonify(event), 201


    if not db.exists(path):
        return "Error: thing doesn't exist"

    event = db.hgetall(path)
    event["ttl"] = db.ttl(path)
    return flask.jsonify(event), 200

If a PUT request is sent to any URL, with an additional key-value pair, the same gets saved in redis with the current time stamp. If we hit the base url, we get a count of how many times the URL has been hit.

The code can be found here.

Containerizing the Microservice

Containerizing Redis

We will use docker compose to containerize our microservice. Our microservice consists of a simple flask application that interacts with the redis database.

The compose file will essentially create two different conainers, first for the flask application and second for the redis.

web:
  build: .
  ports:
   - "5000:5000"
  volumes:
   - .:/code
  links:
   - redis
redis:
  image: redis

The Dockerfile for the flask application is fairly simple, it creates a working directory and installs all the requirements.

FROM python:2.7
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt

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

We are all set to run the containers. docker-compose up -d.

Once the containers are running we can test if the code works we issue a PUT request using curl -H "Content-Type: application/json" -X PUT -d '{"hello":999}' http://localhost:5000/testurl

We receive an output as follows:

{ "last_updated": 1485246805, "ttl": 31104000, "hello": 999 }

On making a GET request to the same URL curl http://localhost:5000/testurl

We receive a successful response as follows :

{ "last_updated": "1485246805", "ttl": 31103997, "hello": "999" }

A fairly straight forward microservice!

Why Kubernetes

Now that we have built our service and containers are running for us, lets dive into production scenarios. We mostly see containers as if they are faster / simpler VMs, in other workds, each container includes an executre specific functionality with a unique lifecycle and dependency on other containers.

Such an approach is useful in early stages and improves the development environment, however, it creates significant difficulties when we migrate to production environment.

We start realising the challenges like :

  • How do multiple containers talk to each other?

  • How do I horizontally scale my container service?

  • How do multiple container services discover each other?

  • How do we secure our container network?

  • How do we deploy new containers and roll back previous ones?

Kubernetes attempts to solve the above such problems. Lets dig into it.

Kubernetes 101

Kubernetes is a system, developed at Google, for managing containerized application in a clustered environment. Most modern day application follow a microservices pattern, Kubernetes helps management of these different services as related components on the same host, configured in a trivial way.

Primarily Kubernetes helps with

  • Replication of components
  • Auto-scaling
  • Load balancing
  • Rolling updates
  • Logging across components
  • Monitoring and health checking
  • Service discovery
  • Authentication

Core Kubernetes Concepts

The controlling service in a Kubernetes cluster are called master or control panel components. They operate as main management points and provide cluster-wise system for worker nodes. Node servers have a few requirements that are necessary to communicate with the master components, configure the networking for containers and run workloads assigned to them.

Kubernetes API offers numerous primitives to work with, the more important ones for our purposes are the following:

  • Pods : Basic unit of a cluster, they are machines running the docker containers that should be controlled as a single "application".

  • Services : It is a logical grouping of a set of pods that perform the same function and constitute a single entity. The IP Address of a service remains stable, irrespective of the number/state/health of the underlying pods. Services enable the communication between pods.

  • Labels : They are key-value pairs which serves as an arbitrary tag to associate one/more kubernetes resource. There can be multiple labels, and the keys will be unique. These tags can then be selected for management purposes and action targeting.

  • Deployments : They ensure that a specified number of pods (of a specific kind) are running at any given time. They are a framework for defining pods that are meant to be horizontally scaled, and maintains a constant number of pods. If a container currently goes down, another will be started.

A detailed description can be found in the official docs.

Running Kubernetes locally

Kubernetes is one of the best tools for managing containerized applications, and has been production-ready, however, it has been difficult for developers to test things locally. Minikube is the missing piece in the puzzle that serves as the local development platform.

Minikube has not been designed for scalability or resiliency. It helps to get started with the Kubernetes CLI and API tools on a small single-node. There is not an easier way to test the first couple of commands with Kubernetes.

Minikube starts a virtual machine locally and runs the necessary Kubernetes components. The VM then gets configured with Docker and Kubernetes via a single binary called localkube, resulting into a local endpoint which can be used with the Kubernetes client kubectl.

Install Minikube locally

Quickstart Minikube

Working with Minikube

Let's go ahead and get started by bringing up our local Kubernetes cluster:

minikube start Starting local Kubernetes cluster... Kubernetes is available at https://192.168.99.101:443

Deploy Redis

We first have to create a redis deployment file.

  apiVersion: extensions/v1beta1
  kind: Deployment
  metadata:
    creationTimestamp: null
    name: redis
  spec:
    replicas: 1
    strategy: {}
    template:
      metadata:
        creationTimestamp: null
        labels:
          app: redis
      spec:
        containers:
        - image: redis
          name: redis
          resources: {}
        restartPolicy: Always
  status: {}

We further use kubectl to create the deployment

  $ kubectl create -f redis-deployment.yaml
Expose the Redis Deployment by creating a service

Here is the service file:

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: redis
  name: redis
spec:
  clusterIP: None
  ports:
  - name: headless
    port: 55555
    targetPort: 0
  selector:
    service.name: redis
status:
  loadBalancer: {}

And we expose the service as follows:

  $ kubectl create -f redis-service.yaml

Deploy Flask

Similarly, we need to creat another deployment for flask, here is the deployment file

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: flask
    tier: frontend
  name: flask-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: flask
      tier: frontend
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: flask
        tier: frontend
    spec:
      containers:
      - image: apcelent/flask-web:v1
        name: flask-app
        ports:
        - containerPort: 5000
        resources: {}
        imagePullPolicy: Never
status: {}

The deployment can be created as follows:

  $ kubectl create -f flask-deployment.yaml

Expose the Flask Service

We first create the services file:

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: flask
    tier: frontend
  name: flask-app
spec:
  ports:
  - port: 5000
    protocol: TCP
    targetPort: 5000
  selector:
    app: flask
    tier: frontend
  type: NodePort
status:
  loadBalancer: {}

And then we create the service

$ kubectl create -f flask-service.yaml

You have exposed your service on an external port on all nodes in your cluster. If you want to expose this service to the external internet, you may need to set up firewall rules for the service port(s) (tcp:30321) to serve traffic.

$ kubectl get service <service_name> --output='jsonpath={.spec.ports[0].NodePort}'

$ port=$(kubectl get service <sevice_name> --output='jsonpath={.spec.ports[0].NodePort}')


We construct the address for ease of reference later on:

$ address="$(minikube ip):$port"
$ echo $address
$ open "http://${address}/?start=23&stop=31"

I hope the article was of help, in our next article we will see how to autoscale the pods.

Comments