├── .github
└── tokenmill-logo.svg
├── .gitignore
├── .gitlab-ci.yml
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── project.clj
├── resources
└── leiningen
│ └── new
│ └── clojure_graalvm_aws_lambda
│ ├── Dockerfile
│ ├── Makefile
│ ├── README.md
│ ├── bootstrap
│ ├── core.clj
│ ├── deps.edn
│ ├── gitignore
│ ├── gitlab-ci.yml
│ └── lambda.yml
└── src
└── leiningen
└── new
└── clojure_graalvm_aws_lambda.clj
/.github/tokenmill-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pom.xml
2 | pom.xml.asc
3 | *.jar
4 | *.class
5 | /lib/
6 | /classes/
7 | /target/
8 | /checkouts/
9 | .lein-deps-sum
10 | .lein-repl-history
11 | .lein-plugins/
12 | .lein-failures
13 | .nrepl-port
14 | .cpcache/
15 | .env
16 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - build
3 | - test
4 | - publish-template
5 |
6 | build-template:
7 | stage: build
8 | image: clojure:openjdk-8-lein-alpine
9 | when: always
10 | script:
11 | - lein new clojure-graalvm-aws-lambda my-lambda
12 | artifacts:
13 | paths:
14 | - my-lambda/
15 | expire_in: 1 week
16 |
17 | test-build-lambda-zip:
18 | stage: test
19 | image: docker:stable
20 | when: manual
21 | variables:
22 | DOCKER_HOST: tcp://docker:2375/
23 | services:
24 | - docker:dind
25 | script:
26 | - cd my-lambda
27 | - docker build --target archiver -t lambda-runtime-archiver .
28 | - docker rm build || true
29 | - docker create --name build lambda-runtime-archiver
30 | - docker cp build:/usr/src/app/function.zip function.zip
31 |
32 | publish:
33 | stage: publish-template
34 | image: clojure:openjdk-8-lein-alpine
35 | when: manual
36 | only:
37 | - master
38 | - tags
39 | script:
40 | - cp $LEIN_PROFILES_CLJ ~/.lein/profiles.clj
41 | - cp $GPG_GEN_KEY_SCRIPT gen-key-script
42 | - gpg --batch --gen-key gen-key-script
43 | - lein deploy clojars
44 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).
3 |
4 | ## 0.3.3 - 2019-05-27
5 | ### Changed
6 | - Updated dependencies
7 |
8 | ## 0.3.2 - 2019-05-27
9 | ### Added
10 | - SCM link
11 |
12 | ## 0.3.1 - 2019-05-27
13 | ### Changed
14 | - Project URL to Github
15 |
16 | ## 0.3.0 - 2019-05-24
17 | ### Changed
18 | - Remove unnecessary files
19 | - Move project to GitHub
20 |
21 | ## 0.2.4 - 2019-05-24
22 | ### Added
23 | - Hint where to initialize Lambda resources
24 |
25 | ## 0.2.3 - 2019-05-24
26 | ### Changed
27 | - Unify environment variable names used in the template
28 |
29 | ## 0.2.2 - 2019-05-24
30 | ### Added
31 | - Include "resources" in :paths in deps.edn
32 | ### Fixed
33 | - Triple mustaches in lambda.yml
34 |
35 | ## 0.2.1 - 2019-05-24
36 | ### Added
37 | - Badge for the pipeline status
38 | ### Changed
39 | - Use `tokenmill/clojure:graalvm-ce-19.0.0-tools-deps-1.10.0.442` as builder
40 |
41 | ## 0.2.0 - 2019-05-17
42 | ### Changed
43 | - Upgrade GraalVM CE Docker to `oracle/graalvm-ce:19.0.0`
44 |
45 | ## 0.1.0 - 2019-04-24
46 | ### Added
47 | - Files for the AWS Lambda template.
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Tokenmill, UAB
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TEMPLATE_MAKE=cd your-lambda && make
2 |
3 | try-template-locally:
4 | rm -rf your-lambda/
5 | lein new clojure-graalvm-aws-lambda your-lambda
6 | ${TEMPLATE_MAKE} build-lambda-zip
7 | cd ..
8 | rm -rf your-lambda/
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # clojure-graalvm-aws-lambda-template
6 |
7 | [](https://clojars.org/clojure-graalvm-aws-lambda/lein-template)
8 | [](https://gitlab.com/Jocas/clojure-graalvm-aws-lambda-template/commits/master)
9 |
10 | Leiningen template for AWS Lambda custom runtime with GraalVM `native-image` compiled Clojure projects.
11 |
12 | Published in [Clojars](https://clojars.org/clojure-graalvm-aws-lambda/lein-template)
13 |
14 | ## Usage
15 |
16 | Run:
17 | ```
18 | lein new clojure-graalvm-aws-lambda your-lambda
19 | ```
20 |
21 | This results in a project structure like this:
22 | ```
23 | $ tree -a your-lambda
24 | your-lambda
25 | ├── bootstrap
26 | ├── deps.edn
27 | ├── Dockerfile
28 | ├── .gitignore
29 | ├── .gitlab-ci.yml
30 | ├── lambda.yml
31 | ├── Makefile
32 | ├── README.md
33 | └── src
34 | └── lambda
35 | └── core.clj
36 |
37 | 2 directories, 9 files
38 |
39 | ```
40 |
41 | Then
42 | ```
43 | cd your-lambda
44 | ```
45 |
46 | Set these environment variables:
47 | - MY_AWS_DEFAULT_REGION
48 | - MY_AWS_ACCESS_KEY_ID
49 | - MY_AWS_SECRET_ACCESS_KEY
50 | - MY_S3_BUCKET
51 | - MY_S3_FOLDER
52 |
53 | Run:
54 | ```
55 | make deploy-lambda-via-container
56 | ```
57 |
58 | Lambda is ready to be used. Go to your AWS Console to work with the new stack named `lambda-custom-runtime-your-lambda`.
59 |
60 | ## License
61 |
62 | Copyright © 2019 [TokenMill UAB](http://www.tokenmill.lt).
63 |
64 | Distributed under the The Apache License, Version 2.0.
65 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject clojure-graalvm-aws-lambda/lein-template "0.3.3"
2 | :description "Template for AWS Lambda with Clojure tools-deps, GraalVM, Docker, and Gitlab CI"
3 | :url "https://github.com/tokenmill/clojure-graalvm-aws-lambda-template"
4 | :license {:name "Apache License Version 2.0, January 2004"
5 | :url "http://www.apache.org/licenses/"}
6 | :deploy-repositories [["releases" {:sign-releases false :url "https://clojars.org"}]
7 | ["snapshots" {:sign-releases false :url "https://clojars.org"}]]
8 | :scm {:name "git"
9 | :url "https://github.com/tokenmill/clojure-graalvm-aws-lambda-template"}
10 | :eval-in-leiningen true)
11 |
--------------------------------------------------------------------------------
/resources/leiningen/new/clojure_graalvm_aws_lambda/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM tokenmill/clojure:graalvm-ce-19.0.0-tools-deps-1.10.0.442 as builder
2 |
3 | RUN mkdir -p /usr/src/app
4 | WORKDIR /usr/src/app
5 |
6 | COPY deps.edn /usr/src/app/
7 | RUN clojure -R:native-image
8 | COPY . /usr/src/app
9 | RUN clojure -A:native-image
10 | RUN cp $JAVA_HOME/jre/lib/amd64/libsunec.so .
11 | RUN cp target/app server
12 | RUN chmod 755 server bootstrap
13 |
14 |
15 | FROM amazonlinux:2 as archiver
16 |
17 | RUN yum -y install zip
18 |
19 | WORKDIR /usr/src/app
20 | COPY --from=builder /usr/src/app/bootstrap bootstrap
21 | COPY --from=builder /usr/src/app/server server
22 | COPY --from=builder /usr/src/app/libsunec.so libsunec.so
23 | RUN zip function.zip bootstrap server libsunec.so
24 |
25 |
26 | FROM amazonlinux:2 as deployer
27 |
28 | RUN yum -y install awscli
29 |
30 | ARG AWS_DEFAULT_REGION
31 | ENV AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION
32 |
33 | ARG AWS_ACCESS_KEY_ID
34 | ENV AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
35 |
36 | ARG AWS_SECRET_ACCESS_KEY
37 | ENV AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
38 |
39 | ARG S3_BUCKET=bucket-name
40 | ENV S3_BUCKET=$S3_BUCKET
41 |
42 | ARG S3_FOLDER=folder-name
43 | ENV S3_FOLDER=$S3_FOLDER
44 |
45 | ARG STACK_NAME=lambda-custom-runtime-{{lambda-name}}
46 | ENV STACK_NAME=$STACK_NAME
47 |
48 | COPY --from=archiver /usr/src/app/function.zip function.zip
49 | COPY lambda.yml lambda.yml
50 |
51 | RUN aws cloudformation package --template-file lambda.yml --s3-bucket ${S3_BUCKET} --s3-prefix ${S3_FOLDER} --output-template-file /tmp/lambda-packaged.yml
52 | RUN aws cloudformation deploy --template-file /tmp/lambda-packaged.yml --stack-name ${STACK_NAME} --capabilities CAPABILITY_IAM --no-fail-on-empty-changeset
53 |
--------------------------------------------------------------------------------
/resources/leiningen/new/clojure_graalvm_aws_lambda/Makefile:
--------------------------------------------------------------------------------
1 | build-native-image:
2 | docker build --target builder -t graalvm-compiler .
3 | docker rm build || true
4 | docker create --name build graalvm-compiler
5 | docker cp build:/usr/src/app/target/app server
6 | docker cp build:/usr/src/app/libsunec.so libsunec.so
7 |
8 | build-lambda-zip:
9 | docker build --target archiver -t lambda-runtime-archiver .
10 | docker rm build || true
11 | docker create --name build lambda-runtime-archiver
12 | docker cp build:/usr/src/app/function.zip function.zip
13 |
14 | stack=lambda-custom-runtime-{{lambda-name}}
15 |
16 | deploy-custom-runtime-lambda: build-lambda-zip
17 | aws cloudformation package \
18 | --template-file lambda.yml \
19 | --s3-bucket bucket-name \
20 | --s3-prefix folder-name \
21 | --output-template-file /tmp/lambda-packaged.yml
22 | aws cloudformation deploy \
23 | --template-file /tmp/lambda-packaged.yml \
24 | --stack-name $(stack) \
25 | --capabilities CAPABILITY_IAM \
26 | --no-fail-on-empty-changeset
27 | rm function.zip
28 |
29 | deploy-lambda-via-container:
30 | docker build --target deployer \
31 | --build-arg AWS_DEFAULT_REGION=${MY_AWS_DEFAULT_REGION} \
32 | --build-arg AWS_ACCESS_KEY_ID=${MY_AWS_ACCESS_KEY_ID} \
33 | --build-arg AWS_SECRET_ACCESS_KEY=${MY_AWS_SECRET_ACCESS_KEY} \
34 | --build-arg S3_BUCKET=$(or ${MY_S3_BUCKET}, "bucket-name") \
35 | --build-arg S3_FOLDER=$(or ${MY_S3_FOLDER}, "folder-name") \
36 | --build-arg STACK_NAME="lambda-custom-runtime-{{lambda-name}}" \
37 | -t lambda-deployer .
38 |
39 | invoke-function:
40 | aws lambda invoke --function-name {{lambda-name}} --payload '{"text":"Hello"}' /dev/stdout
41 |
--------------------------------------------------------------------------------
/resources/leiningen/new/clojure_graalvm_aws_lambda/README.md:
--------------------------------------------------------------------------------
1 | # {{name}}
2 |
3 | Custom AWS Lambda runtime with the Clojure `tools-deps` application compiled with GraalVM `native-image`.
4 |
5 | ## TL;DR
6 |
7 | Set these environment variables:
8 | - MY_AWS_DEFAULT_REGION
9 | - MY_AWS_ACCESS_KEY_ID
10 | - MY_AWS_SECRET_ACCESS_KEY
11 | - MY_S3_BUCKET
12 | - MY_S3_FOLDER
13 |
14 | Run:
15 | ```
16 | make deploy-lambda-via-container
17 | ```
18 |
19 | Lambda is ready to be used. Go to your AWS Console to work with the new stack named `lambda-custom-runtime-{{lambda-name}}`.
20 |
21 | ## What is in the package
22 |
23 | Most common operations are scripted in the Makefile:
24 | - `make build-native-image`: inside the docker with the GraalVM builds native binary for the lambda and copies it to the project folder two files: `server` and `libsunec.so`
25 | - `make build-lambda-zip`: inside the docker creates a custom AWS lambda runtime zip archive and copies it to the project folder as a `function.zip` file (for manual deployments).
26 | - `make deploy-custom-runtime-lambda`: (use it if you have `awscli` installed and configured) builds a deployable lambda zip and deploys it to AWS.
27 | - `make deploy-lambda-via-container`: builds lambda zip and deploys to AWS with your provided credentials (set the environment variables (e.g. `(export MY_AWS_DEFAULT_REGION=eu-west-1 && && make deploy-lambda-via-container)` or use something like [dotenv](https://github.com/robbyrussell/oh-my-zsh/tree/master/plugins/dotenv), or some other way).
28 |
29 | ## Prerequisites
30 |
31 | This demo assumes that docker is available in the system.
32 |
33 | ## Usage
34 |
35 | - Modify `request->response` function to do the work you need.
36 | - When needed add libraries to the `deps.edn` (with a little more work Git deps from private git repositories can be achieved).
37 | - Build native image with GraalVM inside a docker
38 | - Configure CI (for environment variables)
39 | - Deploy to AWS Lambda
40 |
41 | ```clojure
42 | (defn request->response [request]
43 | (let [decoded-request (json/read-value request read-mapper)]
44 | (json/write-value-as-string decoded-request)))
45 | ```
46 |
47 | ## CI/CD
48 |
49 | `.gitlab-ci.yml` file includes a demo on how the lambda can be deployed from the Gitlab CI pipeline. Create environment variables in the CI/CD setup of your project.
50 |
--------------------------------------------------------------------------------
/resources/leiningen/new/clojure_graalvm_aws_lambda/bootstrap:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -euo pipefail
3 | ./server
4 |
--------------------------------------------------------------------------------
/resources/leiningen/new/clojure_graalvm_aws_lambda/core.clj:
--------------------------------------------------------------------------------
1 | (ns lambda.core
2 | (:gen-class)
3 | (:require [jsonista.core :as json]
4 | [org.httpkit.client :as http]))
5 |
6 | (def read-mapper (json/object-mapper {:decode-key-fn true}))
7 |
8 | (defn get-lambda-invocation-request [runtime-api]
9 | @(http/request
10 | {:method :get
11 | :url (str "http://" runtime-api "/2018-06-01/runtime/invocation/next")
12 | :timeout 900000}))
13 |
14 | (defn send-response [runtime-api lambda-runtime-aws-request-id response-body]
15 | @(http/request
16 | {:method :post
17 | :url (str "http://" runtime-api "/2018-06-01/runtime/invocation/" lambda-runtime-aws-request-id "/response")
18 | :body response-body
19 | :headers {"Content-Type" "application/json"}}))
20 |
21 | (defn send-error [runtime-api lambda-runtime-aws-request-id error-body]
22 | @(http/request
23 | {:method :post
24 | :url (str "http://" runtime-api "/2018-06-01/runtime/invocation/" lambda-runtime-aws-request-id "/error")
25 | :body error-body
26 | :headers {"Content-Type" "application/json"}}))
27 |
28 | (defn request->response [request services]
29 | (let [decoded-request (json/read-value request read-mapper)]
30 | (json/write-value-as-string decoded-request)))
31 |
32 | (defn -main [& _]
33 | (let [runtime-api (System/getenv "AWS_LAMBDA_RUNTIME_API")
34 | services {}] ; initialize Lambda resources (e.g. DB connections) here
35 | (while true
36 | (let [{request-body :body
37 | {:keys [lambda-runtime-aws-request-id]} :headers
38 | error :error} (get-lambda-invocation-request runtime-api)]
39 | (when error
40 | (send-error runtime-api lambda-runtime-aws-request-id (str error))
41 | (throw (Exception. (str error))))
42 | (try
43 | (send-response runtime-api
44 | lambda-runtime-aws-request-id
45 | (request->response request-body services))
46 | (catch Exception e
47 | (.printStackTrace e)
48 | (send-error runtime-api lambda-runtime-aws-request-id (str (.getMessage e)))))))))
49 |
--------------------------------------------------------------------------------
/resources/leiningen/new/clojure_graalvm_aws_lambda/deps.edn:
--------------------------------------------------------------------------------
1 | {:deps {org.clojure/clojure {:mvn/version "1.10.1"}
2 | http-kit/http-kit {:mvn/version "2.3.0"}
3 | metosin/jsonista {:mvn/version "0.2.4"}}
4 | :paths ["src" "resources"]
5 | :mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"}
6 | "clojars" {:url "https://repo.clojars.org/"}}
7 | :aliases {:native-image
8 | {:extra-deps {luchiniatwork/cambada {:mvn/version "1.0.2"}}
9 | :main-opts ["-m" "cambada.native-image"
10 | "-m" "lambda.core"
11 | "-a" "lambda.core"
12 | "-O" "-initialize-at-build-time"
13 | "-O" "-static"
14 | "-O" "-enable-all-security-services"
15 | "-O" "-initialize-at-run-time=org.httpkit.client.SslContextFactory"
16 | "-O" "-initialize-at-run-time=org.httpkit.client.HttpClient"]}}}
17 |
--------------------------------------------------------------------------------
/resources/leiningen/new/clojure_graalvm_aws_lambda/gitignore:
--------------------------------------------------------------------------------
1 | pom.xml
2 | pom.xml.asc
3 | *.jar
4 | *.class
5 | /lib/
6 | /classes/
7 | /target/
8 | /checkouts/
9 | .lein-deps-sum
10 | .lein-repl-history
11 | .lein-plugins/
12 | .lein-failures
13 | .nrepl-port
14 | .cpcache/
15 | .idea/
16 | **/*.iml
17 | server
18 | .env
19 |
--------------------------------------------------------------------------------
/resources/leiningen/new/clojure_graalvm_aws_lambda/gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - deploy
3 |
4 | deploy-lambda:
5 | stage: deploy
6 | image: docker:stable
7 | when: manual
8 | variables:
9 | DOCKER_HOST: tcp://docker:2375/
10 | services:
11 | - docker:dind
12 | script:
13 | - >
14 | docker build
15 | --target deployer
16 | --build-arg AWS_DEFAULT_REGION=$MY_AWS_DEFAULT_REGION
17 | --build-arg AWS_ACCESS_KEY_ID=$MY_AWS_ACCESS_KEY_ID
18 | --build-arg AWS_SECRET_ACCESS_KEY=$MY_AWS_SECRET_ACCESS_KEY
19 | --build-arg S3_BUCKET=$MY_S3_BUCKET
20 | --build-arg S3_FOLDER=$MY_S3_FOLDER
21 | --build-arg STACK_NAME="lambda-custom-runtime-{{lambda-name}}"
22 | -t lambda-deployer .
23 |
--------------------------------------------------------------------------------
/resources/leiningen/new/clojure_graalvm_aws_lambda/lambda.yml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Transform: AWS::Serverless-2016-10-31
3 |
4 | Resources:
5 | {{lambda-name}}:
6 | Type: AWS::Serverless::Function
7 | Properties:
8 | Timeout: 60
9 | Tracing: "Active"
10 | MemorySize: 3008
11 | Handler: not.used
12 | Runtime: provided
13 | CodeUri: function.zip
14 | Policies:
15 | - AWSLambdaExecute
16 |
17 | {{lambda-name}}Logs:
18 | Type: AWS::Logs::LogGroup
19 | Properties:
20 | LogGroupName: !Sub /aws/lambda/${ {{lambda-name}} }
21 | RetentionInDays: 1
22 |
--------------------------------------------------------------------------------
/src/leiningen/new/clojure_graalvm_aws_lambda.clj:
--------------------------------------------------------------------------------
1 | (ns leiningen.new.clojure-graalvm-aws-lambda
2 | (:require [clojure.string :as s]
3 | [leiningen.new.templates :refer [renderer name-to-path ->files]]
4 | [leiningen.core.main :as main]))
5 |
6 | (def render (renderer "clojure-graalvm-aws-lambda"))
7 |
8 | (defn name-to-lambda-name [name]
9 | (let [[x & xs] (s/split name #"-")]
10 | (str x (s/join (map s/capitalize xs)))))
11 |
12 | (defn clojure-graalvm-aws-lambda [name]
13 | (let [data {:name name
14 | :sanitized (name-to-path name)
15 | :lambda-name (name-to-lambda-name name)}]
16 | (main/info "Generating fresh 'lein new' clojure-graalvm-aws-lambda project.")
17 | (->files data
18 | ["src/lambda/core.clj" (render "core.clj" data)]
19 | [".gitignore" (render "gitignore" data)]
20 | [".gitlab-ci.yml" (render "gitlab-ci.yml" data)]
21 | ["bootstrap" (render "bootstrap" data)]
22 | ["deps.edn" (render "deps.edn" data)]
23 | ["Dockerfile" (render "Dockerfile" data)]
24 | ["lambda.yml" (render "lambda.yml" data)]
25 | ["Makefile" (render "Makefile" data)]
26 | ["README.md" (render "README.md" data)])))
27 |
--------------------------------------------------------------------------------