├── .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 | image/svg+xml -------------------------------------------------------------------------------- /.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 | [![Clojars Project](https://img.shields.io/clojars/v/clojure-graalvm-aws-lambda/lein-template.svg)](https://clojars.org/clojure-graalvm-aws-lambda/lein-template) 8 | [![pipeline status](https://gitlab.com/Jocas/clojure-graalvm-aws-lambda-template/badges/master/pipeline.svg)](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 | --------------------------------------------------------------------------------