{"id":13602663,"url":"https://github.com/igrishaev/lambda","last_synced_at":"2025-10-28T03:02:54.171Z","repository":{"id":150775803,"uuid":"621890491","full_name":"igrishaev/lambda","owner":"igrishaev","description":"An AWS Lambda in a single binary file","archived":false,"fork":false,"pushed_at":"2023-04-07T19:04:18.000Z","size":9209,"stargazers_count":10,"open_issues_count":4,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-01T20:51:25.981Z","etag":null,"topics":["aws","clojure","graalvm","lambda","native-image"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/igrishaev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-03-31T15:56:46.000Z","updated_at":"2025-03-12T11:29:36.000Z","dependencies_parsed_at":"2024-04-16T01:45:54.671Z","dependency_job_id":"02cdb6c5-ca41-4657-abbb-b25f5e7b16e3","html_url":"https://github.com/igrishaev/lambda","commit_stats":null,"previous_names":["igrishaev/lambda-demo"],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Flambda","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Flambda/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Flambda/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Flambda/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/igrishaev","download_url":"https://codeload.github.com/igrishaev/lambda/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250195054,"owners_count":21390230,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aws","clojure","graalvm","lambda","native-image"],"created_at":"2024-08-01T18:01:33.201Z","updated_at":"2025-10-28T03:02:54.102Z","avatar_url":"https://github.com/igrishaev.png","language":"Clojure","funding_links":[],"categories":["clojure"],"sub_categories":[],"readme":"# Lambda\n\nA small framework to run AWS Lambdas compiled with Native Image.\n\n## Table of Contents\n\n\u003c!-- toc --\u003e\n\n- [Motivation \u0026 Benefits](#motivation--benefits)\n- [Installation](#installation)\n- [Writing Your Lambda](#writing-your-lambda)\n  * [Prepare The Code](#prepare-the-code)\n  * [Compile It](#compile-it)\n    + [Linux (Local Build)](#linux-local-build)\n    + [On MacOS (Docker)](#on-macos-docker)\n  * [Create a Lambda in AWS](#create-a-lambda-in-aws)\n  * [Configuration](#configuration)\n  * [Deploy and Test It](#deploy-and-test-it)\n- [Ring Support (Serving HTTP events)](#ring-support-serving-http-events)\n- [Gzip Support for Ring](#gzip-support-for-ring)\n- [Sharing the State Between Events](#sharing-the-state-between-events)\n- [Component Support](#component-support)\n- [Demo](#demo)\n- [Misc](#misc)\n\n\u003c!-- tocstop --\u003e\n\n## Motivation \u0026 Benefits\n\n[search]: https://clojars.org/search?q=lambda\n\nThere are a lot of Lambda Clojure libraries so far: a [quick search][search] on\nClojars gives several screens of them. What is the point of making a new one?\nWell, because none of the existing libraries covers my requirements, namely:\n\n- I want a framework free from any Java SDK, but pure Clojure only.\n- I want it to compile into a single binary file so no environment is needed.\n- The deployment process must be extremely simple.\n\nAs the result, *this* framework:\n\n- Narrow dependencies to keep the output file as thin as possible;\n- Provides an endless loop that consumes events from AWS and handles them. You\n  only submit a function that processes an event.\n- Provides a Ring middleware that turns HTTP events into a Ring handler. Thus,\n  you can easily serve HTTP requests with Ring stack.\n- Has a built-in logging facility.\n- Stuart Sierra's Component library support.\n- Provides a bunch of Make commands to build a zipped bootstrap file.\n\n## Installation\n\nLeiningen/Boot\n\n```\n[com.github.igrishaev/lambda \"0.1.6\"]\n```\n\nClojure CLI/deps.edn\n\n```\ncom.github.igrishaev/lambda {:mvn/version \"0.1.6\"}\n```\n\n## Writing Your Lambda\n\n### Prepare The Code\n\nCreate a core module with the following code:\n\n```clojure\n(ns demo.core\n  (:require\n   [lambda.log :as log]\n   [lambda.main :as main])\n  (:gen-class))\n\n(defn handler [event]\n  (log/infof \"Event is: %s\" event)\n  (process-event ...)\n  {:result [42]})\n\n(defn -main [\u0026 _]\n  (main/run handler))\n```\n\nThe `handler` function takes a single argument which is a parsed Lambda\npayload. The `lambda.log` namespace provides `debugf`, `infof`, and `errorf`\nmacros for logging. In the `-main` function you start an endless cycle by\ncalling the `run` function.\n\nOn each step of this cycle, the framework fetches a new event, processes it with\nthe passed handler and submits the result to AWS. Should the handler fail, it\ncatches an exception and reports it as well without interrupt the cycle. Thus,\nyou don't need to `try/catch` in your handler.\n\n### Compile It\n\nOnce you have the code, compile it with GraalVM and Native image. The `Makefile`\nof this repository has all the targets you need. You can borrow them with slight\nchanges. Here are the basic definitions:\n\n```make\nNI_TAG = ghcr.io/graalvm/native-image:22.2.0\n\nJAR = target/uberjar/bootstrap.jar\n\nPWD = $(shell pwd)\n\nNI_ARGS = \\\n\t--initialize-at-build-time \\\n\t--report-unsupported-elements-at-runtime \\\n\t--no-fallback \\\n\t-jar ${JAR} \\\n\t-J-Dfile.encoding=UTF-8 \\\n\t--enable-http \\\n\t--enable-https \\\n\t-H:+PrintClassInitialization \\\n\t-H:+ReportExceptionStackTraces \\\n\t-H:Log=registerResource \\\n\t-H:Name=bootstrap\n\nuberjar:\n\tlein \u003c...\u003e uberjar\n\nbootstrap-zip:\n\tzip -j bootstrap.zip bootstrap\n```\n\nPay attention to the following:\n\n- Ensure the jar name is set to `bootstrap.jar` in your project. This might be\n  done by setting these in your `project.clj`:\n\n```clojure\n{:target-path \"target/uberjar\"\n :uberjar-name \"bootstrap.jar\"}\n```\n\n- The `NI_ARGS` might be extended with resources, e.g. if you want an EDN config\n  file baked into the binary file.\n\nThen compile the project either on Linux natively or with Docker.\n\n#### Linux (Local Build)\n\nOn Linux, add the following Make targets:\n\n```make\ngraal-build:\n\tnative-image ${NI_ARGS}\n\nbuild-binary-local: ${JAR} graal-build\n\nbootstrap-local: uberjar build-binary-local bootstrap-zip\n```\n\nThen run `make bootstrap-local`. You'll get a file called `bootstrap.zip` with a single binary file `bootstrap` inside.\n\n#### On MacOS (Docker)\n\nOn MacOS, add these targets:\n\n```make\nbuild-binary-docker: ${JAR}\n\tdocker run -it --rm -v ${PWD}:/build -w /build ${NI_TAG} ${NI_ARGS}\n\nbootstrap-docker: uberjar build-binary-docker bootstrap-zip\n```\n\nRun `make bootstrap-docker` to get the same file but compiled in a Docker\nimage.\n\n### Create a Lambda in AWS\n\nCreate a Lambda function in AWS. For the runtime, choose a custom one called\n`provided.al2` which is based on Amazon Linux 2. The architecture (x86_64/arm64)\nshould match the architecture of your machine. For example, as I build the\nproject on Mac M1, I choose arm64.\n\n### Configuration\n\nThere are some options you can override with environment variables, namely:\n\n| Var                      | Default          | Comment                                         |\n|--------------------------|------------------|-------------------------------------------------|\n| `LAMBDA_RUNTIME_TIMEOUT` | 900000 (15 mins) | How long to wait when polling for a new event   |\n| `LAMBDA_RUNTIME_VERSION` | 2018-06-01       | Which Runtime API version to use                |\n| `AWS_LAMBDA_USE_GZIP`    | nil              | Forcibly gzip-encode Ring responses (see below) |\n\n### Deploy and Test It\n\nUpload the `bootstrap.zip` file from your machine to the lambda. With no\ncompression, the `bootstrap` file takes 25 megabytes. In zip, it's about 9\nmegabytes so you can skip uploading it to S3 first.\n\nTest you Lambda in the console to ensure it works.\n\n## Ring Support (Serving HTTP events)\n\nAWS Lambda can serve HTTP requests as events. Each HTTP request gets transformed\ninto a special message which your lambda processes. It must return another\nmessage that forms an HTTP response.\n\n[ring]: https://github.com/ring-clojure/ring\n\nThis library brings a number of middleware that turn a lambda into\n[Ring-compatible][ring] HTTP server.\n\nThere are the following middleware wrappers in the `lambda.ring` namespace:\n\n- `wrap-ring-event`: turns an incoming HTTP event into a Ring request map,\n  processes it and turns a Ring response map into an Lambda-compatible HTTP\n  message.\n\n- `wrap-ring-exception`: captures any uncaught exception happened while handling\n  an HTTP request. Log it and return an error response (500 Internal server\n  error).\n\n[ring-json]: https://github.com/ring-clojure/ring-json\n\nTo not depend on [ring-json][ring-json] (which in turn depends on Cheshire), we\nprovide our own tree middlware for incoming and outcoming JSON:\n\n- `wrap-json-body`: if the request was JSON, replace the `:body` field with\n  a parsed payload.\n\n- `wrap-json-params`: the same but puts the data into the `:json-params`\n  field. In addition, if the data was a map, merge it into the `:params` map.\n\n- `wrap-json-response`: if the body of the response was a collection, encode it\n  into a JSON string and add the Content-Type: application/json header.\n\n[jsam]: https://github.com/igrishaev/jsam\n\nThese three middleware mimic their counterparts from Ring-json but rely on the\nJSam library to keep dependencies as narrow as possible. Each middleware, in\naddition to a ring handler, accepts an optional map of JSON settings.\n\nThe following example shows how to build a stack of middleware properly:\n\n~~~clojure\n(ns some.demo\n  (:gen-class)\n  (:require\n   [lambda.main :as main]\n   [lambda.ring :as ring]))\n\n(defn handler [request]\n  (let [{:keys [request-method\n                uri\n                headers\n                body]}\n        request]\n    ;; you can branch depending on method and uri,\n    ;; or use compojure/reitit\n    {:status 200\n     :headers {\"foo\" \"bar\"}\n     :body {:some \"JSON date\"}}))\n\n(def fn-event\n  (-\u003e handler\n      (ring/wrap-json-body)\n      (ring/wrap-json-response)\n      (ring/wrap-ring-exception)\n      (ring/wrap-ring-event)))\n\n(defn -main [\u0026 _]\n  (main/run fn-event))\n~~~\n\nFor query- or form parameters, you can use classic `wrap-params`,\n`wrap-keyword-params`, and similar utilities from `ring.middleware.*`\nnamespaces. For this, introduce the `ring-core` library into your project.\n\n## Gzip Support for Ring\n\nThe library provides a special Ring middlware to handle gzip logic. Apply it as\nfollows:\n\n~~~clojure\n(def fn-event\n  (-\u003e handler\n      (ring/wrap-json-body)\n      (ring/wrap-json-response)\n      (ring/wrap-gzip) ;; -- this\n      (ring/wrap-ring-exception)\n      (ring/wrap-ring-event)))\n~~~\n\nThis is what the middleware does under the hood:\n\n- if a client sends a gzipped payload and the `Content-Encoding` header is\n  `gzip`, the incoming `:body` field gets wrapped with the `GzipInputStream`\n  class. By reading from it, you'll get the origin payload. Useful when sending\n  vast JSON objects to Lambda via HTTP.\n\n- If a client sends a header `Accept-Encoding` with `gzip` inside, the body of a\n  response gets gzipped, and the `Content-Encoding: gzip` header is set. It\n  greatly saves traffic. In addition, remember about a limitation in AWS: a\n  response cannot exceed 6Mbs. Gzipping helps bypass this limit.\n\n- If there is a non-empty env var `AWS_LAMBDA_USE_GZIP` set for this Lambda, the\n  response is always gzipped no matter what client specifies in the\n  `Accept-Encoding` header.\n\nAlthough enabling gzip looks trivial, missing it might lead to very strange\nthings. Personally I spent several a couple of days investigating an issue when\nAWS says \"the content was too large\". Turned out, the culprit was **double JSON\nencoding**. When you return JSON from Ring, you encode it once. But when Lambda\nruntime sends this message to AWS, it gets JSON-encoded again. This adds extra\nslashes and blows up payload by 15-20%. For details, see these pages:\n\n- [A StackOverflow question with my answer](https://stackoverflow.com/questions/66971400/aws-lambda-body-size-is-too-large-error-but-body-size-is-under-limit)\n- [A question on AWS:repost with no answer](https://repost.aws/questions/QU57r4NMQIQROXqW4Vl6YDBQ)\n- [My blog post (in Russian, use Google Translate)](https://grishaev.me/aws-1/)\n\n## Sharing the State Between Events\n\nIn AWS, a Lambda can process several events if they happen in series. Thus, it's\nuseful to preserve the state between the handler calls. A state can be a config\nmap read from a resource or an open TCP connection.\n\nAn easy way to share the state is to close your handler function over some\nvariables. In this case, the handler is not a plain function but a function that\nreturns a function:\n\n~~~clojure\n(defn process-event [db event]\n  (jdbc/with-transaction [tx db]\n    (jdbc/insert! tx ...)\n    (jdbc/delete! tx ...)))\n\n\n(defn make-handler []\n\n  (let [config\n        (-\u003e \"config.edn\"\n            io/resource\n            aero/read-config)\n\n        db\n        (jdbc/get-connection (:db config))]\n\n    (fn [event]\n      (process-event db event))))\n\n\n(defn -main [\u0026 _]\n  (let [handler (make-handler)]\n    (main/run handler)))\n~~~\n\nThe `make-handler` call builds a function closed over the `db` variable which\nholds a persistent connection to a database. Under the hood, it calls the\n`process-event` function which accepts the `db` as an argument. The connection\nstays persistent and won't be created from scratch every time you process an\nevent. This, of course, applies only to a case when you have multiple events\nserved in series.\n\nAnother way to preserve state across multiple Lambda invocations is to use\nframeworks like Component, Integrant, or Mount. These libraries bootstrap global\nentities once at the beginning. For example, a database connection pool is\ncreated once and then shared with a message handler.\n\nThe section below describes how to use the Component framework with the Lambda\nlibrary.\n\n## Component Support\n\nThe `lambda.component` namespace ships a function called `lambda` to spawn a\ncomponent (in terms of Stuart Sierra's Component library). When started, it runs\na separate thread that consumes messages from Lambda runtime, processes them and\nsubmits positive or negative acknowledge. On every iteration, the logic checks\nif a thread was interrupted. When it was, the endless cycle exits. Stopping a\ncomponent means interrupting the thread and joining it (will be blocked until\nthe current message gets processed).\n\nThe component depends on a `:handler` slot which should be a function (or an\nobject that implements 1-arity `invoke` method from `clojure.lang.IFn`). You can\npass this handler using constructor as well:\n\n~~~clojure\n(ns some.namespace\n  (:require\n   [com.stuartsierra.component :as component]\n   [lambda.component :as lc]))\n\n(defn event-handler [message]\n  ...)\n\n(def c (lc/lambda event-handler))\n\n\n(def c-started\n  (component/start c))\n\n;; the endless message processing loop starts in the background\n\n(component/stop c-started)\n\n;; the loop stops. Might take a while to join the thread.\n~~~\n\nThis was a toy example: in production, you never run/stop components\nmanually. There is a demo project located in `env/demo3` with a system of\ncomponents which is somewhat close to reality. Here is a fragment from it:\n\n~~~clojure\n(ns demo3.main\n  (:gen-class)\n  (:require\n   [com.stuartsierra.component :as component]\n   [lambda.component :as lc]\n   [lambda.main :as main]\n   [lambda.ring :as ring]))\n\n...\n\n(defn make-system []\n  (component/system-map\n\n   :counter\n   (new-counter)\n\n   :handler\n   (-\u003e {}\n       (map-\u003eRingHandler)\n       (component/using [:counter]))\n\n   :lambda\n   (-\u003e (lc/lambda)\n       (component/using [:handler]))))\n\n\n(defn -main [\u0026 _]\n  (-\u003e (make-system)\n      (component/start)))\n~~~\n\nThe namespace produces a dedicated class (see the `(:gen-class)` form). The\n`make-system` builds a system of components on demand. It must be built in\nruntime rather than be a top-level `def` definition because `native-image`\nfreezes the world, and you'll get weird behavior.\n\nThe `:lambda` component depends on a `:handler` component. Here is a definition:\n\n~~~clojure\n(defrecord RingHandler [counter]\n  component/Lifecycle\n  (start [this]\n    (-\u003e (make-handler counter)\n        (ring/wrap-json-body)\n        (ring/wrap-json-response)\n        (ring/wrap-gzip)\n        (ring/wrap-ring-exception)\n        (ring/wrap-ring-event))))\n~~~\n\nWhen started, it creates a Ring handler and wraps it with a series of\nmiddleware. It's important that we create handler in runtime because it depends\non the `counter` component, which has not been initialized yet. The\n`make-handler` function produces a Ring handler with some simple branching:\n\n~~~clojure\n(defn make-handler [counter]\n  (fn [request]\n    (let [{:keys [uri request-method]}\n          request]\n      (case [request-method uri]\n\n        [:get \"/\"]\n        (handler-index request counter)\n\n        [:get \"/hello\"]\n        (handler-hello request)\n\n        (response-default request counter)))))\n~~~\n\nThe `counter` component is simple: it's an atom closed over a bunch of methods\nto count how many times a certain page was seen:\n\n~~~clojure\n(defprotocol ICounter\n  (-inc-page [this uri])\n  (-get-page [this uri])\n  (-stats [this]))\n\n(defn new-counter []\n  (let [-state (atom {})]\n    (reify ICounter\n      (-inc-page [this uri]\n        (swap! -state update uri (fnil inc 0)))\n      (-get-page [this uri]\n        (get @-state uri 0))\n      (-stats [this]\n        @-state))))\n~~~\n\nOnce started, the system bootstraps all the components. The `lambda` component\nprocesses messages in the background like an ordinary HTTP Ring server does.\n\nSee the `env/demo3/src/demo3/main.clj` file for full example.\n\nIt's important that the `Lambda` library doesn't depend on Component. It extends\nthe `LambdaHandler` object with metadata.\n\nYou can easily extend it with Integrant:\n\n~~~clojure\n(def config\n {:lambda/loop {:handler #ig/ref :ring/handler}\n  :ring/handler {}})\n\n(defmethod ig/init-key :ring/handler [_ _]\n  (-\u003e (make-handler ...)\n      (ring/wrap-json-body)\n      (ring/wrap-json-response)\n      (ring/wrap-gzip)\n      (ring/wrap-ring-exception)\n      (ring/wrap-ring-event)))\n\n(defmethod ig/init-key :lambda/loop [_ {:keys [handler]}]\n  (lc/start (lc/lambda handler)))\n\n(defmethod ig/halt-key! :lambda/loop [_ handler]\n  (lc/stop handler))\n~~~\n\nMount is even easier:\n\n~~~clojure\n(require '[mount.core :refer [defstate]])\n\n(defstate lambda\n  :start (lc/start (lc/lambda handler))\n  :stop (lc/stop lambda))\n~~~\n\n## Demo\n\n[test-lambda]: https://kpryignyuxqx3wwuss7oqvox7q0yhili.lambda-url.us-east-1.on.aws/\n\nThere is a [public Lambda function][test-lambda] available for tests and\nbenchmarks. The index page (`GET /`) holds instructions about what you can do\nwith it.\n\n## Misc\n\n~~~\n©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©\nIvan Grishaev, 2025. © UNLICENSE ©\n©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©\n~~~\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Flambda","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Figrishaev%2Flambda","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Flambda/lists"}