Servers Galore: Using Python's CherryPy For Quick Scalable Prototypes In a Lean Enviroment

TL;DR

One of the results of becoming an older engineer is that I simply want to get projects done. Spending time hacking through a library that does not work, is poorly implemented, or badly documented makes coding a chore rather than a joy. That is why I have been very pleased to work with CherryPy recently:

  • The Docker file is not complicated
  • Static Content can be served quickly
  • Python Scripts are first class citizens
  • Tests are straightforward
  • Scaling my application is a one liner

What Is CherryPy?

CherryPy is a minimalistic Python web framework. It will never replace a bigger more complete framework for enterprise software, but for getting something up and running it is fantastic. Traditionally, I would use a SimpleHttpServer for prototyping, but it is feature light - requiring me to update as soon as I did anything that really pushed the server. CherryPy allows my applications to scale up enough to be useful before having to switch to a bigger framework (if at all). For reference, Digital Ocean did a nice comparison of Python WSGI servers.

Docker and CherryPy

We use Docker for everything at ShareThis, so let us get that done first so that we have a full dev environment with testing. You can jump start this by simply cloning our repo. Setting this up at the beginning takes 10 extra minutes, but will pay itself off many times over within the first few days of coding. This is arguably the only cost we pay for swapping CherryPy in Docker for SimpleHttpServer on the command line.

Dockerfile

FROM python
RUN pip install cherrypy coverage # cherrypy server + python 3 test coverage
ADD *.py /
ADD *.sh /
CMD python run.py

testing.sh

#!/bin/bash
coverage run -a --omit=test.py /test.py
coverage report -m

run.sh

#!/bin/bash
# docker kill and rm prevent funny errors
docker kill cherrypy > /dev/null 2>&1 
docker rm cherrypy > /dev/null 2>&1 
# build, test, and run the image
docker build -t cherrypy .
docker run cherrypy /testing.sh
docker run -ti --name cherrypy -p 80:80 cherrypy $@

test.py

import unittest

class TestMethods(unittest.TestCase):
    def test_simple(self):
        self.assertTrue(True)

if __name__ == '__main__':
    unittest.main()

First Pass: Scripted Content

Let us assume you want to start a lean experiment to see if anyone will land on a page. The quickest thing to do is to serve a static page and log visits. Let us add the missing run.py script:

run.py (V1)

import cherrypy

class Landing(object):
    @cherrypy.expose
    def index(self):
        return <html><body>scripted landing</body></html>

if __name__ == "__main__":
    cherrypy.quickstart(Landing(), '/')

At this point you have a scalable web server that can do just about anything. I use this for dashboards, monitoring, and a host of internal tooling.

At this point, you will want to start mucking with configuration. For example, you want your web server declared on 0.0.0.0 instead of 127.0.0.1 for public routing. You want to serve on port 80 for live traffic. You want more than 10 threads for multiple live connections (more on that below). You want traffic routed despite the trailing slash. You want a host of other things to happen.

Let us add some configs by adding a call to cherrypy.config.update. In addition, let's allow loading static content and a config file by changing cherrypy.quickstart(Landing(), '/') to cherrypy.quickstart(Landing(), '/', "prod.conf"). Don't forget to add prod.conf to your Dockerfile!!

run.py (V2)

import cherrypy

class Landing(object):
    @cherrypy.expose
    def index(self):
        return <html><body>scripted landing</body></html>

# The config.update is optional, but this will prevent scaling issues in a moment
cherrypy.config.update({
    'server.socket_host': '0.0.0.0', # 127.0.0.1 is default
    'server.socket_port': 80, # 8080 is default
    'server.thread_pool': 100, # 10 is default
    'tools.trailing_slash.on': False # True is default
})

if __name__ == "__main__":
    cherrypy.quickstart(Landing(), '/', "prod.conf")

At around this point, you will likely need static assets as well. Let's add the config file with a reference to the static image! You can add any number of static assets. I prefer a declarative approach, but you can serve a directory too.

prod.conf

[/logo.png]
tools.staticfile.on = True
tools.staticfile.filename = "/logo.png"

Expanding quickly

Scaling a prototype means getting rid of blockers quickly. Let us discuss some of the more common problems that affect CherryPy and how to get them out of the way as fast as possible.

Health Checks and Additional Handlers

At ShareThis, we run our applications inside of Kubernetes on AWS. We need to monitor the state of our Docker container for the systems to know if the container is alive and if the load balancer is healthy. This can be broadly filed under the need for additional handlers. Here is a simple example of writing a simple HealthCheck endpoint and adding the handler to our Landing object. Now, we can point to http://localhost/healthcheck to see if the server is alive.

...
class HealthCheck(object):
    def index(self):
        return "Yay<br />\n"
    index.exposed = True

class Landing(object):
    healthcheck = HealthCheck()
    ...

One side effect of monitoring on Kubernetes is that the network hides some complexity. Every node in a Kubernetes cluster has a proxy that shuffles data to wherever the Docker container happens to be running at that moment. The Elastic Load Balancer (ELB) from Amazon will try to keep connections alive to each node to do the health check against the cluster. This means that for every node in the cluster, you have to account for that in the number of threads available to the server. In our 20 node cluster, our test app will not function because the ELB is accessing 20 threads at once. CherryPy has a default of 10 threads, and so the proxies will continually be kicked out of rotation. Hence, our increase to 100 threads above will allow it to scale without operational issues.

Adding Tests

Complexity hampers productivity when you are unsure what will happen as changes are made. Adding simple contracts to your test suite that tell future developers (including yourself) what the code is intended to do, can keep your code scaling without a lot of overhead.

The way we've included the scripts and coverage tool, you should be able to add an import statement for any python script and coverage will take care of the rest. You may want to add the mock package to your pip install statement to facilitate easier tests.

Summary

CherryPy is well documented and scales up rather nicely. Often times, it is all you need to get an app up and running in a scaled environment. By starting with a template, you can immediately have a web server that runs python code natively. For us at ShareThis, we have used it for backend data processing through a rest interface, monitoring feeds on S3 and BQ, and quickly bringing up dashboards that need to connect to API's for data. By using Docker, it becomes trivial to deploy and maintain the application in production.