Getting Started with the Haskell AWS Lambda Runtime

By The Agile Monkeys

Introduction

We were there when Werner Vogels announced the new custom lambda runtimes on stage, and we couldn’t have been more excited. It was definitely one of our favorite announcements that morning. We have been trying Haskell (and other flavors of Haskell, like Eta and PureScript) on AWS lambda since we started working on Serverless more than a year ago. From the beginning we felt like Haskell fit like a glove in AWS Lambda — it produces fast and reliable binaries and it’s a pure functional language! There’s nothing like a pure functional language to write Lambda Functions, right?

Well, the reality is that Haskell didn’t work as well as the supported languages did. We had to apply ugly hacks to make it work, and we ended up switching to TypeScript or other better supported languages for production projects. But that’s all in the past!

After the announcement we couldn’t stop ourselves from running to one of those cubicles at the Venetian and writing a Haskell runtime. Today we’re proudly announcing the release of a fully working Haskell Runtime for AWS Lambda and our commitment to maintain it. We hope you like it and start using it in your projects. It’s still in an early stage, so please send us any issues you may find and let’s make it rock solid together!

Setting up the environment

This guide assumes that you have the Stack build tool installed. If not, you can do so by issuing the following command on your terminal:

curl -sSL https://get.haskellstack.org/ | sh

Haskell compiles to native code, which is super efficient. But it has one main drawback: linking changes from machine to machine. To make sure that our projects always work and are reproducible, we use the Stack feature for Docker support to build our projects. Be sure to install Docker before getting started with the runtime :).

A nice scaffold

We will create a simple function that validates if a person has a positive age or not.

To make things easier, we have provided a Stack template that will scaffold the project for you. To use it, enter the following command:

stack new my-haskell-lambda https://github.com/theam/aws-lambda-haskell-runtime/raw/master/stack-template.hsfiles --resolver=lts-12.13 --omit-packages

This will create a my-haskell-lambdadirectory with the following structure:

Given that the Haskell runtime is brand new, package repositories don’t have it yet on their indices. This will be fixed in the near future, but for now you need to replace the following sections in your stack.yaml:

What’s in the code

Our handler is pre-defined in the src/Lib.hs file. Take a look at this file — there’s quite a lot of stuff going on:

The first line is the definition of the module; after that, we do a bunch of imports:

  1. Aws.Lambda.Runtime for having access to the Context type
  2. GHC.Generics for compatibility with JSON
  3. Data.Aeson for generating JSON (de)serializers for our types

After that, we define a Person type, which is basically the JSON that our function expects; we add the deriving (Generic) clause so Haskell can generate nice code for us at compile time.

After that, we tell the compiler to generate the JSON deserializer and serializer for our type.

Finally, we define our handler. Note that it must be called “handler,” as this is currently a restriction of the Haskell runtime.

The type of our handler is a function that:

  1. Takes a Person as the first parameter
  2. Takes an AWS Context as the second parameter
  3. Performs IO, and returns Either a String if something went wrong, or a Person if everything went Right.

In this handler, we check if the personAge field of person is greater than zero, and if it is, we return that everything was Right containing the person.

If not, something went wrong. And what is the opposite of Right? You guessed it — Left. We return a Left containing the error String.

Making the project aware of our handler

If we open app/Main.hs we will find something like this:

We are importing the Configuration and Runtime files, which are needed for our project to deploy properly on AWS.

Then we import qualified, the module that contains our handler.

Finally, we call configureLambda, which will dynamically generate a dispatcher for our handlers. In comparison to the official runtimes written in C++ and Rust, the Haskell runtime doesn’t make you include the whole runtime in your project.

The C++ and Rust runtimes are written as a library and require you to compile an executable per lambda function.

Our runtime is a separate layer that receives the function handler name from AWS, and our configuration module generates a dispatcher dynamically to check and execute it for you.

If you wanted to create more handlers, remember to import them here, qualified.

Deploying our function

The project scaffold provides you a simple Makefile that runs a couple of commands to make things easier for you. First, pull the docker image by doing a stack docker pull

Now, if you run make, you will end up with a build folder containing a function.zip file in the root of your project. Basically, it builds the project using Docker, to make sure that it will work on AWS Lambda. After that, it creates a zip file containing the dispatcher executable from your Haskell Lambda project.

Now, open your AWS Lambda panel in the AWS Console, and click on functions:

Click on the Create function button.

Give your function a name, select Use custom runtime in function code or layer, and create a new role using the policy template AWSLambdaBasicExecutionRole.

Now click on the layers button, and Add a layer.

Paste in the Layer version ARN field the following ARN to use the Haskell runtime:

arn:aws:lambda:<YOUR REGION>:785355572843:layer:haskell-runtime:2

Substitute <YOUR REGION> with the region that you are using in your project, if you are unsure, you can use us-west-2.

(By the way, we had a small issue with the deployment of us-east-2so if you are using that region, please change the 2 at the end with a10)

Now click on your function box on top of the layers button, and upload the zip file that was generated in the build directory:

Remember to type in the appropriate handler name, which in this case is src/Lib.handler.

Click on Save.

Now click on Test, and use this test data:

{
 “personAge”: 43,
 “personName”: “John Doe”
}

If everything went correctly, you can click on more details and see that our function ran correctly, and it returned the person as is, because the age was positive:

Let’s try to insert a negative age and see what happens:

It fails successfully!

Be aware that we are not constrained to only strings to return errors. We can provide our own error types, as long as we provide the ToJSON serializer.

Now you know how to create an AWS Lambda function based on our Haskell runtime. If you encounter any issue or questions, feel free to point it out in the GitHub issue board.

Thanks for reading — stay tuned for more!