Press "Enter" to skip to content

Deploying a Haskell Web Service to Kubernetes

Haskell is a statically typed purely functional programming language from which other languages such as Scala, Elm, Purescript etc draws a lot of inpsiration. I’ve been interested in learning Haskell for a quite while and I’m still in the beginning of this phase so feel free to point out any obvious flaws in the comment section below. But what I want to talk about in this blog is not so much about Haskell itself, but rather the process of deploying a Haskell generated binary to the Cloud™ to be consumed by the masses. Since I’m into Kubernetes this will be the platform of choice for this article. My goal is that you should be able to follow along even if you have virtually no experience with Haskell before. So let’s get started.

Creating the Web Service

When working with Haskell one typically use Cabal, which is a system for building and packaging Haskell libraries and programs, and Stack which creates a sandboxed environment for the current Haskell project which includes all the projects dependencies and the GHC compiler etc. If you’re on Mac like me you can install Stack like this:

If not then see the installation instructions.

Once Stack is installed we need to determine what library we should use to create our web service. In this article I’m going to use a library called Scotty because it’s easy to get up and running. Scotty is inspired by Sinatra which is a wellknown Ruby web framework.

Here’s the hello world example taken from their website:

But pasting this code into your favourite editor won’t do much, we need be able to compile, package and run it as well! This is where Stack comes into play. Stack can help us bootstrap a new project and generate a good initial file structure for us and it can later help us compiling and package the application. To do this we can just do stack new . But can also supply a template to Stack if we want to use a particular framework for which a template exists. And it turns out that there’s one for Scotty called, scotty-hello-world. Let’s try it out:

This will create a new directory (haskell-webservice) that includes everything needed to get us going. Stepping into this directory and you’ll see a file called Main.hs which (surprise!) contains the hello world Scotty example I listed earlier. Can we run this? Indeed:

The build argument instructs Stack to compile and package up a binary file (that’ll be located at .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/haskell-webservice). You could go into this directly and execute the haskell-webservice binary to start the application but it’s easier to just let Stack execute it using stack exec haskell-webservice. You should now be able to navigate to http://localhost:3000/hello and see the message:

Scotty, hello me up!

Cool! But before we go webscale let’s add a “health check” route that’ll be used by Kubernetes later:

We’d also like to include some logging so that we later can check that our webservice really accepts requests. For this we’re going to import the Network.Wai.Middleware.RequestLogger module defined in the wai-extra package. To do this add the line , wai-extra under build-depends in the haskell-webservice.cabal file:

Next we should add middleware logStdoutDev before the routes in our Main.hs file which should now look this:

Try building and running the application again (stack build && stack exec haskell-webservice) and try to navigate to /health and / and you should see something similar to this:

We’re ready to package this baby up in Docker!

Building Web Service Binary in Docker

Stack helped us compile and package our webservice into a binary file. The problem is that I’m on MacOSX and the binary file generated on my machine won’t run in a Linux box. For this to work one needs to compile the project on Linux. This is where Docker comes into play. What we want to do is to compile the project in a Docker container which emits a binary file that we can later use to execute on Linux. Luckily there’s a convenient Docker image available for this called (simply) haskell which includes stack, cabal, ghc and other things needed to compile and package a Haskell application. The Dockerfile we’re going to use is this (inspired by the Dockfile generated by the servant-docker Stack template):

When building the image it’ll copy the application into Docker and download the dependencies needed for the project:

This will take a loooong time (at least on my machine) but hang tight. Now we have an image (haskell-webservice-builder) tailored for our application that we can use to build the binary. Note how the Dockerfile specifies a VOLUME to /root/.local/bin, this is where the compiled binary will be placed when stack install is executed when we run the container. Thus we should mount this volume to our local file system in order to get a hold of the Linux binary. So let’s create the binary:

Here we’ve mounted /root/.local/bin in Docker to .stack-work/dist/linux/bin. Step into this direct and you should see the generated binary:

Packing the Web Service in Docker

Now we have the application binary but we’d like to run it in Docker as well. One could probably use the haskell base image to do this but it’s 960 Mb large (without the actual application):

So we’d like to use something smaller! I first tried basing the image on Alpine but I couldn’t get it to work. But after a bit of research I found the fpco/haskell-scratch image which was exactly what I was after. It includes the minimal shared libraries for GHC-compiled executables and it has a size of just 4.3 Mb:

Much better 🙂 So let’s package up the application binary in a Docker image using this Dockerfile (name it DockerfileRuntime to not clash with the build-time Dockfile):

and execute this command:

Now we have an image, let’s try it out:

Go back into the browser and navigate to http://localhost:3000/something to see that everything is working.

Deploy to Kubernetes

Kubernetes is a popular open-source system for automating deployment, scaling, and management of containerized applications such as the our newly created haskell-webservice. I won’t cover the basics of Kubernetes in the blog, you can easily read up on it in their docs. The smallest deployable units of computing that can be created and managed in Kubernetes is a pod but in order to deploy such a pod we wrap it in a deployment. A deployment controls the rollout of one or more pods and is also responsible maintaining the specified amount of replicas for this pod. In order to expose our webservice to the outside world we’ll also define a service. But first we need a Kubernetes cluster. We could create one locally using minikube but in this example we’ll use Google Container Engine or GKE for short. Creating such a cluster only takes a few minutes (see quickstart). When you setup a GKE cluster Google is also kind enough to supply us with a private Docker Registry called Container Registry (GCR) to which we can publish our Docker image so that it can be downloaded to the instances in our Kubernetes cluster. Before we can push our Docker image to (any) private registry, we must add the registry name and image name as a tag to the image. In GCR our tag must take the form gcr.io/<google-project-id>/<image-name>. So if you’ve named your Google project “haskell-webservice-test” we’d like our tag to be gcr.io/haskell-webservice-test/haskell-webservice:1.0.0:

Now let’s push the image to GCR:

Next let’s start defining our Kubernetes deployment file. Here’s an example:

Again read up on the details here but there are a couple of things worth mentioning. First of all note that we specify that the “haskell-webservice” container should use the image that we just published to GCR and that we’d like 3 replicas. Next we specify some resource requests and limits for the container followed by a liveness- and readiness probe (see docs here). The readiness probe instructs Kubernetes not to route requests to our container before the /health path returns with 200 (this allows some slack before the container has started up correctly) and liveness probe continuously ping /health during the lifetime of the application to make sure that it’s up and running (if not, it’ll be restarted by the kubelet). Nice! Now let’s ship this specification to Kubernetes:

We’ve now created the deployment controller. Let’s wait until all pods are up and running:

This will block until the deployment of our service is completed. Let’s see what it looks like:

This is nice, but how can we actually access it? For this we’ll define a service which is also a yaml file named service.yaml defined like this:

Here we’ve named the service haskell-webservice (which will be the internal DNS name you can use if you want to reach the app from within the Kubernetes cluster) and we’ve set the type to LoadBalancer. This type is GKE specific and instructs GKE to create an external IP address and load balancer for us which we can use to access our service externally. We also instruct the service to expose port 80 and route to port “external” (which we defined in our deployment.yaml file earlier). The selector is used to by the service to identify which pods it should route to. So let’s deploy it:

Now we must wait until GKE has created the load balancer and the IP address for us:

You’ll see that the EXTERNAL-IP says <pending> but after a while it’ll print the external IP address.

In the meantime you could download a utility called kubetail that helps us tail the logs from multiple pods simultaneously. Start a new terminal and type:

You should see calls to the health check (which is made by Kubernetes):

Once the external service ip has been assigned to the load balancer you should be able to navigate to it in the browser like this http://<external-ip>/kubernetes and our haskell-webservice should give us:

Scotty, kubernetes me up!

The (kubetail) logs should also indicate that a call was received:

Now we’re ready for primetime and can scale the replicas as we find fit:

And now we have 10 replicas ready to serve traffic!

Conclusion

Hopefully you’ve been able to follow along on how you could both create, package, dockerify and deploy a basic Haskell webservice to Kubernetes. Feel free to post improvements or errors in the comments. Thanks for reading.

Be First to Comment

Leave a Reply

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