Increasingly Functional.
by Joshua Miller | on Twitter | on the web | on github

Serverless Web Applications on AWS Lambda with ClojureScript Part I

June 29th 2017

Tagged: clojure clojurescript

Maybe you've heard all the hype about serverless computing. Maybe you've heard disturbing rumors that serverless apps do in fact run, somewhere, somehow, on servers. Maybe you were ok with all that, but not with writing AWS Lambda functions in Java or JavaScript. Me too. Let's do it in Clojure.

First, the use case: Lambda is great for infrequently-called or bursty services. Data processing pipelines, as an example, are a perfect function to be moved to Lambda. You don't need to worry about provisioning capacity for big jobs, or paying for an idle server when not in use. Similarly, very small web services that don't require on-disk operations or shared resources can run (or sleep!) happily on Lambda.

Second, the choice between JVM Clojure and ClojureScript: Even with AOT compilation, the JVM warmup penalty compared to Node.js is huge. Once invoked, Lambda will keep your image warm for an indeterminate (about five minutes, in my experience) period of time, but after that, the next invocation will once again require sometimes something like 15 seconds to get going again. If your function is doing something like chewing through big amounts of data and throwing the results into S3 or a database, that's not a problem. If you need to quickly respond to a web request, it is.

So even though JVM Clojure is easier to write and reliably deploy (just package up a .jar and upload it), we're going to be writing a ClojureScript web service here. To demonstrate how to integrate Node libraries with ClojureScript, the service will take a JSON payload via post and return it signed as a JSON Web Token. We'll use cljs-lambda to get started:

$ lein new cljs-lamba jwtify
Generating fresh 'lein new' cljs-lambda project.
$ cd jwtify/

Now we've got a skeleton of a ClojureScript Lambda project we can flesh out to suit our purposes. We'll need to add two dependencies that we're going to use to project.clj: [bidi "2.1.1"] in :dependencies, and [jsonwebtoken "7.4.1"] in :npm -> :dependencies. Running lein deps now will pull in both of those, using npm to install the jsonwebtoken library.

The src/jwtify/core.cljs file generated by lein cljs-lambda comes with a promise-based demonstration, but we don't need it. We'll be using simple core.async channels to handle the asynchronous pattern Node.js requires, so let's replace all of that and start new:

(ns jwtify.core
  (:require [cljs-lambda.macros :refer-macros [defgateway]]))

(defgateway jwtify
  [& args]
  {:status 200
   :body "{\"hello\": \"there\"}"})

This is the minimal amount of code that will get us a an entry point into a function from Amazon's API Gateway ‎and return a status and a JSON payload.

To test it out, we can create a dev/repl.clj file that looks like:

(require
  '[cljs.repl :as repl]
  '[cljs.repl.node :as node])

(repl/repl* (node/repl-env)
  {:output-dir "target/jwtify"
   :optimizations :none
   :cache-analysis true
   :source-map true})

Then run rlwrap lein trampoline run -m clojure.main dev/repl.clj to get yourself into a ClojureScript/Node.js REPL, and try:

(require '[jwtify.core :refer [jwtify]] '[cljs-lambda.local :refer [channel]] '[cljs.core.async :refer [take!]])
(channel jwtify {})
(take! *1 println)
;; => [:succeed {:body {"hello": "there"}, :statusCode 200}]

What we're doing with channel is locally simulating a Lambda event that our function will eventually receive in real life and putting the result on a core.async channel.

To recap: We've set up a ClojureScript/Node.js environment with the cljs-lambda library that knows how to interpret AWS Lambda events and return responses in the esoteric format Lambda and the API Gateway expect.

In part two, we'll use bidi to set up routes and response handlers for our web service, and jsonwebtoken to generate our JWT payloads. In part three, we'll see how to put it all together and deploy it to The Cloud.