Servant on AWS Lambda, and Two New Libraries

Jack Kelly profile image Jack Kelly 2024-07-08

In a previous post, I mentioned that we run most of our Haskell code on AWS Lambda. Today, I want to look inside those binaries, discuss the libraries we use to do “serverless” web services on AWS, and highlight a few new libraries we’ve recently open sourced.

Serverless Web Services on AWS…

Lambda is AWS’s “function-as-a-service” product. You upload your code to AWS (either as a zip file or a container image), and when it is “invoked”, AWS will find some hardware to run it on. Most invocations are done using an AWS SDK, where you provide a JSON payload and receive a JSON response.

It’s possible to build “serverless” web services out of Lambda Functions by integrating your functions with other AWS products. We currently use AWS API Gateway for this. API Gateways have several integration types, but the one we use most often is the AWS_PROXY integration. This integration has the API Gateway call a Lambda Function to handle the HTTP request, using JSON representations of the HTTP request and response.

…in Haskell

Hackage has a few different “Lambda runtime” packages, but the one we have settled on is hal. It provides functions that implement the request/response loop that Lambda functions are required to perform (requesting the next invocation from AWS, returning results to AWS, etc.) as well as marshalling types for common AWS events. Its maintainer has also been a delight to work with, and we’re grateful for his diligence and receptiveness to enhancements and bug fixes. For API Gateway, hal provides the AWS.Lambda.Events.ApiGateway.ProxyRequest and AWS.Lambda.Events.ApiGateway.ProxyResponse modules, describing the HTTP request/response JSON structures used by an API Gateway REST API.

To fit hal’s mRuntime function and implement our request handler, we need to provide a function ProxyRequest -> m ProxyResponse. This is morally very similar to the Application type provided by wai, the standard interface between Haskell web servers and web frameworks. wai is Haskell’s version of Ruby’s rack or Python’s wsgi: it provides an Application type for frameworks to provide and servers to consume and standard types for HTTP requests and responses, which together form a narrow waist between servers and clients.

If we can convert a hal ProxyRequest to a wai Request and a wai Response to a hal ProxyResponse, we can lift nearly any wai Application into a Lambda Function. One of our first open-source Haskell packages, wai-handler-hal, does exactly this. We have a working example on GitHub if you want to experiment with it, and we were very pleased to discover that the ZuriHac registration system moved off a custom Lambda runtime to hal+wai-handler-hal.

Building APIs: Servant

Now that we can run standard web application stacks on Lambda, which one do we use? Since we build a lot of APIs, servant is the obvious choice. It’s an excellent framework for specifying and building out APIs, but intermediate Haskellers often struggle to implement Servant servers using custom monads.

Many application monads just carry around a “context” of read-only data such as environment variables, and do not do any tricky control flow. Such monads admit a MonadUnliftIO instance, and Servant APIs built on top of them can be converted to wai Applications in a generic way. We recently released unliftio-servant-server, a package of helpers to do this for both traditional and record-based Servant APIs.

Serving ActiveResource APIs

Until recently, the majority of Bellroy’s systems were written in Ruby, often on Rails. As we’ve migrated more systems to Haskell, we’ve occasionally found the activeresource library from Rails to be quite helpful. activeresource is an object-relational mapper (ORM) for RESTful API endpoints: it represents individual instances of resources as Ruby objects with a similar interface to Rails’ database ORM, activerecord. When the database structures are simple enough (not too many joins), the similarity between activerecord and activeresource has let us carve out functionality from our Rails projects without extensive rewrites.

activeresource is a bit particular about the routes and HTTP status codes that it expects, so we developed servant-activeresource to capture the pattern. By encoding the conventions that activeresource expects, we are sure to get the Haskell side right every time.

Conclusions

This basic stack (APIs in Servant, wrapped in wai-handler-hal, running on AWS Lambda, behind an API Gateway) has proven very effective for us, and has become the default way we build services at Bellroy. It’s not a universal answer — some problems are better solved with traditional virtual machines or other persistent compute, Application Load Balancers become more cost-effective than API Gateways at scale, and CloudFront can now use Lambda URLs as origins — but it’s worked very well for us so far. We’ve been lucky that there’s so many good libraries in this space on Hackage, and we’re pleased that we’ve been able to give back with a few of our own.