├── .gitignore
├── LICENSE
├── README.md
├── bb.edn
├── deps.edn
├── examples
├── hello-world
│ ├── README.md
│ ├── bb.edn
│ └── src
│ │ └── hello.clj
├── s3-lister-pod
│ ├── README.md
│ ├── bb.edn
│ └── src
│ │ ├── bb.edn
│ │ ├── handler.clj
│ │ └── s3.clj
├── s3-lister
│ ├── README.md
│ ├── bb.edn
│ └── src
│ │ ├── bb.edn
│ │ ├── handler.clj
│ │ └── s3.clj
└── site-analyser
│ ├── README.md
│ ├── bb.edn
│ ├── src
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── bb.edn
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.clj
│ ├── favicon.ico
│ ├── handler.clj
│ ├── index.html
│ ├── mstile-150x150.png
│ ├── page_views.clj
│ ├── safari-pinned-tab.svg
│ ├── site.webmanifest
│ └── util.clj
│ └── tf
│ └── main.tf
├── resources
├── blambda.tf
├── blambda.tfvars
├── bootstrap
├── bootstrap.clj
└── lambda_layer.tf
└── src
└── blambda
├── api.clj
├── api
└── terraform.clj
├── cli.clj
└── internal.clj
/.gitignore:
--------------------------------------------------------------------------------
1 | /classes/
2 | target/
3 | .work/
4 | .lein-deps-sum
5 | .lein-repl-history
6 | .lein-plugins/
7 | .lein-failures
8 | .nrepl-port
9 | .cpcache/
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022, Josh Glover
4 |
5 | `bootstrap.clj` is derived from https://github.com/tatut/bb-lambda, which is
6 | copyright (c) 2021 Tatu Tarvainen
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy
9 | of this software and associated documentation files (the "Software"), to deal
10 | in the Software without restriction, including without limitation the rights
11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the Software is
13 | furnished to do so, subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | SOFTWARE.
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Blambda
2 |
3 | Blambda is a custom runtime for AWS Lambda that lets you write functions using
4 | Babashka. It is based on the fantastic work that [Tatu
5 | Tarvainen](https://github.com/tatut) did on taking care of the heavy lifting of
6 | interacting with the Lambda runtime API to process function invocations in
7 | [bb-lambda](https://github.com/tatut/bb-lambda). I'm using the
8 | [`bootstrap.clj`](blob/main/bootstrap.clj) from that project directly, and have
9 | just rewritten the machinery around it to remove Docker in favour of zip files,
10 | which I think are simpler (but maybe not easier).
11 |
12 | Blambda also owes a huge debt to Karol Wójcik's awesome [Holy
13 | Lambda](https://github.com/FieryCod/holy-lambda), which is a full-featured and
14 | production-grade runtime for Clojure on AWS Lambda. I've read a lot of Holy
15 | Lambda code to figure out how to do the complicated bits of Babashka-ing on
16 | lambda. 💜
17 |
18 | ## Using Blambda
19 |
20 | Blambda is meant to be used as a library from your Babashka project. The easiest
21 | way to use it is to add tasks to your project's `bb.edn`.
22 |
23 | This example assumes a basic `bb.edn` like this:
24 |
25 | ``` clojure
26 | {:deps {net.jmglov/blambda
27 | {:git/url "https://github.com/jmglov/blambda.git"
28 | :git/tag "v0.2.0"
29 | :git/sha "fa394bb"}}
30 | :tasks
31 | {:requires ([blambda.cli :as blambda])
32 | blambda {:doc "Controls Blambda runtime and layers"
33 | :task (blambda/dispatch
34 | {:deps-layer-name "hello-deps"
35 | :lambda-name "hello"
36 | :lambda-handler "hello/hello"
37 | :lambda-iam-role "arn:aws:iam::123456789100:role/hello-lambda"
38 | :source-files ["hello.clj"]})}}}
39 | ```
40 |
41 | and a simple lambda function contained in a file `src/hello.clj` looking like
42 | this:
43 |
44 | ``` clojure
45 | (ns hello)
46 |
47 | (defn hello [{:keys [name] :or {name "Blambda"} :as event} context]
48 | (prn {:msg "Invoked with event",
49 | :data {:event event}})
50 | {:greeting (str "Hello " name "!")})
51 | ```
52 |
53 | You can find more examples in the [examples](examples/) directory in this repo.
54 |
55 | ## Building
56 |
57 | ### Custom runtime layer
58 |
59 | To build Blambda with the default Babashka version and platform, run:
60 |
61 | ``` sh
62 | bb blambda build-runtime-layer
63 | ```
64 |
65 | To see what the default Babashka version and platform are, run:
66 |
67 | ``` sh
68 | bb blambda build-runtime-layer --help
69 | ```
70 |
71 | To build a custom runtime with Babashka 1.1.173 on amd64, run:
72 |
73 | ``` sh
74 | bb blambda build-runtime-layer --bb-version 1.1.173 --bb-arch arm64
75 | ```
76 |
77 | ### Dependencies
78 |
79 | All but the most basic lambda functions will depend on Clojure libraries.
80 | Blambda has support for keeping these dependencies in a separate layer so that
81 | your function deployment contains only the source of your lambda itself.
82 |
83 | Your lambda should declare its dependencies in `bb.edn` or `deps.edn` as normal;
84 | for example, a lambda function that interacts with S3 using
85 | [awyeah-api](https://github.com/grzm/awyeah-api) might have a `src/bb.edn` that
86 | looks like this:
87 |
88 | ``` clojure
89 | {:paths ["."]
90 | :deps {com.cognitect.aws/endpoints {:mvn/version "1.1.12.206"}
91 | com.cognitect.aws/s3 {:mvn/version "822.2.1109.0"}
92 | com.grzm/awyeah-api {:git/url "https://github.com/grzm/awyeah-api"
93 | :git/sha "0fa7dd51f801dba615e317651efda8c597465af6"}
94 | org.babashka/spec.alpha {:git/url "https://github.com/babashka/spec.alpha"
95 | :git/sha "433b0778e2c32f4bb5d0b48e5a33520bee28b906"}}}
96 | ```
97 |
98 | To build your dependencies layer:
99 |
100 | ``` sh
101 | bb blambda build-deps-layer
102 | ```
103 |
104 | ### Lambda
105 |
106 | To build your lambda artifact:
107 |
108 | ``` sh
109 | bb blambda build-lambda
110 | ```
111 |
112 | ## Deploying
113 |
114 | To deploy Blambda using Terraform (recommended), first write the config files:
115 |
116 | ``` sh
117 | bb blambda terraform write-config
118 | ```
119 |
120 | If you'd like to use S3 to store your lambda artifacts (layers and lambda
121 | zipfile), run:
122 |
123 | ``` sh
124 | bb blambda terraform write-config \
125 | --use-s3 --s3-bucket BUCKET --s3-artifact-path PATH
126 | ```
127 |
128 | Replace `BUCKET` and `PATH` with the appropriate bucket and path. If the bucket
129 | you specify doesn't exist, it will be created by Terraform the first time you
130 | deploy. If you want to use an existing bucket, you'll need to import the bucket
131 | after you generate your Terraform config:
132 |
133 | ``` text
134 | bb blambda terraform import-artifacts-bucket --s3-bucket BUCKET
135 | ```
136 |
137 | You can also include extra Terraform configuration. For example, if you want to
138 | create an IAM role for your lambda, you might have a file called `tf/iam.tf` in
139 | your repo which looks something like this:
140 |
141 | ``` terraform
142 | resource "aws_iam_role" "hello" {
143 | name = "site-analyser-lambda"
144 |
145 | assume_role_policy = jsonencode({
146 | Version = "2012-10-17"
147 | Statement = [
148 | {
149 | Action = "sts:AssumeRole"
150 | Effect = "Allow"
151 | Principal = {
152 | Service = "lambda.amazonaws.com"
153 | }
154 | }
155 | ]
156 | })
157 | }
158 |
159 | resource "aws_iam_policy" "hello" {
160 | name = "site-analyser-lambda"
161 |
162 | policy = jsonencode({
163 | Version = "2012-10-17"
164 | Statement = [
165 | {
166 | Effect = "Allow"
167 | Action = [
168 | "s3:GetObject"
169 | ]
170 | Resource = [
171 | "arn:aws:s3:::logs.jmglov.net/logs/*"
172 | ]
173 | },
174 | {
175 | Effect = "Allow"
176 | Action = [
177 | "logs:CreateLogStream",
178 | "logs:PutLogEvents"
179 | ]
180 | Resource = "${aws_cloudwatch_log_group.lambda.arn}:*"
181 | },
182 | {
183 | Effect = "Allow",
184 | Action = [
185 | "s3:ListBucket"
186 | ]
187 | Resource = [
188 | "arn:aws:s3:::logs.jmglov.net"
189 | ]
190 | Condition = {
191 | "StringLike": {
192 | "s3:prefix": "logs/*"
193 | }
194 | }
195 | }
196 | ]
197 | })
198 | }
199 |
200 | resource "aws_iam_role_policy_attachment" "hello" {
201 | role = aws_iam_role.hello.name
202 | policy_arn = aws_iam_policy.hello.arn
203 | }
204 | ```
205 |
206 | Note how you can refer to resources defined by Blambda, for example
207 | `${aws_cloudwatch_log_group.lambda.arn}`. You can see what resources are defined
208 | by looking at `resources/blambda.tf` and `resources/lambda_layer.tf`.
209 |
210 | You can now use this IAM role with your lambda:
211 |
212 | ``` sh
213 | bb blambda terraform write-config \
214 | --extra-tf-config tf/iam.tf \
215 | --lambda-iam-role '${aws_iam_role.hello.arn}'
216 | ```
217 |
218 | To deploy, run:
219 |
220 | ``` text
221 | bb blambda terraform apply
222 | ```
223 |
224 | To deploy an arm64 runtime so that you can use [AWS Graviton 2
225 | lamdbas](https://aws.amazon.com/blogs/compute/migrating-aws-lambda-functions-to-arm-based-aws-graviton2-processors/)
226 | (which AWS say will give you up to "34%" better price performance), run:
227 |
228 | ``` sh
229 | bb blambda build-runtime-layer --bb-arch arm64 && \
230 | bb blambda terraform write-config --bb-arch arm64 && \
231 | bb blambda terraform apply
232 | ```
233 |
234 | Note that if you do this, you must configure your lambda as follows:
235 | - Runtime: Custom runtime on Amazon Linux 2
236 | - Architecture: arm64
237 |
238 | If you prefer not to use Terraform, you can use the AWS CLI, CloudFormation, or
239 | the AWS console, but Blambda doesn't currently offer any tooling for those
240 | options.
241 |
242 | ## Using pods
243 |
244 | If you want to use one of the pods in Babashka's [pods
245 | registry](https://github.com/babashka/pod-registry?tab=readme-ov-file), you
246 | should add it to your dependencies file as normal, using the `:pods` key. For
247 | example, to use the [tzzh/aws](https://github.com/tzzh/pod-tzzh-aws) pod, your
248 | `src/bb.edn` should look like this:
249 |
250 | ``` clojure
251 | {:paths ["."]
252 | :pods {tzzh/aws {:version "0.0.3"}}}
253 | ```
254 |
255 | Since your lambda won't be invoked as a project, you won't be able to use the
256 | nice automatic pod loading as described in the [Babashka pods
257 | documentation](https://github.com/babashka/pods#in-a-babashka-project), so
258 | you'll need to load the exact pod version you declared in your dependencies. For
259 | example:
260 |
261 | ``` clojure
262 | (ns s3
263 | (:require [babashka.pods :as pods]))
264 |
265 | (pods/load-pod 'tzzh/aws "0.0.3")
266 | (require '[pod.tzzh.s3 :as s3])
267 | ```
268 |
269 | This is gross and I'm sorry. 😢
270 |
271 | There is a full example of a lambda using a pod in
272 | [examples/s3-lister-pod](examples/s3-lister-pod).
273 |
274 | ### Using a local pod
275 |
276 | Babashka also supports using pods from the local filesystem. To do this in
277 | Blambda, make sure your pod executable exists in your `src` directory, then
278 | declare it in your `src/bb.edn` like so:
279 |
280 | ``` clojure
281 | {:paths ["."]
282 | :pods {tzzh/aws {:path "./my-pod"}}}
283 | ```
284 |
285 | Blambda will copy this pod into your deps layer at the root, meaning it will end
286 | up in `/opt` in the deployed lambda image. The Blambda runtime adds `/opt` to
287 | the `PATH` environment variable when invoking `bb`, and Babashka will [look up
288 | pods on the local
289 | filesystem](https://github.com/babashka/pods?tab=readme-ov-file#where-does-the-pod-come-from)
290 | using `PATH`, so you can load the pod in your lambda simply by referring to it
291 | by its name:
292 |
293 | ``` clojure
294 | (pods/load-pod "my-pod")
295 | ```
296 |
--------------------------------------------------------------------------------
/bb.edn:
--------------------------------------------------------------------------------
1 | {:deps {net.jmglov/blambda {:local/root "."}}
2 | :tasks
3 | {:requires ([blambda.cli :as blambda])
4 |
5 | blambda {:doc "Controls Blambda runtime and layers"
6 | :task (blambda/dispatch {})}
7 |
8 | }}
9 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]}
2 |
--------------------------------------------------------------------------------
/examples/hello-world/README.md:
--------------------------------------------------------------------------------
1 | # Hello, Blambda!
2 |
3 | This is an example of a basic lambda without dependencies. To build and deploy
4 | it, make sure you have Terraform installed (or pull a cheeky `nix-shell -p
5 | terraform` if you're down with Nix), then run:
6 |
7 | ``` sh
8 | bb blambda build-all
9 | bb blambda terraform write-config
10 | bb blambda terraform apply
11 | ```
12 |
13 | You should now have a lambda function called **hello-blambda**, which you can
14 | invoke like so (assuming you have the AWS CLI installed):
15 |
16 | ``` sh
17 | aws lambda invoke --function-name hello-blambda /dev/stdout
18 | ```
19 |
--------------------------------------------------------------------------------
/examples/hello-world/bb.edn:
--------------------------------------------------------------------------------
1 | {:paths ["."]
2 | :deps {net.jmglov/blambda
3 | {:git/url "https://github.com/jmglov/blambda.git"
4 | :git/tag "v0.2.0"
5 | :git/sha "fa394bb"}
6 | #_"For local development, use this instead:"
7 | #_{:local/root "../.."}
8 | }
9 | :tasks
10 | {:requires ([blambda.cli :as blambda])
11 |
12 | blambda {:doc "Controls Blambda runtime and layers"
13 | :task (blambda/dispatch
14 | {:lambda-name "hello-blambda"
15 | :lambda-handler "hello/handler"
16 | :source-files ["hello.clj"]})}}}
17 |
--------------------------------------------------------------------------------
/examples/hello-world/src/hello.clj:
--------------------------------------------------------------------------------
1 | (ns hello)
2 |
3 | (defn handler [{:keys [name] :or {name "Blambda"} :as event} context]
4 | (prn {:msg "Invoked with event",
5 | :data {:event event}})
6 | {:greeting (str "Hello " name "!")})
7 |
--------------------------------------------------------------------------------
/examples/s3-lister-pod/README.md:
--------------------------------------------------------------------------------
1 | # Blambda example: listing S3 objects using a pod
2 |
3 | This example uses the [tzzh/aws](https://github.com/tzzh/pod-tzzh-aws) pod to
4 | list objects in the S3 bucket of your choosing.
5 |
6 | ## Building and deploying
7 |
8 | Make sure you have Terraform installed (or pull a cheeky `nix-shell -p
9 | terraform` if you're down with Nix), then run:
10 |
11 | ``` text
12 | bb blambda build-all
13 | bb blambda terraform write-config
14 | bb blambda terraform apply
15 | ```
16 |
17 | **NOTE:** tzzh/aws is only built for AMD64, so you cannot an ARM64 lambda with
18 | this pod.
19 |
20 | This assumes that you have already created an IAM role with an attached policy
21 | that gives your lambda access to the S3 buckets you want to allow listing for,
22 | something like this:
23 |
24 | ``` json
25 | {
26 | "Version": "2012-10-17",
27 | "Statement": [
28 | {
29 | "Effect": "Allow",
30 | "Action": [
31 | "logs:CreateLogStream",
32 | "logs:PutLogEvents"
33 | ],
34 | "Resource": "arn:aws:logs:eu-west-1:YOUR_AWS_ACCOUNT:log-group:/aws/lambda/s3-lister-pod:*"
35 | },
36 | {
37 | "Effect": "Allow",
38 | "Action": "s3:ListBucket",
39 | "Resource": [
40 | "arn:aws:s3:::YOUR_S3_BUCKET/*",
41 | "arn:aws:s3:::ANOTHER_S3_BUCKET/*"
42 | ]
43 | }
44 | ]
45 | }
46 | ```
47 |
48 | You will also need to update the [bb.edn](bb.edn) in this directory to point at
49 | this role:
50 |
51 | ``` clojure
52 | :lambda-iam-role "arn:aws:iam::YOUR_AWS_ACCOUNT:role/s3-lister-pod-role"
53 | ```
54 |
55 | ## Testing
56 |
57 | Open your newly created s3-lister-pod function in the [AWS
58 | console](https://eu-west-1.console.aws.amazon.com/lambda/home?region=eu-west-1#/functions/s3-lister-pod?tab=testing),
59 | then create a test event like this:
60 |
61 | ``` json
62 | {
63 | "bucket": "YOUR_S3_BUCKET",
64 | "prefix": "some-prefix/"
65 | }
66 | ```
67 |
68 | If you click the **Test** button and expand the **Details** section, you should
69 | get a response like this:
70 |
71 | ``` json
72 | {
73 | "bucket": "YOUR_S3_BUCKET",
74 | "prefix": "some-prefix/",
75 | "objects": [
76 | {
77 | "Key": "some-prefix/ep-0-1-agile/ep-0-1-agile.transcript.txt",
78 | "LastModified": "2024-02-09T10:11:00Z",
79 | "Size": 44823
80 | },
81 | {
82 | "Key": "some-prefix/ep-0-1-agile/ep-0-1-agile.mp3",
83 | "LastModified": "2024-02-09T10:11:07Z",
84 | "Size": 63468816
85 | }
86 | ]
87 | }
88 | ```
89 |
90 | The **Log output** should show something like this:
91 |
92 | ``` text
93 | Starting Babashka:
94 | /opt/bb -cp /var/task:src:/opt/m2-repo/com/cognitect/aws/endpoints/1.1.12.398/endpoints-1.1.12.398.jar:/opt/m2-repo/com/cognitect/aws/s3/825.2.1250.0/s3-825.2.1250.0.jar:/opt/gitlibs/libs/com.grzm/awyeah-api/0fa7dd51f801dba615e317651efda8c597465af6/src:/opt/gitlibs/libs/com.grzm/awyeah-api/0fa7dd51f801dba615e317651efda8c597465af6/resources:/opt/gitlibs/libs/org.babashka/spec.alpha/433b0778e2c32f4bb5d0b48e5a33520bee28b906/src/main/java:/opt/gitlibs/libs/org.babashka/spec.alpha/433b0778e2c32f4bb5d0b48e5a33520bee28b906/src/main/clojure:/opt/gitlibs/libs/org.babashka/spec.alpha/433b0778e2c32f4bb5d0b48e5a33520bee28b906/src/main/resources:/opt/m2-repo/org/clojure/clojure/1.11.1/clojure-1.11.1.jar:/opt/m2-repo/org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.jar:/opt/m2-repo/org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.jar /opt/bootstrap.clj
95 | Loading babashka lambda handler: handler/handler
96 | Starting babashka lambda event loop
97 | START RequestId: abd74cfe-1d90-442c-9f29-20812c595cbd Version: $LATEST
98 | {:msg "Invoked with event", :data {:event {:bucket "YOUR_S3_BUCKET", :prefix "some-prefix/"}}}
99 | END RequestId: abd74cfe-1d90-442c-9f29-20812c595cbd
100 | REPORT RequestId: abd74cfe-1d90-442c-9f29-20812c595cbd Duration: 392.25 ms Billed Duration: 1190 ms Memory Size: 512 MB Max Memory Used: 164 MB Init Duration: 796.93 ms
101 | ```
102 |
--------------------------------------------------------------------------------
/examples/s3-lister-pod/bb.edn:
--------------------------------------------------------------------------------
1 | {:paths ["."]
2 | :deps {net.jmglov/blambda
3 | #_{:git/url "https://github.com/jmglov/blambda.git"
4 | :git/tag "v0.2.0"
5 | :git/sha "fa394bb"}
6 | #_"For local development, use this instead:"
7 | {:local/root "../.."}
8 | }
9 | :tasks
10 | {:requires ([blambda.cli :as blambda])
11 | :init (do
12 | (def config {:deps-layer-name "s3-lister-pod-deps"
13 | :lambda-name "s3-lister-pod"
14 | :lambda-handler "handler/handler"
15 | :lambda-iam-role "arn:aws:iam::289341159200:role/s3-lister-pod-role"
16 | :source-files ["handler.clj" "s3.clj"]}))
17 |
18 | blambda {:doc "Controls Blambda runtime and layers"
19 | :task (blambda/dispatch config)}}}
20 |
--------------------------------------------------------------------------------
/examples/s3-lister-pod/src/bb.edn:
--------------------------------------------------------------------------------
1 | {:paths ["."]
2 | :pods {
3 | ;; Using a pod from the registry looks like this:
4 | tzzh/aws {:version "0.0.3"}
5 |
6 | ;; Using a pod from the local filesystem looks like this
7 | ;; (note that you'll need to remove or comment out the registry
8 | ;; version before uncommenting this):
9 | #_tzzh/aws #_{:path "./pod-tzzh-aws"}
10 | }}
11 |
--------------------------------------------------------------------------------
/examples/s3-lister-pod/src/handler.clj:
--------------------------------------------------------------------------------
1 | (ns handler
2 | (:require [s3]))
3 |
4 | (defn handler [{:keys [bucket prefix] :as event} _context]
5 | (prn {:msg "Invoked with event"
6 | :data {:event event}})
7 | {:bucket bucket
8 | :prefix prefix
9 | :objects (->> (s3/list-objects bucket prefix)
10 | s3/sort-mtime
11 | (map s3/summarise))})
12 |
--------------------------------------------------------------------------------
/examples/s3-lister-pod/src/s3.clj:
--------------------------------------------------------------------------------
1 | (ns s3
2 | (:require [babashka.pods :as pods]))
3 |
4 | ;; Using a pod from the registry looks like this:
5 | (pods/load-pod 'tzzh/aws "0.0.3")
6 |
7 | ;; Using a pod from the local filesystem looks like this
8 | ;; (note that you'll need to remove or comment out the registry
9 | ;; version before uncommenting this):
10 | #_(pods/load-pod "pod-tzzh-aws")
11 |
12 | (require '[pod.tzzh.s3 :as s3])
13 |
14 | (defn list-objects [bucket prefix]
15 | (-> (s3/list-objects-v2 {:Bucket bucket
16 | :Prefix prefix})
17 | :Contents))
18 |
19 | (defn sort-mtime [objects]
20 | (sort-by :LastModified objects))
21 |
22 | (defn summarise [object]
23 | (select-keys object [:Key :LastModified :Size]))
24 |
--------------------------------------------------------------------------------
/examples/s3-lister/README.md:
--------------------------------------------------------------------------------
1 | # Blambda example: listing S3 objects
2 |
3 | This example uses the [awyeah-api](https://github.com/grzm/awyeah-api) library
4 | to list objects in the S3 bucket of your choosing.
5 |
6 | ## Building and deploying
7 |
8 | Make sure you have Terraform installed (or pull a cheeky `nix-shell -p
9 | terraform` if you're down with Nix), then run:
10 |
11 | ``` text
12 | bb blambda build-all
13 | bb blambda terraform write-config
14 | bb blambda terraform apply
15 | ```
16 |
17 | This assumes that you have already created an IAM role with an attached policy
18 | that gives your lambda access to the S3 buckets you want to allow listing for,
19 | something like this:
20 |
21 | ``` json
22 | {
23 | "Version": "2012-10-17",
24 | "Statement": [
25 | {
26 | "Effect": "Allow",
27 | "Action": [
28 | "logs:CreateLogStream",
29 | "logs:PutLogEvents"
30 | ],
31 | "Resource": "arn:aws:logs:eu-west-1:YOUR_AWS_ACCOUNT:log-group:/aws/lambda/s3-lister:*"
32 | },
33 | {
34 | "Effect": "Allow",
35 | "Action": "s3:ListBucket",
36 | "Resource": [
37 | "arn:aws:s3:::YOUR_S3_BUCKET/*",
38 | "arn:aws:s3:::ANOTHER_S3_BUCKET/*"
39 | ]
40 | }
41 | ]
42 | }
43 | ```
44 |
45 | You will also need to update the [bb.edn](bb.edn) in this directory to point at
46 | this role:
47 |
48 | ``` clojure
49 | :lambda-iam-role "arn:aws:iam::YOUR_AWS_ACCOUNT:role/s3-lister-role"
50 | ```
51 |
52 | ## Testing
53 |
54 | Open your newly created s3-lister function in the [AWS
55 | console](https://eu-west-1.console.aws.amazon.com/lambda/home?region=eu-west-1#/functions/s3-lister?tab=testing),
56 | then create a test event like this:
57 |
58 | ``` json
59 | {
60 | "bucket": "YOUR_S3_BUCKET",
61 | "prefix": "some-prefix/"
62 | }
63 | ```
64 |
65 | If you click the **Test** button and expand the **Details** section, you should
66 | get a response like this:
67 |
68 | ``` json
69 | {
70 | "bucket": "YOUR_S3_BUCKET",
71 | "prefix": "some-prefix/",
72 | "objects": [
73 | {
74 | "Key": "some-prefix/ep-0-1-agile/ep-0-1-agile.transcript.txt",
75 | "LastModified": "2024-02-09T10:11:00Z",
76 | "Size": 44823
77 | },
78 | {
79 | "Key": "some-prefix/ep-0-1-agile/ep-0-1-agile.mp3",
80 | "LastModified": "2024-02-09T10:11:07Z",
81 | "Size": 63468816
82 | }
83 | ]
84 | }
85 | ```
86 |
87 | The **Log output** should show something like this:
88 |
89 | ``` text
90 | Starting Babashka:
91 | /opt/bb -cp /var/task:src:/opt/m2-repo/com/cognitect/aws/endpoints/1.1.12.398/endpoints-1.1.12.398.jar:/opt/m2-repo/com/cognitect/aws/s3/825.2.1250.0/s3-825.2.1250.0.jar:/opt/gitlibs/libs/com.grzm/awyeah-api/0fa7dd51f801dba615e317651efda8c597465af6/src:/opt/gitlibs/libs/com.grzm/awyeah-api/0fa7dd51f801dba615e317651efda8c597465af6/resources:/opt/gitlibs/libs/org.babashka/spec.alpha/433b0778e2c32f4bb5d0b48e5a33520bee28b906/src/main/java:/opt/gitlibs/libs/org.babashka/spec.alpha/433b0778e2c32f4bb5d0b48e5a33520bee28b906/src/main/clojure:/opt/gitlibs/libs/org.babashka/spec.alpha/433b0778e2c32f4bb5d0b48e5a33520bee28b906/src/main/resources:/opt/m2-repo/org/clojure/clojure/1.11.1/clojure-1.11.1.jar:/opt/m2-repo/org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.jar:/opt/m2-repo/org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.jar /opt/bootstrap.clj
92 | Loading babashka lambda handler: handler/handler
93 | Starting babashka lambda event loop
94 | START RequestId: abd74cfe-1d90-442c-9f29-20812c595cbd Version: $LATEST
95 | {:msg "Invoked with event", :data {:event {:bucket "YOUR_S3_BUCKET", :prefix "some-prefix/"}}}
96 | END RequestId: abd74cfe-1d90-442c-9f29-20812c595cbd
97 | REPORT RequestId: abd74cfe-1d90-442c-9f29-20812c595cbd Duration: 392.25 ms Billed Duration: 1190 ms Memory Size: 512 MB Max Memory Used: 164 MB Init Duration: 796.93 ms
98 | ```
99 |
--------------------------------------------------------------------------------
/examples/s3-lister/bb.edn:
--------------------------------------------------------------------------------
1 | {:paths ["."]
2 | :deps {net.jmglov/blambda
3 | #_{:git/url "https://github.com/jmglov/blambda.git"
4 | :git/tag "v0.2.0"
5 | :git/sha "fa394bb"}
6 | #_"For local development, use this instead:"
7 | {:local/root "../.."}
8 | }
9 | :tasks
10 | {:requires ([blambda.cli :as blambda])
11 | :init (do
12 | (def config {:deps-layer-name "s3-lister-deps"
13 | :lambda-name "s3-lister"
14 | :lambda-handler "handler/handler"
15 | :lambda-iam-role "arn:aws:iam::289341159200:role/s3-lister-role"
16 | :source-files ["handler.clj" "s3.clj"]}))
17 |
18 | blambda {:doc "Controls Blambda runtime and layers"
19 | :task (blambda/dispatch config)}}}
20 |
--------------------------------------------------------------------------------
/examples/s3-lister/src/bb.edn:
--------------------------------------------------------------------------------
1 | {:paths ["."]
2 | :deps {com.cognitect.aws/endpoints {:mvn/version "1.1.12.398"}
3 | com.cognitect.aws/s3 {:mvn/version "825.2.1250.0"}
4 | com.grzm/awyeah-api {:git/url "https://github.com/grzm/awyeah-api"
5 | :git/sha "0fa7dd51f801dba615e317651efda8c597465af6"}
6 | org.babashka/spec.alpha {:git/url "https://github.com/babashka/spec.alpha"
7 | :git/sha "433b0778e2c32f4bb5d0b48e5a33520bee28b906"}}}
8 |
--------------------------------------------------------------------------------
/examples/s3-lister/src/handler.clj:
--------------------------------------------------------------------------------
1 | (ns handler
2 | (:require [s3]))
3 |
4 | (def aws-region (or (System/getenv "AWS_REGION") "eu-west-1"))
5 |
6 | (def s3-client (s3/client aws-region))
7 |
8 | (defn handler [{:keys [bucket prefix] :as event} _context]
9 | (prn {:msg "Invoked with event"
10 | :data {:event event}})
11 | {:bucket bucket
12 | :prefix prefix
13 | :objects (->> (s3/list-objects s3-client bucket prefix)
14 | s3/sort-mtime
15 | (map s3/summarise))})
16 |
--------------------------------------------------------------------------------
/examples/s3-lister/src/s3.clj:
--------------------------------------------------------------------------------
1 | (ns s3
2 | (:require [com.grzm.awyeah.client.api :as aws]))
3 |
4 | (defn client [aws-region]
5 | (aws/client {:api :s3, :region aws-region}))
6 |
7 | (defn list-objects [s3 bucket prefix]
8 | (-> (aws/invoke s3 {:op :ListObjectsV2
9 | :request {:Bucket bucket
10 | :Prefix prefix}})
11 | :Contents))
12 |
13 | (defn sort-mtime [objects]
14 | (sort-by :LastModified objects))
15 |
16 | (defn summarise [object]
17 | (select-keys object [:Key :LastModified :Size]))
18 |
--------------------------------------------------------------------------------
/examples/site-analyser/README.md:
--------------------------------------------------------------------------------
1 | # Site Analyser
2 |
3 | This is an example of a lambda that tracks page visits in a DynamoDB table and
4 | renders a simple HTML dashboard. It's loosely based on the ClojureScript version
5 | described in [Serverless site analytics with Clojure nbb and
6 | AWS](https://www.loop-code-recur.io/simple-site-analytics-with-serverless-clojure/),
7 | by Cyprien Pannier.
8 |
9 | It also keeps its artifacts in S3, which is recommended for large files (such as
10 | the custom runtime and deps layers). If you're actually deploying this example,
11 | make sure to replace `:s3-bucket "YOUR_HERE"` in `bb.edn` with the name of an
12 | actual S3 bucket. If the bucket doesn't already exist, make sure to give it a
13 | name which will be unique.
14 |
15 | To build and deploy it, make sure you have Terraform installed (or pull a cheeky
16 | `nix-shell -p terraform` if you're down with Nix), then run:
17 |
18 | ``` sh
19 | cd examples/site-analyser
20 |
21 | bb blambda build-all
22 | bb blambda terraform write-config
23 |
24 | # If using an existing S3 bucket for lambda artifacts:
25 | bb blambda terraform import-artifacts-bucket
26 |
27 | bb blambda terraform apply
28 | ```
29 |
30 | If all went well, Terraform will display something like this:
31 |
32 | ``` text
33 | Apply complete! Resources: 13 added, 0 changed, 0 destroyed.
34 |
35 | Outputs:
36 |
37 | function_url = "https://fdajhfibugdsx3y5243213b8pa0fvsxy.lambda-url.eu-west-1.on.aws/"
38 | ```
39 |
40 | The `function_url` is what you'll use to invoke the lambda through its HTTP
41 | interface.
42 |
43 | You can track some views like this:
44 |
45 | ``` sh
46 | # Set the value of BASE_URL to the function_url from the Terraform output
47 | export BASE_URL=https://fdajhfibugdsx3y5243213b8pa0fvsxy.lambda-url.eu-west-1.on.aws
48 |
49 | for i in $(seq 0 9); do
50 | curl -X POST $BASE_URL/track?url=https%3A%2F%2Fexample.com%2Ftest$i.html
51 | done
52 | ```
53 |
54 | Now if you visit the dashboard in your web browser, you should see some data:
55 |
56 | https://fdajhfibugdsx3y5243213b8pa0fvsxy.lambda-url.eu-west-1.on.aws/dashboard
57 |
58 | ## Blambda configuration
59 |
60 | There are a few extra customisations in our `examples/site-analyser/bb.edn` for
61 | this more extensive example, which are described below. `bb.edn` looks something
62 | like this:
63 |
64 | ``` clojure
65 | {:paths ["."]
66 | :deps {net.jmglov/blambda {:local/root "../.."}
67 | #_"You use the newest SHA here:"
68 | #_{:git/url "https://github.com/jmglov/blambda.git"
69 | :git/sha "2453e15cf75c03b2b02de5ca89c76081bba40251"}}
70 | :tasks
71 | {:requires ([blambda.cli :as blambda])
72 | :init (do
73 | (def config {
74 | ;; This is the Blambda config stuff
75 | }))
76 |
77 | blambda {:doc "Controls Blambda runtime and layers"
78 | :task (blambda/dispatch config)}}}
79 | ```
80 |
81 | All of the options described below involve setting a key in the `config` map.
82 |
83 | ### AWS Graviton2 architecture
84 |
85 | [According to
86 | AWS](https://aws.amazon.com/blogs/aws/aws-lambda-functions-powered-by-aws-graviton2-processor-run-your-functions-on-arm-and-get-up-to-34-better-price-performance/),
87 | lambda functions running on Graviton2 processors get much better
88 | price-performance. In order to take advantage of this, we add the following to
89 | `config`:
90 |
91 | ``` clojure
92 | :bb-arch "arm64"
93 | ```
94 |
95 | This will inform Blambda to build the custom runtime using the ARM64 version of
96 | Babashka, and create the appropriate architecture and runtime config for lambda
97 | layers and functions.
98 |
99 | ### Dependencies
100 |
101 | This example uses [awyeah-api](https://github.com/grzm/awyeah-api) (a library
102 | which makes Cognitect's [aws-api](https://github.com/cognitect-labs/aws-api)
103 | work with Babashka) to talk to DynamoDB. The appropriate dependencies are
104 | declared in `examples/site-analyser/src/bb.edn`, which should look something
105 | like this:
106 |
107 | ``` clojure
108 | {:paths ["."]
109 | :deps {com.cognitect.aws/endpoints {:mvn/version "1.1.12.373"}
110 | com.cognitect.aws/dynamodb {:mvn/version "825.2.1262.0"}
111 | com.grzm/awyeah-api {:git/url "https://github.com/grzm/awyeah-api"
112 | :git/sha "0fa7dd51f801dba615e317651efda8c597465af6"}
113 | org.babashka/spec.alpha {:git/url "https://github.com/babashka/spec.alpha"
114 | :git/sha "433b0778e2c32f4bb5d0b48e5a33520bee28b906"}}}
115 | ```
116 |
117 | To let Blambda know that it should build a lambda layer for the dependencies, we
118 | add the following to `config`:
119 |
120 | ``` clojure
121 | :deps-layer-name "site-analyser-example-deps"
122 | ```
123 |
124 | Blambda will automatically look in `src/bb.edn` to find the dependencies to
125 | include in the layer. If dependencies are specified elsewhere (for example, a
126 | `deps.edn`), you can set `:deps-path` to point to it.
127 |
128 | ### Custom Terraform config
129 |
130 | The DynamoDB table and Lambda function URL are defined in
131 | `examples/site-analyser/tf/main.tf`, along with an IAM role and policy for the
132 | additional permissions needed by the lambda.
133 |
134 | The DynamoDB table is defined in `main.tf` something like this:
135 |
136 | ``` terraform
137 | resource "aws_dynamodb_table" "site_analyser" {
138 | name = "site-analyser"
139 | billing_mode = "PAY_PER_REQUEST"
140 | hash_key = "date"
141 | range_key = "url"
142 |
143 | attribute {
144 | name = "date"
145 | type = "S"
146 | }
147 |
148 | attribute {
149 | name = "url"
150 | type = "S"
151 | }
152 | }
153 | ```
154 |
155 | In our custom IAM policy, we want to grant the following permissions:
156 | - Update items in the DynamoDB table to increment counters
157 | - Query the DynamoDB table to read counters
158 | - Create log stream and put events to it (for standard lambda logging)
159 |
160 | We've explicitly defined the DynamoDB table in this Terraform file, so we can
161 | refer to it by its resource name, `aws_dynamodb_table.site_analyser`, in the IAM
162 | policy.
163 |
164 | For the logging permissions, we'll take advantage of the fact that Blambda will
165 | automatically generate a Cloudwatch log group for us, with resource name
166 | `aws_cloudwatch_log_group.lambda`.
167 |
168 | Our IAM policy now looks something like this:
169 |
170 | ``` terraform
171 | resource "aws_iam_policy" "lambda" {
172 | name = "site-analyser-example-lambda"
173 |
174 | policy = jsonencode({
175 | Version = "2012-10-17"
176 | Statement = [
177 | {
178 | Effect = "Allow"
179 | Action = [
180 | "logs:CreateLogStream",
181 | "logs:PutLogEvents"
182 | ]
183 | Resource = "${aws_cloudwatch_log_group.lambda.arn}:*"
184 | },
185 | {
186 | Effect = "Allow"
187 | Action = [
188 | "dynamodb:Query",
189 | "dynamodb:UpdateItem",
190 | ]
191 | Resource = aws_dynamodb_table.site_analyser.arn
192 | }
193 | ]
194 | })
195 | }
196 | ```
197 |
198 | We include this Terraform config by adding the following to `config` in
199 | `bb.edn`:
200 |
201 | ``` clojure
202 | :extra-tf-config ["tf/main.tf"]
203 | ```
204 |
205 | Finally, since we're defining a custom IAM role and policy, we need to tell
206 | Blambda to use it instead of generating a default one. We do this by adding the
207 | following to `config`:
208 |
209 | ``` clojure
210 | :lambda-iam-role "${aws_iam_role.lambda.arn}"
211 | ```
212 |
213 | ### Environment variables
214 |
215 | Lambda functions can read their configuration from environment variables. The
216 | site-analyser lambda has four configurable values, which you can see at the top
217 | of `examples/site-analyser/src/handler.clj`:
218 |
219 | ``` clojure
220 | (def config {:aws-region (get-env "AWS_REGION" "eu-west-1")
221 | :views-table (get-env "VIEWS_TABLE")
222 | :num-days (util/->int (get-env "NUM_DAYS" "7"))
223 | :num-top-urls (util/->int (get-env "NUM_TOP_URLS" "10"))})
224 | ```
225 |
226 | In our case, the default values for `:aws-region`, `:num-days`, and
227 | `:num-top-urls` are fine, but we need to set `:views-table` so that the lambda
228 | knows which DynamoDB table to use for counters. We can set the `VIEWS_TABLE`
229 | environment variable by adding the following to `config` in `bb.edn`:
230 |
231 | ``` clojure
232 | :lambda-env-vars ["VIEWS_TABLE=${aws_dynamodb_table.site_analyser.name}"]
233 | ```
234 |
235 | The strange format for environment variables is to support passing them with
236 | `--lambda-env-vars` on the command line.
237 |
238 | ### Source files
239 |
240 | In addition to the Clojure source files for our lambda, we also have an
241 | `index.html` Selmer template for the dashboard and a bunch of files to provide a
242 | "favicon" for the dashboard. We list them in `config` like so:
243 |
244 | ``` clojure
245 | :source-files [
246 | ;; Clojure sources
247 | "handler.clj"
248 | "favicon.clj"
249 | "page_views.clj"
250 | "util.clj"
251 |
252 | ;; HTML templates
253 | "index.html"
254 |
255 | ;; favicon
256 | "android-chrome-192x192.png"
257 | "mstile-150x150.png"
258 | "favicon-16x16.png"
259 | "safari-pinned-tab.svg"
260 | "favicon.ico"
261 | "site.webmanifest"
262 | "android-chrome-512x512.png"
263 | "apple-touch-icon.png"
264 | "browserconfig.xml"
265 | "favicon-32x32.png"
266 | ]
267 | ```
268 |
269 | ### Use S3 for lambda artifacts
270 |
271 | The custom runtime and dependencies layers are quite large in size (22 MB and 5
272 | MB, respectively), so we'll upload them to S3 and instruct lambda to pull them
273 | from there, which is the recommended way to deal with large files. We accomplish
274 | this by adding the following to `config`:
275 |
276 | ``` clojure
277 | :use-s3 true
278 | :s3-bucket "YOUR_BUCKET"
279 | :s3-artifact-path "lambda-artifacts"
280 | ```
281 |
282 | Replace `YOUR_BUCKET` with either an existing bucket you own or a new bucket to
283 | create (note that the name must be globally unique). As noted above, if you use
284 | an existing bucket, you'll need to import it into your Terraform state before
285 | deploying:
286 |
287 | ``` sh
288 | bb blambda terraform write-config
289 | bb blambda terraform import-artifacts-bucket
290 | ```
291 |
--------------------------------------------------------------------------------
/examples/site-analyser/bb.edn:
--------------------------------------------------------------------------------
1 | {:paths ["."]
2 | :deps {net.jmglov/blambda
3 | {:git/url "https://github.com/jmglov/blambda.git"
4 | :git/tag "v0.2.0"
5 | :git/sha "fa394bb"}
6 | #_"For local development, use this instead:"
7 | #_{:local/root "../.."}
8 | }
9 | :tasks
10 | {:requires ([blambda.cli :as blambda])
11 | :init (do
12 | (def config {:bb-arch "arm64"
13 | :deps-layer-name "site-analyser-example-deps"
14 | :lambda-name "site-analyser-example"
15 | :lambda-handler "handler/handler"
16 | :lambda-iam-role "${aws_iam_role.lambda.arn}"
17 | :lambda-env-vars ["VIEWS_TABLE=${aws_dynamodb_table.site_analyser.name}"]
18 | :source-files [
19 | ;; Clojure sources
20 | "handler.clj"
21 | "favicon.clj"
22 | "page_views.clj"
23 | "util.clj"
24 |
25 | ;; HTML templates
26 | "index.html"
27 |
28 | ;; favicon
29 | "android-chrome-192x192.png"
30 | "mstile-150x150.png"
31 | "favicon-16x16.png"
32 | "safari-pinned-tab.svg"
33 | "favicon.ico"
34 | "site.webmanifest"
35 | "android-chrome-512x512.png"
36 | "apple-touch-icon.png"
37 | "browserconfig.xml"
38 | "favicon-32x32.png"
39 | ]
40 | :use-s3 true
41 | :s3-bucket "YOUR_BUCKET"
42 | :s3-artifact-path "lambda-artifacts"
43 | :extra-tf-config ["tf/main.tf"]}))
44 |
45 | blambda {:doc "Controls Blambda runtime and layers"
46 | :task (blambda/dispatch config)}}}
47 |
--------------------------------------------------------------------------------
/examples/site-analyser/src/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmglov/blambda/a7fc65aa77ab859219596774a55a7b54d6674b13/examples/site-analyser/src/android-chrome-192x192.png
--------------------------------------------------------------------------------
/examples/site-analyser/src/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmglov/blambda/a7fc65aa77ab859219596774a55a7b54d6674b13/examples/site-analyser/src/android-chrome-512x512.png
--------------------------------------------------------------------------------
/examples/site-analyser/src/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmglov/blambda/a7fc65aa77ab859219596774a55a7b54d6674b13/examples/site-analyser/src/apple-touch-icon.png
--------------------------------------------------------------------------------
/examples/site-analyser/src/bb.edn:
--------------------------------------------------------------------------------
1 | {:paths ["."]
2 | :deps {com.cognitect.aws/endpoints {:mvn/version "1.1.12.373"}
3 | com.cognitect.aws/dynamodb {:mvn/version "825.2.1262.0"}
4 | com.grzm/awyeah-api {:git/url "https://github.com/grzm/awyeah-api"
5 | :git/sha "0fa7dd51f801dba615e317651efda8c597465af6"}
6 | org.babashka/spec.alpha {:git/url "https://github.com/babashka/spec.alpha"
7 | :git/sha "433b0778e2c32f4bb5d0b48e5a33520bee28b906"}}}
8 |
--------------------------------------------------------------------------------
/examples/site-analyser/src/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
30 | 31 | 32 | 33 | Site Analytics - Powered by Blambda! 34 |
35 |{{date-label}}
36 |Rank | 61 |URL | 62 |Views | 63 |
---|---|---|
{{i.rank}} | 69 |{{i.url}} | 70 |{{i.views}} | 71 |