Servant on AWS Lambda, and Two New Libraries
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
Application
s 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.