Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
grav committed Dec 19, 2018
0 parents commit 8d7f510
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 0 deletions.
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"

97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 [email protected]: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"
}
}
}
```
8 changes: 8 additions & 0 deletions bootstrap
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions my_package/my_ns.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
(ns my-package.my-ns)

(defn my-handler [{:keys [_event _context]
:as input}]
{:hello "Hello from my-handler!"
:input input})
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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://[email protected]/mikkelg/aws-lumo-cljs-runtime.git"
},
"author": "Mikkel Gravgaard <[email protected]> (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"
}
}
18 changes: 18 additions & 0 deletions runloop.js
Original file line number Diff line number Diff line change
@@ -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();

82 changes: 82 additions & 0 deletions runtime.cljs
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 8d7f510

Please sign in to comment.