# wai-handler-hal
[](https://github.com/bellroy/wai-handler-hal/actions/workflows/haskell-ci.yml)
This library lets you run `wai` `Application`s on AWS Lambda, which
means you can now use mature web frameworks like
[`servant`](https://hackage.haskell.org/package/servant).
The main entry point is `Network.Wai.Handler.Hal.run`, which wraps an
`Application` and returns a function that can be passed to `hal`'s
`AWS.Lambda.Runtime.mRuntime`.
**NOTE:** The function returned by `Network.Wai.Handler.Hal.run` is
only for [Lambda Proxy
Integrations](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html)
of AWS API Gateway **REST APIs** (`AWS::ApiGateway::RestApi` in
CloudFormation). If you try to use such a Lambda with an API Gateway
**HTTP API** (`AWS::ApiGatewayV2::Api` in CloudFormation), it will
return 500s.
For code examples, see [this repository](https://github.com/bellroy/wai-handler-hal-example),
which contains:
* [`wai-handler-hal-example`](https://github.com/bellroy/wai-handler-hal-example): A simple `servant` API set up to run on both `hal` and `warp`; and
* [`wai-handler-hal-cdk`](https://github.com/bellroy/wai-handler-hal-example/tree/master/wai-handler-hal-cdk): A small [AWS
CDK](https://docs.aws.amazon.com/cdk/latest/guide/home.html)
application that deploys an API backed by `wai-handler-hal-example`.
## Developing
Instead of serving your application with something like
[`warp`](https://hackage.haskell.org/package/warp), you will set up an
executable that serves your application with
[`hal`](https://hackage.haskell.org/package/hal). The necessary glue
code looks something like this:
```haskell
import AWS.Lambda.Runtime (mRuntime)
import Network.Wai (Application)
import qualified Network.Wai.Handler.Hal as WaiHandler
app :: Application
app = undefined -- From Servant or wherever else
main :: IO ()
main = mRuntime $ WaiHandler.run app
```
### Local testing
Compiled Lambda functions can be awkward to test, especially if you're
relying on an API Gateway integration to translate HTTP
requests. Consider defining a second executable target that serves
your `Application` using `warp`, which you can use for local
testing.
### Caveats
* The Lambda is never told the port that the API Gateway is listening
on. Most APIs will listen on `443` (HTTPS), so that's what the
library reports by default. See `runWithContext` if you need to
change this.
* The Lambda is never told the HTTP version that the client uses when
talking with the API Gateway. We assume HTTP 1.1.
## Packaging
Lambda functions are packaged in one of two ways: as `.zip` files or
as Docker container images. The [`wai-handler-hal-cdk` example](https://github.com/bellroy/wai-handler-hal-example/tree/master/wai-handler-hal-cdk) uses
`.zip` files for simplicity. CDK doesn't give us an easy way to build
container images using `nix build`, so a container-based deployment
using CDK would need to push to ECR first and then reference the image
by name.
### `.zip` files
You will need to create a `.zip` file containing ([at
least](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html#runtimes-custom-build))
an executable named `bootstrap`. This executable needs to run in
Amazon's runtime environment, and there are multiple ways to ensure
this.
#### Static linking
IOHK's [`haskell.nix`](https://github.com/input-output-hk/haskell.nix)
can build static Haskell binaries by cross-compiling against musl
libc. This is convenient, but consider copyleft implications (of
`libgmp`; `wai-handler-hal` is BSD-3-Clause) if you are distributing
the binaries to other people.
#### Dynamic linking
The other option is to compile the executable in an environment with
packages that match the [Lambda runtime
environment](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html). Some
ideas:
* Build the executable in an "imitation environment" like the [`lambci/lambda`](https://hub.docker.com/r/lambci/lambda) Docker container; or
* (Untested) build the executable on an Amazon Linux 2 EC2 instance.
### Docker container images
It is possible to package Lambdas into Docker containers for
deployment. Amazon provide base containers for custom runtimes in the
repository
[`amazon/aws-lambda-provided`](https://hub.docker.com/r/amazon/aws-lambda-provided/). One
nice feature of these images is that they provide a [runtime interface
emulator](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html#images-test-AWSbase),
which they fall back to when not running "for real". This makes it
possible to directly invoke the lambda and see how it behaves. (This
is less important here because we can serve our application off
`warp`, but for writing Lambdas not invoked by an API Gateway Proxy
Integration, it's handy.)
At the time of writing (2021-04-06), the
`amazon/aws-lambda-provided:al2` (Amazon Linux 2 tag) is the newest
base image that Amazon recommends for custom runtimes. You can use a
`Dockerfile` to build your images; there are also commented .nix files
in the [example repository](https://github.com/bellroy/wai-handler-hal-example),
showing a couple of different approaches to building container images using Nix.
## Integrating
Actually calling the Lambda is done with a [Lambda proxy
integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html)
from an [API Gateway REST
API](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html).
The simplest possible integration sends every request to the Lambda
(where the `wai` `Application` can return 404 or whatever if the
endpoint doesn't match). This is done by mapping the paths `/` and
`/{proxy+}` for the HTTP method `ANY`. We do this in our [CDK example](https://github.com/bellroy/wai-handler-hal-example/tree/master/wai-handler-hal-cdk),
and [CDK provides a Construct](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.LambdaRestApi.html) which encapsulates this pattern, making
it extremely simple to deploy.
### Splitting Servant applications
While it's simplest to back the entire API with a single Lambda, it
does mean that you risk one Lambda carrying an ever-expanding set of
IAM permissions. Servant's combinators make it easy to break the API
into parts that are served by individual Lambdas, which can each have
the minimal set of permissions attached to their role. If your Servant
service had endpoints `foo`, `bar`, and `baz`, you'd serve them all
from Warp with an expression like `Warp.runEnv 8080 . Servant.serve $
foo :<|> bar :<|> baz`. But `Servant.serve foo`, `Servant.serve bar`,
and `Servant.serve baz` are also all WAI `Application`s, and can each
be bundled into distinct Lambda functions using `wai-handler-hal`.
### Other caveats
* The stage name in a path segment is not passed to the Lambda, so it
is not passed to the `wai` `Application`. An invoke URL like
`https://abcde12345.execute-api.us-east-1.amazonaws.com/prod/hoot`
will send `pathInfo = ["hoot"]` to the `Application`.
## Formatters
The formatters used in this repo are provided by `shell.nix`:
* `*.hs`: [`ormolu`](https://github.com/tweag/ormolu)
* `*.cabal`:
[`cabal-fmt`](https://hackage.haskell.org/package/cabal-fmt)
(`cabal-fmt --inplace wai-handler-hal.cabal`)
* `*.nix`:
[`nixpkgs-fmt`](https://github.com/nix-community/nixpkgs-fmt)
(`nixpkgs-fmt *.nix`)
## Regenerate CI
This repo uses `haskell-ci`, which is provided by `flake.nix`:
```shell
haskell-ci regenerate
```