From 8d7f510ed56091664ab3f42bbc7e8a7146b4faa7 Mon Sep 17 00:00:00 2001 From: Mikkel Gravgaard Date: Sat, 15 Dec 2018 23:44:44 +0100 Subject: [PATCH] initial commit --- Dockerfile | 14 +++++++ README.md | 97 +++++++++++++++++++++++++++++++++++++++++++ bootstrap | 8 ++++ my_package/my_ns.cljs | 6 +++ package.json | 22 ++++++++++ runloop.js | 18 ++++++++ runtime.cljs | 82 ++++++++++++++++++++++++++++++++++++ 7 files changed, 247 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100755 bootstrap create mode 100644 my_package/my_ns.cljs create mode 100644 package.json create mode 100644 runloop.js create mode 100644 runtime.cljs diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ba65b67 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM amazonlinux + +RUN yum install -y tar gunzip wget git java-1.8.0-openjdk-1.8.0.171 + +RUN bash -c "cd /usr/local/bin && curl -fsSLo boot https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh && chmod 755 boot" + +RUN curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | tee /etc/yum.repos.d/yarn.repo +RUN curl --silent --location https://rpm.nodesource.com/setup_10.x | bash - + +RUN yum install -y yarn gcc-c++ make glibc-static +VOLUME /root/.m2 +WORKDIR /lumo +ENTRYPOINT bash -c "BOOT_AS_ROOT=yes boot release" + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d9530c --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +This repo contains an implementation of a [custom AWS Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) +that enables executions of [ClojureScript](http://clojurescript.org) code in AWS Lambda without any pre-compilation. + +It relies on the awesome [Lumo](https://github.com/anmonteiro/lumo) project, and +was inspired by [this episode of The Repl podcast](https://www.therepl.net/episodes/14/). + +It's based on the [Tutorial from Amazon](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-walkthrough.html) +as well as the [Node 10.x/11.x implementations from LambCI](https://github.com/lambci/node-custom-lambda). + +It's still very alpha, but this document contains a step-by-step guide for getting things started. + + +### Clone Lumo fork +The fork of Lumo at https://github.com/grav/lumo is prepared for creating a +static build of Lumo: + +``` +git clone git@github.com:grav/lumo +``` + +### Build Lumo + +Build Docker image: +``` +docker build . -t ami-lumo +``` + +Build Lumo, pointing out the fork of Lumo: +``` +docker run -v --rm ami-lumo \ + -v /path/to/lumo:/lumo +``` + +You'll get an error in the end, but an executable will nevertheless be created in `build/lumo`. + +### Create the runtime archive +``` +zip -j runtime.zip bootstrap /path/to/lumo/build/lumo runtime.cljs +``` + +The flag `-j` just ignores paths and puts everything in the archive root. + +### Publish layer + +A layer can be used by a lambda to pull in additional code. In this context, the layer contains the actual runtime: + +``` +aws lambda publish-layer-version --layer-name lumo-runtime --zip-file fileb://runtime.zip +``` + +You'll get an `arn` with a layer version back, which you'll need when configurating the lambda. + +### Create function archive +``` +zip -r function.zip my_package +``` + +The `my_package` dir in this repo contains a simple handler, but you can provide your own. + +### Create the lambda + +For the `--role` parameter, you must supply a role that can execute lambdas. +See https://docs.aws.amazon.com/lambda/latest/dg/runtimes-walkthrough.html#runtimes-walkthrough-function + +The `--handler` parameter must correspond to the directory structure of the ClojureScript code that you provide: + +``` +aws lambda create-function --function-name test-lumo --zip-file fileb://function.zip --handler my-package.my-ns/my-handler --runtime provided --role arn:aws:iam::xxx:role/lambda-role +``` + +Use the layer `arn` that you received when publishing the layer, including the layer version, to configure the lambda: + +``` +aws lambda update-function-configuration --function-name test-lumo --layers arn:aws:lambda:eu-west-1:xxx:layer:lumo-runtime:1 +``` + +### Invoke the lambda +``` +aws lambda invoke --function-name test-lumo --payload '{"foo":42}' response.txt +``` + +You should receive something like this in `response.txt`: + +``` +{ + "hello": "Hello from my-handler!", + "input": { + "event": { + "foo": 42 + }, + "context": { + "aws-request-id": "b64259ce-03e0-11e9-8db3-1bbff8d08d21", + "lambda-runtime-invoked-function-arn": "arn:aws:lambda:eu-west-1:xxx:function:test-lumo" + } + } +} +``` \ No newline at end of file diff --git a/bootstrap b/bootstrap new file mode 100755 index 0000000..5cc0046 --- /dev/null +++ b/bootstrap @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail + +APP_PARTS=(${_HANDLER//\// }) +tmpfile=`mktemp` +echo "(require '${APP_PARTS[0]})" | cat - /opt/runtime.cljs > $tmpfile + +/opt/lumo $tmpfile \ No newline at end of file diff --git a/my_package/my_ns.cljs b/my_package/my_ns.cljs new file mode 100644 index 0000000..66ba388 --- /dev/null +++ b/my_package/my_ns.cljs @@ -0,0 +1,6 @@ +(ns my-package.my-ns) + +(defn my-handler [{:keys [_event _context] + :as input}] + {:hello "Hello from my-handler!" + :input input}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..24b7450 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "aws-lumo-cljs-runtime", + "version": "1.0.0", + "description": "", + "main": "runloop.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@gitlab.com/mikkelg/aws-lumo-cljs-runtime.git" + }, + "author": "Mikkel Gravgaard (https://github.com/grav)", + "license": "ISC", + "bugs": { + "url": "https://gitlab.com/mikkelg/aws-lumo-cljs-runtime/issues" + }, + "homepage": "https://gitlab.com/mikkelg/aws-lumo-cljs-runtime#README", + "dependencies": { + "lumo-cljs": "^1.9.0" + } +} diff --git a/runloop.js b/runloop.js new file mode 100644 index 0000000..8420970 --- /dev/null +++ b/runloop.js @@ -0,0 +1,18 @@ +function sleep(s){ + return new Promise(function(resolve,reject) { + setTimeout(_ => { + resolve(); + }, 1000 * s); + }); +} + +async function start(){ + while(true){ + console.log("while start") + await sleep(2); + } + +} + +start(); + diff --git a/runtime.cljs b/runtime.cljs new file mode 100644 index 0000000..07a0c35 --- /dev/null +++ b/runtime.cljs @@ -0,0 +1,82 @@ +(require 'http) +(require 'clojure.string) + +(def runtime-path (str "http://" (.-AWS_LAMBDA_RUNTIME_API js/process.env) "/2018-06-01/runtime")) + +(defn request [{:keys [url method headers body] + :or {method :get}}] + (js/Promise. + (fn [resolve reject] + (let [headers (merge headers + (when body + {"Content-Length" (js/Buffer.byteLength body)})) + request (http/request + url + (clj->js {:method (clojure.string/upper-case (name method)) + :headers headers}) + (fn [response] + (let [s (atom nil)] + (.on response "data" (fn [chunk] + (swap! s conj (.toString chunk "utf8")))) + (.on response "end" (fn [] + (resolve {:body (apply str @s) + :status (.-statusCode response) + :headers (js->clj (.-headers response))}))) + (.on response "error" reject))))] + (.on request "error" reject) + (when body + (.write request body)) + (.end request))))) + +(def handle + (eval (symbol (.-_HANDLER js/process.env)))) + +(defn post-error [{error :error + {aws-request-id :aws-request-id} :context}] + (let [url (str runtime-path "/invocation/" aws-request-id "/error")] + (-> (request {:url url + :headers {"Content-Type" "application/json" + "Lambda-Runtime-Function-Error-Type" (.-name error)} + :body (js/JSON.stringify #js {:errorType (.-name error) + :errorMessage (.-message error) + :stackTrace (-> (or (.-stack error) "") + (.split "\n") + (.slice 1))})}) + (.then (fn [{:keys [status] + :as res}] + (assert (= status 200) + (str "Unexpected " url "response: " (js/JSON.stringify res))) + res))))) + +(defonce state (atom nil)) + +(defn start [] + (-> (request {:url (str runtime-path "/invocation/next")}) + (.then (fn [{:keys [status body] + {aws-request-id "lambda-runtime-aws-request-id" + lambda-runtime-invoked-function-arn "lambda-runtime-invoked-function-arn"} :headers + :as response}] + (let [context {:aws-request-id aws-request-id + :lambda-runtime-invoked-function-arn lambda-runtime-invoked-function-arn}] + (swap! state assoc :context context) + (assert (= status 200) (str "Unexpected /invocation/next response: " (pr-str response))) + {:event (-> (js/JSON.parse body) + js->clj) + :context context}))) + (.then handle) + (.then (fn [response] + (let [{:keys [aws-request-id]} (:context @state)] + (request {:url (str runtime-path "/invocation/" aws-request-id "/response") + :method :post + :headers {"Content-Type" "application/json"} + :body (-> (clj->js response) + js/JSON.stringify)})))) + (.then (fn [{:keys [status] + :as response}] + (assert (= status 202) (str "Unexpected /invocation/response response:" (pr-str response))))) + (.catch (fn [err] + (post-error {:error err + :context (:context @state)}))) + (.then start))) + +(start)