Description
I've been trying to use aws-lambda-haskell-runtime with the AWS SAM CLI. I've run into multiple problems. I'll describe them in this issue. I'm new to AWS Lambda and AWS SAM, so you should take this issue with a grain of salt.
Resources
I learned about SAM from the AWS SAM Developer Guide. In particular,
Setup
I've setup an example Haskell application using aws-lambda-haskell-runtime
. I've used sam
to generate a template.yaml
file, and then edited to match my Haskell application.
Here's my sam
version:
$ sam --version
SAM CLI, version 1.37.0
I believe I generated my template.yaml
file with a command like sam init
, but I forget the specifics. You can find an example in the above resources section.
My template.yaml
looks like the following:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: |
haskell-app
SAM App for custom runtime with haskell
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 3
Resources:
HeyWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
Description: Says hello world
CodeUri: .
Handler: handler
Runtime: provided.al2
Events:
HeyWorld:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /hey
Method: post
Metadata:
BuildMethod: makefile
Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
HeyWorldApi:
Description: "API Gateway endpoint URL for Prod stage for Hey World function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hey/"
HeyWorldFunction:
Description: "Hey World Lambda Function ARN"
Value: !GetAtt HeyWorldFunction.Arn
HeyWorldFunctionIamRole:
Description: "Implicit IAM Role created for Hey World function"
Value: !GetAtt HeyWorldFunctionRole.Arn
The one thing to note here is that the value for Resources.HeyWorldFunction.Properties.Handler
is handler
. I use this function in my Haskell application.
I have a Makefile
for building this application that is used by sam build
:
clean:
rm -rf .aws-sam/build*
# Build application.
build:
sam build
.PHONY: build
# Run application in Docker.
#
# XXX: hot loading doesn't work correctly if the .aws-sam/build directory exists
# for some reason.
#
# https://github.com/aws/aws-sam-cli/issues/1921
# https://github.com/aws/aws-sam-cli/issues/921
#
# Note that this works for interpreted languages (like bash) since they
# don't need a build step, but won't work for compiled languages.
#
# For compiled languages, you have to run `sam build` before `sam local
# start-api`. While `sam local start-api` is running, you have to run `sam
# build` for any changes to be reflected.
start-local:
sam local start-api
.PHONY: start-local
invoke-local:
sam local invoke
.PHONY: invoke-local
# This command can be used for the initial deploy. This is just for
# documentation purposes. I don't expect to use this.
guided-deploy: build
sam deploy --guided
.PHONY: guided-deploy
# Re-deploy application.
#
# Note that the application has to be built
deploy: build
sam deploy
.PHONY: deploy
# Completely delete the whole application cloudformation stack.
#
# Note that once you delete the whole application cloudformation stack,
# then you much do `make guided-deploy` to redeploy it.
destroy:
aws cloudformation delete-stack --stack-name bash-app # --region region
.PHONY: destroy
# Validate template.yaml
validate:
sam validate
.PHONY: validate
#############################################
## Targets used internally by `sam build`. ##
#############################################
build-HeyWorldFunction:
cabal build
cp ./dist-newstyle/build/x86_64-linux/ghc-9.0.2/bootstrap-0.1.0.0/x/bootstrap/build/bootstrap/bootstrap $(ARTIFACTS_DIR)/
.PHONY: build-HeyWorldFunction
The only important target here is build-HeyWorldFunction
(but it is not directly related to this issue).
Here is the .cabal
file for my application.
bootstrap.cabal
:
cabal-version: 2.4
name: bootstrap
version: 0.1.0.0
executable bootstrap
main-is: Main.hs
build-depends: base == 4.15.1.0
, aeson
, amazonka-dynamodb
, aws-lambda-haskell-runtime
, bytestring
, base64-bytestring
, http-conduit
, text
default-extensions: DataKinds
, DefaultSignatures
, DeriveAnyClass
, DeriveFoldable
, DeriveFunctor
, DeriveGeneric
, DerivingStrategies
, EmptyCase
, ExistentialQuantification
, FlexibleContexts
, FlexibleInstances
, GADTs
, GeneralizedNewtypeDeriving
, InstanceSigs
, KindSignatures
, LambdaCase
, MultiParamTypeClasses
, NamedFieldPuns
, OverloadedLabels
, OverloadedLists
, OverloadedStrings
, PatternSynonyms
, PolyKinds
, RankNTypes
, RecordWildCards
, ScopedTypeVariables
, StandaloneDeriving
, TypeApplications
, TypeFamilies
, TypeOperators
other-extensions: TemplateHaskell
, QuasiQuotes
, UndecidableInstances
hs-source-dirs: app
default-language: Haskell2010
Here's my app/Main.hs
file:
module Main where
import Aws.Lambda (ApiGatewayRequest, ApiGatewayResponse, Context, addAPIGatewayHandler, addStandaloneLambdaHandler, defaultDispatcherOptions, mkApiGatewayResponse, runLambdaHaskellRuntime)
import Data.Aeson (FromJSON (parseJSON), ToJSON, Value, genericParseJSON, defaultOptions, eitherDecode)
import Data.Aeson.Types (Parser, parseEither)
import qualified Data.ByteString.Base64 as B64
import Data.Text (Text)
import GHC.Generics (Generic)
import System.IO (hFlush, stdout, stderr)
import Data.Text.Encoding (encodeUtf8)
import Data.ByteString.Lazy (fromStrict)
import Debug.Trace (traceM)
import System.IO.Unsafe (unsafePerformIO)
data Person = Person
{ name :: String
, age :: Int
}
deriving (Generic, Show, ToJSON)
instance FromJSON Person where
parseJSON :: Value -> Parser Person
parseJSON v = do
-- v might be a base64-encoded JSON String, or a normal JSON value of
-- Person.
case parseEither (genericParseJSON defaultOptions) v of
-- v was a JSON-encoded Person, just return it.
Right person -> do
pure person
-- v may be a base64-encoded JSON String.
Left jsonErr -> do
-- parse it as a JSON String
b64str :: Text <- parseJSON v
case B64.decode (encodeUtf8 b64str) of
-- failed to decode the JSON String as base64
Left b64Err -> do
fail "Trying to parse Person, but input value was not a JSON-encoded Person, nor a base64-encoded String"
Right rawPerson -> do
-- try to decode it as JSON
case eitherDecode (fromStrict rawPerson) of
Left innerJsonErr -> do
fail innerJsonErr
Right person -> do
pure person
examplePerson :: Person
examplePerson = Person "hellohello" 33
handler2 :: ApiGatewayRequest Person -> Context () -> IO (Either (ApiGatewayResponse String) (ApiGatewayResponse Person))
handler2 _ _context = do
let resp = mkApiGatewayResponse 200 [] examplePerson
pure (Right resp)
main :: IO ()
main = do
runLambdaHaskellRuntime
defaultDispatcherOptions
(pure ())
id
(addAPIGatewayHandler "handler" handler2)
This application can be built by running sam build
. You can find the built application in the current directory at .aws-sam/build/HeyWorldFunction/bootstrap
.
Problem 1: sam local generate-event
leaves out some fields that aws-lambda-haskell-runtime
is expecting
(I've sent a PR for this at #119.)
AWS SAM gives you a way to define example events in files, and then run your function passing it an event from a file. AWS SAM provides the command sam local generate-event
to generate event files. This is described in the document Invoking functions locally.
One problem is that the events that sam local generate-event
generates don't have all the fields that ApiGatewayRequest
is expecting.
For instance, here is an example of generating an event, and then the resulting event file:
$ sam local generate-event apigateway aws-proxy --path "hey" > events/event2.json
This generates an API Gateway Lambda proxy event. Here is the resulting event file:
{
"body": "{\"name\": \"helll\", \"age\": 39}",
"resource": "/{proxy+}",
"path": "/hey",
"httpMethod": "POST",
"isBase64Encoded": false,
"queryStringParameters": {
"foo": "bar"
},
"multiValueQueryStringParameters": {
"foo": [
"bar"
]
},
"pathParameters": {
"proxy": "/hey"
},
"stageVariables": {
"baz": "qux"
},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, sdch",
"Accept-Language": "en-US,en;q=0.8",
"Cache-Control": "max-age=0",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "US",
"Host": "1234567890.execute-api.us-east-1.amazonaws.com",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Custom User Agent String",
"Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
"X-Forwarded-For": "127.0.0.1, 127.0.0.2",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
"multiValueHeaders": {
"Accept": [
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
],
"Accept-Encoding": [
"gzip, deflate, sdch"
],
"Accept-Language": [
"en-US,en;q=0.8"
],
"Cache-Control": [
"max-age=0"
],
"CloudFront-Forwarded-Proto": [
"https"
],
"CloudFront-Is-Desktop-Viewer": [
"true"
],
"CloudFront-Is-Mobile-Viewer": [
"false"
],
"CloudFront-Is-SmartTV-Viewer": [
"false"
],
"CloudFront-Is-Tablet-Viewer": [
"false"
],
"CloudFront-Viewer-Country": [
"US"
],
"Host": [
"0123456789.execute-api.us-east-1.amazonaws.com"
],
"Upgrade-Insecure-Requests": [
"1"
],
"User-Agent": [
"Custom User Agent String"
],
"Via": [
"1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
],
"X-Amz-Cf-Id": [
"cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
],
"X-Forwarded-For": [
"127.0.0.1, 127.0.0.2"
],
"X-Forwarded-Port": [
"443"
],
"X-Forwarded-Proto": [
"https"
]
},
"requestContext": {
"accountId": "123456789012",
"resourceId": "123456",
"stage": "prod",
"requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
"requestTime": "09/Apr/2015:12:34:56 +0000",
"requestTimeEpoch": 1428582896000,
"identity": {
"cognitoIdentityPoolId": null,
"accountId": null,
"cognitoIdentityId": null,
"caller": null,
"accessKey": null,
"sourceIp": "127.0.0.1",
"cognitoAuthenticationType": null,
"cognitoAuthenticationProvider": null,
"userArn": null,
"userAgent": "Custom User Agent String",
"user": null
},
"path": "/prod/hey",
"resourcePath": "/{proxy+}",
"httpMethod": "POST",
"apiId": "1234567890",
"protocol": "HTTP/1.1"
}
}
You can run the Haskell application built with sam build
by running:
$ sam local invoke --event ./events/event2.json
If you do this, you'll see errors that aws-lambda-haskell-runtime
is expecting more fields than are present in the above input event file. For example, aws-lambda-haskell-runtime
expects the extendedRequestId
field, but that is not defined in this event. There are a few other fields like this as well.