├── .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 |
--------------------------------------------------------------------------------