├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── beam.mk ├── examples ├── ec2 │ ├── Makefile │ ├── README.md │ ├── cloud │ │ ├── aws.ts │ │ ├── cdk.json │ │ └── package.json │ ├── rebar.config │ ├── rebar.lock │ ├── src │ │ ├── ec2.app.src │ │ └── ec2.erl │ └── test │ │ ├── ec2_SUITE.erl │ │ ├── event.json │ │ └── tests.config └── helloworld │ ├── Makefile │ ├── README.md │ ├── cloud │ ├── aws.ts │ ├── cdk.json │ └── package.json │ ├── rebar.config │ ├── rebar.lock │ ├── src │ ├── helloworld.app.src │ └── helloworld.erl │ └── test │ ├── cover.spec │ ├── event.json │ ├── helloworld_SUITE.erl │ └── tests.config ├── rebar.config ├── rebar.config.script ├── rebar.lock ├── serverless.mk ├── src ├── serverless.app.src ├── serverless.erl ├── serverless_api.erl ├── serverless_app.erl ├── serverless_lambda.erl ├── serverless_logger.erl ├── serverless_logger_std.erl ├── serverless_mock.erl └── serverless_sup.erl └── test ├── serverless_mock_SUITE.erl ├── serverless_test.erl └── tests.config /.gitignore: -------------------------------------------------------------------------------- 1 | *.*~ 2 | *.log 3 | *.aux 4 | *.beam 5 | *.dump 6 | *.tag.gz 7 | *.tgz 8 | *.zip 9 | *.sublime-* 10 | package-lock.json 11 | test.* 12 | tests/ 13 | ebin/ 14 | rebar3 15 | _build/ 16 | node_modules/ 17 | cdk.out/ 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | dist: trusty 3 | 4 | script: 5 | - make 6 | - ./rebar3 coveralls send 7 | 8 | otp_release: 9 | - 20.1 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Dmitry Kolesnikov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP = serverless 2 | ORG = fogfish 3 | URI = 4 | 5 | include beam.mk 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless 2 | 3 | Serverless Erlang runtime for AWS Lambda Service, widen horizon of Erlang applications. 4 | 5 | [![Build Status](https://secure.travis-ci.org/fogfish/serverless.svg?branch=master)](http://travis-ci.org/fogfish/serverless) 6 | [![Coverage Status](https://coveralls.io/repos/github/fogfish/serverless/badge.svg?branch=master)](https://coveralls.io/github/fogfish/serverless?branch=master) 7 | [![Hex.pm](https://img.shields.io/hexpm/v/serverless.svg)](https://hex.pm/packages/serverless) 8 | 9 | 10 | ## Inspiration 11 | 12 | Run code without provisioning or managing servers is a modern way to deliver applications. This library enables Erlang runtime at AWS Lambda service using [AWS Lambda Runtime Interface](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). The Erlang runtime is deployed as [AWS Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html) to your AWS account. 13 | 14 | The library uses [escript](http://erlang.org/doc/man/escript.html) executable to package, debug and deploy lambda functions. 15 | 16 | 17 | ## Key features 18 | 19 | * Implements Erlang runtime for AWS Lambda service. 20 | * Deploys Erlang\OTP as AWS Lambda Layer. 21 | * Defines a lambda's life-cycle workflow using AWS CDK. 22 | * Provides command line tools to orchestrate local development and production deployments. 23 | 24 | 25 | ## Getting started 26 | 27 | The latest version of the library is available at its `master` branch. All development, including new features and bug fixes, take place on the `master` branch using forking and pull requests as described in contribution guidelines. 28 | 29 | The stable library release is available via hex packages, add the library as dependency to rebar.config 30 | 31 | ```erlang 32 | {deps, [ 33 | serverless 34 | ]}. 35 | ``` 36 | 37 | Latest development version is available at GitHub, add the library as dependency to rebar.config 38 | 39 | ```erlang 40 | {deps, [ 41 | {serverless, ".*", 42 | {git, "https://github.com/fogfish/serverless", {branch, master}} 43 | } 44 | ]}. 45 | ``` 46 | 47 | The easiest way to start with Erlang serverless function development is a template provided by [serverless.mk](serverless.mk). Please look on [hello world](examples/helloworld) example as a play ground to get things up and running. 48 | 49 | 50 | ### Workflow 51 | 52 | The library defines a life-cycle workflow to distribute Erlang application from sources to the cloud - [AWS Lambda service](https://aws.amazon.com/lambda/). The workflow builds a distribution package of Erlang application using rebar3 with help of Makefile orchestration. The file [serverless.mk](serverless.mk) implements the workflow: 53 | 54 | 1. [Configure AWS account](#configure-aws-account). 55 | 2. [Create a new function](#create-a-new-function). 56 | 3. [Build function](#build-function) 57 | 4. [Run function locally](#run-function-locally) 58 | 5. [Package function](#package-function) 59 | 6. [Deploy function](#deploy-function) 60 | 7. [Clean up](#clean-up) 61 | 62 | 63 | ### Configure AWS account 64 | 65 | Usage of [AWS Lambda Layers](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html) simplifies an experience of lambda development and deployment. A custom Erlang runtime is provisioned with layers feature. The library uses docker image `fogfish/erlang-serverless` to distribute a runtime compatible with AWS Lambda service, the image is managed by [erlang-in-docker](https://github.com/fogfish/erlang-in-docker). 66 | 67 | You have to deploy a layer to you account before you'll be able to run any function. The following command builds a zip archive with Erlang runtime and deploys it your AWS account. Please notice, this operation has to be executed only once per AWS Account. 68 | 69 | ``` 70 | cd examples/helloworld 71 | 72 | make layer 73 | ``` 74 | 75 | 76 | ### Create a new function 77 | 78 | The easiest way to start with Erlang serverless function development is a template provided by [serverless.mk](serverless.mk). Create a new folder for your function and download the workflow orchestration file. 79 | 80 | ```bash 81 | mkdir myfun && cd myfun 82 | 83 | curl -O -L https://raw.githubusercontent.com/fogfish/serverless/master/serverless.mk 84 | ``` 85 | 86 | then, create a Makefile. Please note, `EVENT` a default mock event used by local execution. 87 | 88 | ```bash 89 | cat > Makefile <> => #{}} = serverless:mock( 142 | helloworld, %% Function entry point 143 | #{} %% Mock input, mock function returns Lambda output 144 | ). 145 | ``` 146 | 147 | 148 | ### Run function locally 149 | 150 | Runs your Amazon Lambda function locally. It passes content of `test/event.json` as Amazon Lambda event object. 151 | 152 | ```bash 153 | make run 154 | ``` 155 | 156 | Use `EVENT` variable to pass other than default event 157 | 158 | ```bash 159 | make run EVENT=test/kinesis.json 160 | ``` 161 | 162 | Often, AWS Lambda Event contains in-line JSON as string (e.g. Gateway API, Kinesis, etc). Maintainability of such content for mock purposes is tedious. You can use template feature to maintain event metadata and payload in different files. 163 | 164 | ```json 165 | // test/event.json 166 | { 167 | "path": "/test/hello", 168 | "headers": { ... }, 169 | "body": $json 170 | } 171 | 172 | // test/payload.json 173 | { 174 | "foo": "bar", 175 | "boo": "baa" 176 | } 177 | ``` 178 | 179 | Then use `JSON` variable to bind content of `$json` variable to a file 180 | 181 | ```bash 182 | make run JSON=test/payload.json 183 | ``` 184 | 185 | ### Package function 186 | 187 | Use **dist** target to assemble an escript executable containing the project's and its dependencies' BEAM files. 188 | 189 | ```bash 190 | make dist 191 | ``` 192 | 193 | As the result, `_build/default/bin/name-of-my-function` executable. 194 | 195 | 196 | ### Deploy function 197 | 198 | The workflow implements a target **dist-up** to orchestrate deployment with help of [AWS CDK](https://aws.amazon.com/cdk/). 199 | 200 | To deploy a function, you need a stack configuration. The serverless library manages environment configurations at `cloud` folder. The stack contains reference to an execution role, lambda layer and other. A minimal configuration is supplied as part of function template. Please refers to official AWS CDK documentation if you need to make advanced config. 201 | 202 | After the stack configuration is completed, deploy it 203 | 204 | ```bash 205 | make dist-up 206 | ``` 207 | 208 | **Congratulations, your function is production ready!** 209 | 210 | 211 | ### Clean up 212 | 213 | A few targets are supported: 214 | 215 | * **dist-rm** remove lambda deployment(s) at specified environment. 216 | * **clean** build artifacts. 217 | * **distclean** clean up everything except source code. 218 | 219 | 220 | ## How To Contribute 221 | 222 | The library is [MIT](https://en.wikipedia.org/wiki/MIT_License) licensed and accepts contributions via GitHub pull requests: 223 | 224 | 1. Fork it 225 | 2. Create your feature branch (`git checkout -b my-new-feature`) 226 | 3. Commit your changes (`git commit -am 'Added some feature'`) 227 | 4. Push to the branch (`git push origin my-new-feature`) 228 | 5. Create new Pull Request 229 | 230 | The development requires [Erlang/OTP](http://www.erlang.org/downloads) version 20.0 or later and essential build tools. 231 | 232 | ### commit message 233 | 234 | The commit message helps us to write a good release note, speed-up review process. The message should address two question what changed and why. The project follows the template defined by chapter [Contributing to a Project](http://git-scm.com/book/ch5-2.html) of Git book. 235 | 236 | > 237 | > Short (50 chars or less) summary of changes 238 | > 239 | > More detailed explanatory text, if necessary. Wrap it to about 72 characters or so. In some contexts, the first line is treated as the subject of an email and the rest of the text as the body. The blank line separating the summary from the body is critical (unless you omit the body entirely); tools like rebase can get confused if you run the two together. 240 | > 241 | > Further paragraphs come after blank lines. 242 | > 243 | > Bullet points are okay, too 244 | > 245 | > Typically a hyphen or asterisk is used for the bullet, preceded by a single space, with blank lines in between, but conventions vary here 246 | > 247 | > 248 | 249 | ### bugs 250 | 251 | If you experience any issues with the library, please let us know via [GitHub issues](https://github.com/fogfish/serverless/issue). We appreciate detailed and accurate reports that help us to identity and replicate the issue. 252 | 253 | * **Specify** the configuration of your environment. Include which operating system you use and the versions of runtime environments. 254 | 255 | * **Attach** logs, screenshots and exceptions, in possible. 256 | 257 | * **Reveal** the steps you took to reproduce the problem, include code snippet or links to your project. 258 | 259 | 260 | ## License 261 | 262 | [![See LICENSE](https://img.shields.io/github/license/fogfish/serverless.svg?style=for-the-badge)](LICENSE) 263 | -------------------------------------------------------------------------------- /beam.mk: -------------------------------------------------------------------------------- 1 | ## 2 | ## Copyright (C) 2012 Dmitry Kolesnikov 3 | ## 4 | ## This Makefile may be modified and distributed under the terms 5 | ## of the MIT license. See the LICENSE file for details. 6 | ## https://github.com/fogfish/makefile 7 | ## 8 | ## @doc 9 | ## This makefile is the wrapper of rebar to build and ship erlang software 10 | ## 11 | ## @version 1.0.12 12 | .PHONY: all compile test unit clean distclean run console mock-up mock-rm benchmark release dist 13 | 14 | APP := $(strip $(APP)) 15 | ORG := $(strip $(ORG)) 16 | URI := $(strip $(URI)) 17 | 18 | ## 19 | ## config 20 | PREFIX ?= /usr/local 21 | APP ?= $(notdir $(CURDIR)) 22 | ARCH = $(shell uname -m) 23 | PLAT ?= $(shell uname -s) 24 | VSN ?= $(shell test -z "`git status --porcelain`" && git describe --tags --long | sed -e 's/-g[0-9a-f]*//' | sed -e 's/-0//' || echo "`git describe --abbrev=0 --tags`-dev") 25 | LATEST ?= latest 26 | REL = ${APP}-${VSN} 27 | PKG = ${REL}+${ARCH}.${PLAT} 28 | TEST ?= tests 29 | COOKIE ?= nocookie 30 | DOCKER ?= fogfish/erlang-alpine 31 | IID = ${URI}${ORG}/${APP} 32 | 33 | ## required tools 34 | ## - rebar version (no spaces at end) 35 | ## - path to basho benchmark 36 | REBAR ?= 3.9.1 37 | BB = ../basho_bench 38 | 39 | 40 | ## erlang runtime configration flags 41 | ROOT = $(shell pwd) 42 | ADDR = localhost.localdomain 43 | EFLAGS = \ 44 | -name ${APP}@${ADDR} \ 45 | -setcookie ${COOKIE} \ 46 | -pa ${ROOT}/_build/default/lib/*/ebin \ 47 | -pa ${ROOT}/_build/default/lib/*/priv \ 48 | -pa ${ROOT}/rel \ 49 | -kernel inet_dist_listen_min 32100 \ 50 | -kernel inet_dist_listen_max 32199 \ 51 | +P 1000000 \ 52 | +K true +A 160 -sbt ts 53 | 54 | 55 | ## erlang common test bootstrap 56 | BOOT_CT = \ 57 | -module(test). \ 58 | -export([run/1]). \ 59 | run(Spec) -> \ 60 | {ok, Test} = file:consult(Spec), \ 61 | case lists:keymember(node, 1, Test) of \ 62 | false -> \ 63 | erlang:halt(element(2, ct:run_test([{spec, Spec}]))); \ 64 | true -> \ 65 | ct_master:run(Spec), \ 66 | erlang:halt(0) \ 67 | end. 68 | 69 | 70 | ## 71 | BUILDER = FROM ${DOCKER}\nARG VERSION=\nRUN mkdir ${APP}\nCOPY . ${APP}/\nRUN cd ${APP} && make VSN=\x24{VERSION} && make release VSN=\x24{VERSION}\n 72 | SPAWNER = FROM ${DOCKER}\nENV VERSION=${VSN}\nRUN mkdir ${APP}\nCOPY . ${APP}/\nRUN cd ${APP} && make VSN=\x24{VERSION} && make release VSN=\x24{VERSION}\nCMD sh -c 'cd ${APP} && make console VSN=\x24{VERSION} RELX_REPLACE_OS_VARS=true ERL_NODE=${APP}'\n 73 | 74 | ## self extracting bundle archive 75 | BUNDLE_INIT = PREFIX=${PREFIX}\nREL=${PREFIX}/${REL}\nAPP=${APP}\nVSN=${VSN}\nLINE=`grep -a -n "BUNDLE:$$" $$0`\nmkdir -p $${REL}\ntail -n +$$(( $${LINE%%%%:*} + 1)) $$0 | gzip -vdc - | tar -C $${REL} -xvf - > /dev/null\n 76 | BUNDLE_FREE = exit\nBUNDLE:\n 77 | 78 | 79 | ##################################################################### 80 | ## 81 | ## build 82 | ## 83 | ##################################################################### 84 | all: rebar3 compile test 85 | 86 | compile: rebar3 87 | @./rebar3 compile 88 | 89 | 90 | ## 91 | ## execute common test and terminate node 92 | test: 93 | @./rebar3 ct --config=test/${TEST}.config --cover --verbose 94 | @./rebar3 cover 95 | 96 | # test: _build/test.beam 97 | # @mkdir -p /tmp/test/${APP} 98 | # @erl ${EFLAGS} -noshell -pa _build/ -pa test/ -run test run test/${TEST}.config 99 | # @F=`ls /tmp/test/${APP}/ct_run*/all.coverdata | tail -n 1` ;\ 100 | # cp $$F /tmp/test/${APP}/ct.coverdata 101 | # 102 | # _build/test.beam: _build/test.erl 103 | # @erlc -o _build $< 104 | # 105 | # _build/test.erl: 106 | # @mkdir -p _build && echo "${BOOT_CT}" > $@ 107 | # 108 | 109 | testclean: 110 | @rm -f _build/test.beam 111 | @rm -f _build/test.erl 112 | @rm -f test/*.beam 113 | @rm -rf test.*-temp-data 114 | @rm -rf tests 115 | 116 | ## 117 | ## execute unit test 118 | unit: all 119 | @./rebar3 skip_deps=true eunit 120 | 121 | ## 122 | ## clean 123 | clean: testclean dockerclean 124 | -@./rebar3 clean 125 | @rm -Rf _build/builder 126 | @rm -Rf _build/default/rel 127 | @rm -rf log 128 | @rm -f relx.config 129 | @rm -f *.tar.gz 130 | @rm -f *.bundle 131 | 132 | distclean: clean 133 | -@make mock-rm 134 | -@make dist-rm 135 | -@rm -Rf _build 136 | -@rm rebar3 137 | 138 | ##################################################################### 139 | ## 140 | ## debug 141 | ## 142 | ##################################################################### 143 | run: 144 | @erl ${EFLAGS} 145 | 146 | console: ${PKG}.tar.gz 147 | @_build/default/rel/${APP}/bin/${APP} foreground 148 | 149 | mock-up: test/mock/docker-compose.yml 150 | @docker-compose -f $< up 151 | 152 | mock-rm: test/mock/docker-compose.yml 153 | -@docker-compose -f $< down --rmi all -v --remove-orphans 154 | 155 | dist-up: docker-compose.yml _build/spawner 156 | @docker-compose build 157 | @docker-compose -f $< up 158 | 159 | dist-rm: docker-compose.yml 160 | -@rm -f _build/spawner 161 | -@docker-compose -f $< down --rmi all -v --remove-orphans 162 | 163 | benchmark: 164 | @echo "==> benchmark: ${TEST}" ;\ 165 | $(BB)/basho_bench -N bb@127.0.0.1 -C nocookie priv/${TEST}.benchmark ;\ 166 | $(BB)/priv/summary.r -i tests/current ;\ 167 | open tests/current/summary.png 168 | 169 | ##################################################################### 170 | ## 171 | ## release 172 | ## 173 | ##################################################################### 174 | release: ${PKG}.tar.gz 175 | 176 | ## assemble VM release 177 | ifeq (${PLAT},$(shell uname -s)) 178 | ${PKG}.tar.gz: relx.config 179 | @./rebar3 tar -n ${APP} -v ${VSN} ;\ 180 | mv _build/default/rel/${APP}/${APP}-${VSN}.tar.gz $@ ;\ 181 | echo "==> tarball: $@" 182 | 183 | relx.config: rel/relx.config.src 184 | @cat $< | sed "s/release/release, {'${APP}', \"${VSN}\"}/" > $@ 185 | else 186 | ${PKG}.tar.gz: _build/builder 187 | @docker build --file=$< --force-rm=true --build-arg="VERSION=${VSN}" --tag=build/${APP}:latest . ;\ 188 | I=`docker create build/${APP}:latest` ;\ 189 | docker cp $$I:/${APP}/$@ $@ ;\ 190 | docker rm -f $$I ;\ 191 | docker rmi build/${APP}:latest ;\ 192 | test -f $@ && echo "==> tarball: $@" 193 | 194 | _build/builder: 195 | @mkdir -p _build && echo "${BUILDER}" > $@ 196 | endif 197 | 198 | ## build docker image 199 | docker: Dockerfile 200 | git status --porcelain 201 | test -z "`git status --porcelain`" || exit -1 202 | docker build \ 203 | --build-arg APP=${APP} \ 204 | --build-arg VSN=${VSN} \ 205 | -t ${IID}:${VSN} -f $< . 206 | docker tag ${IID}:${VSN} ${IID}:${LATEST} 207 | 208 | dockerclean: 209 | -@docker rmi -f ${IID}:${LATEST} 210 | -@docker rmi -f ${IID}:${VSN} 211 | 212 | _build/spawner: 213 | @mkdir -p _build && echo "${SPAWNER}" > $@ 214 | 215 | 216 | dist: ${PKG}.tar.gz ${PKG}.bundle 217 | 218 | 219 | ${PKG}.bundle: rel/bootstrap.sh 220 | @printf '${BUNDLE_INIT}' > $@ ;\ 221 | cat $< >> $@ ;\ 222 | printf '${BUNDLE_FREE}' >> $@ ;\ 223 | cat ${PKG}.tar.gz >> $@ ;\ 224 | chmod ugo+x $@ ;\ 225 | echo "==> bundle: $@" 226 | 227 | 228 | ##################################################################### 229 | ## 230 | ## dependencies 231 | ## 232 | ##################################################################### 233 | rebar3: 234 | @echo "==> install rebar (${REBAR})" ;\ 235 | curl -L -O -s https://github.com/erlang/rebar3/releases/download/${REBAR}/rebar3 ;\ 236 | chmod +x $@ 237 | 238 | -------------------------------------------------------------------------------- /examples/ec2/Makefile: -------------------------------------------------------------------------------- 1 | APP = ec2 2 | EVENT ?= test/event.json 3 | 4 | include ../../serverless.mk 5 | -------------------------------------------------------------------------------- /examples/ec2/README.md: -------------------------------------------------------------------------------- 1 | # Erlang AWS Lambda Example 2 | 3 | The bare minimum for an Erlang application running on Amazon Lambda. 4 | 5 | There are a few commands to use on this example. For more info and usage descriptions, see the [serverless](https://github.com/fogfish/serverless) repository. 6 | 7 | ```bash 8 | make 9 | make run 10 | make deploy 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/ec2/cloud/aws.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from '@aws-cdk/core' 2 | import * as iam from '@aws-cdk/aws-iam' 3 | import * as lambda from '@aws-cdk/aws-lambda' 4 | import * as logs from '@aws-cdk/aws-logs' 5 | import { IaaC, iaac, root, join, use } from 'aws-cdk-pure' 6 | 7 | const f = iaac(lambda.Function) 8 | const role = iaac(iam.Role) 9 | 10 | // 11 | // 12 | function Lambda(parent: cdk.Construct): lambda.FunctionProps { 13 | return { 14 | description: 'Erlang AWS Lambda - EC2 API Example', 15 | runtime: lambda.Runtime.PROVIDED, 16 | code: new lambda.AssetCode('../_build/default/bin'), 17 | handler: 'index.handler', 18 | role: Role()(parent), 19 | timeout: cdk.Duration.seconds(10), 20 | memorySize: 256, 21 | logRetention: logs.RetentionDays.FIVE_DAYS, 22 | layers: [ 23 | lambda.LayerVersion.fromLayerVersionArn(parent, 'Layer', 24 | `arn:aws:lambda:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:layer:erlang-serverless:3`) 25 | ] 26 | } 27 | } 28 | 29 | // 30 | // 31 | function Role(): IaaC { 32 | return use({role: role(Principal)}) 33 | .effect(x => x.role.addManagedPolicy(Allow())) 34 | .yield('role') 35 | } 36 | 37 | function Principal(): iam.RoleProps { 38 | return { 39 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com') 40 | } 41 | } 42 | 43 | function Allow(): iam.IManagedPolicy { 44 | return iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonEC2ReadOnlyAccess") 45 | } 46 | 47 | // 48 | // 49 | function ExampleErlEC2(stack: cdk.Construct): cdk.Construct { 50 | join(stack, f(Lambda)) 51 | return stack 52 | } 53 | 54 | const app = new cdk.App() 55 | root(app, ExampleErlEC2) 56 | app.synth() -------------------------------------------------------------------------------- /examples/ec2/cloud/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "ts-node aws", 3 | "requireApproval": "never" 4 | } -------------------------------------------------------------------------------- /examples/ec2/cloud/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iaac-ec2", 3 | "version": "0.0.0", 4 | "description": "...", 5 | "private": true, 6 | "author": { 7 | "name": "fogfish", 8 | "url": "https://github.com/fogfish/serverless" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "@types/node": "*", 13 | "aws-cdk-pure": "*", 14 | "@aws-cdk/core": "*", 15 | "@aws-cdk/aws-iam": "*", 16 | "@aws-cdk/aws-lambda": "*", 17 | "@aws-cdk/aws-logs": "*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/ec2/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, []}. 2 | 3 | {deps, [ 4 | {serverless, {path, "../../"}} 5 | ]}. 6 | 7 | {profiles, [ 8 | {test, [{deps, [meck]}]} 9 | ]}. 10 | 11 | {plugins, [ 12 | rebar3_path_deps 13 | ]}. 14 | 15 | %% 16 | %% 17 | {escript_main_app , ec2}. 18 | {escript_emu_args , "%%! -name ec2 -setcookie ec2 -smp -sbt ts +A10 +K true\n"}. 19 | {escript_incl_apps , [serverless]}. 20 | -------------------------------------------------------------------------------- /examples/ec2/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"base16">>,{pkg,<<"base16">>,<<"1.0.0">>},2}, 3 | {<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.1">>},3}, 4 | {<<"datum">>, 5 | {git,"https://github.com/fogfish/datum", 6 | {ref,"2b1e35d44270ef4c58b8520a9eaa8402f1996857"}}, 7 | 1}, 8 | {<<"eini">>,{pkg,<<"eini">>,<<"1.2.4">>},2}, 9 | {<<"erlcloud">>, 10 | {git,"https://github.com/fogfish/erlcloud", 11 | {ref,"69c00beac4144b9ae699c20d84de87cf6ce87a60"}}, 12 | 1}, 13 | {<<"hackney">>,{pkg,<<"hackney">>,<<"1.15.1">>},2}, 14 | {<<"idna">>,{pkg,<<"idna">>,<<"6.0.0">>},3}, 15 | {<<"jsx">>, 16 | {git,"https://github.com/fogfish/jsx", 17 | {ref,"5dbb9b5f24dc603ebf7c1196e2483ecf0b49079c"}}, 18 | 1}, 19 | {<<"lhttpc">>,{pkg,<<"lhttpc">>,<<"1.5.4">>},2}, 20 | {<<"m_http">>, 21 | {git,"https://github.com/fogfish/m_http", 22 | {ref,"e72afcfb483e85d7344ada63bf17f44338ffda8d"}}, 23 | 1}, 24 | {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},3}, 25 | {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},3}, 26 | {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.0">>},4}, 27 | {<<"pipe">>,{pkg,<<"pipes">>,<<"2.0.1">>},1}, 28 | {<<"serverless">>,{path,"../../",{mtime,<<"2019-09-16T22:17:10Z">>}},0}, 29 | {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.4">>},3}, 30 | {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.4.1">>},4}]}. 31 | [ 32 | {pkg_hash,[ 33 | {<<"base16">>, <<"283644E2B21BD5915ACB7178BED7851FB07C6E5749B8FAD68A53C501092176D9">>}, 34 | {<<"certifi">>, <<"867CE347F7C7D78563450A18A6A28A8090331E77FA02380B4A21962A65D36EE5">>}, 35 | {<<"eini">>, <<"ABD64A0533398A6D714D21219BB85F2D41FDB42665AC4080939B7BFA8E55F386">>}, 36 | {<<"hackney">>, <<"9F8F471C844B8CE395F7B6D8398139E26DDCA9EBC171A8B91342EE15A19963F4">>}, 37 | {<<"idna">>, <<"689C46CBCDF3524C44D5F3DDE8001F364CD7608A99556D8FBD8239A5798D4C10">>}, 38 | {<<"lhttpc">>, <<"405E4F460D67FE8F43F0E6D64E4D168F8A5CFF3AD34D78114688753DAA80FBEB">>}, 39 | {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, 40 | {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>}, 41 | {<<"parse_trans">>, <<"09765507A3C7590A784615CFD421D101AEC25098D50B89D7AA1D66646BC571C1">>}, 42 | {<<"pipe">>, <<"A2B56796C63690ED0E78BB77BB389AF250BD70AFA15A6869369DBDC11087D68F">>}, 43 | {<<"ssl_verify_fun">>, <<"F0EAFFF810D2041E93F915EF59899C923F4568F4585904D010387ED74988E77B">>}, 44 | {<<"unicode_util_compat">>, <<"D869E4C68901DD9531385BB0C8C40444EBF624E60B6962D95952775CAC5E90CD">>}]} 45 | ]. 46 | -------------------------------------------------------------------------------- /examples/ec2/src/ec2.app.src: -------------------------------------------------------------------------------- 1 | {application, ec2, 2 | [ 3 | {description, "serverless ec2"}, 4 | {vsn, "git"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications,[ 8 | kernel 9 | ,stdlib 10 | ]}, 11 | {env, []} 12 | ] 13 | }. 14 | -------------------------------------------------------------------------------- /examples/ec2/src/ec2.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(ec2). 9 | -export([main/1]). 10 | -compile({parse_transform, category}). 11 | 12 | 13 | %% 14 | %% 15 | main(Opts) -> 16 | serverless:spawn(fun ec2/1, Opts). 17 | 18 | %% 19 | %% 20 | ec2(_) -> 21 | [either || 22 | serverless:notice(#{spawn => ec2}), 23 | erlcloud_aws:auto_config(), 24 | erlcloud_ec2:describe_instances(_), 25 | cats:unit(lens:get(ids(), _)) 26 | ]. 27 | 28 | ids() -> 29 | lens:c( 30 | lens:traverse(), 31 | lens:pair(instances_set), 32 | lens:traverse(), 33 | lens:pair(instance_id), 34 | fun(Fun, Focus) -> 35 | lens:fmap(fun(_) -> Focus end, Fun(typecast:s(Focus))) 36 | end 37 | ). 38 | 39 | -------------------------------------------------------------------------------- /examples/ec2/test/ec2_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(ec2_SUITE). 9 | 10 | -export([all/0]). 11 | -export([ec2/1]). 12 | 13 | all() -> 14 | [Test || {Test, NAry} <- ?MODULE:module_info(exports), 15 | Test =/= module_info, 16 | Test =/= init_per_suite, 17 | Test =/= end_per_suite, 18 | NAry =:= 1 19 | ]. 20 | 21 | ec2(_) -> 22 | meck:new(erlcloud_aws, [unstick, passthrough]), 23 | meck:new(erlcloud_ec2, [unstick, passthrough]), 24 | 25 | meck:expect(erlcloud_aws, auto_config, fun() -> {ok, undefined} end), 26 | meck:expect(erlcloud_ec2, describe_instances, fun(_) -> {ok, instances()} end), 27 | 28 | [[<<"1">>, <<"2">>], [<<"3">>, <<"4">>]] = serverless:mock(ec2, #{}), 29 | 30 | meck:unload(erlcloud_ec2), 31 | meck:unload(erlcloud_aws). 32 | 33 | instances() -> 34 | [ 35 | [ 36 | {instances_set, [ 37 | [{instance_id, 1}], 38 | [{instance_id, 2}] 39 | ]} 40 | ], 41 | [ 42 | {instances_set, [ 43 | [{instance_id, 3}], 44 | [{instance_id, 4}] 45 | ]} 46 | ] 47 | ]. -------------------------------------------------------------------------------- /examples/ec2/test/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1": "Key1" 3 | } -------------------------------------------------------------------------------- /examples/ec2/test/tests.config: -------------------------------------------------------------------------------- 1 | %% 2 | %% suites 3 | {suites, ".", all}. 4 | -------------------------------------------------------------------------------- /examples/helloworld/Makefile: -------------------------------------------------------------------------------- 1 | APP = helloworld 2 | EVENT ?= test/event.json 3 | 4 | include ../../serverless.mk 5 | -------------------------------------------------------------------------------- /examples/helloworld/README.md: -------------------------------------------------------------------------------- 1 | # Erlang AWS Lambda Example 2 | 3 | The bare minimum for an Erlang application running on Amazon Lambda. 4 | 5 | There are a few commands to use on this example. For more info and usage descriptions, see the [serverless](https://github.com/fogfish/serverless) repository. 6 | 7 | ```bash 8 | make 9 | make run 10 | make deploy 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/helloworld/cloud/aws.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from '@aws-cdk/core' 2 | import * as lambda from '@aws-cdk/aws-lambda' 3 | import * as logs from '@aws-cdk/aws-logs' 4 | import { iaac, root, join } from 'aws-cdk-pure' 5 | 6 | const f = iaac(lambda.Function) 7 | 8 | // 9 | // 10 | function Lambda(parent: cdk.Construct): lambda.FunctionProps { 11 | return { 12 | description: 'Erlang AWS Lambda - Hello World Example', 13 | runtime: lambda.Runtime.PROVIDED, 14 | code: new lambda.AssetCode('../_build/default/bin'), 15 | handler: 'index.handler', 16 | timeout: cdk.Duration.seconds(10), 17 | memorySize: 256, 18 | logRetention: logs.RetentionDays.FIVE_DAYS, 19 | layers: [ 20 | lambda.LayerVersion.fromLayerVersionArn(parent, 'Layer', 21 | `arn:aws:lambda:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:layer:erlang-serverless:3`) 22 | ] 23 | } 24 | } 25 | 26 | function ExampleErlHW(stack: cdk.Construct): cdk.Construct { 27 | join(stack, f(Lambda)) 28 | return stack 29 | } 30 | 31 | const app = new cdk.App() 32 | root(app, ExampleErlHW) 33 | app.synth() -------------------------------------------------------------------------------- /examples/helloworld/cloud/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "ts-node aws", 3 | "requireApproval": "never" 4 | } -------------------------------------------------------------------------------- /examples/helloworld/cloud/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iaac-ec2", 3 | "version": "0.0.0", 4 | "description": "...", 5 | "private": true, 6 | "author": { 7 | "name": "fogfish", 8 | "url": "https://github.com/fogfish/serverless" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "@types/node": "*", 13 | "aws-cdk-pure": "*", 14 | "@aws-cdk/core": "*", 15 | "@aws-cdk/aws-iam": "*", 16 | "@aws-cdk/aws-lambda": "*", 17 | "@aws-cdk/aws-logs": "*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/helloworld/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, []}. 2 | 3 | {deps, [ 4 | {serverless, {path, "../../"}} 5 | ]}. 6 | 7 | {profiles, [ 8 | {test, [{deps, [meck]}]} 9 | ]}. 10 | 11 | {plugins, [ 12 | rebar3_path_deps 13 | ]}. 14 | 15 | %% 16 | %% Note 17 | %% Enable networking: -name helloworld -setcookie helloworld 18 | {escript_main_app , helloworld}. 19 | {escript_emu_args , "%%! -name helloworld -setcookie helloworld -smp -sbt ts +A10 +K true\n"}. 20 | {escript_incl_apps , [serverless]}. 21 | -------------------------------------------------------------------------------- /examples/helloworld/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"base16">>,{pkg,<<"base16">>,<<"1.0.0">>},2}, 3 | {<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.1">>},3}, 4 | {<<"datum">>, 5 | {git,"https://github.com/fogfish/datum", 6 | {ref,"2b1e35d44270ef4c58b8520a9eaa8402f1996857"}}, 7 | 1}, 8 | {<<"eini">>,{pkg,<<"eini">>,<<"1.2.4">>},2}, 9 | {<<"erlcloud">>, 10 | {git,"https://github.com/fogfish/erlcloud", 11 | {ref,"69c00beac4144b9ae699c20d84de87cf6ce87a60"}}, 12 | 1}, 13 | {<<"hackney">>,{pkg,<<"hackney">>,<<"1.15.1">>},2}, 14 | {<<"idna">>,{pkg,<<"idna">>,<<"6.0.0">>},3}, 15 | {<<"jsx">>, 16 | {git,"https://github.com/fogfish/jsx", 17 | {ref,"5dbb9b5f24dc603ebf7c1196e2483ecf0b49079c"}}, 18 | 1}, 19 | {<<"lhttpc">>,{pkg,<<"lhttpc">>,<<"1.5.4">>},2}, 20 | {<<"m_http">>, 21 | {git,"https://github.com/fogfish/m_http", 22 | {ref,"e72afcfb483e85d7344ada63bf17f44338ffda8d"}}, 23 | 1}, 24 | {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},3}, 25 | {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},3}, 26 | {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.0">>},4}, 27 | {<<"pipe">>,{pkg,<<"pipes">>,<<"2.0.1">>},1}, 28 | {<<"serverless">>,{path,"../../",{mtime,<<"2019-09-16T22:08:42Z">>}},0}, 29 | {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.4">>},3}, 30 | {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.4.1">>},4}]}. 31 | [ 32 | {pkg_hash,[ 33 | {<<"base16">>, <<"283644E2B21BD5915ACB7178BED7851FB07C6E5749B8FAD68A53C501092176D9">>}, 34 | {<<"certifi">>, <<"867CE347F7C7D78563450A18A6A28A8090331E77FA02380B4A21962A65D36EE5">>}, 35 | {<<"eini">>, <<"ABD64A0533398A6D714D21219BB85F2D41FDB42665AC4080939B7BFA8E55F386">>}, 36 | {<<"hackney">>, <<"9F8F471C844B8CE395F7B6D8398139E26DDCA9EBC171A8B91342EE15A19963F4">>}, 37 | {<<"idna">>, <<"689C46CBCDF3524C44D5F3DDE8001F364CD7608A99556D8FBD8239A5798D4C10">>}, 38 | {<<"lhttpc">>, <<"405E4F460D67FE8F43F0E6D64E4D168F8A5CFF3AD34D78114688753DAA80FBEB">>}, 39 | {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, 40 | {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>}, 41 | {<<"parse_trans">>, <<"09765507A3C7590A784615CFD421D101AEC25098D50B89D7AA1D66646BC571C1">>}, 42 | {<<"pipe">>, <<"A2B56796C63690ED0E78BB77BB389AF250BD70AFA15A6869369DBDC11087D68F">>}, 43 | {<<"ssl_verify_fun">>, <<"F0EAFFF810D2041E93F915EF59899C923F4568F4585904D010387ED74988E77B">>}, 44 | {<<"unicode_util_compat">>, <<"D869E4C68901DD9531385BB0C8C40444EBF624E60B6962D95952775CAC5E90CD">>}]} 45 | ]. 46 | -------------------------------------------------------------------------------- /examples/helloworld/src/helloworld.app.src: -------------------------------------------------------------------------------- 1 | {application, helloworld, 2 | [ 3 | {description, "serverless helloworld"}, 4 | {vsn, "git"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications,[ 8 | kernel 9 | ,stdlib 10 | ]}, 11 | {env, []} 12 | ] 13 | }. 14 | -------------------------------------------------------------------------------- /examples/helloworld/src/helloworld.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(helloworld). 9 | -export([main/1]). 10 | 11 | %% 12 | %% 13 | main(Opts) -> 14 | serverless:spawn(fun identity/1, Opts). 15 | 16 | %% 17 | %% 18 | -spec identity(map()) -> datum:either(map()). 19 | 20 | identity(Json) -> 21 | serverless:notice(#{spawn => helloworld}), 22 | serverless:notice(#{input => Json}), 23 | 24 | {ok, #{ 25 | node => typecast:s(erlang:node()), 26 | helloworld => Json 27 | }}. 28 | 29 | -------------------------------------------------------------------------------- /examples/helloworld/test/cover.spec: -------------------------------------------------------------------------------- 1 | {incl_app, helloworld, details}. 2 | -------------------------------------------------------------------------------- /examples/helloworld/test/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1": "Key1" 3 | } -------------------------------------------------------------------------------- /examples/helloworld/test/helloworld_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(helloworld_SUITE). 9 | 10 | -export([all/0]). 11 | -export([hw/1]). 12 | 13 | all() -> 14 | [Test || {Test, NAry} <- ?MODULE:module_info(exports), 15 | Test =/= module_info, 16 | Test =/= init_per_suite, 17 | Test =/= end_per_suite, 18 | NAry =:= 1 19 | ]. 20 | 21 | hw(_) -> 22 | #{ 23 | helloworld := #{}, 24 | node := <<"nonode@nohost">> 25 | } = serverless:mock(helloworld, #{}). 26 | -------------------------------------------------------------------------------- /examples/helloworld/test/tests.config: -------------------------------------------------------------------------------- 1 | %% 2 | %% logs 3 | {logdir, "/tmp/test/helloworld/"}. 4 | 5 | %% 6 | %% suites 7 | {suites, ".", all}. 8 | 9 | %% 10 | %% code coverage 11 | {cover, "cover.spec"}. 12 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, []}. 2 | 3 | {deps, [ 4 | {datum, "4.6.0"} 5 | , {pipe, "2.0.1", {pkg, pipes}} 6 | , {erlcloud, "3.2.7"} 7 | , {m_http, "0.2.2"} 8 | , {jsx, "2.10.0"} 9 | ]}. 10 | 11 | {profiles, [ 12 | {test, [{deps, [meck]}]} 13 | ]}. 14 | 15 | %% 16 | %% 17 | {plugins , [coveralls]}. 18 | {cover_enabled , true}. 19 | {cover_export_enabled , true}. 20 | {coveralls_coverdata , "_build/test/cover/ct.coverdata"}. 21 | {coveralls_service_name , "travis-ci"}. 22 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | case os:getenv("TRAVIS") of 2 | "true" -> 3 | JobId = os:getenv("TRAVIS_JOB_ID"), 4 | lists:keystore(coveralls_service_job_id, 1, CONFIG, {coveralls_service_job_id, JobId}); 5 | _ -> 6 | CONFIG 7 | end. 8 | 9 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"base16">>,{pkg,<<"base16">>,<<"1.0.0">>},1}, 3 | {<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.1">>},2}, 4 | {<<"datum">>,{pkg,<<"datum">>,<<"4.6.0">>},0}, 5 | {<<"eini">>,{pkg,<<"eini">>,<<"1.2.6">>},1}, 6 | {<<"erlcloud">>,{pkg,<<"erlcloud">>,<<"3.2.7">>},0}, 7 | {<<"hackney">>,{pkg,<<"hackney">>,<<"1.15.1">>},1}, 8 | {<<"idna">>,{pkg,<<"idna">>,<<"6.0.0">>},2}, 9 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.10.0">>},0}, 10 | {<<"lhttpc">>,{pkg,<<"lhttpc">>,<<"1.6.2">>},1}, 11 | {<<"m_http">>,{pkg,<<"m_http">>,<<"0.2.2">>},0}, 12 | {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2}, 13 | {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},2}, 14 | {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.0">>},3}, 15 | {<<"pipe">>,{pkg,<<"pipes">>,<<"2.0.1">>},0}, 16 | {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.4">>},2}, 17 | {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.4.1">>},3}]}. 18 | [ 19 | {pkg_hash,[ 20 | {<<"base16">>, <<"283644E2B21BD5915ACB7178BED7851FB07C6E5749B8FAD68A53C501092176D9">>}, 21 | {<<"certifi">>, <<"867CE347F7C7D78563450A18A6A28A8090331E77FA02380B4A21962A65D36EE5">>}, 22 | {<<"datum">>, <<"680B4694E1D13A140CF7759DBCA1BFA8139F602A05B1A1B03B998129BC80C515">>}, 23 | {<<"eini">>, <<"DFFA48476FD89FB6E41CEEA0ADFA1BC6E7862CCD6584417442F8BB37E5D34715">>}, 24 | {<<"erlcloud">>, <<"85A947B7F53C58A884590423437670679B61AF03536D7FB583BEEB2B5054D0F2">>}, 25 | {<<"hackney">>, <<"9F8F471C844B8CE395F7B6D8398139E26DDCA9EBC171A8B91342EE15A19963F4">>}, 26 | {<<"idna">>, <<"689C46CBCDF3524C44D5F3DDE8001F364CD7608A99556D8FBD8239A5798D4C10">>}, 27 | {<<"jsx">>, <<"77760560D6AC2B8C51FD4C980E9E19B784016AA70BE354CE746472C33BEB0B1C">>}, 28 | {<<"lhttpc">>, <<"044F16F0018C7AA7E945E9E9406C7F6035E0B8BC08BF77B00C78CE260E1071E3">>}, 29 | {<<"m_http">>, <<"A869974B4651658786BCE18F2019E9B8DA5E5F8AEFD9A5CCE1291B941F05A2F9">>}, 30 | {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, 31 | {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>}, 32 | {<<"parse_trans">>, <<"09765507A3C7590A784615CFD421D101AEC25098D50B89D7AA1D66646BC571C1">>}, 33 | {<<"pipe">>, <<"A2B56796C63690ED0E78BB77BB389AF250BD70AFA15A6869369DBDC11087D68F">>}, 34 | {<<"ssl_verify_fun">>, <<"F0EAFFF810D2041E93F915EF59899C923F4568F4585904D010387ED74988E77B">>}, 35 | {<<"unicode_util_compat">>, <<"D869E4C68901DD9531385BB0C8C40444EBF624E60B6962D95952775CAC5E90CD">>}]} 36 | ]. 37 | -------------------------------------------------------------------------------- /serverless.mk: -------------------------------------------------------------------------------- 1 | ## 2 | ## Copyright (C) 2018 Dmitry Kolesnikov 3 | ## 4 | ## This Makefile may be modified and distributed under the terms 5 | ## of the MIT license. See the LICENSE file for details. 6 | ## https://github.com/fogfish/serverless 7 | ## 8 | ## @doc 9 | ## This makefile is the wrapper of rebar to build serverless applications 10 | ## 11 | ## @version 0.6.1 12 | .PHONY: all compile test dist distclean dist-up dist-rm 13 | 14 | APP := $(strip $(APP)) 15 | VSN ?= $(shell test -z "`git status --porcelain 2> /dev/null`" && git describe --tags --long 2> /dev/null | sed -e 's/-g[0-9a-f]*//' | sed -e 's/-0//' || echo "`git describe --abbrev=0 --tags 2> /dev/null`-a") 16 | TEST ?= tests 17 | REBAR ?= 3.9.1 18 | DOCKER = fogfish/erlang-serverless:20.3 19 | 20 | ## erlang runtime configration flags 21 | ROOT = $(shell pwd) 22 | ADDR = localhost.localdomain 23 | EFLAGS = \ 24 | -name ${APP}@${ADDR} \ 25 | -setcookie ${COOKIE} \ 26 | -pa ${ROOT}/_build/default/lib/*/ebin \ 27 | -pa ${ROOT}/_build/default/lib/*/priv \ 28 | -pa ${ROOT}/rel \ 29 | -kernel inet_dist_listen_min 32100 \ 30 | -kernel inet_dist_listen_max 32199 \ 31 | +P 1000000 \ 32 | +K true +A 160 -sbt ts 33 | 34 | ##################################################################### 35 | ## 36 | ## build 37 | ## 38 | ##################################################################### 39 | all: rebar3 test 40 | 41 | compile: rebar3 42 | @./rebar3 compile 43 | 44 | deps: rebar3 45 | @./rebar3 get-deps 46 | 47 | run: _build/default/bin/${APP} 48 | @test -z ${JSON} \ 49 | && $^ -f ${EVENT} \ 50 | || { T=`mktemp /tmp/lambda.XXXXXXX` ; trap "{ rm -f $$T; }" EXIT ;\ 51 | jq -n --arg json "`cat ${JSON}`" -f ${EVENT} > $$T | $^ -f $$T; } 52 | 53 | shell: 54 | @erl ${EFLAGS} 55 | 56 | ## 57 | ## execute common test and terminate node 58 | test: 59 | @./rebar3 ct --config test/${TEST}.config --cover --verbose 60 | @./rebar3 cover 61 | 62 | ## 63 | ## clean 64 | clean: 65 | -@./rebar3 clean 66 | -@rm -rf _build/builder 67 | -@rm -rf _build/layer 68 | -@rm -rf _build/*.zip 69 | -@rm -rf log 70 | -@rm -rf _build/default/bin 71 | 72 | ## 73 | ## 74 | dist: _build/default/bin/${APP} _build/default/bin/bootstrap 75 | 76 | _build/default/bin/bootstrap: 77 | @printf "#!/bin/sh\nexport HOME=/opt\n/opt/serverless/bin/escript ${APP}\n" > $@ ;\ 78 | chmod ugo+x $@ 79 | 80 | _build/default/bin/${APP}: src/*.erl src/*.app.src 81 | @./rebar3 escriptize 82 | 83 | ## 84 | ## 85 | distclean: clean 86 | -@rm -Rf _build 87 | -@rm rebar3 88 | -@rm -Rf cloud/node_modules 89 | 90 | 91 | function: 92 | @I=`docker create ${DOCKER}` ;\ 93 | docker cp $$I:/function/cloud . ;\ 94 | docker cp $$I:/function/src . ;\ 95 | docker cp $$I:/function/test . ;\ 96 | docker cp $$I:/function/rebar.config . ;\ 97 | docker rm -f $$I ;\ 98 | sed -i '' -e "s/APP/${APP}/" cloud/* ;\ 99 | sed -i '' -e "s/APP/${APP}/" src/* ;\ 100 | sed -i '' -e "s/APP/${APP}/" test/* ;\ 101 | sed -i '' -e "s/APP/${APP}/g" rebar.config ;\ 102 | mv src/app.app.src src/${APP}.app.src ;\ 103 | mv src/app.erl src/${APP}.erl ;\ 104 | mv test/app_SUITE.erl test/${APP}_SUITE.erl ;\ 105 | mkdir -p test ;\ 106 | echo "{}" > ${EVENT} 107 | 108 | 109 | ##################################################################### 110 | ## 111 | ## deploy 112 | ## 113 | ##################################################################### 114 | 115 | dist-up: dist cloud/node_modules 116 | cd cloud && cdk deploy 117 | 118 | dist-rm: cloud/node_modules 119 | cd cloud && cdk destroy -f 120 | 121 | cloud/node_modules: 122 | cd cloud && npm install 123 | 124 | layer: _build/erlang-serverless.zip 125 | @aws lambda publish-layer-version \ 126 | --layer-name erlang-serverless \ 127 | --description "${DOCKER}" \ 128 | --zip-file fileb://./$^ 129 | 130 | _build/erlang-serverless.zip: 131 | @mkdir -p _build ;\ 132 | echo "FROM ${DOCKER}\nRUN cd /opt && zip erlang-serverless.zip -r * > /dev/null" > _build/layer ;\ 133 | docker build --force-rm=true --tag=build/erlang-serverless:latest - < _build/layer ;\ 134 | I=`docker create build/erlang-serverless:latest` ;\ 135 | docker cp $$I:/opt/erlang-serverless.zip $@ ;\ 136 | docker rm -f $$I ;\ 137 | docker rmi build/erlang-serverless:latest ;\ 138 | test -f $@ && echo "==> $@" 139 | 140 | 141 | ##################################################################### 142 | ## 143 | ## dependencies 144 | ## 145 | ##################################################################### 146 | rebar3: 147 | @echo "==> install rebar (${REBAR})" ;\ 148 | curl -L -O -s https://github.com/erlang/rebar3/releases/download/${REBAR}/rebar3 ;\ 149 | chmod +x $@ 150 | -------------------------------------------------------------------------------- /src/serverless.app.src: -------------------------------------------------------------------------------- 1 | {application, serverless, 2 | [ 3 | {description, "serverless bootstrap for Erlang"}, 4 | {vsn, "git"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications,[ 8 | kernel, 9 | stdlib, 10 | datum, 11 | pipe, 12 | jsx, 13 | m_http, 14 | erlcloud 15 | ]}, 16 | {mod, {serverless_app, []}}, 17 | {env, []}, 18 | 19 | {licenses, ["MIT"]}, 20 | {maintainers, ["Dmitry Kolesnikov"]}, 21 | {links, [ 22 | {"GitHub", "https://github.com/fogfish/serverless"} 23 | ]} 24 | ] 25 | }. -------------------------------------------------------------------------------- /src/serverless.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(serverless). 9 | -compile({parse_transform, category}). 10 | 11 | -export([ 12 | spawn/2, 13 | 14 | %% logger api 15 | emergency/1, 16 | alert/1, 17 | critical/1, 18 | error/1, 19 | warning/1, 20 | notice/1, 21 | info/1, 22 | debug/1, 23 | 24 | %% mock lambda runtime 25 | mock/2, 26 | mock/3 27 | ]). 28 | 29 | %%%------------------------------------------------------------------ 30 | %%% 31 | %%% api 32 | %%% 33 | %%%------------------------------------------------------------------ 34 | spawn(Lambda, ["-f", File]) -> 35 | application:set_env(serverless, crlf, <<$\n>>), 36 | {ok, _} = application:ensure_all_started(serverless, permanent), 37 | application:start(serverless, permanent), 38 | {ok, Event} = file:read_file(File), 39 | {ok, Reply} = Lambda(jsx:decode(Event, [return_maps])), 40 | serverless:notice($-), 41 | serverless:notice(Reply); 42 | 43 | spawn(Lambda, _Opts) -> 44 | {ok, _} = application:ensure_all_started(serverless, permanent), 45 | {ok, _} = serverless_sup:spawn(Lambda), 46 | spawn_loop(). 47 | 48 | spawn_loop() -> 49 | receive _ -> spawn_loop() end. 50 | 51 | %%%------------------------------------------------------------------ 52 | %%% 53 | %%% error logger 54 | %%% 55 | %%%------------------------------------------------------------------ 56 | 57 | %% 58 | %% system us unusable 59 | emergency(Msg) -> 60 | serverless_logger:log(emergency, self(), Msg). 61 | 62 | %% 63 | %% action must be taken immediately 64 | alert(Msg) -> 65 | serverless_logger:log(alert, self(), Msg). 66 | 67 | %% 68 | %% 69 | critical(Msg) -> 70 | serverless_logger:log(critical, self(), Msg). 71 | 72 | %% 73 | %% 74 | error(Msg) -> 75 | serverless_logger:log(error, self(), Msg). 76 | 77 | %% 78 | %% 79 | warning(Msg) -> 80 | serverless_logger:log(warning, self(), Msg). 81 | 82 | %% 83 | %% normal but significant conditions 84 | notice(Msg) -> 85 | serverless_logger:log(notice, self(), Msg). 86 | 87 | %% 88 | %% informational messages 89 | info(Msg) -> 90 | serverless_logger:log(info, self(), Msg). 91 | 92 | %% 93 | %% debug-level messages 94 | debug(Msg) -> 95 | serverless_logger:log(debug, self(), Msg). 96 | 97 | 98 | %%%------------------------------------------------------------------ 99 | %%% 100 | %%% lambda runtime mock api 101 | %%% 102 | %%%------------------------------------------------------------------ 103 | 104 | mock(Lambda, Mock) -> 105 | serverless_mock:test(Lambda, Mock, 5000). 106 | 107 | mock(Lambda, Mock, Timeout) -> 108 | serverless_mock:test(Lambda, Mock, Timeout). 109 | 110 | -------------------------------------------------------------------------------- /src/serverless_api.erl: -------------------------------------------------------------------------------- 1 | %% @doc 2 | %% AWS API Gateway integrations 3 | -module(serverless_api). 4 | 5 | -export([ 6 | return/1 7 | ]). 8 | 9 | %% 10 | %% 11 | return({error, Reason}) -> 12 | {Code, _} = Error = error_code(Reason), 13 | Json = error_json(Error, Reason), 14 | serverless:error(#{ 15 | api => error, 16 | reason => Json, 17 | status => Code 18 | }), 19 | return({Code, Json}); 20 | 21 | return({Code, Json}) when is_map(Json) orelse is_list(Json) -> 22 | return({Code, jsx:encode(Json)}); 23 | 24 | return({Code, Text}) when is_binary(Text) -> 25 | {ok, 26 | #{ 27 | statusCode => return_code(Code), 28 | body => Text, 29 | headers => default_cors() 30 | } 31 | }; 32 | 33 | return({Code, Head, Json}) when is_map(Json) orelse is_list(Json) -> 34 | return({Code, Head, jsx:encode(Json)}); 35 | 36 | return({Code, Head, Text}) when is_binary(Text) -> 37 | {ok, 38 | #{ 39 | statusCode => return_code(Code), 40 | body => Text, 41 | headers => maps:merge(Head, default_cors()) 42 | } 43 | }. 44 | 45 | return_code({Code, _}) -> 46 | Code; 47 | return_code(Code) when is_tuple(Code) -> 48 | erlang:element(1, status_code(erlang:element(1, Code))); 49 | return_code(Code) -> 50 | erlang:element(1, status_code(Code)). 51 | 52 | %% 53 | %% 54 | error_code({Reason, _}) -> 55 | status_code(Reason); 56 | error_code({require, _, ActualCode}) 57 | when is_integer(ActualCode) -> 58 | status_code(ActualCode); 59 | error_code(Code) when is_tuple(Code) -> 60 | status_code(erlang:element(1, Code)); 61 | error_code(Reason) -> 62 | status_code(Reason). 63 | 64 | error_json({Code, Text}, Reason) -> 65 | #{ 66 | type => filename:join([<<"https://httpstatuses.com">>, typecast:s(Code)]), 67 | status => Code, 68 | title => Text, 69 | details => error_reason(Reason) 70 | }. 71 | 72 | error_reason({_, #{} = Reason}) -> 73 | Reason; 74 | error_reason({Reason, Details}) -> 75 | erlang:iolist_to_binary( 76 | io_lib:format("~2048.p : ~2048.p", [Reason, Details]) 77 | ); 78 | error_reason(#{} = Reason) -> 79 | Reason; 80 | error_reason(Reason) -> 81 | erlang:iolist_to_binary( 82 | io_lib:format("~2048.p", [Reason]) 83 | ). 84 | 85 | 86 | %% 87 | %% 88 | default_cors() -> 89 | #{ 90 | <<"Access-Control-Allow-Origin">> => <<"*">>, 91 | <<"Access-Control-Allow-Methods">> => <<"GET, PUT, POST, DELETE, OPTIONS">>, 92 | <<"Access-Control-Allow-Headers">> => <<"Content-Type, Authorization, Accept">>, 93 | <<"Access-Control-Max-Age">> => 600 94 | }. 95 | 96 | %% 97 | %% 98 | status_code(100) -> {100, <<"Continue">>}; 99 | status_code(101) -> {101, <<"Switching Protocols">>}; 100 | status_code(200) -> {200, <<"OK">>}; 101 | status_code(201) -> {201, <<"Created">>}; 102 | status_code(202) -> {202, <<"Accepted">>}; 103 | status_code(203) -> {203, <<"Non-Authoritative Information">>}; 104 | status_code(204) -> {204, <<"No Content">>}; 105 | status_code(205) -> {205, <<"Reset Content">>}; 106 | status_code(206) -> {206, <<"Partial Content">>}; 107 | status_code(300) -> {300, <<"Multiple Choices">>}; 108 | status_code(301) -> {301, <<"Moved Permanently">>}; 109 | status_code(302) -> {302, <<"Found">>}; 110 | status_code(303) -> {303, <<"See Other">>}; 111 | status_code(304) -> {304, <<"Not Modified">>}; 112 | status_code(307) -> {307, <<"Temporary Redirect">>}; 113 | status_code(400) -> {400, <<"Bad Request">>}; 114 | status_code(401) -> {401, <<"Unauthorized">>}; 115 | status_code(402) -> {402, <<"Payment Required">>}; 116 | status_code(403) -> {403, <<"Forbidden">>}; 117 | status_code(404) -> {404, <<"Not Found">>}; 118 | status_code(405) -> {405, <<"Method Not Allowed">>}; 119 | status_code(406) -> {406, <<"Not Acceptable">>}; 120 | status_code(407) -> {407, <<"Proxy Authentication Required">>}; 121 | status_code(408) -> {408, <<"Request Timeout">>}; 122 | status_code(409) -> {409, <<"Conflict">>}; 123 | status_code(410) -> {410, <<"Gone">>}; 124 | status_code(411) -> {411, <<"Length Required">>}; 125 | status_code(412) -> {412, <<"Precondition Failed">>}; 126 | status_code(413) -> {413, <<"Request Entity Too Large">>}; 127 | status_code(414) -> {414, <<"Request-URI Too Long">>}; 128 | status_code(415) -> {415, <<"Unsupported Media Type">>}; 129 | status_code(416) -> {416, <<"Requested Range Not Satisfiable">>}; 130 | status_code(417) -> {417, <<"Expectation Failed">>}; 131 | status_code(422) -> {422, <<"Unprocessable Entity">>}; 132 | status_code(500) -> {500, <<"Internal Server Error">>}; 133 | status_code(501) -> {501, <<"Not Implemented">>}; 134 | status_code(502) -> {502, <<"Bad Gateway">>}; 135 | status_code(503) -> {503, <<"Service Unavailable">>}; 136 | status_code(504) -> {504, <<"Gateway Timeout">>}; 137 | status_code(505) -> {505, <<"HTTP Version Not Supported">>}; 138 | 139 | %status_code(100) -> <<"100 Continue">>; 140 | %status_code(101) -> <<"101 Switching Protocols">>; 141 | status_code(ok) -> status_code(200); 142 | status_code(created) -> status_code(201); 143 | status_code(accepted) -> status_code(202); 144 | %status(203) -> <<"203 Non-Authoritative Information">>; 145 | status_code(no_content) -> status_code(204); 146 | %status(205) -> <<"205 Reset Content">>; 147 | %status(206) -> <<"206 Partial Content">>; 148 | %status(300) -> <<"300 Multiple Choices">>; 149 | %status(301) -> <<"301 Moved Permanently">>; 150 | status_code(redirect) -> status_code(302); 151 | %status(303) -> <<"303 See Other">>; 152 | %status(304) -> <<"304 Not Modified">>; 153 | %status(307) -> <<"307 Temporary Redirect">>; 154 | status_code(badarg) -> status_code(400); 155 | status_code(unauthorized) -> status_code(401); 156 | status_code(expired) -> status_code(401); 157 | %status(402) -> <<"402 Payment Required">>; 158 | status_code(forbidden) -> status_code(403); 159 | status_code(not_found) -> status_code(404); 160 | status_code(enoent) -> status_code(404); 161 | status_code(not_allowed) -> status_code(405); 162 | status_code(not_acceptable) -> status_code(406); 163 | %status(407) -> <<"407 Proxy Authentication Required">>; 164 | %status(408) -> <<"408 Request Timeout">>; 165 | status_code(conflict) -> status_code(409); 166 | status_code(duplicate)-> status_code(409); 167 | %status(410) -> <<"410 Gone">>; 168 | %status(411) -> <<"411 Length Required">>; 169 | %status(412) -> <<"412 Precondition Failed">>; 170 | %status(413) -> <<"413 Request Entity Too Large">>; 171 | %status(414) -> <<"414 Request-URI Too Long">>; 172 | status_code(bad_mime_type) -> status_code(415); 173 | %status(416) -> <<"416 Requested Range Not Satisfiable">>; 174 | %status(417) -> <<"417 Expectation Failed">>; 175 | %status(422) -> <<"422 Unprocessable Entity">>; 176 | status_code(required) -> status_code(500); 177 | status_code(not_implemented) -> status_code(501); 178 | %status(502) -> <<"502 Bad Gateway">>; 179 | status_code(not_available) -> status_code(503); 180 | %status(504) -> <<"504 Gateway Timeout">>; 181 | %status(505) -> <<"505 HTTP Version Not Supported">>. 182 | status_code(Code) when is_atom(Code) -> status_code(500); 183 | status_code(Code) when is_integer(Code) -> status_code(Code). 184 | -------------------------------------------------------------------------------- /src/serverless_app.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(serverless_app). 9 | -behaviour(application). 10 | 11 | -export([start/2, stop/1]). 12 | 13 | %% 14 | %% 15 | start(_Type, _Args) -> 16 | io:setopts([binary]), 17 | serverless_sup:start_link(). 18 | 19 | %% 20 | %% 21 | stop(_State) -> 22 | ok. 23 | -------------------------------------------------------------------------------- /src/serverless_lambda.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(serverless_lambda). 9 | -behaviour(pipe). 10 | -compile({parse_transform, category}). 11 | 12 | -export([ 13 | start_link/1, 14 | init/1, 15 | free/2, 16 | handle/3, 17 | lifecycle/2 18 | ]). 19 | 20 | -define(PROTOCOL, "http://"). 21 | -define(VERSION, "/2018-06-01"). 22 | 23 | %%----------------------------------------------------------------------------- 24 | %% 25 | %% factory 26 | %% 27 | %%----------------------------------------------------------------------------- 28 | start_link(Lambda) -> 29 | pipe:start_link({local, ?MODULE}, ?MODULE, [Lambda], []). 30 | 31 | init([Lambda]) -> 32 | Host = ?PROTOCOL ++ os:getenv("AWS_LAMBDA_RUNTIME_API", "127.0.0.1:8888") ++ ?VERSION, 33 | {ok, handle, 34 | spawn_link( 35 | fun() -> 36 | loop( 37 | lifecycle(Host, Lambda), 38 | #{so => [ 39 | %% socket options (so) are mandatory 40 | %% tune HTTP timeouts so that runtime api would not fail 41 | {checkout_timeout, infinity} 42 | , {connect_timeout, 30000} 43 | , {recv_timeout, infinity} 44 | ]} 45 | ) 46 | end 47 | ) 48 | }. 49 | 50 | free(_, _) -> 51 | ok. 52 | 53 | handle(_, _, State) -> 54 | {next_state, handle, State}. 55 | 56 | 57 | %%----------------------------------------------------------------------------- 58 | %% 59 | %% private 60 | %% 61 | %%----------------------------------------------------------------------------- 62 | 63 | loop(Lambda, State0) -> 64 | try 65 | [_Result | State1] = Lambda(State0), 66 | loop(Lambda, State1) 67 | catch _:Reason -> 68 | serverless_logger:log(emergency, self(), Reason), 69 | serverless_logger:log(emergency, self(), erlang:get_stacktrace()), 70 | exit(Reason) 71 | end. 72 | 73 | %% 74 | lifecycle(Host, Lambda) -> 75 | [m_state || 76 | queue(Host), 77 | cats:unit( exec(Lambda, _) ), 78 | finalise(Host, _) 79 | ]. 80 | 81 | %% 82 | queue(Host) -> 83 | [m_http || 84 | _ > "GET " ++ Host ++ "/runtime/invocation/next", 85 | _ > "Accept: application/json", 86 | _ > "Connection: keep-alive", 87 | 88 | _ < 200, 89 | ReqId < "Lambda-Runtime-Aws-Request-Id: _", 90 | Json < '*', 91 | cats:unit({erlang:binary_to_list(ReqId), Json}) 92 | ]. 93 | 94 | %% 95 | finalise(Host, {ok, RequestId, Json}) -> 96 | [m_http || 97 | _ > "POST " ++ Host ++ "/runtime/invocation/" ++ RequestId ++ "/response", 98 | _ > "Content-Type: application/json", 99 | _ > "Connection: keep-alive", 100 | _ > Json, 101 | 102 | _ < 202 103 | ]; 104 | 105 | finalise(Host, {error, RequestId, #{} = Reason}) -> 106 | [m_http || 107 | _ > "POST " ++ Host ++ "/runtime/invocation/" ++ RequestId ++ "/error", 108 | _ > "Content-Type: application/json", 109 | _ > "Connection: keep-alive", 110 | _ > Reason, 111 | 112 | _ < 202 113 | ]; 114 | 115 | finalise(Host, {error, RequestId, Reason}) -> 116 | [m_http || 117 | _ > "POST " ++ Host ++ "/runtime/invocation/" ++ RequestId ++ "/error", 118 | _ > "Content-Type: text/plain", 119 | _ > "Connection: keep-alive", 120 | _ > erlang:iolist_to_binary(io_lib:format("[~s] ~p", [RequestId, Reason])), 121 | 122 | _ < 202 123 | ]. 124 | 125 | %% 126 | exec(Lambda, {RequestId, Json}) -> 127 | Self = self(), 128 | {Pid, Ref} = erlang:spawn_opt( 129 | fun() -> 130 | case Lambda(Json) of 131 | {ok, Result} -> 132 | Self ! {ok, Result}; 133 | {error, Reason} -> 134 | serverless_logger:log(critical, self(), Reason), 135 | Self ! {error, Reason}; 136 | ok -> 137 | {ok, undefined}; 138 | Any -> 139 | {ok, Any} 140 | end 141 | end, 142 | [monitor] 143 | ), 144 | receive 145 | {ok, Result} -> 146 | {ok, RequestId, Result}; 147 | {error, Reason} -> 148 | {error, RequestId, Reason}; 149 | {'DOWN', Ref, process, Pid, Reason} -> 150 | serverless_logger:log(error, self(), Reason), 151 | {error, RequestId, Reason} 152 | end. 153 | -------------------------------------------------------------------------------- /src/serverless_logger.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(serverless_logger). 9 | -behaviour(pipe). 10 | -compile({parse_transform, category}). 11 | -include_lib("datum/include/datum.hrl"). 12 | 13 | -export([log/3]). 14 | -export([ 15 | start_link/0, 16 | init/1, 17 | free/2, 18 | handle/3 19 | ]). 20 | 21 | %% 22 | -record(state, {crlf}). 23 | 24 | %%----------------------------------------------------------------------------- 25 | %% 26 | %% api 27 | %% 28 | %%----------------------------------------------------------------------------- 29 | log(Type, Pid, Msg) -> 30 | pipe:call(?MODULE, {os:timestamp(), Type, Pid, Msg}, infinity). 31 | 32 | %%----------------------------------------------------------------------------- 33 | %% 34 | %% factory 35 | %% 36 | %%----------------------------------------------------------------------------- 37 | start_link() -> 38 | pipe:start_link({local, ?MODULE}, ?MODULE, [], []). 39 | 40 | init(_) -> 41 | [either || 42 | cats:unit( erlang:process_flag(trap_exit, true) ), 43 | serverless_logger_std:start(), 44 | cats:unit(handle, 45 | #state{ 46 | crlf = application:get_env(serverless, crlf, <<$\r>>) 47 | } 48 | ) 49 | ]. 50 | 51 | free(_, _) -> 52 | serverless_logger_std:stop(). 53 | 54 | %%----------------------------------------------------------------------------- 55 | %% 56 | %% state machine 57 | %% 58 | %%----------------------------------------------------------------------------- 59 | handle({_T, Type, Pid, Msg}, _, #state{crlf = CRLF} = State) -> 60 | io:fwrite(standard_error, message(Type, Pid, Msg, CRLF), []), 61 | {reply, ok, State}; 62 | 63 | handle(resume, _, #state{} = State) -> 64 | {reply, ok, State}; 65 | 66 | handle(suspend, _, #state{} = State) -> 67 | {reply, ok, State}. 68 | 69 | %%----------------------------------------------------------------------------- 70 | %% 71 | %% private 72 | %% 73 | %%----------------------------------------------------------------------------- 74 | 75 | %% 76 | message(_Type, _Pid, $-, CRLF) -> 77 | <>; 78 | 79 | message(Type, Pid, [H | _] = Msg, CRLF) 80 | when is_integer(H) -> 81 | erlang:iolist_to_binary( 82 | io_lib:format("[~s] ~p: ~s~s", [Type, Pid, Msg, CRLF]) 83 | ); 84 | 85 | message(Type, Pid, Msg, CRLF) 86 | when is_map(Msg) -> 87 | erlang:iolist_to_binary( 88 | io_lib:format("[~s] ~p:~s~s~n", [Type, Pid, CRLF, 89 | jsx:format(jsx:encode(Msg), [space, {indent, 2}, {newline, CRLF}]) 90 | ]) 91 | ); 92 | 93 | message(Type, Pid, Msg, CRLF) 94 | when is_binary(Msg) -> 95 | erlang:iolist_to_binary( 96 | io_lib:format("[~s] ~p: ~s~s", [Type, Pid, Msg, CRLF]) 97 | ); 98 | message(Type, Pid, Msg, CRLF) -> 99 | erlang:iolist_to_binary( 100 | io_lib:format("[~s] ~p: ~2048.p~s", [Type, Pid, Msg, CRLF]) 101 | ). 102 | -------------------------------------------------------------------------------- /src/serverless_logger_std.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(serverless_logger_std). 9 | -behaviour(gen_event). 10 | 11 | -export([ 12 | start/0, 13 | stop/0, 14 | init/1, 15 | terminate/2, 16 | handle_event/2, 17 | handle_call/2 18 | ]). 19 | 20 | 21 | start() -> 22 | error_logger:tty(false), 23 | error_logger:add_report_handler(?MODULE). 24 | 25 | stop() -> 26 | error_logger:delete_report_handler(?MODULE). 27 | 28 | init(_) -> 29 | {ok, undefined}. 30 | 31 | terminate(_, _) -> 32 | ok. 33 | 34 | handle_event({error_report, _, {Pid, Type, Msg}}, State) -> 35 | log(Type, Pid, Msg), 36 | {ok, State}; 37 | 38 | handle_event({warning_report, _, {Pid, Type, Msg}}, State) -> 39 | log(Type, Pid, Msg), 40 | {ok, State}; 41 | 42 | handle_event({info_report, _, {Pid, Type, Msg}}, State) -> 43 | log(Type, Pid, Msg), 44 | {ok, State}; 45 | 46 | handle_event({error, _, {Pid, Format, Data}}, State) -> 47 | log(error, Pid, erlang:iolist_to_binary(io_lib:format(Format, Data))), 48 | {ok, State}; 49 | 50 | handle_event({warning_msg, _, {Pid, Format, Data}}, State) -> 51 | log(warning, Pid, erlang:iolist_to_binary(io_lib:format(Format, Data))), 52 | {ok, State}; 53 | 54 | handle_event({info_msg, _, {Pid, Format, Data}}, State) -> 55 | log(info, Pid, erlang:iolist_to_binary(io_lib:format(Format, Data))), 56 | {ok, State}; 57 | 58 | handle_event(_, State) -> 59 | {ok, State}. 60 | 61 | handle_call(_, State) -> 62 | {noreply, State}. 63 | 64 | log(emergency, Pid, Msg) -> 65 | serverless_logger:log(emergency, Pid, Msg); 66 | 67 | log(alert, Pid, Msg) -> 68 | serverless_logger:log(alert, Pid, Msg); 69 | 70 | log(critical, Pid, Msg) -> 71 | serverless_logger:log(critical, Pid, Msg); 72 | 73 | log(error, Pid, Msg) -> 74 | serverless_logger:log(error, Pid, Msg); 75 | 76 | log(warning, Pid, Msg) -> 77 | serverless_logger:log(warning, Pid, Msg); 78 | 79 | log(notice, Pid, Msg) -> 80 | serverless_logger:log(notice, Pid, Msg); 81 | 82 | log(info, Pid, Msg) -> 83 | serverless_logger:log(info, Pid, Msg); 84 | 85 | log(debug, Pid, Msg) -> 86 | serverless_logger:log(debug, Pid, Msg); 87 | 88 | log(_, _, _) -> 89 | ok. 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/serverless_mock.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | %% 9 | %% @doc 10 | %% mock serverless environment 11 | -module(serverless_mock). 12 | -include_lib("common_test/include/ct.hrl"). 13 | 14 | -export([ 15 | test/3 16 | ]). 17 | 18 | %% 19 | %% 20 | test(Lambda, Mock, Timeout) -> 21 | application:ensure_all_started(serverless), 22 | meck:new(serverless, [passthrough]), 23 | meck:new(serverless_logger, [passthrough]), 24 | 25 | meck:expect(serverless_logger, log, 26 | fun(Type, Pid, Msg) -> 27 | ct:pal("[~s]: ~p ~p", [Type, Pid, Msg]) 28 | end 29 | ), 30 | 31 | Ref = erlang:make_ref(), 32 | Self = self(), 33 | meck:expect(serverless, spawn, 34 | fun(Fun, _) -> 35 | case (catch Fun(Mock)) of 36 | {ok, Value} -> 37 | Self ! {Ref, Value}; 38 | Value -> 39 | Self ! {Ref, Value} 40 | end 41 | end 42 | ), 43 | Lambda:main([]), 44 | Result = receive 45 | {Ref, Value} -> 46 | Value 47 | after Timeout -> 48 | exit(timeout) 49 | end, 50 | 51 | meck:unload(serverless_logger), 52 | meck:unload(serverless), 53 | 54 | Result. 55 | -------------------------------------------------------------------------------- /src/serverless_sup.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(serverless_sup). 9 | -behaviour(supervisor). 10 | 11 | -export([ 12 | start_link/0, 13 | init/1, 14 | spawn/1 15 | ]). 16 | 17 | %% 18 | -define(CHILD(Type, I), {I, {I, start_link, []}, permanent, 10, Type, dynamic}). 19 | -define(CHILD(Type, I, Args), {I, {I, start_link, Args}, permanent, 10, Type, dynamic}). 20 | -define(CHILD(Type, ID, I, Args), {ID, {I, start_link, Args}, permanent, 10, Type, dynamic}). 21 | 22 | %%----------------------------------------------------------------------------- 23 | %% 24 | %% supervisor 25 | %% 26 | %%----------------------------------------------------------------------------- 27 | 28 | start_link() -> 29 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 30 | 31 | init([]) -> 32 | {ok, 33 | { 34 | {one_for_one, 4, 300}, 35 | [ 36 | ?CHILD(worker, serverless_logger) 37 | ] 38 | } 39 | }. 40 | 41 | %% 42 | spawn(Lambda) -> 43 | supervisor:start_child(?MODULE, ?CHILD(worker, serverless_lambda, [Lambda])). 44 | -------------------------------------------------------------------------------- /test/serverless_mock_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(serverless_mock_SUITE). 9 | -include_lib("common_test/include/ct.hrl"). 10 | 11 | -export([all/0]). 12 | -compile(export_all). 13 | 14 | all() -> 15 | [Test || {Test, NAry} <- ?MODULE:module_info(exports), 16 | Test =/= module_info, 17 | NAry =:= 1 18 | ]. 19 | 20 | spawn_success(_) -> 21 | #{<<"code">> := 200} = serverless:mock( 22 | serverless_test, 23 | #{<<"do">> => <<"ok">>} 24 | ), 25 | ok. 26 | 27 | spawn_failure(_) -> 28 | {'EXIT', fail} = serverless:mock( 29 | serverless_test, 30 | #{<<"do">> => <<"fail">>} 31 | ), 32 | ok. 33 | 34 | spawn_error(_) -> 35 | {error, badarg} = serverless:mock( 36 | serverless_test, 37 | #{<<"do">> => <<"error">>} 38 | ), 39 | ok. 40 | -------------------------------------------------------------------------------- /test/serverless_test.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Copyright (C) 2018 Dmitry Kolesnikov 3 | %% 4 | %% This file may be modified and distributed under the terms 5 | %% of the MIT license. See the LICENSE file for details. 6 | %% https://github.com/fogfish/serverless 7 | %% 8 | -module(serverless_test). 9 | -export([main/1]). 10 | 11 | main(Opts) -> 12 | serverless:spawn(fun identity/1, Opts). 13 | 14 | 15 | identity(#{<<"do">> := <<"ok">>}) -> 16 | {ok, #{<<"code">> => 200}}; 17 | 18 | identity(#{<<"do">> := <<"fail">>}) -> 19 | exit(fail); 20 | 21 | identity(#{<<"do">> := <<"error">>}) -> 22 | {error, badarg}. 23 | -------------------------------------------------------------------------------- /test/tests.config: -------------------------------------------------------------------------------- 1 | %% 2 | %% suites 3 | {suites, ".", all}. 4 | --------------------------------------------------------------------------------