AWS Compute Blog
Writing AWS Lambda Functions in Clojure
Tim Wagner, AWS Lambda General Manager
Bryan Moffatt, AWS Lambda Software Developer
AWS Lambda’s Java support also makes it easy to write Lambda functions in other jvm-based languages. Previously we looked at doing this for Scala; today’ll we’ll see how it can be done with Clojure.
Getting Started with Clojure
We’ll build our Clojure project with Leiningen, but you can use Boot, Maven, or another build tool as well. To follow along below, make sure you’ve first installed leiningen, that lein is on your path (for example):
PATH=%PATH%;C:\Users\timwagne\.lein\bin
and that you have a Java 8 SDK installed.
Next, open a command line prompt where you want to do your development (for example, “C:\tmp\clojure-demo”) and create the following directory structure:
C:\tmp\clojure-demo src java
At the same level as src, create a file named ‘project.clj’ with the following content:
(defproject lambda-clj-examples "0.1.0"
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/data.json "0.2.6"]
[com.amazonaws/aws-lambda-java-core "1.0.0"]]
:java-source-paths ["src/java"]
:aot :all)
Ok, time to write some code…
Clojure Meets Lambda: Hello World!
Let’s start with the classic: Create a file in your src directory named ‘hello.clj’ with the following content:
(ns hello
(:gen-class
:methods [^:static [handler [String] String]]))
(defn -handler [s]
(str "Hello " s "!"))
Now at the root of your tree, execute
lein uberjar
When this completes, it should have created a subdirectory called ‘target’ containing the file ‘lambda-clj-examples-0.1.0-standalone.jar’, which is ready to be uploaded to AWS Lambda.
You can use a command line or the Lambda console to do the upload/creation. If you’re using the cli, the command will look like the following, but you’ll need to use a valid role argument (this also assumes you’re currently in the clojure-demo directory). If you use the console, the handler is the same as the one in the command below (“hello::handler”).
$ aws lambda create-function \
--function-name clj-hello \
--handler hello::handler \
--runtime java8 \
--memory 512 \
--timeout 10 \
--role arn:aws:iam::awsaccountid:role/lambda_exec_role \
--zip-file fileb://./target/lambda-clj-examples-0.1.0-standalone.jar
You can invoke and test from the command line or the console; the console view looks like this if I test it with a sample input of “Tim” (including the quotes):
Fun with Java
With HelloWorld under our belt, let’s tackle Java integration, as a first step on the road toward processing some Amazon S3 events. First, let’s extend our code slightly as follows:
(ns hello
(:gen-class
:methods [^:static [handler [String] String]]))
(defn -handler [s]
(str "Hello " s "!"))
; Add POJO handling
(defn -handlepojo [this event]
(str "Hello " (.getFirstName event) " " (.getLastName event)))
(gen-class
:name PojoHandler
:methods [[handlepojo [example.MyEvent] String]])
Next, create a subdirectory called “example” in src/java, and in it create a file called “MyEvent.java” with the following content:
package example;
public class MyEvent {
private String firstName;
private String lastName;
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getFirstName() {
return firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getLastName() {
return lastName;
}
}
Again in ‘C:\tmp\clojure-demo’, execute
lein uberjar
and create a new Lambda function from the command line or console; the cli command looks like
$ aws lambda create-function \
--function-name clj-hellopojo \
--handler PojoHandler::handlepojo \
--runtime java8 \
--memory 512 \
--timeout 10 \
--role arn:aws:iam::awsaccountid:role/lambda_exec_role \
--zip-file fileb://./target/lambda-clj-examples-0.1.0-standalone.jar
if you test this this function with Lambda test input like
{
firstName: "Tim",
lastName: "Wagner"
}
you should get a response like “Hello Tim Wagner”. Now that we have Java integration working, let’s tackle a more real-world example.
Processing an Amazon S3 Event in Clojure
Now we’ll process a more interesting type – a bucket notification sent by Amazon S3 when an object is added. We’ll also see how to make a POJO (in this case the S3 event class) fit more gracefully into Clojure.
The S3 event itself is fairly complex; here’s the sample event from the Lambda console:
{
"Records": [
{
"eventVersion": "2.0",
"eventSource": "aws:s3",
"awsRegion": "us-east-1",
"eventTime": "1970-01-01T00:00:00.000Z",
"eventName": "ObjectCreated:Put",
"userIdentity": {
"principalId": "EXAMPLE"
},
"requestParameters": {
"sourceIPAddress": "127.0.0.1"
},
"responseElements": {
"x-amz-request-id": "C3D13FE58DE4C810",
"x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"
},
"s3": {
"s3SchemaVersion": "1.0",
"configurationId": "testConfigRule",
"bucket": {
"name": "sourcebucket",
"ownerIdentity": {
"principalId": "EXAMPLE"
},
"arn": "arn:aws:s3:::mybucket"
},
"object": {
"key": "HappyFace.jpg",
"size": 1024,
"eTag": "d41d8cd98f00b204e9800998ecf8427e"
}
}
}
]
}
We could tackle this just like we handled the name POJO in the previous section. To do that, add
[com.amazonaws/aws-lambda-java-events "1.0.0"]
to the list of dependencies in your lein configuration and proceed as we did above for the name POJO.
Alternatively, we can treat this complex type in a more idiomatic way within Clojure. We’ll need to integrate with Lambda’s Java environment using a raw stream handler and then craft a “native” type by parsing the content. To see this in action, let’s create a new file in src called “stream_handler.clj” that contains the following:
(ns stream-handler
(:gen-class
:implements [com.amazonaws.services.lambda.runtime.RequestStreamHandler])
(:require [clojure.data.json :as json]
[clojure.string :as s]
[clojure.java.io :as io]
[clojure.pprint :refer [pprint]]))
(defn handle-event [event]
(pprint event)
{:who-done-it (get-in event [:records 0 :request-parameters :source-ip-address])
:bucket-owner (get-in event [:records 0 :s3 :bucket :owner-identity :principal-id])})
(defn key->keyword [key-string]
(-> key-string
(s/replace #"([a-z])([A-Z])" "$1-$2")
(s/replace #"([A-Z]+)([A-Z])" "$1-$2")
(s/lower-case)
(keyword)))
(defn -handleRequest [this is os context]
(let [w (io/writer os)]
(-> (json/read (io/reader is) :key-fn key->keyword)
(handle-event)
(json/write w))
(.flush w)))
Again, build with
lein uberjar
and then upload with a command (or console actions) like
$ aws lambda create-function \
--function-name clj-s3 \
--handler stream_handler \
--runtime java8 \
--memory 512 \
--timeout 10 \
--role arn:aws:iam::awsaccountid:role/lambda_exec_role \
--zip-file fileb://./target/lambda-clj-examples-0.1.0-standalone.jar
To test this in the Lambda console, configure the sample event template to be “S3 Put” (which is shown above) and invoke it. You should get
{
"who-done-it": "127.0.0.1",
"bucket-owner": "EXAMPLE"
}
as a result.
You can see the “Clojure-ified” form of the S3 event in the logs; here’s what it looks like if you’re testing in the Lambda console:
START RequestId: 69dee059-27eb-11e5-89ff-5113959a98f1
{:records
[{:event-name "ObjectCreated:Put",
:event-version "2.0",
:event-source "aws:s3",
:response-elements
{:x-amz-request-id "C3D13FE58DE4C810",
:x-amz-id-2
"FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"},
:aws-region "us-east-1",
:event-time "1970-01-01T00:00:00.000Z",
:user-identity {:principal-id "EXAMPLE"},
:s3
{:s3schema-version "1.0",
:configuration-id "testConfigRule",
:bucket
{:name "sourcebucket",
:owner-identity {:principal-id "EXAMPLE"},
:arn "arn:aws:s3:::mybucket"},
:object
{:key "HappyFace.jpg",
:size 1024,
:e-tag "d41d8cd98f00b204e9800998ecf8427e"}},
:request-parameters {:source-ip-address "127.0.0.1"}}]}
END RequestId: 69dee059-27eb-11e5-89ff-5113959a98f1
We hope this article helps developers who love Clojure get started using it in Lambda. Happy Lambda (and Clojure) coding!
-Tim and Bryan
Follow Tim’s Lambda adventures on Twitter