Skip to content

troubles with aws sam cli #118

Open
Open
@cdepillabout

Description

@cdepillabout

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions