Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/nike-inc/hal

hal provides an AWS Lambda Custom Runtime environment for your Haskell applications.
https://github.com/nike-inc/hal

aws aws-lambda aws-lambda-haskell aws-lambda-runtime bsd-license haskell haskell-library library nike

Last synced: 1 day ago
JSON representation

hal provides an AWS Lambda Custom Runtime environment for your Haskell applications.

Awesome Lists containing this project

README

        

[![Stack Haskell Builds](https://github.com/Nike-Inc/hal/actions/workflows/haskell_stack.yaml/badge.svg)](https://github.com/Nike-Inc/hal/actions/workflows/haskell_stack.yaml)
[![Cabal Haskell Builds](https://github.com/Nike-Inc/hal/actions/workflows/haskell_cabal.yaml/badge.svg)](https://github.com/Nike-Inc/hal/actions/workflows/haskell_cabal.yaml)

# hal

A runtime environment for [Haskell] applications running on [AWS Lambda].

#### Flexible

This library uniquely supports different types of AWS Lambda Handlers for your needs/comfort with advanced Haskell.
Instead of exposing a single function that constructs a Lambda, this library exposes many.

For lambdas that are pure and safe, then `pureRuntime` is ideal.
It accepts a handler with the signature `(FromJSON a, ToJSON b) => a -> b`.
This runtime guarantees that side-effects cannot occur.

For advanced use cases `mRuntime` unlocks the full power of Monad Transformers.
It accepts handlers with the signature `(MonadCatch m, MonadIO m, FromJSON event, ToJSON result) => (event -> m result)`
This enables users to add caching logic or expose complex environments.

With numerous options in between these two, developers can choose the right balance of flexibility vs simplicity.

#### Performant

Measuring lambda performance is tricky, so investigation and optimization is ongoing.
Current indications show a _warm_ execution overhead of only ~20% more than the official [Rust Runtime] (a much lower level language).

#### Robust

While testing continues, we have executed over 30k test events without error caused by the runtime.
Naive approaches lead to error rates well over 10%.

## Table of Contents

- [Supported Platforms / GHC Versions](#supported-platforms-ghc-versions)
- [Quick Start](#quick-start)
- [Local Testing](#local-testing)

## Supported Platforms / GHC Versions

We currently support this library under the same environment that [AWS Lambda
supports][lambda-env].

Our [CI] currently targets the latest three [LTS Stackage Versions][stackage],
the latest three minor versions of [GHC] under [Cabal]
(e.g. `8.6.x`, `8.4.x`, and `8.2.x`), and GHC-head / Stackage nightly builds.

If you haven't already, adding `docker: { enable: true }` to your `stack.yaml`
file will ensure that you're building a binary that can run in
[AWS Lambda][lambda-env].

## Quick Start

This quick start assumes you have the following tools installed:

- [Stack][stack.yaml]
- [Docker]
- [aws-cli]

Add `hal` to your [stack.yaml]'s [`extra-deps`] and enable
[Docker] integration so that your binary is automatically compiled in a
compatible environment for AWS. Also add `hal` to your project's
dependency list (either `project-name.cabal` or `package.yaml`)

```yaml
#...
extra-deps:
- hal-${DESIRED_VERSION}
# ...
docker:
enable: true
# ...
```

Then, define your types and handler:

```haskell
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NamedFieldPuns #-}

module Main where

import AWS.Lambda.Runtime (pureRuntime)
import Data.Aeson (FromJSON, ToJSON)
import GHC.Generics (Generic)

data IdEvent = IdEvent { input :: String } deriving Generic
instance FromJSON IdEvent

data IdResult = IdResult { output :: String } deriving Generic
instance ToJSON IdResult

handler :: IdEvent -> IdResult
handler IdEvent { input } = IdResult { output = input }

main :: IO ()
main = pureRuntime handler
```

Your binary should be called `bootstrap` in order for the custom runtime
to execute properly:

```yaml
# Example snippet of package.yaml
# ...
executables:
bootstrap:
source-dirs: src
main: Main.hs # e.g. {project root}/src/Main.hs
# ...
```

You'll need to either build on a compatible linux host or inside a compatible docker container (or some other mechanism like nix).
Note that current Stack LTS images are _not_ compatible.
If you see an error message that contains "version 'GLIBC_X.XX' not found" when running (hosted or locally), then your build environment is not compatible.

Enable stack's docker integration and define an optional image within stack.yaml:

```yaml
# file: stack.yaml
docker:
enabled: true
# If omitted, this defaults to fpco/stack-build:lts-${YOUR_LTS_VERSION}
image: ${BUILD_IMAGE}
```

Don't forget to define your [CloudFormation] stack:

```yaml
# file: template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: Test for the Haskell Runtime.
Resources:
HelloWorldApp:
Type: 'AWS::Serverless::Function'
Properties:
Handler: NOT_USED
Runtime: provided
# CodeUri is a relative path from the directory that this CloudFormation
# file is defined.
CodeUri: .stack-work/docker/_home/.local/bin/
Description: My Haskell runtime.
MemorySize: 128
Timeout: 3
```

Finally, build, upload and test your lambda!

```bash
# Build the binary, make sure your executable is named `bootstrap`
stack build --copy-bins

# Create your function package
aws cloudformation package \
--template-file template.yaml \
--s3-bucket your-existing-bucket > \
deployment_stack.yaml

# Deploy your function
aws cloudformation deploy \
--stack-name "hello-world-haskell" \
--region us-west-2 \
--capabilities CAPABILITY_IAM \
--template-file deployment_stack.yaml

# Take it for a spin!
aws lambda invoke \
--function-name your-function-name \
--region us-west-2 \
--payload '{"input": "foo"}' \
output.txt
```

## Local Testing

### Dependencies

- [Stack][stack.yaml]
- [Docker]

### Build

```bash
docker pull fpco/stack-build:lts-{version} # First build only, find the latest version in stack.yaml
stack build --copy-bins
```

### Execute w/ Docker

```bash
echo '{ "accountId": "byebye" }' | docker run -i --rm \
-e DOCKER_LAMBDA_USE_STDIN=1 \
-v ${PWD}/.stack-work/docker/_home/.local/bin/:/var/task \
lambci/lambda:provided
```

### Execute w/ SAM Local

Note that hal currently only supports [aws-sam-cli] on versions <1.0.

```bash
echo '{ "accountId": "byebye" }' | sam local invoke --region us-east-1
```

[AWS Lambda]: https://docs.aws.amazon.com/lambda/latest/dg/welcome.html
[Haskell]: https://www.haskell.org/
[stack.yaml]: https://docs.haskellstack.org/
[`extra-deps`]: https://docs.haskellstack.org/en/stable/yaml_configuration/#yaml-configuration
[Docker]: https://www.docker.com/why-docker
[aws-cli]: https://aws.amazon.com/cli/
[CloudFormation]: https://aws.amazon.com/cloudformation/
[aws-sam-cli]: https://github.com/awslabs/aws-sam-cli
[Rust Runtime]: https://github.com/awslabs/aws-lambda-rust-runtime
[lambda-env]: https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html
[ci]: https://github.com/Nike-Inc/hal/actions
[stackage]: https://www.stackage.org/
[GHC]: https://www.haskell.org/ghc/download.html
[Cabal]: https://www.haskell.org/cabal/download.html