├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .hlint.yaml ├── .stylish-haskell.yaml ├── .tintin.yml ├── LICENSE ├── README.md ├── Setup.hs ├── aws-lambda-haskell-runtime.cabal ├── doc ├── 01-getting-started.md ├── 02-adding-a-handler.md ├── 03-configuring-the-dispatcher.md ├── 04-usage-with-api-gateway.md ├── 05-common-errors.md ├── 06-deploying-a-lambda.md └── index.md ├── examples └── wai-app │ ├── .gitignore │ ├── ChangeLog.md │ ├── LICENSE │ ├── Lambda.Dockerfile │ ├── Makefile │ ├── README.md │ ├── Setup.hs │ ├── app │ └── Main.hs │ ├── package.yaml │ ├── src │ └── Lib.hs │ ├── stack.yaml │ ├── stack.yaml.lock │ ├── test │ └── Spec.hs │ └── wai-app.cabal ├── hie.yaml ├── package.yaml ├── src └── Aws │ ├── Lambda.hs │ └── Lambda │ ├── Runtime.hs │ ├── Runtime │ ├── ALB │ │ └── Types.hs │ ├── API │ │ ├── Endpoints.hs │ │ └── Version.hs │ ├── APIGateway │ │ └── Types.hs │ ├── ApiInfo.hs │ ├── Common.hs │ ├── Configuration.hs │ ├── Context.hs │ ├── Environment.hs │ ├── Error.hs │ ├── Publish.hs │ └── StandaloneLambda │ │ └── Types.hs │ ├── Setup.hs │ └── Utilities.hs ├── stack-template.hsfiles ├── stack.yaml ├── stack.yaml.lock └── test └── Spec.hs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build & test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - uses: haskell/actions/setup@v2 17 | with: 18 | ghc-version: "9.6.4" # Exact version of ghc to use 19 | # cabal-version: 'latest'. Omitted, but defaults to 'latest' 20 | enable-stack: true 21 | stack-version: "2.15.1" 22 | 23 | # Attempt to load cached dependencies 24 | - name: Cache Stack dependencies 25 | uses: actions/cache@v2 26 | with: 27 | path: | 28 | ~/.stack 29 | .stack-work 30 | key: ${{ runner.os }}-stack-${{ hashFiles('stack.yaml') }} 31 | 32 | - name: Build and test 33 | run: | 34 | stack build --test --no-run-tests --bench --no-run-benchmarks --haddock --no-haddock-deps --no-haddock-hyperlink-source --fast 35 | 36 | # Save cached dependencies 37 | - name: Cache Stack dependencies 38 | uses: actions/cache@v2 39 | with: 40 | path: | 41 | ~/.stack 42 | .stack-work 43 | key: ${{ runner.os }}-stack-${{ hashFiles('stack.yaml') }} 44 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | HACKAGE_USERNAME: ${{ secrets.HACKAGE_USERNAME }} 10 | HACKAGE_PASSWORD: ${{ secrets.HACKAGE_PASSWORD }} 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | 20 | - uses: haskell/actions/setup@v2 21 | with: 22 | ghc-version: "9.6.4" # Exact version of ghc to use 23 | # cabal-version: 'latest'. Omitted, but defaults to 'latest' 24 | enable-stack: true 25 | stack-version: "2.15.1" 26 | 27 | # Attempt to load cached dependencies 28 | - name: Cache Stack dependencies 29 | uses: actions/cache@v2 30 | with: 31 | path: | 32 | ~/.stack 33 | .stack-work 34 | key: ${{ runner.os }}-stack-${{ hashFiles('stack.yaml') }} 35 | 36 | - name: Bump version field 37 | run: | 38 | if grep -q "feat" <<< "$GITHUB_REF"; then 39 | echo "feat: bumping minor version" 40 | stack exec bump-version minor package.yaml 41 | elif grep -q "fix" <<< "$GITHUB_REF"; then 42 | echo "fix: bumping patch version" 43 | stack exec bump-version patch package.yaml 44 | elif grep -q "breaking" <<< "$GITHUB_REF"; then 45 | echo "breaking: bumping major version" 46 | stack exec bump-version major package.yaml 47 | else 48 | echo "no version bump required" 49 | fi 50 | 51 | - name: Publish to Hackage 52 | run: stack upload . 53 | env: 54 | HACKAGE_USERNAME: ${{ secrets.HACKAGE_USERNAME }} 55 | HACKAGE_PASSWORD: ${{ secrets.HACKAGE_PASSWORD }} 56 | 57 | # Attempt to load cached dependencies 58 | - name: Cache Stack dependencies 59 | uses: actions/cache@v2 60 | with: 61 | path: | 62 | ~/.stack 63 | .stack-work 64 | key: ${{ runner.os }}-stack-${{ hashFiles('stack.yaml') }} 65 | 66 | - name: Push changes 67 | uses: ad-m/github-push-action@master 68 | with: 69 | branch: main 70 | force: true 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | *~ 3 | 4 | # Created by https://www.gitignore.io/api/haskell 5 | # Edit at https://www.gitignore.io/?templates=haskell 6 | 7 | ### Haskell ### 8 | dist 9 | dist-* 10 | cabal-dev 11 | *.o 12 | *.hi 13 | *.chi 14 | *.chs.h 15 | *.dyn_o 16 | *.dyn_hi 17 | .hpc 18 | .hsenv 19 | .cabal-sandbox/ 20 | cabal.sandbox.config 21 | *.prof 22 | *.aux 23 | *.hp 24 | *.eventlog 25 | .stack-work/ 26 | cabal.project.local 27 | cabal.project.local~ 28 | .HTF/ 29 | .ghc.environment.* 30 | 31 | # VSCode 32 | .vscode/settings.json 33 | 34 | # IntelliJ 35 | .idea 36 | *.iml 37 | 38 | # End of https://www.gitignore.io/api/haskell 39 | .DS_Store 40 | -------------------------------------------------------------------------------- /.hlint.yaml: -------------------------------------------------------------------------------- 1 | # HLint configuration file 2 | # https://github.com/ndmitchell/hlint 3 | ########################## 4 | 5 | # This file contains a template configuration file, which is typically 6 | # placed as .hlint.yaml in the root of your project 7 | 8 | 9 | # Specify additional command line arguments 10 | # 11 | - arguments: 12 | - TemplateHaskell 13 | - OverloadedStrings 14 | - RecordWildCards 15 | - ScopedTypeVariables 16 | - DeriveGeneric 17 | - TypeApplications 18 | - FlexibleContexts 19 | - DeriveAnyClass 20 | - QuasiQuotes 21 | 22 | 23 | # Control which extensions/flags/modules/functions can be used 24 | # 25 | # - extensions: 26 | # - name: 27 | # - TemplateHaskell 28 | # - OverloadedStrings 29 | # - RecordWildCards 30 | # - ScopedTypeVariables 31 | # - DeriveGeneric 32 | # - TypeApplications 33 | # - FlexibleContexts 34 | # - DeriveAnyClass 35 | # - QuasiQuotes 36 | # 37 | # - flags: 38 | # - {name: -w, within: []} # -w is allowed nowhere 39 | # 40 | # - modules: 41 | # - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set' 42 | # - {name: Control.Arrow, within: []} # Certain modules are banned entirely 43 | # 44 | # - functions: 45 | # - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules 46 | 47 | 48 | # Add custom hints for this project 49 | # 50 | # Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar" 51 | # - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x} 52 | 53 | 54 | # Turn on hints that are off by default 55 | # 56 | # Ban "module X(module X) where", to require a real export list 57 | # - warn: {name: Use explicit module export list} 58 | # 59 | # Replace a $ b $ c with a . b $ c 60 | # - group: {name: dollar, enabled: true} 61 | # 62 | # Generalise map to fmap, ++ to <> 63 | # - group: {name: generalise, enabled: true} 64 | 65 | 66 | # Ignore some builtin hints 67 | # - ignore: {name: Use let} 68 | # - ignore: {name: Use const, within: SpecialModule} # Only within certain modules 69 | 70 | 71 | # Define some custom infix operators 72 | # - fixity: infixr 3 ~^#^~ 73 | 74 | 75 | # To generate a suitable file for HLint do: 76 | # $ hlint --default > .hlint.yaml 77 | -------------------------------------------------------------------------------- /.stylish-haskell.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - simple_align: 3 | cases: true 4 | top_level_patterns: true 5 | records: true 6 | 7 | # Import cleanup 8 | - imports: 9 | align: none 10 | list_align: after_alias 11 | pad_module_names: false 12 | long_list_align: inline 13 | empty_list_align: inherit 14 | list_padding: 4 15 | separate_lists: true 16 | space_surround: false 17 | 18 | - language_pragmas: 19 | style: vertical 20 | remove_redundant: true 21 | 22 | # Remove trailing whitespace 23 | - trailing_whitespace: {} 24 | 25 | columns: 100 26 | 27 | newline: native 28 | 29 | language_extensions: 30 | - BangPatterns 31 | - ConstraintKinds 32 | - DataKinds 33 | - DefaultSignatures 34 | - DeriveAnyClass 35 | - DeriveDataTypeable 36 | - DeriveGeneric 37 | - DerivingStrategies 38 | - FlexibleContexts 39 | - FlexibleInstances 40 | - FunctionalDependencies 41 | - GADTs 42 | - GeneralizedNewtypeDeriving 43 | - InstanceSigs 44 | - KindSignatures 45 | - LambdaCase 46 | - MultiParamTypeClasses 47 | - MultiWayIf 48 | - NamedFieldPuns 49 | - NoImplicitPrelude 50 | - OverloadedStrings 51 | - QuasiQuotes 52 | - RecordWildCards 53 | - ScopedTypeVariables 54 | - StandaloneDeriving 55 | - TemplateHaskell 56 | - TupleSections 57 | - TypeApplications 58 | - TypeFamilies 59 | - ViewPatterns 60 | -------------------------------------------------------------------------------- /.tintin.yml: -------------------------------------------------------------------------------- 1 | name: AWS λ Runtime 2 | synopsis: Build fast and solid serverless applications 3 | github: theam/aws-lambda-haskell-runtime 4 | author: The Agile Monkeys 5 | authorWebsite: https://www.theagilemonkeys.com 6 | color: #662480 7 | logo: https://i.imgur.com/I64VEiF.png 8 | titleFont: Lato 9 | bodyFont: Lato 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 The Agile Monkeys 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/theam/aws-lambda-haskell-runtime.svg?style=shield)](https://circleci.com/gh/theam/aws-lambda-haskell-runtime) 2 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=shield)](http://makeapullrequest.com) 3 | [![Hackage version](https://img.shields.io/hackage/v/aws-lambda-haskell-runtime.svg)](https://hackage.haskell.org/package/aws-lambda-haskell-runtime) 4 | [![Open Source Love png1](https://raw.githubusercontent.com/ellerbrock/open-source-badges/master/badges/open-source-v1/open-source.png)](https://github.com/ellerbrock/open-source-badges/) 5 | [![Linter](https://img.shields.io/badge/code%20style-HLint-brightgreen.svg)](https://github.com/ndmitchell/hlint) 6 | 7 | [Proceed to site](https://theam.github.io/aws-lambda-haskell-runtime) to know how to use the AWS Lambda Haskell Runtime. 8 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /aws-lambda-haskell-runtime.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 1.12 2 | 3 | -- This file has been generated from package.yaml by hpack version 0.36.0. 4 | -- 5 | -- see: https://github.com/sol/hpack 6 | 7 | name: aws-lambda-haskell-runtime 8 | version: 4.3.2 9 | synopsis: Haskell runtime for AWS Lambda 10 | description: Please see the README on GitHub at 11 | category: AWS 12 | homepage: https://github.com/theam/aws-lambda-haskell-runtime#readme 13 | bug-reports: https://github.com/theam/aws-lambda-haskell-runtime/issues 14 | author: Nikita Tchayka 15 | maintainer: hackers@theagilemonkeys.com 16 | copyright: 2023 The Agile Monkeys SL 17 | license: Apache-2.0 18 | license-file: LICENSE 19 | build-type: Simple 20 | extra-source-files: 21 | README.md 22 | 23 | source-repository head 24 | type: git 25 | location: https://github.com/theam/aws-lambda-haskell-runtime 26 | 27 | library 28 | exposed-modules: 29 | Aws.Lambda 30 | other-modules: 31 | Aws.Lambda.Runtime 32 | Aws.Lambda.Runtime.ALB.Types 33 | Aws.Lambda.Runtime.API.Endpoints 34 | Aws.Lambda.Runtime.API.Version 35 | Aws.Lambda.Runtime.APIGateway.Types 36 | Aws.Lambda.Runtime.ApiInfo 37 | Aws.Lambda.Runtime.Common 38 | Aws.Lambda.Runtime.Configuration 39 | Aws.Lambda.Runtime.Context 40 | Aws.Lambda.Runtime.Environment 41 | Aws.Lambda.Runtime.Error 42 | Aws.Lambda.Runtime.Publish 43 | Aws.Lambda.Runtime.StandaloneLambda.Types 44 | Aws.Lambda.Setup 45 | Aws.Lambda.Utilities 46 | Paths_aws_lambda_haskell_runtime 47 | hs-source-dirs: 48 | src 49 | default-extensions: 50 | TemplateHaskell 51 | OverloadedStrings 52 | RecordWildCards 53 | ScopedTypeVariables 54 | DeriveGeneric 55 | TypeApplications 56 | FlexibleContexts 57 | DeriveAnyClass 58 | QuasiQuotes 59 | ghc-options: -Wall -optP-Wno-nonportable-include-path -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat -Widentities -Wredundant-constraints -Wmissing-export-lists -Wpartial-fields -fhide-source-paths -freverse-errors 60 | build-depends: 61 | aeson >2 62 | , base >=4.7 && <5 63 | , bytestring 64 | , case-insensitive 65 | , exceptions 66 | , hashable 67 | , http-client 68 | , http-types 69 | , mtl 70 | , path >0.7 71 | , path-io 72 | , safe-exceptions 73 | , template-haskell 74 | , text 75 | , unordered-containers 76 | default-language: Haskell2010 77 | 78 | test-suite aws-lambda-haskell-runtime-test 79 | type: exitcode-stdio-1.0 80 | main-is: Spec.hs 81 | other-modules: 82 | Paths_aws_lambda_haskell_runtime 83 | hs-source-dirs: 84 | test 85 | default-extensions: 86 | TemplateHaskell 87 | OverloadedStrings 88 | RecordWildCards 89 | ScopedTypeVariables 90 | DeriveGeneric 91 | TypeApplications 92 | FlexibleContexts 93 | DeriveAnyClass 94 | QuasiQuotes 95 | ghc-options: -Wall -optP-Wno-nonportable-include-path -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat -Widentities -Wredundant-constraints -Wmissing-export-lists -Wpartial-fields -fhide-source-paths -freverse-errors -threaded -rtsopts -with-rtsopts=-N 96 | build-depends: 97 | base >=4.7 && <5 98 | , hspec 99 | default-language: Haskell2010 100 | -------------------------------------------------------------------------------- /doc/01-getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | --- 4 | 5 | # Getting Started 6 | 7 | This guide assumes that you have the [Stack](https://www.haskellstack.org/) build tool installed. 8 | If not, you can do so by issuing the following command on your terminal: 9 | 10 | ```bash 11 | curl -sSL https://get.haskellstack.org/ | sh 12 | ``` 13 | 14 | Haskell compiles to **native code**, which is super efficient. But it has one main drawback: linking changes from machine to machine. It's very hard to make sure that the executable you build will work when deployed to AWS Lambda. 15 | 16 | To make sure our projects work consistently, we use AWS Lambda's [docker image](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/) feature. Be sure to install Docker before getting started with the runtime 😄 17 | 18 | ## Using the template 19 | 20 | If you are testing the package, or you are starting a new project, we have provided a Stack template that will scaffold the project for you. 21 | To use it, enter the following command: 22 | 23 | ```bash 24 | stack new my-haskell-lambda https://raw.githubusercontent.com/theam/aws-lambda-haskell-runtime/main/stack-template.hsfiles 25 | ``` 26 | 27 | This will create a `my-haskell-lambda` directory with the following structure: 28 | 29 | ```text 30 | . 31 | ├── LICENSE 32 | ├── Dockerfile 33 | ├── Makefile 34 | ├── README.md 35 | ├── Setup.hs 36 | ├── app 37 | │   └── Main.hs 38 | ├── my-haskell-lambda.cabal 39 | ├── package.yaml 40 | ├── src 41 | │   └── Lib.hs 42 | └── stack.yaml 43 | ``` 44 | 45 | The project contains a sample handler that you can use as a starting point. 46 | 47 | ## Adding the dependency to an existing project 48 | 49 | If you want to add the runtime to an existing project, you can do so by adding the following `extra-dep` entry to the `stack.yaml` file: 50 | 51 | ```yaml 52 | extra-deps: 53 | - aws-lambda-haskell-runtime-4.0.0 54 | ``` 55 | 56 | and, to the `package.yaml` file: 57 | 58 | ```yaml 59 | dependencies: 60 | - ... # other dependencies of your project 61 | - aws-lambda-haskell-runtime >= 4.0.0 62 | ``` 63 | 64 | If you have completed these steps, you should be able to execute `stack build` and see the project build correctly. 65 | 66 | ## Keep reading! 67 | 68 | Let's see how we can add our first handler! 69 | -------------------------------------------------------------------------------- /doc/02-adding-a-handler.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding a handler 3 | --- 4 | 5 | # Adding a handler 6 | 7 | In this example, we'll create a person age validator. 8 | 9 | If you have used the Stack template, you will have a handler that is pre-defined in the `src/Lib.hs` file. 10 | 11 | First, we need to enable some language extensions in order to make working with JSON easier. We'll also import a few required modules: 12 | 13 | ```haskell top 14 | {-# LANGUAGE DeriveGeneric #-} 15 | {-# LANGUAGE DeriveAnyClass #-} 16 | 17 | module Lib where 18 | 19 | import Aws.Lambda 20 | import GHC.Generics 21 | import Data.Aeson 22 | ``` 23 | 24 | We'll create a basic handler that validates a person's age is positive. Let's create a `Person` type to use. 25 | 26 | ```haskell top 27 | data Person = Person 28 | { name :: String 29 | , age :: Int 30 | } -- We kindly ask the compiler to autogenerate JSON instances for us 31 | deriving (Generic, FromJSON, ToJSON) 32 | ``` 33 | 34 | Now, let's implement the actual handler. 35 | 36 | A handler is a function with the following type signature: 37 | 38 | ```haskell 39 | -- Note that request, error and response must all implement ToJSON and FromJSON 40 | handler :: request -> Context context -> IO (Either error response) 41 | ``` 42 | 43 | For our person validator usecase this means the following: 44 | 45 | ```haskell 46 | handler :: Person -> Context () -> IO (Either String Person) 47 | ``` 48 | 49 | This means we expect to be given a `Person` object and we'll return either some `String` as an error or some other `Person` object (that passed validation). 50 | 51 | You can ignore the `Context ()` parameter at this point. This is the Lambda context object which is present in every runtime. By specifying `()` as an inner value, we say we don't want to have anything there. 52 | 53 | The implementation of our handler will look like this: 54 | 55 | ```haskell 56 | handler :: Person -> Context () -> IO (Either String Person) 57 | handler person _context = 58 | if age person > 0 then 59 | pure (Right person) 60 | else 61 | pure (Left "A person's age must be positive") 62 | ``` 63 | 64 | Note how we are using `Right` to return the value in case everything went **right**, and `Left` if something went wrong. 65 | 66 | Now let's see how to register this handler into our runtime. -------------------------------------------------------------------------------- /doc/03-configuring-the-dispatcher.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuring the Dispatcher 3 | --- 4 | 5 | # Configuring the Dispatcher 6 | 7 | The dispatcher is a special main function that checks which handler it has to run, and runs it. 8 | 9 | This is very easy to do in `aws-lambda-haskell-runtime`. Just use the `runLambdaHaskellRuntime` function. 10 | 11 | ```haskell 12 | runLambdaHaskellRuntime :: 13 | RuntimeContext handlerType m context request response error => 14 | DispatcherOptions -> 15 | IO context -> 16 | (forall a. m a -> IO a) -> 17 | HandlersM handlerType m context request response error () -> 18 | IO () 19 | ``` 20 | 21 | It may seem like an intimidating type signature, but let's go through each parameter and see what it means. 22 | 23 | * `DispatcherOptions` are the configuration options of the dispatcher function. You can just use `defaultDispatcherOptions`. 24 | * The `IO context` action is how you initialize the context object. 25 | * `(forall a. m a -> IO a)` is used when you have your handler in a custom monad. It transforms that custom monadic action into `IO`. If your handlers run in `IO`, just use `id`. 26 | * `HandlersM handlerType m context request response error ()` is the action that registers the handlers under a given name. 27 | 28 | For the person validator example we set up in the previous section, it will look like this: 29 | 30 | ```haskell 31 | import Aws.Lambda 32 | import qualified Lib 33 | 34 | main :: IO () 35 | main = 36 | runLambdaHaskellRuntime 37 | defaultDispatcherOptions 38 | (pure ()) 39 | id $ do 40 | -- You could also register multiple handlers 41 | addStandaloneLambdaHandler "handler" Lib.handler 42 | ``` -------------------------------------------------------------------------------- /doc/04-usage-with-api-gateway.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage with API Gateway or ALB 3 | --- 4 | 5 | # Usage with API Gateway or ALB 6 | 7 | A common use-case is to expose your lambdas through API Gateway or an ALB. Luckily, `aws-lambda-haskell-runtime` has native support for that. 8 | 9 | If you want to add such handlers simply use the corresponding `add` function in `runLambdaHaskellRuntime`. 10 | 11 | ```haskell 12 | import Aws.Lambda 13 | 14 | main :: IO () 15 | main = 16 | runLambdaHaskellRuntime 17 | defaultDispatcherOptions 18 | (pure ()) 19 | id $ do 20 | addAPIGatewayHandler "api-gateway" gatewayHandler 21 | addALBHandler "alb" albHandler 22 | addStandaloneLambdaHandler "standalone" regularHandler 23 | 24 | gatewayHandler :: 25 | ApiGatewayRequest request -> 26 | Context context -> 27 | IO (Either (ApiGatewayResponse error) (ApiGatewayResponse response)) 28 | gatewayHandler = doSomething 29 | 30 | albHandler :: 31 | ALBRequest request -> 32 | Context context -> 33 | IO (Either (ALBResponse error) (ALBResponse response)) 34 | albHandler = doSomething 35 | 36 | regularHandler :: 37 | request -> 38 | Context context -> 39 | IO (Either error response) 40 | regularHandler = doSomething 41 | ``` 42 | 43 | You can use the `ApiGatewayRequest` or `ALBRequest` wrapper to access additional request information that API Gateway or ALB provide, such as path, query string parameters, authentication info and much more. 44 | 45 | You can find more information about that under the `Input format of a Lambda function for proxy integration` section [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html). 46 | 47 | You can use the `ApiGatewayResponse` or `ALBResponse` wrapper to return a custom status code or attach custom headers to the response. 48 | 49 | # Supporting both API Gateway and ALB at once 50 | 51 | You could use a standalone handler with a dynamic check to support both API Gateway and ALB. 52 | 53 | For an example, see how `aws-lambda-haskell-runtime-wai` does it: 54 | 55 | ```haskell 56 | runWaiAsProxiedHttpLambda :: 57 | DispatcherOptions -> 58 | Maybe ALBIgnoredPathPortion -> 59 | HandlerName -> 60 | IO Application -> 61 | IO () 62 | runWaiAsProxiedHttpLambda options ignoredAlbPath handlerName mkApp = 63 | runLambdaHaskellRuntime options mkApp id $ 64 | addStandaloneLambdaHandler handlerName $ \(request :: Value) context -> 65 | case parse parseIsAlb request of 66 | Success isAlb -> do 67 | if isAlb 68 | then case fromJSON @(ALBRequest Text) request of 69 | Success albRequest -> 70 | bimap toJSON toJSON <$> albWaiHandler ignoredAlbPath albRequest context 71 | Error err -> error $ "Could not parse the request as a valid ALB request: " <> err 72 | else case fromJSON @(ApiGatewayRequest Text) request of 73 | Success apiGwRequest -> 74 | bimap toJSON toJSON <$> apiGatewayWaiHandler apiGwRequest context 75 | Error err -> error $ "Could not parse the request as a valid API Gateway request: " <> err 76 | Error err -> 77 | error $ 78 | "Could not parse the request as a valid API Gateway or ALB proxy request: " <> err 79 | ``` -------------------------------------------------------------------------------- /doc/05-common-errors.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Common errors 3 | --- 4 | 5 | # `c_poll` permission denied 6 | 7 | **TL;DR**: The error is harmless. 8 | 9 | **Long version**: 10 | 11 | Sometimes, it is possible that the runtime can print an error that looks more or less like this: 12 | 13 | ```text 14 | bootstrap: c_poll: permission denied (Operation not permitted) 15 | ``` 16 | 17 | This is because when the Haskell runtime system (the Haskell RTS is equivalent to Java's JVM) is initialized, the 18 | runtime system polls some OS stuff, to see what features of the OS it can use for IO execution. Given that Amazon Linux 19 | does not support some of these features, it fails with a `permission denied` error. 20 | 21 | If this error makes your application crash for some reason, please [do open an issue](https://github.com/theam/aws-lambda-haskell-runtime/issues/43). 22 | 23 | # Error while loading shared libraries 24 | 25 | Given the native nature of Haskell, and it's possibility to interoperate with shared libraries, 26 | it might be possible that you have encountered yourself facing an error that looks like: 27 | 28 | ```text 29 | /var/task/bootstrap: error while loading shared libraries: libpcre.so.3: cannot open shared object file: No such file or directory 30 | ``` 31 | 32 | ## Solution 33 | 34 | ### Solution 1: Enable static linking 35 | 36 | The easiest solution is likely to just enable static linking for your project by adding the following to your `package.yaml` or `.cabal` file (this will be already included if you're using the Stack template): 37 | 38 | ```yaml 39 | ghc-options: -O2 -static -threaded 40 | cc-options: -static 41 | ld-options: -static -pthread 42 | 43 | # If you want to pass a custom path to your dependencies 44 | # extra-lib-dirs: ./some/path 45 | ``` 46 | 47 | ### Solution 2: Deploy the libraries with your lambda 48 | 49 | To solve this, we have to make sure that whatever is packaging/deploying our function is copying these libraries to our function package. If you are using the Stack template, then you should substitute the last line of your makefile for these lines: 50 | 51 | ```text 52 | @rsync -rl 53 | @cd build && zip -r function.zip bootstrap && rm bootstrap && cd .. 54 | ``` 55 | 56 | Additionally, you will want to make sure you have the `amd64` version of these libraries, and when you download them that they live in `lib/x86_64/`. When you download `amd64` files, they are usually saved to `lib/x86_64_gnu_linux`. 57 | 58 | Please note that you might have to adjust your `LD_LIBRARY_PATH` environment variable, so it also points to the directory where you've 59 | placed these files. `$LAMBDA_TASK_ROOT/lib` is already part of `LD_LIBRARY_PATH`, so if `` is `lib`, this should be handled automatically. 60 | 61 | Here is a more comprehensive list of libraries that you might want to copy if they are used by your project. You can use it to check if 62 | some library of those might appear in your project. 63 | 64 | ```text 65 | libz.so.1 66 | libxml2.so.2 67 | libverto.so.1 68 | libutil.so.1 69 | libunistring.so.0 70 | libtinfo.so.5 71 | libtic.so.5 72 | libthread_db.so.1 73 | libtasn1.so.3 74 | libstdc++.so.6 75 | libssl3.so 76 | libssl.so.10 77 | libssh2.so.1 78 | libsqlite3.so.0 79 | libsoftokn3.so 80 | libsmime3.so 81 | libsepol.so.1 82 | libselinux.so.1 83 | libsasl2.so.2 84 | librt.so.1 85 | librpmsign.so.1 86 | librpmio.so.3 87 | librpmbuild.so.3 88 | librpm.so.3 89 | libresolv.so.2 90 | libreadline.so.6 91 | libp11-kit.so.0 92 | libpython2.7.so.1.0 93 | libpthread.so.0 94 | libpth.so.20 95 | libpsl.so.0 96 | libpopt.so.0 97 | libplds4.so 98 | libplc4.so 99 | libpcreposix.so.0 100 | libpcrecpp.so.0 101 | libpcre.so.0 102 | libpcprofile.so 103 | libpanelw.so.5 104 | libpanel.so.5 105 | libnss3.so 106 | libnssutil3.so 107 | libnsssysinit.so 108 | libnsspem.so 109 | libnssdbm3.so 110 | libnssckbi.so 111 | libnss_nisplus.so.2 112 | libnss_nis.so.2 113 | libnss_hesiod.so.2 114 | libnss_files.so.2 115 | libnss_dns.so.2 116 | libnss_db.so.2 117 | libnss_compat.so.2 118 | libnspr4.so 119 | libnsl.so.1 120 | libncursesw.so.5 121 | libncurses.so.5 122 | libmenuw.so.5 123 | libmenu.so.5 124 | libmemusage.so 125 | libmagic.so.1 126 | libm.so.6 127 | liblzma.so.5 128 | liblua-5.1.so 129 | libldif-2.4.so.2 130 | libldap_r-2.4.so.2 131 | libldap-2.4.so.2 132 | liblber-2.4.so.2 133 | libk5crypto.so.3 134 | libkrb5support.so.0 135 | libkrb5.so.3 136 | libkrad.so.0 137 | libkeyutils.so.1 138 | libkdb5.so.8 139 | libidn2.so.0 140 | libicuuc.so.50 141 | libicutu.so.50 142 | libicutest.so.50 143 | libiculx.so.50 144 | libicule.so.50 145 | libicui18n.so.50 146 | libicuio.so.50 147 | libicudata.so.50 148 | libhistory.so.6 149 | libgthread-2.0.so.0 150 | libgssrpc.so.4 151 | libgssapi_krb5.so.2 152 | libgpgme.so.11 153 | libgpgme-pthread.so.11 154 | libgpg-error.so.0 155 | libgobject-2.0.so.0 156 | libgmpxx.so.4 157 | libgmp.so.10 158 | libgmodule-2.0.so.0 159 | libglib-2.0.so.0 160 | libgio-2.0.so.0 161 | libgdbm.so.2 162 | libgcrypt.so.11 163 | libgcc_s.so.1 164 | libfreebl3.so 165 | libfreeblpriv3.so 166 | libformw.so.5 167 | libform.so.5 168 | libffi.so.6 169 | libexpat.so.1 170 | libelf.so.1 171 | libdl.so.2 172 | libdb-4.7.so 173 | libcurl.so.4 174 | libcrypto.so.10 175 | libcrypt.so.1 176 | libcom_err.so.2 177 | libcidn.so.1 178 | libcap.so.2 179 | libc.so.6 180 | libbz2.so.1 181 | libattr.so.1 182 | libassuan.so.0 183 | libanl.so.1 184 | libacl.so.1 185 | libSegFault.so 186 | libBrokenLocale.so.1 187 | ld-linux-x86-64.so.2 188 | ``` 189 | -------------------------------------------------------------------------------- /doc/06-deploying-a-lambda.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deploying a Lambda 3 | --- 4 | 5 | ## Using the template Makefile 6 | 7 | To deploy your project to AWS Lambda, we have provided a `Makefile` to make your life easier. 8 | 9 | Run `make` from the root of your project, and you will have a [**Docker**](https://www.docker.com) image generated for you that is ready to be deployed onto AWS Lambda. 10 | 11 | ## Building the executable manually 12 | 13 | You could also build the executable manually, but that is troublesome because it either needs to be static or you need to make sure to ship all library dependencies as well as build it on the same environment it's going to run on. 14 | 15 | For simple executables without dependencies, you could just add the following to your `package.yaml`, build the `bootstrap` and ship it to AWS using a `.zip` file. 16 | 17 | ```yaml 18 | ghc-options: 19 | .. other options 20 | - -O2 21 | - -static 22 | cc-options: -static 23 | ld-options: -static -pthread 24 | ``` -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | --- 4 | 5 | # Haskell Runtime for AWS Lambda 6 | 7 | [![CircleCI](https://circleci.com/gh/theam/aws-lambda-haskell-runtime.svg?style=shield)](https://circleci.com/gh/theam/aws-lambda-haskell-runtime) 8 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=shield)](http://makeapullrequest.com) 9 | [![Hackage version](https://img.shields.io/hackage/v/aws-lambda-haskell-runtime.svg)](https://hackage.haskell.org/package/aws-lambda-haskell-runtime) 10 | [![Open Source Love png1](https://badges.frapsoft.com/os/v1/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/) 11 | [![Linter](https://img.shields.io/badge/code%20style-HLint-brightgreen.svg)](https://github.com/ndmitchell/hlint) 12 | 13 | **aws-lambda-haskell-runtime** allows you to use Haskell as a first-class citizen of AWS Lambda. It allows you to deploy projects built with Haskell, and select the handler as you would do with Node.js. It discovers which modules of the project implement a handler for AWS, and generates a dispatcher dynamically, so you don't have to worry about wiring the lambda calls, as it uses the 14 | handler name specified in the AWS Lambda console. 15 | 16 | It makes deploying Haskell code to AWS very easy: 17 | 18 | ```haskell 19 | module WordCount where 20 | 21 | import Aws.Lambda 22 | 23 | handler :: String -> Context () -> IO (Either String Int) 24 | handler someText _ = do 25 | let wordsCount = length (words someText) 26 | if wordsCount > 0 then 27 | pure (Right wordsCount) 28 | else 29 | pure (Left "Sorry, your text was empty") 30 | ``` 31 | 32 | Then, in the `Main` module: 33 | 34 | ```haskell 35 | import Aws.Lambda 36 | import qualified WordCount 37 | 38 | initializeContext :: IO () 39 | initializeContext = return () 40 | 41 | generateLambdaDispatcher StandaloneLambda defaultDispatcherOptions 42 | ``` 43 | 44 | # Performance 45 | 46 | Performance is overall good, meaning that it faster than Java apps on AWS Lambda, but still not as fast as Node.js: 47 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
RuntimeBest Cold StartWorst Cold Startexecution timeMax memory used
Haskell60.30 ms98.38 ms0.86 ms48 MB
Java790 ms812 ms0.89 ms109 MB
Node.js3.85 ms43.8 ms0.26 ms66 MB
89 |
90 | 91 | # A bit of background 92 | 93 | We were there when [Werner Vogels](https://twitter.com/Werner) 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? 94 | 95 | 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, like compiling an executable/dynamic library and then wrapping it around in a Node.js module that performed a native call. We always ended up switching to TypeScript or other better supported languages for production projects. But now since AWS started supporting [custom lambda runtimes](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html), that’s all in the past! 96 | 97 | Excited as we are? Keep reading! 👇 98 | -------------------------------------------------------------------------------- /examples/wai-app/.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | *~ 3 | build 4 | *.iml 5 | -------------------------------------------------------------------------------- /examples/wai-app/ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Changelog for lambda-testing 2 | 3 | ## Unreleased changes 4 | -------------------------------------------------------------------------------- /examples/wai-app/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Dobromir Nikolov (c) 2020 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Dobromir Nikolov nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /examples/wai-app/Lambda.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lambci/lambda:build-provided 2 | 3 | SHELL ["/bin/bash", "--rcfile", "~/.profile", "-c"] 4 | 5 | USER root 6 | 7 | # Saving default system libraries before doing anything else 8 | RUN du -a /lib64 /usr/lib64 | cut -f2 > /root/default-libraries 9 | 10 | # Installing basic dependencies 11 | RUN yum install -y postgresql-devel \ 12 | git-core \ 13 | tar \ 14 | sudo \ 15 | xz \ 16 | make \ 17 | postgresql-devel \ 18 | libyaml libyaml-devel 19 | 20 | # Installing Haskell Stack 21 | RUN sudo curl -sSL https://get.haskellstack.org/ | sh 22 | 23 | ARG STACK_RESOLVER=lts-15.16 24 | 25 | # Setting up GHC 26 | RUN stack setup --resolver=${STACK_RESOLVER} 27 | 28 | # Installing common packages so that docker builds are faster 29 | RUN stack install --resolver=${STACK_RESOLVER} postgresql-simple persistent-template persistent-postgresql persistent 30 | RUN stack install --resolver=${STACK_RESOLVER} text bytestring http-client http-types path template-haskell case-insensitive aeson unordered-containers 31 | RUN stack install --resolver=${STACK_RESOLVER} servant wai servant-server 32 | 33 | RUN mkdir /root/lambda-function 34 | 35 | ARG PACKAGER_COMMIT_SHA=6404623b59a4189c17cadeb2c5a2eb96f1a76722 36 | RUN cd /tmp && \ 37 | git clone https://github.com/saurabhnanda/aws-lambda-packager.git && \ 38 | cd /tmp/aws-lambda-packager && \ 39 | git checkout ${PACKAGER_COMMIT_SHA} && \ 40 | stack install --resolver=${STACK_RESOLVER} 41 | 42 | # Copying the source code of the lambda function into the Docker container 43 | COPY . /root/lambda-function/ 44 | 45 | RUN pwd 46 | 47 | # Building the lambda-function and copying it to the output directory 48 | RUN cd /root/lambda-function 49 | WORKDIR /root/lambda-function/ 50 | RUN ls 51 | RUN stack clean --full 52 | RUN stack build --fast 53 | 54 | ARG OUTPUT_DIR=/root/output 55 | RUN mkdir ${OUTPUT_DIR} && \ 56 | mkdir ${OUTPUT_DIR}/lib 57 | 58 | RUN cp $(stack path --local-install-root)/bin/bootstrap ${OUTPUT_DIR}/bootstrap 59 | 60 | # Finally, copying over all custom/extra libraries with the help of aws-lambda-packager 61 | RUN /root/.local/bin/aws-lambda-packager copy-custom-libraries \ 62 | -l /root/default-libraries \ 63 | -f /root/output/bootstrap \ 64 | -o /root/output/lib 65 | 66 | ENTRYPOINT sh 67 | -------------------------------------------------------------------------------- /examples/wai-app/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @rm -rf ./build/* 3 | DOCKER_BUILDKIT=1 docker build --file Lambda.Dockerfile -t example-lambda . 4 | id=$$(docker create example-lambda); docker cp $$id:/root/output ./build; docker rm -v $$id 5 | cd build/output; zip -r function.zip * 6 | -------------------------------------------------------------------------------- /examples/wai-app/README.md: -------------------------------------------------------------------------------- 1 | # lambda-testing 2 | -------------------------------------------------------------------------------- /examples/wai-app/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /examples/wai-app/app/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ScopedTypeVariables #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | 4 | module Main where 5 | 6 | import Aws.Lambda 7 | import Lib 8 | import qualified Lib 9 | 10 | -- This tells the Lambda runtime how to initialize your application context. 11 | -- If you do not wish to use a shared context, you can just use Unit as the context value. 12 | -- E.g. 13 | -- initializeContext :: IO () 14 | -- initializeContext = return () 15 | initializeContext :: IO AppConfig 16 | initializeContext = Lib.initializeAppConfig 17 | 18 | generateLambdaDispatcher UseWithAPIGateway defaultDispatcherOptions 19 | -------------------------------------------------------------------------------- /examples/wai-app/package.yaml: -------------------------------------------------------------------------------- 1 | name: wai-app 2 | version: 0.1.0.0 3 | github: "theam/aws-lambda-haskell-runtime" 4 | license: BSD3 5 | author: "Dobromir Nikolov" 6 | 7 | extra-source-files: 8 | - README.md 9 | - ChangeLog.md 10 | 11 | # Metadata used when publishing your package 12 | # synopsis: Short description of your package 13 | # category: Web 14 | 15 | # To avoid duplicated efforts in documentation and dealing with the 16 | # complications of embedding Haddock markup inside cabal files, it is 17 | # common to point users to the README.md file. 18 | description: Please see the README on GitHub at 19 | 20 | dependencies: 21 | - base >= 4.7 && < 5 22 | - aeson 23 | - aws-lambda-haskell-runtime 24 | - aws-lambda-haskell-runtime-wai 25 | - text 26 | - servant 27 | - servant-server 28 | - wai 29 | - mtl 30 | 31 | library: 32 | source-dirs: src 33 | 34 | executables: 35 | bootstrap: 36 | main: Main.hs 37 | source-dirs: app 38 | ghc-options: 39 | - -threaded 40 | - -rtsopts 41 | - -with-rtsopts=-N 42 | dependencies: 43 | - wai-app 44 | 45 | tests: 46 | lambda-testing-test: 47 | main: Spec.hs 48 | source-dirs: test 49 | ghc-options: 50 | - -threaded 51 | - -rtsopts 52 | - -with-rtsopts=-N 53 | dependencies: 54 | - wai-app 55 | -------------------------------------------------------------------------------- /examples/wai-app/src/Lib.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE FlexibleContexts #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE ScopedTypeVariables #-} 7 | {-# LANGUAGE TypeApplications #-} 8 | {-# LANGUAGE TypeOperators #-} 9 | 10 | module Lib where 11 | 12 | import Aws.Lambda 13 | import Aws.Lambda.Wai 14 | import Control.Exception (try) 15 | import Control.Monad.Except (ExceptT (..)) 16 | import Data.Aeson 17 | import Data.IORef (modifyIORef, readIORef) 18 | import Data.Text as T 19 | import GHC.Generics (Generic) 20 | import Servant hiding (Context) 21 | 22 | -- The actual handler. 23 | handler :: WaiHandler AppConfig 24 | handler request context = do 25 | 26 | -- You can access your custom application config using the `customContext` IORef. 27 | -- This application config will be preserved while the Lambda is warm, so you can reuse 28 | -- resources like database connections. 29 | appConfig :: AppConfig <- readIORef $ customContext context 30 | 31 | -- You can also mutate the config by modifying the IORef. On the next Lambda call, you will receive your updated config. 32 | modifyIORef (customContext context) (\cfg -> let myModifiedCfg = cfg in myModifiedCfg) 33 | 34 | -- This uses Aws.Lambda.Wai from aws-lambda-haskell-runtime-wai in order to convert the Wai application to Lambda. 35 | waiHandler initializeApplication request context 36 | 37 | -- Your application config. This config will be initialized using `initializeAppConfig` 38 | -- once per every cold start. It will be preserved while the lambda is warm. 39 | data AppConfig = 40 | AppConfig 41 | { appConfigDbConnection :: DbConnection } 42 | 43 | initializeAppConfig :: IO AppConfig 44 | initializeAppConfig = newDbConnection >>= \connection -> 45 | return $ AppConfig connection 46 | 47 | -- We're mocking the DbConnection, but using a real one (e.g. ConnectionPool) will be just as easy. 48 | type DbConnection = () 49 | 50 | newDbConnection :: IO DbConnection 51 | newDbConnection = return () 52 | 53 | data SomeType = 54 | SomeType 55 | { aField :: Int 56 | , anotherField :: Text 57 | } deriving (Generic, ToJSON, FromJSON) 58 | 59 | -- A mock API returning different content types so you can test around. 60 | type API = 61 | "html" :> Get '[PlainText] (Headers '[Header "Content-Type" Text] Text) :<|> 62 | "json" :> Get '[JSON] SomeType :<|> 63 | "plain" :> Get '[PlainText] Text :<|> 64 | "empty" :> Get '[PlainText] Text :<|> 65 | "post" :> ReqBody '[JSON] SomeType :> Post '[JSON] SomeType :<|> 66 | "error" :> Get '[JSON] SomeType 67 | 68 | server :: ServerT API IO 69 | server = 70 | htmlHandler :<|> 71 | jsonHandler :<|> 72 | plainHandler :<|> 73 | emptyHandler :<|> 74 | postTest :<|> 75 | errorHandler 76 | where 77 | htmlHandler = 78 | return $ addHeader @"Content-Type" "text/html" "

" 79 | jsonHandler = 80 | return $ SomeType 1203 "a field" 81 | plainHandler = 82 | return "plain text" 83 | emptyHandler = 84 | return "" 85 | postTest = 86 | return 87 | errorHandler = 88 | error "I blew up" 89 | 90 | -- Wai application initialization logic 91 | initializeApplication :: IO Application 92 | initializeApplication = return $ 93 | serve (Proxy @API) hoistedServer 94 | where 95 | hoistedServer = hoistServer (Proxy @API) ioToHandler server 96 | 97 | ioToHandler 98 | :: IO a 99 | -> Handler a 100 | ioToHandler = Handler . ExceptT . try 101 | -------------------------------------------------------------------------------- /examples/wai-app/stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # https://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # 15 | # The location of a snapshot can be provided as a file or url. Stack assumes 16 | # a snapshot provided as a file might change, whereas a url resource does not. 17 | # 18 | # resolver: ./custom-snapshot.yaml 19 | # resolver: https://example.com/snapshots/2018-01-01.yaml 20 | resolver: lts-15.16 21 | 22 | # User packages to be built. 23 | # Various formats can be used as shown in the example below. 24 | # 25 | # packages: 26 | # - some-directory 27 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 28 | # subdirs: 29 | # - auto-update 30 | # - wai 31 | packages: 32 | - . 33 | # Dependency packages to be pulled from upstream that are not in the resolver. 34 | # These entries can reference officially published versions as well as 35 | # forks / in-progress versions pinned to a git hash. For example: 36 | # 37 | # extra-deps: 38 | # - acme-missiles-0.3 39 | # - git: https://github.com/commercialhaskell/stack.git 40 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 41 | # 42 | extra-deps: 43 | - git: https://github.com/eir-forsakring/aws-lambda-haskell-runtime-wai.git 44 | commit: 5bedba584b50d21040ecbcc019c77581f4f77cba 45 | - git: https://github.com/dnikolovv/aws-lambda-haskell-runtime.git 46 | commit: 41356c91433d470410818713291d753974dfb12f 47 | # Override default flag values for local packages and extra-deps 48 | # flags: {} 49 | 50 | # Extra package databases containing global packages 51 | # extra-package-dbs: [] 52 | 53 | # Control whether we use the GHC we find on the path 54 | # system-ghc: true 55 | # 56 | # Require a specific version of stack, using version ranges 57 | # require-stack-version: -any # Default 58 | # require-stack-version: ">=2.3" 59 | # 60 | # Override the architecture used by stack, especially useful on Windows 61 | # arch: i386 62 | # arch: x86_64 63 | # 64 | # Extra directories used by stack for building 65 | # extra-include-dirs: [/path/to/dir] 66 | # extra-lib-dirs: [/path/to/dir] 67 | # 68 | # Allow a newer minor version of GHC than the snapshot specifies 69 | # compiler-check: newer-minor 70 | -------------------------------------------------------------------------------- /examples/wai-app/stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: 7 | - completed: 8 | name: aws-lambda-haskell-runtime-wai 9 | version: 0.1.0.0 10 | git: https://github.com/eir-forsakring/aws-lambda-haskell-runtime-wai.git 11 | pantry-tree: 12 | size: 710 13 | sha256: ee5e5ff5409b6bb647a60319ece4d737f9e6967f7fce5fe24a56d06c514645da 14 | commit: 5bedba584b50d21040ecbcc019c77581f4f77cba 15 | original: 16 | git: https://github.com/eir-forsakring/aws-lambda-haskell-runtime-wai.git 17 | commit: 5bedba584b50d21040ecbcc019c77581f4f77cba 18 | - completed: 19 | name: aws-lambda-haskell-runtime 20 | version: 2.0.6 21 | git: https://github.com/dnikolovv/aws-lambda-haskell-runtime.git 22 | pantry-tree: 23 | size: 2464 24 | sha256: 4412446adbfcd194d619b815ad8029ec556f400253943ccb7d9009e41db94606 25 | commit: 41356c91433d470410818713291d753974dfb12f 26 | original: 27 | git: https://github.com/dnikolovv/aws-lambda-haskell-runtime.git 28 | commit: 41356c91433d470410818713291d753974dfb12f 29 | snapshots: 30 | - completed: 31 | size: 496120 32 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/15/16.yaml 33 | sha256: cf30623a2c147f51eecc8f9d6440f5d1b671af21380505e633faff32b565f3d5 34 | original: lts-15.16 35 | -------------------------------------------------------------------------------- /examples/wai-app/test/Spec.hs: -------------------------------------------------------------------------------- 1 | main :: IO () 2 | main = putStrLn "Test suite not yet implemented" 3 | -------------------------------------------------------------------------------- /examples/wai-app/wai-app.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 1.12 2 | 3 | -- This file has been generated from package.yaml by hpack version 0.33.0. 4 | -- 5 | -- see: https://github.com/sol/hpack 6 | -- 7 | -- hash: 16238ed9054bd93918a4d420860664c23cd3ed472cd080fadc7f694a02b4df78 8 | 9 | name: wai-app 10 | version: 0.1.0.0 11 | description: Please see the README on GitHub at 12 | homepage: https://github.com/theam/aws-lambda-haskell-runtime#readme 13 | bug-reports: https://github.com/theam/aws-lambda-haskell-runtime/issues 14 | author: Dobromir Nikolov 15 | maintainer: Dobromir Nikolov 16 | license: BSD3 17 | license-file: LICENSE 18 | build-type: Simple 19 | extra-source-files: 20 | README.md 21 | ChangeLog.md 22 | 23 | source-repository head 24 | type: git 25 | location: https://github.com/theam/aws-lambda-haskell-runtime 26 | 27 | library 28 | exposed-modules: 29 | Lib 30 | other-modules: 31 | Paths_wai_app 32 | hs-source-dirs: 33 | src 34 | build-depends: 35 | aeson 36 | , aws-lambda-haskell-runtime 37 | , aws-lambda-haskell-runtime-wai 38 | , base >=4.7 && <5 39 | , mtl 40 | , servant 41 | , servant-server 42 | , text 43 | , wai 44 | default-language: Haskell2010 45 | 46 | executable bootstrap 47 | main-is: Main.hs 48 | other-modules: 49 | Paths_wai_app 50 | hs-source-dirs: 51 | app 52 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 53 | build-depends: 54 | aeson 55 | , aws-lambda-haskell-runtime 56 | , aws-lambda-haskell-runtime-wai 57 | , base >=4.7 && <5 58 | , mtl 59 | , servant 60 | , servant-server 61 | , text 62 | , wai 63 | , wai-app 64 | default-language: Haskell2010 65 | 66 | test-suite lambda-testing-test 67 | type: exitcode-stdio-1.0 68 | main-is: Spec.hs 69 | other-modules: 70 | Paths_wai_app 71 | hs-source-dirs: 72 | test 73 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 74 | build-depends: 75 | aeson 76 | , aws-lambda-haskell-runtime 77 | , aws-lambda-haskell-runtime-wai 78 | , base >=4.7 && <5 79 | , mtl 80 | , servant 81 | , servant-server 82 | , text 83 | , wai 84 | , wai-app 85 | default-language: Haskell2010 86 | -------------------------------------------------------------------------------- /hie.yaml: -------------------------------------------------------------------------------- 1 | cradle: 2 | stack: 3 | - path: "./src" 4 | component: "aws-lambda-haskell-runtime:lib" 5 | 6 | - path: "./test" 7 | component: "aws-lambda-haskell-runtime:test:aws-lambda-haskell-runtime-test" 8 | -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | name: aws-lambda-haskell-runtime 2 | version: 4.3.2 3 | github: "theam/aws-lambda-haskell-runtime" 4 | license: Apache-2.0 5 | author: Nikita Tchayka 6 | maintainer: hackers@theagilemonkeys.com 7 | copyright: 2023 The Agile Monkeys SL 8 | 9 | extra-source-files: 10 | - README.md 11 | 12 | synopsis: Haskell runtime for AWS Lambda 13 | category: AWS 14 | description: Please see the README on GitHub at 15 | 16 | dependencies: 17 | - base >= 4.7 && < 5 18 | 19 | library: 20 | dependencies: 21 | - aeson > 2 22 | - bytestring 23 | - http-client 24 | - http-types 25 | - template-haskell 26 | - text 27 | - safe-exceptions 28 | - exceptions 29 | - path > 0.7 30 | - path-io 31 | - unordered-containers 32 | - case-insensitive 33 | - mtl 34 | - hashable 35 | source-dirs: src 36 | exposed-modules: 37 | - Aws.Lambda 38 | 39 | tests: 40 | aws-lambda-haskell-runtime-test: 41 | main: Spec.hs 42 | source-dirs: test 43 | ghc-options: 44 | - -threaded 45 | - -rtsopts 46 | - -with-rtsopts=-N 47 | dependencies: 48 | - hspec 49 | 50 | default-extensions: 51 | - TemplateHaskell 52 | - OverloadedStrings 53 | - RecordWildCards 54 | - ScopedTypeVariables 55 | - DeriveGeneric 56 | - TypeApplications 57 | - FlexibleContexts 58 | - DeriveAnyClass 59 | - QuasiQuotes 60 | 61 | ghc-options: 62 | - -Wall 63 | # - -Werror 64 | - -optP-Wno-nonportable-include-path 65 | - -Wincomplete-uni-patterns 66 | - -Wincomplete-record-updates 67 | - -Wcompat 68 | - -Widentities 69 | - -Wredundant-constraints 70 | - -Wmissing-export-lists 71 | - -Wpartial-fields 72 | - -fhide-source-paths 73 | - -freverse-errors 74 | -------------------------------------------------------------------------------- /src/Aws/Lambda.hs: -------------------------------------------------------------------------------- 1 | module Aws.Lambda 2 | ( module Reexported, 3 | ) 4 | where 5 | 6 | import Aws.Lambda.Runtime as Reexported 7 | import Aws.Lambda.Runtime.ALB.Types as Reexported 8 | import Aws.Lambda.Runtime.APIGateway.Types as Reexported 9 | import Aws.Lambda.Runtime.Common as Reexported 10 | import Aws.Lambda.Runtime.Configuration as Reexported 11 | import Aws.Lambda.Runtime.Context as Reexported 12 | import Aws.Lambda.Setup as Reexported 13 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GADTs #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | {-# LANGUAGE TypeApplications #-} 4 | {-# OPTIONS_GHC -fno-warn-name-shadowing #-} 5 | 6 | module Aws.Lambda.Runtime 7 | ( runLambda, 8 | Runtime.LambdaResult (..), 9 | Runtime.ApiGatewayDispatcherOptions (..), 10 | Error.Parsing (..), 11 | ) 12 | where 13 | 14 | import qualified Aws.Lambda.Runtime.ApiInfo as ApiInfo 15 | import qualified Aws.Lambda.Runtime.Common as Runtime 16 | import qualified Aws.Lambda.Runtime.Context as Context 17 | import qualified Aws.Lambda.Runtime.Environment as Environment 18 | import qualified Aws.Lambda.Runtime.Error as Error 19 | import qualified Aws.Lambda.Runtime.Publish as Publish 20 | import Aws.Lambda.Runtime.StandaloneLambda.Types (StandaloneLambdaResponseBody (..)) 21 | import qualified Control.Exception as Unchecked 22 | import Control.Exception.Safe 23 | import Control.Monad (forever) 24 | import Data.Aeson (encode) 25 | import Data.IORef (newIORef) 26 | import Data.Text (Text, unpack) 27 | import qualified Network.HTTP.Client as Http 28 | import System.IO (hFlush, stderr, stdout) 29 | 30 | -- | Runs the user @haskell_lambda@ executable and posts back the 31 | -- results. This is called from the layer's @main@ function. 32 | runLambda :: forall context handlerType. IO context -> Runtime.RunCallback handlerType context -> IO () 33 | runLambda initializeCustomContext callback = do 34 | manager <- Http.newManager httpManagerSettings 35 | customContext <- initializeCustomContext 36 | customContextRef <- newIORef customContext 37 | context <- Context.initialize @context customContextRef `catch` errorParsing `catch` variableNotSet 38 | forever $ do 39 | lambdaApi <- Environment.apiEndpoint `catch` variableNotSet 40 | event <- ApiInfo.fetchEvent manager lambdaApi `catch` errorParsing 41 | 42 | -- Purposefully shadowing to prevent using the initial "empty" context 43 | context <- Context.setEventData context event 44 | 45 | ( ( ( invokeAndRun callback manager lambdaApi event context 46 | `catch` \err -> Publish.parsingError err lambdaApi context manager 47 | ) 48 | `catch` \err -> Publish.invocationError err lambdaApi context manager 49 | ) 50 | `catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError err lambdaApi context manager 51 | ) 52 | `Unchecked.catch` \err -> Publish.invocationError err lambdaApi context manager 53 | 54 | httpManagerSettings :: Http.ManagerSettings 55 | httpManagerSettings = 56 | -- We set the timeout to none, as AWS Lambda freezes the containers. 57 | Http.defaultManagerSettings 58 | { Http.managerResponseTimeout = Http.responseTimeoutNone 59 | } 60 | 61 | invokeAndRun :: 62 | Runtime.RunCallback handlerType context -> 63 | Http.Manager -> 64 | Text -> 65 | ApiInfo.Event -> 66 | Context.Context context -> 67 | IO () 68 | invokeAndRun callback manager lambdaApi event context = do 69 | result <- invokeWithCallback callback event context 70 | 71 | Publish.result result lambdaApi context manager 72 | `catch` \err -> Publish.invocationError err lambdaApi context manager 73 | 74 | invokeWithCallback :: 75 | Runtime.RunCallback handlerType context -> 76 | ApiInfo.Event -> 77 | Context.Context context -> 78 | IO (Runtime.LambdaResult handlerType) 79 | invokeWithCallback callback event context = do 80 | handlerName <- Runtime.HandlerName <$> Environment.handlerName 81 | let lambdaOptions = 82 | Runtime.LambdaOptions 83 | { eventObject = ApiInfo.event event, 84 | functionHandler = handlerName, 85 | executionUuid = "", -- DirectCall doesnt use UUID 86 | contextObject = context 87 | } 88 | result <- callback lambdaOptions 89 | -- Flush output to insure output goes into CloudWatch logs 90 | flushOutput 91 | case result of 92 | Left lambdaError -> case lambdaError of 93 | Runtime.StandaloneLambdaError (StandaloneLambdaResponseBodyPlain err) -> 94 | throw $ Error.Invocation $ encode err 95 | Runtime.StandaloneLambdaError (StandaloneLambdaResponseBodyJson err) -> 96 | throw $ Error.Invocation err 97 | Runtime.APIGatewayLambdaError err -> 98 | throw $ Error.Invocation $ encode err 99 | Runtime.ALBLambdaError err -> 100 | throw $ Error.Invocation $ encode err 101 | Right value -> 102 | pure value 103 | 104 | variableNotSet :: Error.EnvironmentVariableNotSet -> IO a 105 | variableNotSet (Error.EnvironmentVariableNotSet env) = 106 | error ("Error initializing, variable not set: " <> unpack env) 107 | 108 | errorParsing :: Error.Parsing -> IO a 109 | errorParsing Error.Parsing {..} = 110 | error ("Failed parsing " <> unpack errorMessage <> ", got" <> unpack actualValue) 111 | 112 | -- | Flush standard output ('stdout') and standard error output ('stderr') handlers 113 | flushOutput :: IO () 114 | flushOutput = do 115 | hFlush stdout 116 | hFlush stderr -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/ALB/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingStrategies #-} 2 | {-# LANGUAGE DuplicateRecordFields #-} 3 | {-# LANGUAGE FlexibleInstances #-} 4 | {-# LANGUAGE GeneralisedNewtypeDeriving #-} 5 | {-# LANGUAGE UndecidableInstances #-} 6 | 7 | module Aws.Lambda.Runtime.ALB.Types 8 | ( ALBRequest (..), 9 | ALBRequestContext (..), 10 | ALBResponse (..), 11 | ALBResponseBody (..), 12 | ToALBResponseBody (..), 13 | mkALBResponse, 14 | ) 15 | where 16 | 17 | import Aws.Lambda.Utilities (toJSONText, tshow) 18 | import Data.Aeson 19 | ( FromJSON (parseJSON), 20 | KeyValue ((.=)), 21 | Object, 22 | ToJSON (toJSON), 23 | Value (Null, Object, String), 24 | eitherDecodeStrict, 25 | object, 26 | (.:), 27 | ) 28 | import Data.Aeson.Types (Parser) 29 | import qualified Data.Aeson.Types as T 30 | import qualified Data.Aeson.Key as K 31 | import qualified Data.CaseInsensitive as CI 32 | import Data.HashMap.Strict (HashMap) 33 | import Data.Text (Text) 34 | import qualified Data.Text as T 35 | import qualified Data.Text.Encoding as T 36 | import GHC.Generics (Generic) 37 | import Network.HTTP.Types (Header, ResponseHeaders) 38 | 39 | data ALBRequest body = ALBRequest 40 | { albRequestPath :: !Text, 41 | albRequestHttpMethod :: !Text, 42 | albRequestHeaders :: !(Maybe (HashMap Text Text)), 43 | -- TODO: They won't be url decoded in ALB 44 | albRequestQueryStringParameters :: !(Maybe (HashMap Text Text)), 45 | albRequestIsBase64Encoded :: !Bool, 46 | albRequestRequestContext :: !ALBRequestContext, 47 | albRequestBody :: !(Maybe body) 48 | } 49 | deriving (Show) 50 | 51 | -- We special case String and Text in order 52 | -- to avoid unneeded encoding which will wrap them in quotes and break parsing 53 | instance {-# OVERLAPPING #-} FromJSON (ALBRequest Text) where 54 | parseJSON = parseALBRequest (.:) 55 | 56 | instance {-# OVERLAPPING #-} FromJSON (ALBRequest String) where 57 | parseJSON = parseALBRequest (.:) 58 | 59 | instance FromJSON body => FromJSON (ALBRequest body) where 60 | parseJSON = parseALBRequest parseObjectFromStringField 61 | 62 | -- We need this because API Gateway is going to send us the payload as a JSON string 63 | parseObjectFromStringField :: FromJSON a => Object -> T.Key -> Parser (Maybe a) 64 | parseObjectFromStringField obj fieldName = do 65 | fieldContents <- obj .: fieldName 66 | case fieldContents of 67 | String stringContents -> 68 | case eitherDecodeStrict (T.encodeUtf8 stringContents) of 69 | Right success -> pure success 70 | Left err -> fail err 71 | Null -> pure Nothing 72 | other -> T.typeMismatch "String or Null" other 73 | 74 | parseALBRequest :: (Object -> T.Key -> Parser (Maybe body)) -> Value -> Parser (ALBRequest body) 75 | parseALBRequest bodyParser (Object v) = 76 | ALBRequest 77 | <$> v .: "path" 78 | <*> v .: "httpMethod" 79 | <*> v .: "headers" 80 | <*> v .: "queryStringParameters" 81 | <*> v .: "isBase64Encoded" 82 | <*> v .: "requestContext" 83 | <*> v `bodyParser` "body" 84 | parseALBRequest _ _ = fail "Expected ALBRequest to be an object." 85 | 86 | newtype ALBRequestContext = ALBRequestContext 87 | {albRequestContextElb :: ALBELB} 88 | deriving (Show) 89 | 90 | instance FromJSON ALBRequestContext where 91 | parseJSON (Object v) = 92 | ALBRequestContext 93 | <$> v .: "elb" 94 | parseJSON _ = fail "Expected ALBRequestContext to be an object." 95 | 96 | newtype ALBELB = ALBELB 97 | {albElbTargetGroupArn :: Text} 98 | deriving (Show) 99 | 100 | instance FromJSON ALBELB where 101 | parseJSON (Object v) = 102 | ALBELB 103 | <$> v .: "targetGroupArn" 104 | parseJSON _ = fail "Expected ALBELB to be an object." 105 | 106 | newtype ALBResponseBody 107 | = ALBResponseBody Text 108 | deriving newtype (ToJSON, FromJSON) 109 | 110 | class ToALBResponseBody a where 111 | toALBResponseBody :: a -> ALBResponseBody 112 | 113 | -- We special case Text and String to avoid unneeded encoding which will wrap them in quotes 114 | instance {-# OVERLAPPING #-} ToALBResponseBody Text where 115 | toALBResponseBody = ALBResponseBody 116 | 117 | instance {-# OVERLAPPING #-} ToALBResponseBody String where 118 | toALBResponseBody = ALBResponseBody . T.pack 119 | 120 | instance ToJSON a => ToALBResponseBody a where 121 | toALBResponseBody = ALBResponseBody . toJSONText 122 | 123 | data ALBResponse body = ALBResponse 124 | { albResponseStatusCode :: !Int, 125 | albResponseStatusDescription :: !Text, 126 | albResponseHeaders :: !ResponseHeaders, 127 | albResponseMultiValueHeaders :: !ResponseHeaders, 128 | albResponseBody :: !body, 129 | albResponseIsBase64Encoded :: !Bool 130 | } 131 | deriving (Generic, Show) 132 | 133 | instance Functor ALBResponse where 134 | fmap f v = v {albResponseBody = f (albResponseBody v)} 135 | 136 | instance ToJSON body => ToJSON (ALBResponse body) where 137 | toJSON = albResponseToJSON toJSON 138 | 139 | albResponseToJSON :: (body -> Value) -> ALBResponse body -> Value 140 | albResponseToJSON bodyTransformer ALBResponse {..} = 141 | object 142 | [ "statusCode" .= albResponseStatusCode, 143 | "body" .= bodyTransformer albResponseBody, 144 | "headers" .= object (map headerToPair albResponseHeaders), 145 | "multiValueHeaders" .= object (map headerToPair albResponseHeaders), 146 | "isBase64Encoded" .= albResponseIsBase64Encoded 147 | ] 148 | 149 | mkALBResponse :: Int -> ResponseHeaders -> payload -> ALBResponse payload 150 | mkALBResponse code headers payload = 151 | ALBResponse code (codeDescription code) headers headers payload False 152 | where 153 | codeDescription 200 = "200 OK" 154 | codeDescription 201 = "201 Created" 155 | codeDescription 202 = "202 Accepted" 156 | codeDescription 203 = "203 Non-Authoritative Information" 157 | codeDescription 204 = "204 No Content" 158 | codeDescription 400 = "400 Bad Request" 159 | codeDescription 401 = "401 Unauthorized" 160 | codeDescription 402 = "402 Payment Required" 161 | codeDescription 403 = "403 Forbidden" 162 | codeDescription 404 = "404 Not Found" 163 | codeDescription 405 = "405 Method Not Allowed" 164 | codeDescription 406 = "406 Not Acceptable" 165 | codeDescription 500 = "500 Internal Server Error" 166 | codeDescription other = tshow other 167 | 168 | headerToPair :: Header -> T.Pair 169 | headerToPair (cibyte, bstr) = k .= v 170 | where 171 | k = (K.fromText . T.decodeUtf8 . CI.original) cibyte 172 | v = T.decodeUtf8 bstr 173 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/API/Endpoints.hs: -------------------------------------------------------------------------------- 1 | module Aws.Lambda.Runtime.API.Endpoints 2 | ( response, 3 | invocationError, 4 | runtimeInitError, 5 | nextInvocation, 6 | Endpoint (..), 7 | ) 8 | where 9 | 10 | import qualified Aws.Lambda.Runtime.API.Version as Version 11 | import Data.Text (Text) 12 | 13 | newtype Endpoint 14 | = Endpoint Text 15 | deriving (Show) 16 | 17 | -- | Endpoint that provides the ID of the next invocation 18 | nextInvocation :: Text -> Endpoint 19 | nextInvocation lambdaApi = 20 | Endpoint $ 21 | mconcat 22 | [ "http://", 23 | lambdaApi, 24 | "/", 25 | Version.value, 26 | "/runtime/invocation/next" 27 | ] 28 | 29 | -- | Where the response of the Lambda gets published 30 | response :: Text -> Text -> Endpoint 31 | response lambdaApi requestId = 32 | Endpoint $ 33 | mconcat 34 | [ "http://", 35 | lambdaApi, 36 | "/", 37 | Version.value, 38 | "/runtime/invocation/", 39 | requestId, 40 | "/response" 41 | ] 42 | 43 | -- | Invocation (runtime) errors should be published here 44 | invocationError :: Text -> Text -> Endpoint 45 | invocationError lambdaApi requestId = 46 | Endpoint $ 47 | mconcat 48 | [ "http://", 49 | lambdaApi, 50 | "/", 51 | Version.value, 52 | "/runtime/invocation/", 53 | requestId, 54 | "/error" 55 | ] 56 | 57 | -- | Runtime initialization errors should go here 58 | runtimeInitError :: Text -> Endpoint 59 | runtimeInitError lambdaApi = 60 | Endpoint $ 61 | mconcat 62 | [ "http://", 63 | lambdaApi, 64 | "/", 65 | Version.value, 66 | "/runtime/init/error" 67 | ] 68 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/API/Version.hs: -------------------------------------------------------------------------------- 1 | module Aws.Lambda.Runtime.API.Version 2 | ( value, 3 | ) 4 | where 5 | 6 | import Data.Text (Text) 7 | 8 | -- | Version of the AWS Lambda runtime REST API 9 | value :: Text 10 | value = "2018-06-01" -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/APIGateway/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingStrategies #-} 2 | {-# LANGUAGE DuplicateRecordFields #-} 3 | {-# LANGUAGE FlexibleInstances #-} 4 | {-# LANGUAGE GeneralisedNewtypeDeriving #-} 5 | {-# LANGUAGE UndecidableInstances #-} 6 | 7 | module Aws.Lambda.Runtime.APIGateway.Types 8 | ( ApiGatewayRequest (..), 9 | ApiGatewayRequestContext (..), 10 | ApiGatewayRequestContextIdentity (..), 11 | ApiGatewayResponse (..), 12 | ApiGatewayResponseBody (..), 13 | ToApiGatewayResponseBody (..), 14 | ApiGatewayDispatcherOptions (..), 15 | mkApiGatewayResponse, 16 | ) 17 | where 18 | 19 | import Aws.Lambda.Utilities (toJSONText) 20 | import Data.Aeson 21 | ( FromJSON (parseJSON), 22 | KeyValue ((.=)), 23 | Object, 24 | ToJSON (toJSON), 25 | Value (Null, Object, String), 26 | eitherDecodeStrict, 27 | object, 28 | (.:), 29 | (.:?) 30 | ) 31 | import Data.Aeson.Types (Parser) 32 | import qualified Data.Aeson.Key as K 33 | import qualified Data.Aeson.Types as T 34 | import qualified Data.CaseInsensitive as CI 35 | import Data.HashMap.Strict (HashMap) 36 | import Data.Text (Text) 37 | import qualified Data.Text as T 38 | import qualified Data.Text.Encoding as T 39 | import GHC.Generics (Generic) 40 | import Network.HTTP.Types (Header, ResponseHeaders) 41 | 42 | -- | API Gateway specific dispatcher options 43 | newtype ApiGatewayDispatcherOptions = ApiGatewayDispatcherOptions 44 | { -- | Should impure exceptions be propagated through the API Gateway interface 45 | propagateImpureExceptions :: Bool 46 | } 47 | 48 | data ApiGatewayRequest body = ApiGatewayRequest 49 | { apiGatewayRequestResource :: !Text, 50 | apiGatewayRequestPath :: !Text, 51 | apiGatewayRequestHttpMethod :: !Text, 52 | apiGatewayRequestHeaders :: !(Maybe (HashMap Text Text)), 53 | apiGatewayRequestQueryStringParameters :: !(Maybe (HashMap Text Text)), 54 | apiGatewayRequestPathParameters :: !(Maybe (HashMap Text Text)), 55 | apiGatewayRequestStageVariables :: !(Maybe (HashMap Text Text)), 56 | apiGatewayRequestIsBase64Encoded :: !Bool, 57 | apiGatewayRequestRequestContext :: !ApiGatewayRequestContext, 58 | apiGatewayRequestBody :: !(Maybe body) 59 | } 60 | deriving (Show) 61 | 62 | -- We special case String and Text in order 63 | -- to avoid unneeded encoding which will wrap them in quotes and break parsing 64 | instance {-# OVERLAPPING #-} FromJSON (ApiGatewayRequest Text) where 65 | parseJSON = parseApiGatewayRequest (.:) 66 | 67 | instance {-# OVERLAPPING #-} FromJSON (ApiGatewayRequest String) where 68 | parseJSON = parseApiGatewayRequest (.:) 69 | 70 | instance FromJSON body => FromJSON (ApiGatewayRequest body) where 71 | parseJSON = parseApiGatewayRequest parseObjectFromStringField 72 | 73 | -- We need this because API Gateway is going to send us the payload as a JSON string 74 | parseObjectFromStringField :: FromJSON a => Object -> T.Key -> Parser (Maybe a) 75 | parseObjectFromStringField obj fieldName = do 76 | fieldContents <- obj .: fieldName 77 | case fieldContents of 78 | String stringContents -> 79 | case eitherDecodeStrict (T.encodeUtf8 stringContents) of 80 | Right success -> pure success 81 | Left err -> fail err 82 | Null -> pure Nothing 83 | other -> T.typeMismatch "String or Null" other 84 | 85 | parseApiGatewayRequest :: (Object -> T.Key -> Parser (Maybe body)) -> Value -> Parser (ApiGatewayRequest body) 86 | parseApiGatewayRequest bodyParser (Object v) = 87 | ApiGatewayRequest 88 | <$> v .: "resource" 89 | <*> v .: "path" 90 | <*> v .: "httpMethod" 91 | <*> v .: "headers" 92 | <*> v .: "queryStringParameters" 93 | <*> v .: "pathParameters" 94 | <*> v .: "stageVariables" 95 | <*> v .: "isBase64Encoded" 96 | <*> v .: "requestContext" 97 | <*> v `bodyParser` "body" 98 | parseApiGatewayRequest _ _ = fail "Expected ApiGatewayRequest to be an object." 99 | 100 | data ApiGatewayRequestContext = ApiGatewayRequestContext 101 | { apiGatewayRequestContextResourceId :: !Text, 102 | apiGatewayRequestContextResourcePath :: !Text, 103 | apiGatewayRequestContextHttpMethod :: !Text, 104 | apiGatewayRequestContextExtendedRequestId :: !Text, 105 | apiGatewayRequestContextRequestTime :: !Text, 106 | apiGatewayRequestContextPath :: !Text, 107 | apiGatewayRequestContextAccountId :: !Text, 108 | apiGatewayRequestContextProtocol :: !Text, 109 | apiGatewayRequestContextStage :: !Text, 110 | apiGatewayRequestContextDomainPrefix :: !Text, 111 | apiGatewayRequestContextRequestId :: !Text, 112 | apiGatewayRequestContextDomainName :: !Text, 113 | apiGatewayRequestContextApiId :: !Text, 114 | apiGatewayRequestContextIdentity :: !ApiGatewayRequestContextIdentity, 115 | apiGatewayRequestContextAuthorizer :: !(Maybe Value) 116 | } 117 | deriving (Show) 118 | 119 | instance FromJSON ApiGatewayRequestContext where 120 | parseJSON (Object v) = 121 | ApiGatewayRequestContext 122 | <$> v .: "resourceId" 123 | <*> v .: "path" 124 | <*> v .: "httpMethod" 125 | <*> v .: "extendedRequestId" 126 | <*> v .: "requestTime" 127 | <*> v .: "path" 128 | <*> v .: "accountId" 129 | <*> v .: "protocol" 130 | <*> v .: "stage" 131 | <*> v .: "domainPrefix" 132 | <*> v .: "requestId" 133 | <*> v .: "domainName" 134 | <*> v .: "apiId" 135 | <*> v .: "identity" 136 | <*> v .:? "authorizer" 137 | parseJSON _ = fail "Expected ApiGatewayRequestContext to be an object." 138 | 139 | data ApiGatewayRequestContextIdentity = ApiGatewayRequestContextIdentity 140 | { apiGatewayRequestContextIdentityCognitoIdentityPoolId :: !(Maybe Text), 141 | apiGatewayRequestContextIdentityAccountId :: !(Maybe Text), 142 | apiGatewayRequestContextIdentityCognitoIdentityId :: !(Maybe Text), 143 | apiGatewayRequestContextIdentityCaller :: !(Maybe Text), 144 | apiGatewayRequestContextIdentitySourceIp :: !(Maybe Text), 145 | apiGatewayRequestContextIdentityPrincipalOrgId :: !(Maybe Text), 146 | apiGatewayRequestContextIdentityAccesskey :: !(Maybe Text), 147 | apiGatewayRequestContextIdentityCognitoAuthenticationType :: !(Maybe Text), 148 | apiGatewayRequestContextIdentityCognitoAuthenticationProvider :: !(Maybe Value), 149 | apiGatewayRequestContextIdentityUserArn :: !(Maybe Text), 150 | apiGatewayRequestContextIdentityUserAgent :: !(Maybe Text), 151 | apiGatewayRequestContextIdentityUser :: !(Maybe Text) 152 | } 153 | deriving (Show) 154 | 155 | instance FromJSON ApiGatewayRequestContextIdentity where 156 | parseJSON (Object v) = 157 | ApiGatewayRequestContextIdentity 158 | <$> v .: "cognitoIdentityPoolId" 159 | <*> v .: "accountId" 160 | <*> v .: "cognitoIdentityId" 161 | <*> v .: "caller" 162 | <*> v .: "sourceIp" 163 | <*> v .: "principalOrgId" 164 | <*> v .: "accessKey" 165 | <*> v .: "cognitoAuthenticationType" 166 | <*> v .: "cognitoAuthenticationProvider" 167 | <*> v .: "userArn" 168 | <*> v .: "userAgent" 169 | <*> v .: "user" 170 | parseJSON _ = fail "Expected ApiGatewayRequestContextIdentity to be an object." 171 | 172 | newtype ApiGatewayResponseBody 173 | = ApiGatewayResponseBody Text 174 | deriving newtype (ToJSON, FromJSON) 175 | 176 | class ToApiGatewayResponseBody a where 177 | toApiGatewayResponseBody :: a -> ApiGatewayResponseBody 178 | 179 | -- We special case Text and String to avoid unneeded encoding which will wrap them in quotes 180 | instance {-# OVERLAPPING #-} ToApiGatewayResponseBody Text where 181 | toApiGatewayResponseBody = ApiGatewayResponseBody 182 | 183 | instance {-# OVERLAPPING #-} ToApiGatewayResponseBody String where 184 | toApiGatewayResponseBody = ApiGatewayResponseBody . T.pack 185 | 186 | instance ToJSON a => ToApiGatewayResponseBody a where 187 | toApiGatewayResponseBody = ApiGatewayResponseBody . toJSONText 188 | 189 | data ApiGatewayResponse body = ApiGatewayResponse 190 | { apiGatewayResponseStatusCode :: !Int, 191 | apiGatewayResponseHeaders :: !ResponseHeaders, 192 | apiGatewayResponseBody :: !body, 193 | apiGatewayResponseIsBase64Encoded :: !Bool 194 | } 195 | deriving (Generic, Show) 196 | 197 | instance Functor ApiGatewayResponse where 198 | fmap f v = v {apiGatewayResponseBody = f (apiGatewayResponseBody v)} 199 | 200 | instance ToJSON body => ToJSON (ApiGatewayResponse body) where 201 | toJSON = apiGatewayResponseToJSON toJSON 202 | 203 | apiGatewayResponseToJSON :: (body -> Value) -> ApiGatewayResponse body -> Value 204 | apiGatewayResponseToJSON bodyTransformer ApiGatewayResponse {..} = 205 | object 206 | [ "statusCode" .= apiGatewayResponseStatusCode, 207 | "body" .= bodyTransformer apiGatewayResponseBody, 208 | "headers" .= object (map headerToPair apiGatewayResponseHeaders), 209 | "isBase64Encoded" .= apiGatewayResponseIsBase64Encoded 210 | ] 211 | 212 | mkApiGatewayResponse :: Int -> ResponseHeaders -> payload -> ApiGatewayResponse payload 213 | mkApiGatewayResponse code headers payload = 214 | ApiGatewayResponse code headers payload False 215 | 216 | headerToPair :: Header -> T.Pair 217 | headerToPair (cibyte, bstr) = k .= v 218 | where 219 | k = (K.fromText . T.decodeUtf8 . CI.original) cibyte 220 | v = T.decodeUtf8 bstr 221 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/ApiInfo.hs: -------------------------------------------------------------------------------- 1 | module Aws.Lambda.Runtime.ApiInfo 2 | ( Event (..), 3 | fetchEvent, 4 | ) 5 | where 6 | 7 | import qualified Aws.Lambda.Runtime.API.Endpoints as Endpoints 8 | import qualified Aws.Lambda.Runtime.Error as Error 9 | import Control.Exception.Safe 10 | import qualified Control.Monad as Monad 11 | import Data.ByteString.Char8 (ByteString) 12 | import qualified Data.ByteString.Char8 as ByteString 13 | import qualified Data.ByteString.Lazy.Char8 as Lazy 14 | import Data.Text (Text, pack) 15 | import qualified Data.Text as Text 16 | import qualified Network.HTTP.Client as Http 17 | import qualified Network.HTTP.Types.Header as Http 18 | import qualified Text.Read as Read 19 | 20 | -- | Event that is fetched out of the AWS Lambda API 21 | data Event = Event 22 | { deadlineMs :: !Int, 23 | traceId :: !Text, 24 | awsRequestId :: !Text, 25 | invokedFunctionArn :: !Text, 26 | event :: !Lazy.ByteString 27 | } 28 | deriving (Show) 29 | 30 | -- | Performs a GET to the endpoint that provides the next event 31 | fetchEvent :: Http.Manager -> Text -> IO Event 32 | fetchEvent manager lambdaApi = do 33 | response <- fetchApiData manager lambdaApi 34 | let body = Http.responseBody response 35 | headers = Http.responseHeaders response 36 | Monad.foldM reduceEvent (initialEvent body) headers 37 | 38 | fetchApiData :: Http.Manager -> Text -> IO (Http.Response Lazy.ByteString) 39 | fetchApiData manager lambdaApi = do 40 | let Endpoints.Endpoint endpoint = Endpoints.nextInvocation lambdaApi 41 | request <- Http.parseRequest . Text.unpack $ endpoint 42 | keepRetrying $ Http.httpLbs request manager 43 | 44 | reduceEvent :: Event -> (Http.HeaderName, ByteString) -> IO Event 45 | reduceEvent event header = 46 | case header of 47 | ("Lambda-Runtime-Deadline-Ms", value) -> 48 | case Read.readMaybe $ ByteString.unpack value of 49 | Just ms -> pure event {deadlineMs = ms} 50 | Nothing -> throw (Error.Parsing "Could not parse deadlineMs." (pack $ ByteString.unpack value) "deadlineMs") 51 | ("Lambda-Runtime-Trace-Id", value) -> 52 | pure event {traceId = pack $ ByteString.unpack value} 53 | ("Lambda-Runtime-Aws-Request-Id", value) -> 54 | pure event {awsRequestId = pack $ ByteString.unpack value} 55 | ("Lambda-Runtime-Invoked-Function-Arn", value) -> 56 | pure event {invokedFunctionArn = pack $ ByteString.unpack value} 57 | _ -> 58 | pure event 59 | 60 | initialEvent :: Lazy.ByteString -> Event 61 | initialEvent body = 62 | Event 63 | { deadlineMs = 0, 64 | traceId = "", 65 | awsRequestId = "", 66 | invokedFunctionArn = "", 67 | event = body 68 | } 69 | 70 | keepRetrying :: IO (Http.Response Lazy.ByteString) -> IO (Http.Response Lazy.ByteString) 71 | keepRetrying action = do 72 | result <- try action :: IO (Either IOException (Http.Response Lazy.ByteString)) 73 | case result of 74 | Right success -> pure success 75 | _ -> keepRetrying action 76 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/Common.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DerivingStrategies #-} 3 | {-# LANGUAGE FlexibleInstances #-} 4 | {-# LANGUAGE GADTs #-} 5 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 6 | {-# LANGUAGE KindSignatures #-} 7 | {-# LANGUAGE UndecidableInstances #-} 8 | 9 | module Aws.Lambda.Runtime.Common 10 | ( RunCallback, 11 | LambdaResult (..), 12 | LambdaError (..), 13 | LambdaOptions (..), 14 | ApiGatewayDispatcherOptions (..), 15 | HandlerType (..), 16 | HandlerName (..), 17 | RawEventObject, 18 | ) 19 | where 20 | 21 | import Aws.Lambda.Runtime.ALB.Types 22 | import Aws.Lambda.Runtime.APIGateway.Types 23 | ( ApiGatewayDispatcherOptions (..), 24 | ApiGatewayResponse, 25 | ApiGatewayResponseBody, 26 | ) 27 | import Aws.Lambda.Runtime.Context (Context) 28 | import Aws.Lambda.Runtime.StandaloneLambda.Types 29 | ( StandaloneLambdaResponseBody, 30 | ) 31 | import qualified Data.ByteString.Lazy as Lazy 32 | import Data.Hashable (Hashable) 33 | import Data.Text (Text) 34 | import GHC.Generics (Generic) 35 | import Data.String (IsString) 36 | 37 | -- | Callback that we pass to the dispatcher function 38 | type RunCallback (handlerType :: HandlerType) context = 39 | LambdaOptions context -> IO (Either (LambdaError handlerType) (LambdaResult handlerType)) 40 | 41 | -- | A handler name used to configure the lambda in AWS 42 | newtype HandlerName = HandlerName {unHandlerName :: Text} 43 | deriving newtype (Eq, Show, Read, Ord, Hashable, IsString) 44 | 45 | -- | The type of the handler depending on how you proxy the Lambda 46 | data HandlerType 47 | = StandaloneHandlerType 48 | | APIGatewayHandlerType 49 | | ALBHandlerType 50 | 51 | -- | Wrapper type for lambda execution results 52 | data LambdaError (handlerType :: HandlerType) where 53 | StandaloneLambdaError :: StandaloneLambdaResponseBody -> LambdaError 'StandaloneHandlerType 54 | APIGatewayLambdaError :: ApiGatewayResponse ApiGatewayResponseBody -> LambdaError 'APIGatewayHandlerType 55 | ALBLambdaError :: ALBResponse ALBResponseBody -> LambdaError 'ALBHandlerType 56 | 57 | -- | Wrapper type to handle the result of the user 58 | data LambdaResult (handlerType :: HandlerType) where 59 | StandaloneLambdaResult :: StandaloneLambdaResponseBody -> LambdaResult 'StandaloneHandlerType 60 | APIGatewayResult :: ApiGatewayResponse ApiGatewayResponseBody -> LambdaResult 'APIGatewayHandlerType 61 | ALBResult :: ALBResponse ALBResponseBody -> LambdaResult 'ALBHandlerType 62 | 63 | -- | The event received by the lambda before any processing 64 | type RawEventObject = Lazy.ByteString 65 | 66 | -- | Options that the generated main expects 67 | data LambdaOptions context = LambdaOptions 68 | { eventObject :: !RawEventObject, 69 | functionHandler :: !HandlerName, 70 | executionUuid :: !Text, 71 | contextObject :: !(Context context) 72 | } 73 | deriving (Generic) 74 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/Configuration.hs: -------------------------------------------------------------------------------- 1 | module Aws.Lambda.Runtime.Configuration 2 | ( DispatcherOptions (..), 3 | defaultDispatcherOptions, 4 | ) 5 | where 6 | 7 | import Aws.Lambda.Runtime.APIGateway.Types (ApiGatewayDispatcherOptions (..)) 8 | 9 | -- | Options that the dispatcher generator expects 10 | newtype DispatcherOptions = DispatcherOptions 11 | { apiGatewayDispatcherOptions :: ApiGatewayDispatcherOptions 12 | } 13 | 14 | defaultDispatcherOptions :: DispatcherOptions 15 | defaultDispatcherOptions = 16 | DispatcherOptions (ApiGatewayDispatcherOptions True) 17 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/Context.hs: -------------------------------------------------------------------------------- 1 | module Aws.Lambda.Runtime.Context 2 | ( Context (..), 3 | initialize, 4 | setEventData, 5 | ) 6 | where 7 | 8 | import qualified Aws.Lambda.Runtime.ApiInfo as ApiInfo 9 | import qualified Aws.Lambda.Runtime.Environment as Environment 10 | import Data.IORef (IORef) 11 | import Data.Text (Text) 12 | 13 | -- | Context that is passed to all the handlers 14 | data Context context = Context 15 | { memoryLimitInMb :: !Int, 16 | functionName :: !Text, 17 | functionVersion :: !Text, 18 | invokedFunctionArn :: !Text, 19 | awsRequestId :: !Text, 20 | xrayTraceId :: !Text, 21 | logStreamName :: !Text, 22 | logGroupName :: !Text, 23 | deadline :: !Int, 24 | customContext :: !(IORef context) 25 | } 26 | 27 | -- | Initializes the context out of the environment 28 | initialize :: 29 | IORef context -> 30 | IO (Context context) 31 | initialize customContextRef = do 32 | functionName <- Environment.functionName 33 | version <- Environment.functionVersion 34 | logStream <- Environment.logStreamName 35 | logGroup <- Environment.logGroupName 36 | memoryLimitInMb <- Environment.functionMemory 37 | 38 | pure $ 39 | Context 40 | { functionName = functionName, 41 | functionVersion = version, 42 | logStreamName = logStream, 43 | logGroupName = logGroup, 44 | memoryLimitInMb = memoryLimitInMb, 45 | customContext = customContextRef, 46 | -- We set those to "empty" values because they will be assigned 47 | -- from the incoming event once one has been received. (see setEventData) 48 | invokedFunctionArn = mempty, 49 | xrayTraceId = mempty, 50 | awsRequestId = mempty, 51 | deadline = 0 52 | } 53 | 54 | -- | Sets the context's event data 55 | setEventData :: 56 | Context context -> 57 | ApiInfo.Event -> 58 | IO (Context context) 59 | setEventData context ApiInfo.Event {..} = do 60 | Environment.setXRayTrace traceId 61 | 62 | return $ 63 | context 64 | { invokedFunctionArn = invokedFunctionArn, 65 | xrayTraceId = traceId, 66 | awsRequestId = awsRequestId, 67 | deadline = deadlineMs 68 | } -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/Environment.hs: -------------------------------------------------------------------------------- 1 | -- | Provides all the values out of 2 | -- the environment variables of the system 3 | module Aws.Lambda.Runtime.Environment 4 | ( functionMemory, 5 | apiEndpoint, 6 | handlerName, 7 | taskRoot, 8 | functionName, 9 | functionVersion, 10 | logStreamName, 11 | logGroupName, 12 | setXRayTrace, 13 | ) 14 | where 15 | 16 | import qualified Aws.Lambda.Runtime.Error as Error 17 | import Control.Exception.Safe (throw) 18 | import Data.Text (Text, pack, unpack) 19 | import qualified System.Environment as Environment 20 | import qualified Text.Read as Read 21 | 22 | logGroupName :: IO Text 23 | logGroupName = 24 | readEnvironmentVariable "AWS_LAMBDA_LOG_GROUP_NAME" 25 | 26 | logStreamName :: IO Text 27 | logStreamName = 28 | readEnvironmentVariable "AWS_LAMBDA_LOG_STREAM_NAME" 29 | 30 | functionVersion :: IO Text 31 | functionVersion = 32 | readEnvironmentVariable "AWS_LAMBDA_FUNCTION_VERSION" 33 | 34 | functionName :: IO Text 35 | functionName = 36 | readEnvironmentVariable "AWS_LAMBDA_FUNCTION_NAME" 37 | 38 | setXRayTrace :: Text -> IO () 39 | setXRayTrace = Environment.setEnv "_X_AMZN_TRACE_ID" . unpack 40 | 41 | taskRoot :: IO Text 42 | taskRoot = 43 | readEnvironmentVariable "LAMBDA_TASK_ROOT" 44 | 45 | handlerName :: IO Text 46 | handlerName = 47 | readEnvironmentVariable "_HANDLER" 48 | 49 | apiEndpoint :: IO Text 50 | apiEndpoint = 51 | readEnvironmentVariable "AWS_LAMBDA_RUNTIME_API" 52 | 53 | functionMemory :: IO Int 54 | functionMemory = do 55 | let envVar = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE" 56 | memoryValue <- readEnvironmentVariable envVar 57 | case Read.readMaybe (unpack memoryValue) of 58 | Just value -> pure value 59 | Nothing -> throw (Error.Parsing envVar memoryValue envVar) 60 | 61 | readEnvironmentVariable :: Text -> IO Text 62 | readEnvironmentVariable envVar = do 63 | v <- Environment.lookupEnv (unpack envVar) 64 | case v of 65 | Just value -> pure . pack $ value 66 | Nothing -> throw (Error.EnvironmentVariableNotSet envVar) 67 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/Error.hs: -------------------------------------------------------------------------------- 1 | -- | All the errors that the runtime can throw 2 | module Aws.Lambda.Runtime.Error 3 | ( EnvironmentVariableNotSet (..), 4 | Parsing (..), 5 | Invocation (..), 6 | ) 7 | where 8 | 9 | import Control.Exception.Safe 10 | import Data.Aeson (ToJSON (..), object, (.=)) 11 | import qualified Data.ByteString.Lazy as LBS 12 | import Data.Text (Text) 13 | 14 | newtype EnvironmentVariableNotSet 15 | = EnvironmentVariableNotSet Text 16 | deriving (Show, Exception) 17 | 18 | instance ToJSON EnvironmentVariableNotSet where 19 | toJSON (EnvironmentVariableNotSet msg) = 20 | object 21 | [ "errorType" .= ("EnvironmentVariableNotSet" :: Text), 22 | "errorMessage" .= msg 23 | ] 24 | 25 | data Parsing = Parsing 26 | { errorMessage :: Text, 27 | actualValue :: Text, 28 | valueName :: Text 29 | } 30 | deriving (Show, Exception) 31 | 32 | instance ToJSON Parsing where 33 | toJSON (Parsing errorMessage _ valueName) = 34 | object 35 | [ "errorType" .= ("Parsing" :: Text), 36 | "errorMessage" .= ("Could not parse '" <> valueName <> "': " <> errorMessage) 37 | ] 38 | 39 | newtype Invocation 40 | = Invocation LBS.ByteString 41 | deriving (Show, Exception) 42 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/Publish.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GADTs #-} 2 | 3 | -- | Publishing of results/errors back to the 4 | -- AWS Lambda runtime API 5 | module Aws.Lambda.Runtime.Publish 6 | ( result, 7 | invocationError, 8 | parsingError, 9 | runtimeInitError, 10 | ) 11 | where 12 | 13 | import qualified Aws.Lambda.Runtime.API.Endpoints as Endpoints 14 | import Aws.Lambda.Runtime.Common 15 | import Aws.Lambda.Runtime.Context (Context (..)) 16 | import qualified Aws.Lambda.Runtime.Error as Error 17 | import Aws.Lambda.Runtime.StandaloneLambda.Types 18 | import Control.Monad (void) 19 | import Data.Aeson 20 | import qualified Data.ByteString.Lazy as LBS 21 | import Data.Text (Text, unpack) 22 | import qualified Data.Text.Encoding as T 23 | import qualified Network.HTTP.Client as Http 24 | 25 | -- | Publishes the result back to AWS Lambda 26 | result :: LambdaResult handlerType -> Text -> Context context -> Http.Manager -> IO () 27 | result lambdaResult lambdaApi context manager = do 28 | let Endpoints.Endpoint endpoint = Endpoints.response lambdaApi (awsRequestId context) 29 | rawRequest <- Http.parseRequest . unpack $ endpoint 30 | 31 | let requestBody = case lambdaResult of 32 | (StandaloneLambdaResult (StandaloneLambdaResponseBodyPlain res)) -> 33 | Http.RequestBodyBS (T.encodeUtf8 res) 34 | (StandaloneLambdaResult (StandaloneLambdaResponseBodyJson res)) -> 35 | Http.RequestBodyLBS res 36 | (APIGatewayResult res) -> Http.RequestBodyLBS (encode res) 37 | (ALBResult res) -> Http.RequestBodyLBS (encode res) 38 | request = 39 | rawRequest 40 | { Http.method = "POST", 41 | Http.requestBody = requestBody 42 | } 43 | 44 | void $ Http.httpNoBody request manager 45 | 46 | -- | Publishes an invocation error back to AWS Lambda 47 | invocationError :: Error.Invocation -> Text -> Context context -> Http.Manager -> IO () 48 | invocationError (Error.Invocation err) lambdaApi context = 49 | publish err (Endpoints.invocationError lambdaApi $ awsRequestId context) context 50 | 51 | -- | Publishes a parsing error back to AWS Lambda 52 | parsingError :: Error.Parsing -> Text -> Context context -> Http.Manager -> IO () 53 | parsingError err lambdaApi context = 54 | publish 55 | (encode err) 56 | (Endpoints.invocationError lambdaApi $ awsRequestId context) 57 | context 58 | 59 | -- | Publishes a runtime initialization error back to AWS Lambda 60 | runtimeInitError :: ToJSON err => err -> Text -> Context context -> Http.Manager -> IO () 61 | runtimeInitError err lambdaApi = 62 | publish (encode err) (Endpoints.runtimeInitError lambdaApi) 63 | 64 | publish :: LBS.ByteString -> Endpoints.Endpoint -> Context context -> Http.Manager -> IO () 65 | publish err (Endpoints.Endpoint endpoint) _context manager = do 66 | rawRequest <- Http.parseRequest . unpack $ endpoint 67 | 68 | let requestBody = Http.RequestBodyLBS err 69 | request = 70 | rawRequest 71 | { Http.method = "POST", 72 | Http.requestBody = requestBody 73 | } 74 | 75 | void $ Http.httpNoBody request manager 76 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Runtime/StandaloneLambda/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingStrategies #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 4 | {-# LANGUAGE UndecidableInstances #-} 5 | 6 | module Aws.Lambda.Runtime.StandaloneLambda.Types 7 | ( StandaloneLambdaResponseBody (..), 8 | ToStandaloneLambdaResponseBody (..), 9 | ) 10 | where 11 | 12 | import Data.Aeson (ToJSON, encode) 13 | import qualified Data.ByteString.Lazy as LBS 14 | import Data.Text (Text) 15 | import qualified Data.Text as Text 16 | 17 | -- | Wrapper type for lambda response body 18 | data StandaloneLambdaResponseBody 19 | = StandaloneLambdaResponseBodyPlain Text 20 | | StandaloneLambdaResponseBodyJson LBS.ByteString 21 | 22 | class ToStandaloneLambdaResponseBody a where 23 | toStandaloneLambdaResponse :: a -> StandaloneLambdaResponseBody 24 | 25 | -- We need to special case String and Text to avoid unneeded encoding 26 | -- which results in extra quotes put around plain text responses 27 | instance {-# OVERLAPPING #-} ToStandaloneLambdaResponseBody String where 28 | toStandaloneLambdaResponse = StandaloneLambdaResponseBodyPlain . Text.pack 29 | 30 | instance {-# OVERLAPPING #-} ToStandaloneLambdaResponseBody Text where 31 | toStandaloneLambdaResponse = StandaloneLambdaResponseBodyPlain 32 | 33 | instance ToJSON a => ToStandaloneLambdaResponseBody a where 34 | toStandaloneLambdaResponse = StandaloneLambdaResponseBodyJson . encode 35 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Setup.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ConstraintKinds #-} 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE DerivingStrategies #-} 4 | {-# LANGUAGE GADTs #-} 5 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 6 | {-# LANGUAGE KindSignatures #-} 7 | {-# LANGUAGE RankNTypes #-} 8 | {-# OPTIONS_GHC -fno-warn-unused-binds #-} 9 | {-# OPTIONS_GHC -fno-warn-unused-imports #-} 10 | 11 | module Aws.Lambda.Setup 12 | ( Handler (..), 13 | HandlerName (..), 14 | Handlers, 15 | run, 16 | addStandaloneLambdaHandler, 17 | addAPIGatewayHandler, 18 | addALBHandler, 19 | runLambdaHaskellRuntime, 20 | ) 21 | where 22 | 23 | import Aws.Lambda.Runtime (runLambda) 24 | import Aws.Lambda.Runtime.ALB.Types 25 | ( ALBRequest, 26 | ALBResponse, 27 | ToALBResponseBody (..), 28 | mkALBResponse, 29 | ) 30 | import Aws.Lambda.Runtime.APIGateway.Types 31 | ( ApiGatewayDispatcherOptions (propagateImpureExceptions), 32 | ApiGatewayRequest, 33 | ApiGatewayResponse, 34 | ToApiGatewayResponseBody (..), 35 | mkApiGatewayResponse, 36 | ) 37 | import Aws.Lambda.Runtime.Common 38 | ( HandlerName (..), 39 | HandlerType (..), 40 | LambdaError (..), 41 | LambdaOptions (LambdaOptions), 42 | LambdaResult (..), 43 | RawEventObject, 44 | ) 45 | import Aws.Lambda.Runtime.Configuration 46 | ( DispatcherOptions (apiGatewayDispatcherOptions), 47 | ) 48 | import Aws.Lambda.Runtime.Context (Context) 49 | import Aws.Lambda.Runtime.StandaloneLambda.Types 50 | ( ToStandaloneLambdaResponseBody (..), 51 | ) 52 | import Aws.Lambda.Utilities (decodeObj) 53 | import Control.Exception (SomeException) 54 | import Control.Monad.Catch (MonadCatch (catch), throwM) 55 | import Control.Monad.State as State 56 | ( MonadIO (..), 57 | MonadState, 58 | StateT (..), 59 | modify, 60 | ) 61 | import Data.Aeson (FromJSON) 62 | import qualified Data.HashMap.Strict as HM 63 | import qualified Data.Text as Text 64 | import Data.Typeable (Typeable) 65 | import GHC.IO.Handle.FD (stderr) 66 | import GHC.IO.Handle.Text (hPutStr) 67 | 68 | type Handlers handlerType m context request response error = 69 | HM.HashMap HandlerName (Handler handlerType m context request response error) 70 | 71 | type StandaloneCallback m context request response error = 72 | (request -> Context context -> m (Either error response)) 73 | 74 | type APIGatewayCallback m context request response error = 75 | (ApiGatewayRequest request -> Context context -> m (Either (ApiGatewayResponse error) (ApiGatewayResponse response))) 76 | 77 | type ALBCallback m context request response error = 78 | (ALBRequest request -> Context context -> m (Either (ALBResponse error) (ALBResponse response))) 79 | 80 | data Handler (handlerType :: HandlerType) m context request response error where 81 | StandaloneLambdaHandler :: StandaloneCallback m context request response error -> Handler 'StandaloneHandlerType m context request response error 82 | APIGatewayHandler :: APIGatewayCallback m context request response error -> Handler 'APIGatewayHandlerType m context request response error 83 | ALBHandler :: ALBCallback m context request response error -> Handler 'ALBHandlerType m context request response error 84 | 85 | newtype HandlersM (handlerType :: HandlerType) m context request response error a = HandlersM 86 | {runHandlersM :: StateT (Handlers handlerType m context request response error) IO a} 87 | deriving newtype 88 | ( Functor, 89 | Applicative, 90 | Monad, 91 | MonadState (Handlers handlerType m context request response error) 92 | ) 93 | 94 | type RuntimeContext (handlerType :: HandlerType) m context request response error = 95 | ( MonadIO m, 96 | MonadCatch m, 97 | ToStandaloneLambdaResponseBody error, 98 | ToStandaloneLambdaResponseBody response, 99 | ToApiGatewayResponseBody error, 100 | ToApiGatewayResponseBody response, 101 | ToALBResponseBody error, 102 | ToALBResponseBody response, 103 | FromJSON (ApiGatewayRequest request), 104 | FromJSON (ALBRequest request), 105 | FromJSON request, 106 | Typeable request 107 | ) 108 | 109 | runLambdaHaskellRuntime :: 110 | RuntimeContext handlerType m context request response error => 111 | DispatcherOptions -> 112 | IO context -> 113 | (forall a. m a -> IO a) -> 114 | HandlersM handlerType m context request response error () -> 115 | IO () 116 | runLambdaHaskellRuntime options initializeContext mToIO initHandlers = do 117 | handlers <- fmap snd . flip runStateT HM.empty . runHandlersM $ initHandlers 118 | runLambda initializeContext (run options mToIO handlers) 119 | 120 | run :: 121 | RuntimeContext handlerType m context request response error => 122 | DispatcherOptions -> 123 | (forall a. m a -> IO a) -> 124 | Handlers handlerType m context request response error -> 125 | LambdaOptions context -> 126 | IO (Either (LambdaError handlerType) (LambdaResult handlerType)) 127 | run dispatcherOptions mToIO handlers (LambdaOptions eventObject functionHandler _executionUuid contextObject) = do 128 | let asIOCallbacks = HM.map (mToIO . handlerToCallback dispatcherOptions eventObject contextObject) handlers 129 | case HM.lookup functionHandler asIOCallbacks of 130 | Just handlerToCall -> handlerToCall 131 | Nothing -> 132 | throwM $ 133 | userError $ 134 | "Could not find handler '" <> (Text.unpack . unHandlerName $ functionHandler) <> "'." 135 | 136 | addStandaloneLambdaHandler :: 137 | HandlerName -> 138 | StandaloneCallback m context request response error -> 139 | HandlersM 'StandaloneHandlerType m context request response error () 140 | addStandaloneLambdaHandler handlerName handler = 141 | State.modify (HM.insert handlerName (StandaloneLambdaHandler handler)) 142 | 143 | addAPIGatewayHandler :: 144 | HandlerName -> 145 | APIGatewayCallback m context request response error -> 146 | HandlersM 'APIGatewayHandlerType m context request response error () 147 | addAPIGatewayHandler handlerName handler = 148 | State.modify (HM.insert handlerName (APIGatewayHandler handler)) 149 | 150 | addALBHandler :: 151 | HandlerName -> 152 | ALBCallback m context request response error -> 153 | HandlersM 'ALBHandlerType m context request response error () 154 | addALBHandler handlerName handler = 155 | State.modify (HM.insert handlerName (ALBHandler handler)) 156 | 157 | handlerToCallback :: 158 | forall handlerType m context request response error. 159 | RuntimeContext handlerType m context request response error => 160 | DispatcherOptions -> 161 | RawEventObject -> 162 | Context context -> 163 | Handler handlerType m context request response error -> 164 | m (Either (LambdaError handlerType) (LambdaResult handlerType)) 165 | handlerToCallback dispatcherOptions rawEventObject context handlerToCall = 166 | call `catch` handleError 167 | where 168 | call = 169 | case handlerToCall of 170 | StandaloneLambdaHandler handler -> 171 | case decodeObj @request rawEventObject of 172 | Right request -> 173 | either 174 | (Left . StandaloneLambdaError . toStandaloneLambdaResponse) 175 | (Right . StandaloneLambdaResult . toStandaloneLambdaResponse) 176 | <$> handler request context 177 | Left err -> return . Left . StandaloneLambdaError . toStandaloneLambdaResponse $ err 178 | APIGatewayHandler handler -> do 179 | case decodeObj @(ApiGatewayRequest request) rawEventObject of 180 | Right request -> 181 | either 182 | (Left . APIGatewayLambdaError . fmap toApiGatewayResponseBody) 183 | (Right . APIGatewayResult . fmap toApiGatewayResponseBody) 184 | <$> handler request context 185 | Left err -> apiGatewayErr 400 . toApiGatewayResponseBody . Text.pack . show $ err 186 | ALBHandler handler -> 187 | case decodeObj @(ALBRequest request) rawEventObject of 188 | Right request -> 189 | either 190 | (Left . ALBLambdaError . fmap toALBResponseBody) 191 | (Right . ALBResult . fmap toALBResponseBody) 192 | <$> handler request context 193 | Left err -> albErr 400 . toALBResponseBody . Text.pack . show $ err 194 | 195 | handleError (exception :: SomeException) = do 196 | liftIO $ hPutStr stderr . show $ exception 197 | case handlerToCall of 198 | StandaloneLambdaHandler _ -> 199 | return . Left . StandaloneLambdaError . toStandaloneLambdaResponse . Text.pack . show $ exception 200 | ALBHandler _ -> 201 | albErr 500 . toALBResponseBody . Text.pack . show $ exception 202 | APIGatewayHandler _ -> 203 | if propagateImpureExceptions . apiGatewayDispatcherOptions $ dispatcherOptions 204 | then apiGatewayErr 500 . toApiGatewayResponseBody . Text.pack . show $ exception 205 | else apiGatewayErr 500 . toApiGatewayResponseBody . Text.pack $ "Something went wrong." 206 | 207 | apiGatewayErr statusCode = 208 | pure . Left . APIGatewayLambdaError . mkApiGatewayResponse statusCode [] 209 | 210 | albErr statusCode = 211 | pure . Left . ALBLambdaError . mkALBResponse statusCode [] 212 | -------------------------------------------------------------------------------- /src/Aws/Lambda/Utilities.hs: -------------------------------------------------------------------------------- 1 | module Aws.Lambda.Utilities 2 | ( toJSONText, 3 | tshow, 4 | decodeObj, 5 | ) 6 | where 7 | 8 | import qualified Aws.Lambda.Runtime.Error as Error 9 | import Data.Aeson (FromJSON, ToJSON, eitherDecode, encode) 10 | import qualified Data.ByteString.Lazy.Char8 as LazyByteString 11 | import Data.Text (Text, pack) 12 | import qualified Data.Text as T 13 | import qualified Data.Text.Encoding as T 14 | import Data.Typeable (Proxy (..), Typeable, typeRep) 15 | 16 | toJSONText :: ToJSON a => a -> Text 17 | toJSONText = T.decodeUtf8 . LazyByteString.toStrict . encode 18 | 19 | tshow :: Show a => a -> Text 20 | tshow = T.pack . show 21 | 22 | -- | Helper function that the dispatcher will use to 23 | -- decode the JSON that comes as an AWS Lambda event into the 24 | -- appropriate type expected by the handler. 25 | decodeObj :: forall a. (FromJSON a, Typeable a) => LazyByteString.ByteString -> Either Error.Parsing a 26 | decodeObj x = 27 | let objName = pack . show $ typeRep (Proxy :: Proxy a) 28 | in case eitherDecode x of 29 | Left e -> Left $ Error.Parsing (pack e) (pack . LazyByteString.unpack $ x) objName 30 | Right v -> return v -------------------------------------------------------------------------------- /stack-template.hsfiles: -------------------------------------------------------------------------------- 1 | {-# START_FILE package.yaml #-} 2 | name: {{name}} 3 | version: 0.1.0 4 | github: "{{github-username}}{{^github-username}}githubuser{{/github-username}}/{{name}}" 5 | license: BSD3 6 | author: "{{author-name}}{{^author-name}}Author name here{{/author-name}}" 7 | maintainer: "{{author-email}}{{^author-email}}example@example.com{{/author-email}}" 8 | copyright: "{{copyright}}{{^copyright}}{{year}}{{^year}}2018{{/year}} {{author-name}}{{^author-name}}Author name here{{/author-name}}{{/copyright}}" 9 | 10 | description: Please see the README on GitHub at 11 | 12 | dependencies: 13 | - base >= 4.7 && < 5 14 | - aws-lambda-haskell-runtime >= 3.0.0 15 | - aeson 16 | 17 | library: 18 | source-dirs: src 19 | 20 | executables: 21 | bootstrap: 22 | main: Main.hs 23 | source-dirs: app 24 | ghc-options: 25 | - -threaded 26 | - -rtsopts 27 | - -O2 28 | - -with-rtsopts=-N 29 | dependencies: 30 | - {{name}} 31 | 32 | default-extensions: 33 | - RecordWildCards 34 | - OverloadedLists 35 | - OverloadedStrings 36 | - DeriveGeneric 37 | 38 | {-# START_FILE Setup.hs #-} 39 | import Distribution.Simple 40 | main = defaultMain 41 | 42 | {-# START_FILE Makefile #-} 43 | all: 44 | # You can then tag the image and upload it to a registry of your choice 45 | @docker build . -t my-haskell-lambda-image 46 | 47 | {-# START_FILE src/Lib.hs #-} 48 | module Lib where 49 | 50 | import GHC.Generics 51 | import Data.Aeson 52 | import Aws.Lambda 53 | 54 | data Person = Person 55 | { personName :: String 56 | , personAge :: Int 57 | } deriving (Generic) 58 | 59 | instance FromJSON Person 60 | instance ToJSON Person 61 | 62 | handler :: Person -> Context () -> IO (Either String Person) 63 | handler person context = 64 | if personAge person > 0 then 65 | return (Right person) 66 | else 67 | return (Left "A person's age must be positive") 68 | 69 | {-# START_FILE Dockerfile #-} 70 | ARG OUTPUT_DIR=/root/output 71 | ARG EXECUTABLE_NAME=bootstrap 72 | 73 | FROM lambci/lambda:build-provided as build 74 | 75 | COPY . . 76 | 77 | SHELL ["/bin/bash", "--rcfile", "~/.profile", "-c"] 78 | 79 | USER root 80 | 81 | RUN yum update -y ca-certificates 82 | 83 | # Installing Haskell Stack 84 | RUN curl -sSL https://get.haskellstack.org/ | sh 85 | 86 | # Build the lambda 87 | COPY . /root/lambda-function/ 88 | 89 | RUN pwd 90 | 91 | RUN cd /root/lambda-function 92 | WORKDIR /root/lambda-function/ 93 | 94 | RUN ls 95 | 96 | RUN stack clean --full 97 | RUN stack build 98 | 99 | ARG OUTPUT_DIR 100 | 101 | RUN mkdir -p ${OUTPUT_DIR} && \ 102 | mkdir -p ${OUTPUT_DIR}/lib 103 | 104 | ARG EXECUTABLE_NAME 105 | 106 | RUN cp $(stack path --local-install-root)/bin/${EXECUTABLE_NAME} ${OUTPUT_DIR}/${EXECUTABLE_NAME} 107 | 108 | ENTRYPOINT sh 109 | 110 | FROM public.ecr.aws/lambda/provided:al2 as deploy 111 | 112 | ARG EXECUTABLE_NAME 113 | 114 | WORKDIR ${LAMBDA_RUNTIME_DIR} 115 | 116 | ARG OUTPUT_DIR 117 | 118 | COPY --from=build ${OUTPUT_DIR} . 119 | 120 | RUN ls 121 | RUN mv ${EXECUTABLE_NAME} bootstrap || true 122 | RUN ls 123 | 124 | CMD [ "handler" ] 125 | 126 | {-# START_FILE stack.yaml #-} 127 | resolver: lts-16.31 128 | 129 | packages: 130 | - . 131 | 132 | extra-deps: 133 | - git: https://github.com/dnikolovv/aws-lambda-haskell-runtime.git 134 | commit: 3a9d26dee8a39fdbe7bd26bc0ec058f904cdf828 135 | 136 | {-# START_FILE app/Main.hs #-} 137 | module Main where 138 | 139 | import Aws.Lambda 140 | import qualified Lib 141 | 142 | main :: IO () 143 | main = 144 | runLambdaHaskellRuntime 145 | defaultDispatcherOptions 146 | (pure ()) 147 | id 148 | (addStandaloneLambdaHandler "handler" Lib.handler) 149 | {-# START_FILE README.md #-} 150 | # {{name}} 151 | 152 | {-# START_FILE LICENSE #-} 153 | Copyright {{author-name}}{{^author-name}}Author name here{{/author-name}} (c) {{year}}{{^year}}2018{{/year}} 154 | 155 | All rights reserved. 156 | 157 | Redistribution and use in source and binary forms, with or without 158 | modification, are permitted provided that the following conditions are met: 159 | 160 | * Redistributions of source code must retain the above copyright 161 | notice, this list of conditions and the following disclaimer. 162 | 163 | * Redistributions in binary form must reproduce the above 164 | copyright notice, this list of conditions and the following 165 | disclaimer in the documentation and/or other materials provided 166 | with the distribution. 167 | 168 | * Neither the name of {{author-name}}{{^author-name}}Author name here{{/author-name}} nor the names of other 169 | contributors may be used to endorse or promote products derived 170 | from this software without specific prior written permission. 171 | 172 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 173 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 174 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 175 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 176 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 177 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 178 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 179 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 180 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 181 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 182 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 183 | 184 | {-# START_FILE .gitignore #-} 185 | .stack-work/ 186 | build/ 187 | *~ 188 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # https://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # 15 | # The location of a snapshot can be provided as a file or url. Stack assumes 16 | # a snapshot provided as a file might change, whereas a url resource does not. 17 | # 18 | # resolver: ./custom-snapshot.yaml 19 | # resolver: https://example.com/snapshots/2018-01-01.yaml 20 | resolver: lts-22.11 21 | save-hackage-creds: false 22 | 23 | # User packages to be built. 24 | # Various formats can be used as shown in the example below. 25 | # 26 | # packages: 27 | # - some-directory 28 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 29 | # - location: 30 | # git: https://github.com/commercialhaskell/stack.git 31 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 32 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a 33 | # subdirs: 34 | # - auto-update 35 | # - wai 36 | packages: 37 | - . 38 | # Dependency packages to be pulled from upstream that are not in the resolver 39 | # using the same syntax as the packages field. 40 | # (e.g., acme-missiles-0.3) 41 | extra-deps: [] 42 | # Override default flag values for local packages and extra-deps 43 | # flags: {} 44 | 45 | # Extra package databases containing global packages 46 | # extra-package-dbs: [] 47 | 48 | # Control whether we use the GHC we find on the path 49 | # system-ghc: true 50 | # 51 | # Require a specific version of stack, using version ranges 52 | # require-stack-version: -any # Default 53 | # require-stack-version: ">=1.9" 54 | # 55 | # Override the architecture used by stack, especially useful on Windows 56 | # arch: i386 57 | # arch: x86_64 58 | # 59 | # Extra directories used by stack for building 60 | # extra-include-dirs: [/path/to/dir] 61 | # extra-lib-dirs: [/path/to/dir] 62 | # 63 | # Allow a newer minor version of GHC than the snapshot specifies 64 | # compiler-check: newer-minor 65 | nix: 66 | packages: 67 | - zlib 68 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: [] 7 | snapshots: 8 | - completed: 9 | sha256: 2fdd7d3e54540062ef75ca0a73ca3a804c527dbf8a4cadafabf340e66ac4af40 10 | size: 712469 11 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/11.yaml 12 | original: lts-22.11 13 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | import Test.Hspec 2 | 3 | main :: IO () 4 | main = hspec $ 5 | describe "Useless test spec" $ do 6 | it "runs" $ do 7 | (1 + 1 :: Int) `shouldBe` (2 :: Int) 8 | --------------------------------------------------------------------------------