├── .editorconfig ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── bootstrap ├── elixir-lambda-blog.md ├── elixir_meetup_12_Feb_2019.txt ├── example ├── .formatter.exs ├── .gitignore ├── README.md ├── config │ └── config.exs ├── lib │ └── example.ex ├── mix.exs └── test │ ├── example_test.exs │ └── test_helper.exs ├── runtime ├── .formatter.exs ├── .gitignore ├── config │ └── config.exs ├── lib │ ├── lambda_runtime.ex │ └── mix │ │ └── tasks │ │ └── package.ex ├── mix.exs ├── mix.lock └── test │ ├── lambda_runtime_test.exs │ └── test_helper.exs └── templates ├── artifact-bucket.yaml └── elixir-example.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [Makefile] 3 | indent_style = tab 4 | indent_size = 8 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | elixir-runtime-*.zip 2 | example-*.zip 3 | .s3-upload-* 4 | .cfn-artifact-bucket 5 | .cfn-elixir-example-* 6 | test-output.txt 7 | bash/ 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lambci/lambda-base:build 2 | 3 | ARG ERLANG_VERSION 4 | ARG ELIXIR_VERSION 5 | 6 | LABEL erlang.version=${ERLANG_VERSION} \ 7 | elixir.version=${ELIXIR_VERSION} 8 | 9 | WORKDIR /work 10 | 11 | RUN curl -SL http://erlang.org/download/otp_src_${ERLANG_VERSION}.tar.gz | tar xz && \ 12 | cd /work/otp_src_${ERLANG_VERSION} && \ 13 | ./configure --disable-hipe --without-termcap --without-javac \ 14 | --without-dialyzer --without-diameter --without-debugger --without-edoc \ 15 | --without-eldap --without-erl_docgen --without-mnesia --without-observer \ 16 | --without-odbc --without-tftp --without-wx --without-xmerl --without-otp_mibs \ 17 | --without-reltool --without-snmp --without-tftp \ 18 | --without-common_test --without-eunit --without-ftp --without-hipe \ 19 | --without-megaco --without-sasl --without-syntax_tools --without-tools \ 20 | --prefix=/opt && \ 21 | make install && \ 22 | rm -r /opt/lib/erlang/lib/*/src /opt/lib/erlang/misc /opt/lib/erlang/usr /opt/bin/ct_run /opt/bin/dialyzer /opt/bin/typer 23 | 24 | RUN curl -SLo /work/Precompiled.zip https://github.com/elixir-lang/elixir/releases/download/v${ELIXIR_VERSION}/Precompiled.zip && \ 25 | cd /opt && \ 26 | unzip -q /work/Precompiled.zip && \ 27 | rm -r /opt/lib/elixir/lib /opt/lib/eex/lib /opt/lib/logger/lib /opt/man /opt/lib/ex_unit/lib /opt/lib/iex /opt/bin/*.bat /opt/bin/*.ps1 28 | 29 | COPY bootstrap /opt/ 30 | COPY runtime/ /work/runtime/ 31 | 32 | RUN cd /work/runtime && \ 33 | mix local.hex --force && \ 34 | mix deps.get && \ 35 | mix test && \ 36 | MIX_ENV=prod mix package && \ 37 | rm -r _build/prod/lib/*/.mix _build/prod/lib/runtime/consolidated && \ 38 | cp -r _build/prod/lib/* /opt/lib && \ 39 | chmod 555 /opt/bootstrap 40 | 41 | # Package 42 | RUN cd /opt && \ 43 | zip -qyr /tmp/runtime.zip ./* 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | LAYER_NAME=elixir-runtime 3 | 4 | ERLANG_VERSION=22.2 5 | ELIXIR_VERSION=1.10.0 6 | 7 | RUNTIME_ZIP=$(LAYER_NAME)-$(ELIXIR_VERSION).zip 8 | EXAMPLE_BIN_ZIP=example/example-0.1.0.zip 9 | EXAMPLE_SRC_ZIP=example/example-src.zip 10 | 11 | REV=$(shell git rev-parse --short HEAD) 12 | 13 | S3_RUNTIME_ZIP=$(LAYER_NAME)-$(ELIXIR_VERSION)-$(REV).zip 14 | S3_EXAMPLE_BIN_ZIP=example-bin-$(REV).zip 15 | S3_EXAMPLE_SRC_ZIP=example-src-$(REV).zip 16 | 17 | 18 | # Targets: 19 | 20 | all: test 21 | 22 | build: $(RUNTIME_ZIP) $(EXAMPLE_BIN_ZIP) $(EXAMPLE_SRC_ZIP) 23 | 24 | clean: 25 | rm -f .cfn-* .s3-* 26 | 27 | artifact-bucket: aws-check .cfn-artifact-bucket 28 | 29 | elixir-examples: aws-check .cfn-elixir-example-bin .cfn-elixir-example-src 30 | 31 | test: aws-check elixir-examples 32 | aws lambda invoke --function-name elixir-example-bin --payload '{"text":"Hello"}' test-output.txt && \ 33 | echo "=== Lambda responded with: ===" && cat test-output.txt && echo && echo "=== end-of-output ===" 34 | aws lambda invoke --function-name elixir-example-src --payload '{"text":"Hello"}' test-output.txt && \ 35 | echo "=== Lambda responded with: ===" && cat test-output.txt && echo && echo "=== end-of-output ===" 36 | 37 | clean-aws: 38 | aws cloudformation delete-stack --stack-name elixir-example-bin && \ 39 | aws cloudformation delete-stack --stack-name elixir-example-src && \ 40 | rm -f .cfn-elixir-example-* 41 | 42 | .PHONY: all build artifact-bucket elixir-examples test aws-check 43 | 44 | # Internals: 45 | 46 | $(RUNTIME_ZIP): Dockerfile bootstrap 47 | docker build --build-arg ERLANG_VERSION=$(ERLANG_VERSION) \ 48 | --build-arg ELIXIR_VERSION=$(ELIXIR_VERSION) \ 49 | -t $(LAYER_NAME) . && \ 50 | docker run --rm $(LAYER_NAME) cat /tmp/runtime.zip > ./$(RUNTIME_ZIP) 51 | 52 | $(EXAMPLE_BIN_ZIP): example/lib/example.ex example/mix.exs $(RUNTIME_ZIP) 53 | docker run --rm -w /code -v $(PWD)/example:/code -u $(shell id -u):$(shell id -g) -e MIX_ENV=prod $(LAYER_NAME) mix do test, package 54 | 55 | $(EXAMPLE_SRC_ZIP): example/lib/example.ex example/mix.exs 56 | cd example && zip -X $$(basename $(EXAMPLE_SRC_ZIP)) lib/example.ex mix.exs 57 | 58 | aws-check: 59 | @echo "Performing pre-flight check..." 60 | @aws cloudformation describe-account-limits > /dev/null || { echo "Could not reach AWS, please set your AWS_PROFILE or access keys." >&2 && false; } 61 | 62 | .cfn-artifact-bucket: ./templates/artifact-bucket.yaml 63 | aws cloudformation deploy \ 64 | --stack-name artifact-bucket \ 65 | --template-file ./templates/artifact-bucket.yaml \ 66 | --no-fail-on-empty-changeset && \ 67 | touch .cfn-artifact-bucket 68 | 69 | .s3-upload-runtime-$(REV): .cfn-artifact-bucket $(RUNTIME_ZIP) 70 | ARTIFACT_STORE=$(shell aws cloudformation list-exports | python -c "import sys, json; print(filter(lambda e: e['Name'] == 'artifact-store', json.load(sys.stdin)['Exports'])[0]['Value'])") && \ 71 | aws s3 cp $(RUNTIME_ZIP) s3://$${ARTIFACT_STORE}/$(S3_RUNTIME_ZIP) && \ 72 | touch .s3-upload-runtime-$(REV) 73 | 74 | .s3-upload-example-bin-$(REV): .cfn-artifact-bucket $(EXAMPLE_BIN_ZIP) 75 | ARTIFACT_STORE=$(shell aws cloudformation list-exports | python -c "import sys, json; print(filter(lambda e: e['Name'] == 'artifact-store', json.load(sys.stdin)['Exports'])[0]['Value'])") && \ 76 | aws s3 cp $(EXAMPLE_BIN_ZIP) s3://$${ARTIFACT_STORE}/$(S3_EXAMPLE_BIN_ZIP) && \ 77 | touch .s3-upload-example-bin-$(REV) 78 | 79 | .s3-upload-example-src-$(REV): .cfn-artifact-bucket $(EXAMPLE_SRC_ZIP) 80 | ARTIFACT_STORE=$(shell aws cloudformation list-exports | python -c "import sys, json; print(filter(lambda e: e['Name'] == 'artifact-store', json.load(sys.stdin)['Exports'])[0]['Value'])") && \ 81 | aws s3 cp $(EXAMPLE_SRC_ZIP) s3://$${ARTIFACT_STORE}/$(S3_EXAMPLE_SRC_ZIP) && \ 82 | touch .s3-upload-example-src-$(REV) 83 | 84 | .cfn-elixir-example-bin: ./templates/elixir-example.yaml .s3-upload-runtime-$(REV) .s3-upload-example-bin-$(REV) 85 | aws cloudformation deploy \ 86 | --stack-name elixir-example-bin \ 87 | --template-file ./templates/elixir-example.yaml \ 88 | --parameter-overrides \ 89 | "RuntimeZip=$(S3_RUNTIME_ZIP)" \ 90 | "ExampleZip=$(S3_EXAMPLE_BIN_ZIP)" \ 91 | "ErlangVersion=$(ERLANG_VERSION)" \ 92 | "ElixirVersion=$(ELIXIR_VERSION)" \ 93 | --capabilities "CAPABILITY_IAM" \ 94 | --no-fail-on-empty-changeset && \ 95 | touch .cfn-elixir-example-bin 96 | 97 | .cfn-elixir-example-src: ./templates/elixir-example.yaml .s3-upload-runtime-$(REV) .s3-upload-example-src-$(REV) 98 | aws cloudformation deploy \ 99 | --stack-name elixir-example-src \ 100 | --template-file ./templates/elixir-example.yaml \ 101 | --parameter-overrides \ 102 | "RuntimeZip=$(S3_RUNTIME_ZIP)" \ 103 | "ExampleZip=$(S3_EXAMPLE_SRC_ZIP)" \ 104 | "ErlangVersion=$(ERLANG_VERSION)" \ 105 | "ElixirVersion=$(ELIXIR_VERSION)" \ 106 | --capabilities "CAPABILITY_IAM" \ 107 | --no-fail-on-empty-changeset && \ 108 | touch .cfn-elixir-example-src 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir for AWS Lambda 2 | 3 | The whole point of AWS Lambda is to provide functions that can be run without 4 | the need to manage any servers. Functions are invoked by passing them messages. 5 | Ehm, that sounds a lot like Erlang/Elixir to me! The clean syntax of Elixir and 6 | the functional concepts the language make it a really good match for use on 7 | [AWS Lambda](https://aws.amazon.com/lambda/). Unfortunately the AWS folks 8 | haven't put any effort in supporting Elixir, so it looks like we have to do it 9 | ourselves. 10 | 11 | This project provides a simple way to get started with running Lambda functions 12 | written in Elixir. This project contains the runtime layer needed to build your 13 | lambda functions, an example function, and some Cloudformation templates to get 14 | you started. 15 | 16 | ## Design principles 17 | 18 | - Stay close to the current way Lambda functions work: it should be enough to 19 | provide one file alone, no full projects. 20 | - The approach should be leaner than OTP releases, if possible. In general, 21 | we're only trying to execute one function. 22 | - This implementation follows the [Lambda runtime 23 | API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). 24 | 25 | In order to keep the deployment code as small as possible, many OTP 26 | applications (≈ components) have been left out. The applications bundled with 27 | this layer are reduced to the ones used for networking, including SSL, and 28 | standard library functions. Most notably tooling like Mnesia is left out. It 29 | should have no place of a Lambda function IMHO. 30 | 31 | All in all, this keeps the layer relatively small (23MB) for a complete system. 32 | 33 | 34 | # Getting it up and running 35 | 36 | In general, it's good practice to deploy code on AWS by means of Cloudformation 37 | templates. The example setup provided is no different. It does deploy 3 stacks: 38 | 39 | 1. An S3 bucket acts as an intermediate storage location for Lambda code 40 | 2. A stack featuring the Elixir runtime, with an example function containing 41 | compiled code (BEAM files). 42 | 3. A stack containing Elixir source code. This project can be edited with the 43 | AWS Lambda editor. 44 | 45 | To work with this repo, there are a few prerequisites to set up: 46 | 47 | 1. [docker](https://www.docker.com), used to build the custom runtime and example 48 | 2. [aws-cli](https://aws.amazon.com/cli/), installed using Python 2 or 3 49 | 3. make ([GNU Make](https://www.gnu.org/software/make/)) 50 | 51 | To get started, make sure you can access your AWS account (e.g. try `aws 52 | cloudformation list-stacks`). If this does not work, set your `AWS_PROFILE` or 53 | access keys. You do not need to have Erlang/Elixir installed on your system since 54 | we do the building from Docker containers. 55 | 56 | To deploy the S3 bucket stack and the example stacks, simply type: 57 | 58 | make 59 | 60 | This will build the zip files, upload them to S3 and deploy the custom runtime 61 | and Lambda functions. 62 | 63 | To test the function, simply call: 64 | 65 | make test 66 | 67 | ## Building a Lambda function 68 | 69 | A Lambda function can be any function defined by `ModuleName.function_name`. The 70 | function should take two arguments, `event` and `context`. 71 | 72 | A simple Lambda handler module could look like this: 73 | 74 | defmodule Example do 75 | 76 | def hello(_event, _context) do 77 | {:ok, %{ :message => "Elixir on AWS Lambda" }} 78 | end 79 | 80 | end 81 | 82 | The event is a map with event information. The contents depend on the type of event 83 | received (API Gateway, SQS, etc.). 84 | 85 | The response can be in one of the following forms: 86 | 87 | {:ok, content} 88 | {:ok, content_type, content} 89 | {:error, message} 90 | 91 | Content can be a map or list, in which case it's serialized to JSON. If its a binary (string) 92 | it will be returned as `text/plain` by default. Any other type will be "inspected" returned 93 | as `application/octet-stream` by default. 94 | 95 | If a `content_type` is provided that is used instead. Binary content is returned as is, the 96 | rest is "inspected". 97 | 98 | The context map contains some extra info about the event, as charlists(!): 99 | 100 | %{ 101 | :content_type => 'application/json' 102 | :request_id => 'abcdef-1234-1234` 103 | :deadline => 1547815888328 104 | :function_arn => 'arn:aws:lambda:eu-west-1:1234567890:function:elixir-runtime-example' 105 | :trace_id => 'Root=1-5c4...' 106 | :client_context => 'a6f...' 107 | :cognito_identity => '6d8...' 108 | } 109 | 110 | The runtime is bundled with [Jason](https://hex.pm/packages/jason), a fast 100% Elixir JSON 111 | serializer/deserializer. 112 | -------------------------------------------------------------------------------- /bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xeuo pipefail 3 | 4 | export HOME="/tmp/task" 5 | 6 | echo "HOME=${HOME} ($(pwd))" 7 | 8 | ls -la 9 | id 10 | ls -la /tmp 11 | 12 | if test -f mix.exs 13 | then 14 | mkdir -p ${HOME} 15 | cp -r ${LAMBDA_TASK_ROOT}/* ${HOME} 16 | cd ${HOME} 17 | 18 | export MIX_ENV=${MIX_ENV:-prod} 19 | export ERL_LIBS="${HOME}/_build/${MIX_ENV}/lib" 20 | 21 | mix compile 22 | else 23 | export ERL_LIBS="${LAMBDA_TASK_ROOT}/lib" 24 | fi 25 | 26 | elixir -e "LambdaRuntime.run()" 27 | -------------------------------------------------------------------------------- /elixir-lambda-blog.md: -------------------------------------------------------------------------------- 1 | # Building an Elixir custom runtime for AWS Lambda 2 | 3 | At the most recent AWS Re:invent, Amazon announced support for custom runtimes on AWS Lambda. Layers provide the ability to enrich a Lambda runtime environment with shared code, such as libraries or a custom startup script. 4 | 5 | AWS has support for quite a few languages out of the box. NodeJS being the fastest, but not always the most readable one. You can edit Python from the AWS Console, while for Java, C# and Go you'll have to upload binaries. 6 | 7 | The odd thing, in my opinion, is that there are no functional languages in the list of supported languages[1](#footnote1). Although the service name would assume something in the area of [functional programming](https://en.wikipedia.org/wiki/Functional_programming). The working of a function itself is also pretty straightforward: an input event gets processed and an output event is returned (_emitted_ if you like). 8 | 9 | Therefore it seemed a logical step to implement a runtime for a functional programming language. My language of choice is [Elixir](https://elixir-lang.org/), a very readable functional programming language that runs on the BEAM, the Erlang VM. 10 | 11 | ## Building a runtime 12 | 13 | The process of building a runtime is pretty well explained in the [AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html). In my case, I gained a bit of experience by implementing the [bash-based runtime example](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-walkthrough.html). 14 | This gives a good basis for any custom runtime. The runtime will be started by a script called "bootstrap". Already having a Bash-based script will allow you to test a bit while you set up the runtime. 15 | 16 | The runtime itself should be bundled as a zip file. An easy way to build such a zip file -- especially when there are binaries involved -- is with the [lambda-base image](https://hub.docker.com/r/lambci/lambda-base) from the [LambCI project](https://github.com/lambci). This Docker container replicates what can be found on a Lambda instance. 17 | 18 | In order to make the zip file not too big, I had to strip it down considerably. The combined layers, including the runtime, should be no bigger than 65MB. Many tools, like Observer (monitoring), Mnesia (database), terminal and GUI related components can all be left out: the VM will not run for a long time and has no console/GUI access. This way I was able to bring down the size to a decent 23MB (a full distro will be about 57MB). 19 | 20 | The de facto way to distribute an Erlang application is by means of an OTP release. This bundles the code and the BEAM in one single package. For Lambda I want this to be leaner: you'd just have to deploy your compiled code and that should be it. This makes deployments faster since there are fewer bytes to move around and the application can be kept in the runtime layer. 21 | 22 | ## Benchmarks 23 | 24 | We all want it to be fast. I have not done a full-blown performance test. For a Hello-world function I deployed the responses were quite okay. As low as twenty ms, and many times only a couple of milliseconds. 25 | 26 | The cold start speed is about 1.3 seconds, according to AWS X-Ray. This is comparable to Java. After starting the Lambda function is "hot" and only shuts down after 15 minutes of idle time. I want to see if I can bring the startup time down even further. One area of investigation is the bootstrap script used by Erlang. Maybe it can be made smaller, e.g. removing all code related to clustering. At this point Erlang's legacy is kind of in the way for its use as a Lambda language: the Erlang/OTP ecosystem is built to create applications that never go down, like telephone switches. For Lambda, we have the certainty that this will never be a long-lived process. 27 | 28 | ## Final thoughts 29 | 30 | The Lambda model is straight forward. It's good to see that the use of custom runtimes does not involve a performance hit. With the tools described above, it's quite simple to add support for a language not present on AWS Lambda today. You can even use the web editor, which is nice, but not a big deal since your code needs to be put in source control anyway. 31 | 32 | Have a look at the [Elixir Lambda](https://github.com/amolenaar/elixir_lambda) repository and give it a go. I've added Cloudformation templates and a Makefile for convenience. Let me know what you think! 33 | 34 | ---- 35 | 36 | [1] Well, you could execute F# code with the .Net runtime. 37 | -------------------------------------------------------------------------------- /elixir_meetup_12_Feb_2019.txt: -------------------------------------------------------------------------------- 1 | -- 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Elixir on AWS Lambda 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Elixir Meetup Arjan Molenaar 22 | Feb 12, 2019 Software consultant @ Xebia 23 | 24 | 25 | -- 26 | 27 | 28 | 29 | Elixir on AWS Lambda 30 | 31 | 32 | - What is AWS Lambda 33 | 34 | - Why AWS Lambda 35 | 36 | - How to deploy 37 | 38 | - AWS Lambda execution model 39 | 40 | - About the runtime 41 | 42 | - Packaging the function 43 | 44 | 45 | 46 | 47 | 48 | 49 | -- 50 | 51 | 52 | 53 | What is AWS Lambda 54 | 55 | 56 | - Serverless 57 | 58 | - No hardware? Right! 59 | 60 | - Short lived 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -- 74 | 75 | 76 | 77 | Why AWS Lambda 78 | 79 | 80 | - Pay per use 81 | 82 | - No hardware! Easy to startup 83 | 84 | - Extends functional paradigm 85 | (data in -> data out) 86 | 87 | - Elixir is a nice language 88 | (pattern matching!) 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -- 98 | 99 | 100 | 101 | How to deploy 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | (Note to self: prepare demo) 119 | 120 | 121 | -- 122 | 123 | 124 | 125 | Execution model 126 | 127 | 128 | +---+ 129 | | | 130 | | A | 131 | | W | 132 | | S | 133 | | | 134 | Incoming request | L | 135 | ------------------>+ a | 136 | | m | 137 | | b | 138 | | d | 139 | | a | 140 | | | 141 | | | 142 | +---+ 143 | 144 | 145 | -- 146 | 147 | 148 | 149 | Execution model (2) 150 | 151 | 152 | +---+ +--------------------------+ 153 | | | | | 154 | | A | | VM | 155 | | W | | | 156 | | S | | | 157 | | | | | 158 | Incoming request | L | | | 159 | ------------------>+ a | | | 160 | | m | | | 161 | | b | | | 162 | | d | | | 163 | | a | | | 164 | | | | | 165 | | | | | 166 | +---+ +--------------------------+ 167 | 168 | 169 | -- 170 | 171 | 172 | 173 | Execution model (3) 174 | 175 | 176 | +---+ +--------------------------+ 177 | | | | | 178 | | A | | VM | 179 | | W | | | 180 | | S | | +--------------------+ | 181 | | | | | | | 182 | Incoming request | L | | | Our function | | 183 | ------------------>+ a | | | | | 184 | | m | | +--------------------+ | 185 | | b | | | | | 186 | | d | | | Runtime layer | | 187 | | a | | | | | 188 | | | | +--------------------+ | 189 | | | | | 190 | +---+ +--------------------------+ 191 | 192 | 193 | -- 194 | 195 | 196 | 197 | Execution model (4) 198 | 199 | 200 | +---+ +--------------------------+ 201 | | | | | 202 | | A | | VM | 203 | | W | | | 204 | | S | | +--------------------+ | 205 | | | | | | | 206 | Incoming request | L | | | Our function | | 207 | ------------------>+ a | | | | | 208 | | m | Poll for request | +--------------------+ | 209 | | b +<---------------------+ | | 210 | | d | | | Runtime layer | | 211 | | a | | | | | 212 | | | | +--------------------+ | 213 | | | | | 214 | +---+ +--------------------------+ 215 | 216 | 217 | -- 218 | 219 | 220 | 221 | Execution model (5) 222 | 223 | 224 | +---+ +--------------------------+ 225 | | | | | 226 | | A | | VM | 227 | | W | | | 228 | | S | | +--------------------+ | 229 | | | | | | | 230 | Incoming request | L | | | Our function | | 231 | ------------------>+ a | | | | | 232 | | m | Poll for request | +--------------------+ | 233 | | b +<---------------------+ | | 234 | | d | | | Runtime layer | | 235 | | a +<---------------------+ | | 236 | | | POST result | +--------------------+ | 237 | | | | | 238 | +---+ +--------------------------+ 239 | 240 | 241 | -- 242 | 243 | 244 | 245 | Execution model (6) 246 | 247 | 248 | +---+ +--------------------------+ 249 | | | | | 250 | | A | | VM | 251 | | W | | | 252 | | S | | +--------------------+ | 253 | | | | | | | 254 | Incoming request | L | | | Our function | | 255 | ------------------>+ a | | | | | 256 | | m | Poll for request | +--------------------+ | 257 | | b +<---------------------+ | | 258 | | d | | | Runtime layer | | 259 | Response | a +<---------------------+ | | 260 | <------------------+ | POST result | +--------------------+ | 261 | | | | | 262 | +---+ +--------------------------+ 263 | 264 | 265 | -- 266 | 267 | 268 | 269 | About the runtime 270 | 271 | 272 | - The runtime is short running 273 | 274 | - Applications like Mnesia, 275 | Observer can be left out 276 | 277 | - Reduced runtime to about 23MB 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | -- 290 | 291 | 292 | 293 | Packaging the function 294 | 295 | 296 | - Keep it small 297 | 298 | - Just the function 299 | 300 | - Function defined as parameter 301 | 302 | - No OTP release 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | -- 314 | 315 | 316 | 317 | What's next 318 | 319 | 320 | - Allow uploading of exs files 321 | 322 | - Is this usable? 323 | 324 | - Convenience package for 325 | testing & packaging? 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | -- 338 | 339 | 340 | 341 | Elixir on AWS Lambda 342 | 343 | 344 | 345 | Arjan Molenaar 346 | 347 | Twitter: @ajmolenaar 348 | 349 | https://github.com/amolenaar 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | ** 361 | -------------------------------------------------------------------------------- /example/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | example-*.tar 24 | 25 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | This is a small echo example Lambda function. 4 | 5 | To build the package call `MIX_ENV=prod mix package` or use the Make target in the toplevel directory. 6 | 7 | For more details, read the toplevel `README.md`. -------------------------------------------------------------------------------- /example/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :example, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:example, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /example/lib/example.ex: -------------------------------------------------------------------------------- 1 | defmodule Example do 2 | @moduledoc """ 3 | Example Lambda function. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> Example.hello(%{}, %{}) 12 | {:ok, %{ :message => "Elixir on AWS Lambda", event: %{} }} 13 | 14 | """ 15 | 16 | def hello(event, _context), 17 | do: 18 | #{:ok, %{:message => "Elixir on AWS Lambda", :event => event, :context => inspect(context)}} 19 | {:ok, %{:message => "Elixir on AWS Lambda", :event => event}} 20 | end 21 | -------------------------------------------------------------------------------- /example/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Example.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :example, 7 | version: "0.1.0", 8 | elixir: "~> 1.7" 9 | ] 10 | end 11 | 12 | def application do 13 | [ 14 | extra_applications: [:logger] 15 | ] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /example/test/example_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExampleTest do 2 | use ExUnit.Case 3 | doctest Example 4 | 5 | test "greets the world" do 6 | assert Example.hello(%{}, %{}) == 7 | {:ok, %{:message => "Elixir on AWS Lambda", event: %{}}} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /example/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /runtime/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /runtime/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | *.tar 24 | 25 | -------------------------------------------------------------------------------- /runtime/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :lambda_bootstrap, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:lambda_bootstrap, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /runtime/lib/lambda_runtime.ex: -------------------------------------------------------------------------------- 1 | defmodule LambdaRuntime do 2 | @moduledoc """ 3 | Read Lambda request, process, return, repeat. 4 | 5 | This module provides a simple loop that handles 6 | Lambda requests. 7 | 8 | Documentation on the Lambda interface can be found at 9 | https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html. 10 | """ 11 | 12 | @content_type_json 'application/json' 13 | @lambda_max_timeout_ms 900_000 14 | 15 | def run(httpc \\ :httpc) do 16 | Application.ensure_all_started(:inets) 17 | 18 | lambda_runtime_api = System.get_env("AWS_LAMBDA_RUNTIME_API") 19 | handler_name = System.get_env("_HANDLER") 20 | base_url = "http://#{lambda_runtime_api}/2018-06-01" |> String.to_charlist() 21 | 22 | backend = fn 23 | :get, url_path, _, _ -> 24 | httpc.request(:get, {base_url ++ url_path, []}, [], []) 25 | 26 | :post, url_path, content_type, body -> 27 | httpc.request(:post, {base_url ++ url_path, [], content_type, body}, [], []) 28 | end 29 | 30 | time_source = fn -> :erlang.system_time(:millisecond) end 31 | 32 | cond do 33 | Regex.match?(~r/^[A-Z:][A-Za-z0-9_.]+$/, handler_name) -> 34 | # TODO: check prerequisites. else report to '/runtime/init/error' and quit 35 | {handler, _} = Code.eval_string("&#{handler_name}/2") 36 | 37 | loop(backend, handler, time_source) 38 | true -> 39 | send_init_error( 40 | "Invalid handler signature: #{handler_name}. Expected something like \"Module.function\".", 41 | backend 42 | ) 43 | end 44 | end 45 | 46 | def loop(backend, handler, time_source) do 47 | task = Task.async(fn -> handle(backend, handler, time_source.()) end) 48 | Task.await(task, @lambda_max_timeout_ms) 49 | loop(backend, handler, time_source) 50 | end 51 | 52 | def handle(backend, handler, timestamp) do 53 | with {:ok, request} <- backend.(:get, '/runtime/invocation/next', nil, nil), 54 | {:ok, event, context, request_id} <- parse_request(request) do 55 | task = Task.async(fn -> handler.(event, context) end) 56 | Task.await(task, context.deadline - timestamp) 57 | |> send_response(request_id, backend) 58 | else 59 | maybe_error -> 60 | IO.puts("Error while requesting Lambda request: #{inspect(maybe_error)}. So long!") 61 | end 62 | end 63 | 64 | defp parse_request({{'HTTP/1.1', 200, 'OK'}, headers, body}) do 65 | headers = Map.new(headers) 66 | content_type = Map.get(headers, 'content-type') 67 | request_id = Map.get(headers, 'lambda-runtime-aws-request-id') 68 | 69 | event = 70 | case content_type do 71 | @content_type_json -> Jason.decode!(body) 72 | _ -> body 73 | end 74 | 75 | context = %{ 76 | :content_type => content_type, 77 | :request_id => request_id, 78 | :deadline => Map.get(headers, 'lambda-runtime-deadline-ms') |> List.to_integer(), 79 | :function_arn => Map.get(headers, 'lambda-runtime-invoked-function-arn'), 80 | :trace_id => Map.get(headers, 'lambda-runtime-trace-id'), 81 | :client_context => Map.get(headers, 'lambda-runtime-client-context'), 82 | :cognito_identity => Map.get(headers, 'lambda-runtime-cognito-identity') 83 | } 84 | 85 | {:ok, event, context, request_id} 86 | end 87 | 88 | defp parse_request(maybe_error), do: {:error, maybe_error} 89 | 90 | defp send_response({:ok, response}, request_id, backend) 91 | when is_map(response) or is_list(response) do 92 | {:ok, response} = Jason.encode(response) 93 | send_response({:ok, @content_type_json, response}, request_id, backend) 94 | end 95 | 96 | defp send_response({:ok, response}, request_id, backend) when is_binary(response) do 97 | send_response({:ok, 'text/plain', response}, request_id, backend) 98 | end 99 | 100 | defp send_response({:ok, response}, request_id, backend), 101 | do: 102 | send_response( 103 | {:ok, 'application/octet-stream', Kernel.inspect(response)}, 104 | request_id, 105 | backend 106 | ) 107 | 108 | defp send_response({:ok, content_type, response}, request_id, backend) 109 | when is_binary(content_type) do 110 | send_response({:ok, content_type |> String.to_charlist(), response}, request_id, backend) 111 | end 112 | 113 | defp send_response({:ok, content_type, response}, request_id, backend) 114 | when is_binary(response) do 115 | url = '/runtime/invocation/' ++ request_id ++ '/response' 116 | backend.(:post, url, content_type, response) 117 | end 118 | 119 | defp send_response({:ok, content_type, response}, request_id, backend), 120 | do: send_response({:ok, content_type, Kernel.inspect(response)}, request_id, backend) 121 | 122 | defp send_response({:error, message}, request_id, backend) when is_binary(message) do 123 | send_error(message, request_id, backend) 124 | end 125 | 126 | defp send_response(what_else, request_id, backend), 127 | do: send_error(Kernel.inspect(what_else), request_id, backend) 128 | 129 | defp send_error(message, request_id, backend) do 130 | url = '/runtime/invocation/' ++ request_id ++ '/error' 131 | 132 | body = 133 | Jason.encode!(%{ 134 | "errorMessage" => message, 135 | "errorType" => "RuntimeException" 136 | }) 137 | 138 | backend.(:post, url, @content_type_json, body) 139 | end 140 | 141 | defp send_init_error(message, backend) do 142 | url = '/runtime/init/error' 143 | 144 | body = 145 | Jason.encode!(%{ 146 | "errorMessage" => message, 147 | "errorType" => "InitializationError" 148 | }) 149 | 150 | backend.(:post, url, @content_type_json, body) 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /runtime/lib/mix/tasks/package.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Package do 2 | use Mix.Task 3 | 4 | def run(_args) do 5 | Mix.Task.run("compile", []) 6 | 7 | config = Mix.Project.config() 8 | app = Keyword.fetch!(config, :app) 9 | version = Keyword.fetch!(config, :version) 10 | 11 | build_path = Mix.Project.build_path() 12 | 13 | package_files = 14 | ls_r(build_path <> "/lib") 15 | |> Enum.map(&String.trim_leading(&1, build_path <> "/")) 16 | 17 | case zip("#{app}-#{version}.zip", package_files, build_path) do 18 | {:ok, zipfile} -> IO.inspect(zipfile, label: "Created archive") 19 | {:error, err} -> IO.inspect(err, label: "Could not create zip file") 20 | end 21 | end 22 | 23 | def ls_r(path \\ ".") do 24 | cond do 25 | File.regular?(path) -> 26 | [path] 27 | 28 | File.dir?(path) -> 29 | File.ls!(path) 30 | |> Enum.map(&Path.join(path, &1)) 31 | |> Enum.map(&ls_r/1) 32 | |> Enum.concat() 33 | 34 | true -> 35 | [] 36 | end 37 | end 38 | 39 | def zip(name, package_files, build_path) do 40 | :zip.zip( 41 | name |> String.to_charlist(), 42 | package_files |> Enum.map(&String.to_charlist/1), 43 | cwd: build_path |> String.to_charlist() 44 | ) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /runtime/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LambdaBootstrap.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :runtime, 7 | version: "0.1.0", 8 | elixir: "~> 1.7", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | {:jason, "~> 1.1"} 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /runtime/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "artificery": {:hex, :artificery, "0.2.6", "f602909757263f7897130cbd006b0e40514a541b148d366ad65b89236b93497a", [:mix], []}, 3 | "distillery": {:hex, :distillery, "2.0.12", "6e78fe042df82610ac3fa50bd7d2d8190ad287d120d3cd1682d83a44e8b34dfb", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, optional: false]}]}, 4 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, 5 | } 6 | -------------------------------------------------------------------------------- /runtime/test/lambda_runtime_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LambdaRuntimeTest do 2 | use ExUnit.Case 3 | doctest LambdaRuntime 4 | 5 | @timestamp_ms 0 6 | 7 | defp handle(handler), 8 | do: LambdaRuntime.handle(&BackendMock.backend/4, handler, @timestamp_ms) 9 | 10 | test "handling of an event with a dictionary" do 11 | assert handle(fn _e, _c -> {:ok, %{:message => "Hello Elixir"}} end) == 12 | {:mock, 'application/json', "{\"message\":\"Hello Elixir\"}"} 13 | end 14 | 15 | test "handling of an event with a list" do 16 | assert handle(fn _e, _c -> {:ok, [1, 2, 3]} end) == {:mock, 'application/json', "[1,2,3]"} 17 | end 18 | 19 | test "handling of an event with a charlist" do 20 | assert handle(fn _e, _c -> {:ok, 'abc'} end) == {:mock, 'application/json', "[97,98,99]"} 21 | end 22 | 23 | test "handling of an event with a binary" do 24 | assert handle(fn _e, _c -> {:ok, "Hello Elixir"} end) == {:mock, 'text/plain', "Hello Elixir"} 25 | end 26 | 27 | test "handling of an event with a tuple" do 28 | assert handle(fn _e, _c -> {:ok, {1, 2, 3}} end) == 29 | {:mock, 'application/octet-stream', "{1, 2, 3}"} 30 | end 31 | 32 | test "handling of an event with a number" do 33 | assert handle(fn _e, _c -> {:ok, 42} end) == {:mock, 'application/octet-stream', "42"} 34 | end 35 | 36 | test "handling of an event with custom content type" do 37 | assert handle(fn _e, _c -> {:ok, 'text/numeral', 42} end) == {:mock, 'text/numeral', "42"} 38 | end 39 | 40 | test "handling of an event with custom content type defined with a string" do 41 | assert handle(fn _e, _c -> {:ok, "text/numeral", 42} end) == {:mock, 'text/numeral', "42"} 42 | end 43 | 44 | test "an error response" do 45 | assert handle(fn _e, _c -> {:error, "Error message"} end) == 46 | {:mock, 'application/json', 47 | "{\"errorMessage\":\"Error message\",\"errorType\":\"RuntimeException\"}"} 48 | end 49 | 50 | test "an non-string error response" do 51 | assert handle(fn _e, _c -> {:error, %{:message => "Error message"}} end) == 52 | {:mock, 'application/json', 53 | "{\"errorMessage\":\"{:error, %{message: \\\"Error message\\\"}}\",\"errorType\":\"RuntimeException\"}"} 54 | end 55 | 56 | test "handling of an event with just some output" do 57 | assert handle(fn _e, _c -> {:meaning, 42} end) == 58 | {:mock, 'application/json', 59 | "{\"errorMessage\":\"{:meaning, 42}\",\"errorType\":\"RuntimeException\"}"} 60 | end 61 | 62 | test "initialization error" do 63 | System.put_env([{"AWS_LAMBDA_RUNTIME_API", "lambdahost"}, {"_HANDLER", "wrong handler"}]) 64 | 65 | assert LambdaRuntime.run(HttpcMock) == 66 | {:mock, 'application/json', 67 | "{\"errorMessage\":\"Invalid handler signature: wrong handler. Expected something like \\\"Module.function\\\".\",\"errorType\":\"InitializationError\"}"} 68 | end 69 | end 70 | 71 | defmodule BackendMock do 72 | def backend(:get, '/runtime/invocation/next', _, _), 73 | do: 74 | {:ok, 75 | {{'HTTP/1.1', 200, 'OK'}, 76 | [ 77 | {'lambda-runtime-aws-request-id', '--request-id--'}, 78 | {'lambda-runtime-deadline-ms', '100000'} 79 | ], 80 | """ 81 | { 82 | "path": "/test/hello", 83 | "headers": { 84 | "X-Forwarded-Proto": "https" 85 | }, 86 | "pathParameters": { 87 | "proxy": "hello" 88 | }, 89 | "requestContext": { 90 | "accountId": "123456789012", 91 | "resourceId": "us4z18", 92 | "stage": "test", 93 | "requestId": "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9", 94 | "identity": { 95 | "cognitoIdentityPoolId": "" 96 | }, 97 | "resourcePath": "/{proxy+}", 98 | "httpMethod": "GET", 99 | "apiId": "wt6mne2s9k" 100 | }, 101 | "resource": "/{proxy+}", 102 | "httpMethod": "GET", 103 | "queryStringParameters": { 104 | "name": "me" 105 | }, 106 | "stageVariables": { 107 | "stageVarName": "stageVarValue" 108 | } 109 | } 110 | """}} 111 | 112 | def backend( 113 | :post, 114 | '/runtime/invocation/--request-id--/response', 115 | content_type, 116 | body 117 | ), 118 | do: {:mock, content_type, body} 119 | 120 | def backend( 121 | :post, 122 | '/runtime/invocation/--request-id--/error', 123 | content_type, 124 | body 125 | ), 126 | do: {:mock, content_type, body} 127 | end 128 | 129 | defmodule HttpcMock do 130 | def request( 131 | :post, 132 | {'http://lambdahost/2018-06-01/runtime/init/error', [], content_type, body}, 133 | [], 134 | [] 135 | ), 136 | do: {:mock, content_type, body} 137 | end 138 | -------------------------------------------------------------------------------- /runtime/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /templates/artifact-bucket.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | 3 | Resources: 4 | Bucket: 5 | Type: AWS::S3::Bucket 6 | Properties: 7 | BucketName: !Sub "artifact-store-${AWS::AccountId}" 8 | AccessControl: Private 9 | 10 | Parameter: 11 | Type: AWS::SSM::Parameter 12 | Properties: 13 | Name: artifact-store 14 | Type: "String" 15 | Value: !Ref Bucket 16 | Description: "S3 bucket for (intermediate) artifact storage" 17 | 18 | Outputs: 19 | ArtifactStore: 20 | Value: !Ref Bucket 21 | Export: 22 | Name: artifact-store 23 | -------------------------------------------------------------------------------- /templates/elixir-example.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | 3 | Parameters: 4 | S3Bucket: 5 | Type : "AWS::SSM::Parameter::Value" 6 | Default: artifact-store 7 | Description: "The artifact store bucket" 8 | ErlangVersion: 9 | Type : String 10 | Description: "Erlang version of the runtime" 11 | ElixirVersion: 12 | Type : String 13 | Description: "Elixir version of the runtime" 14 | RuntimeZip: 15 | Type : String 16 | Description: "Name of the S3 artifact that points to the runtime zip fie" 17 | ExampleZip: 18 | Type : String 19 | Description: "Name of the S3 artifact that points to the lambda function zip fie" 20 | 21 | Resources: 22 | ElixirRuntime: 23 | Type: AWS::Lambda::LayerVersion 24 | Properties: 25 | LayerName: elixir-runtime 26 | Description: !Sub "Elixir ${ElixirVersion} / Erlang ${ErlangVersion} runtime layer for AWS Lambda" 27 | LicenseInfo: Apache License, Version 2 28 | Content: 29 | S3Bucket: !Ref S3Bucket 30 | S3Key: !Ref RuntimeZip 31 | 32 | ExampleFunction: 33 | Type: AWS::Lambda::Function 34 | Properties: 35 | FunctionName: !Ref AWS::StackName 36 | Handler: Example.hello 37 | Runtime: provided 38 | Code: 39 | S3Bucket: !Ref S3Bucket 40 | S3Key: !Ref ExampleZip 41 | Description: Our custom Elixir function 42 | MemorySize: 128 43 | Timeout: 5 44 | Layers: 45 | - !Ref ElixirRuntime 46 | Role: !GetAtt FunctionRole.Arn 47 | 48 | FunctionRole: 49 | Type: AWS::IAM::Role 50 | Properties: 51 | AssumeRolePolicyDocument: 52 | Version: "2012-10-17" 53 | Statement: 54 | - Effect: Allow 55 | Principal: 56 | Service: lambda.amazonaws.com 57 | Action: "sts:AssumeRole" 58 | Path: "/" 59 | ManagedPolicyArns: 60 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 61 | - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess 62 | --------------------------------------------------------------------------------