├── .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 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/site-analyser/src/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmglov/blambda/a7fc65aa77ab859219596774a55a7b54d6674b13/examples/site-analyser/src/favicon-16x16.png -------------------------------------------------------------------------------- /examples/site-analyser/src/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmglov/blambda/a7fc65aa77ab859219596774a55a7b54d6674b13/examples/site-analyser/src/favicon-32x32.png -------------------------------------------------------------------------------- /examples/site-analyser/src/favicon.clj: -------------------------------------------------------------------------------- 1 | (ns favicon 2 | (:require [babashka.fs :as fs]) 3 | (:import (java.util Base64))) 4 | 5 | (def encoder (Base64/getEncoder)) 6 | 7 | (def favicon-files 8 | {"android-chrome-192x192.png" "image/png" 9 | "mstile-150x150.png" "image/png" 10 | "favicon-16x16.png" "image/png" 11 | "safari-pinned-tab.svg" "image/svg+xml" 12 | "favicon.ico" "image/x-icon" 13 | "site.webmanifest" "application/json" 14 | "android-chrome-512x512.png" "image/png" 15 | "apple-touch-icon.png" "image/png" 16 | "browserconfig.xml" "application/xml" 17 | "favicon-32x32.png" "image/png"}) 18 | 19 | (defn read-file [file] 20 | (.encodeToString encoder (fs/read-all-bytes file))) 21 | 22 | (defn serve-favicon [abs-path] 23 | (let [[_ file] (re-find #"^/(.+)$" abs-path)] 24 | (when-let [content-type (favicon-files file)] 25 | {:statusCode 200 26 | :headers {"Content-Type" content-type} 27 | :isBase64Encoded true 28 | :body (read-file file)}))) 29 | -------------------------------------------------------------------------------- /examples/site-analyser/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmglov/blambda/a7fc65aa77ab859219596774a55a7b54d6674b13/examples/site-analyser/src/favicon.ico -------------------------------------------------------------------------------- /examples/site-analyser/src/handler.clj: -------------------------------------------------------------------------------- 1 | (ns handler 2 | (:require [cheshire.core :as json] 3 | [selmer.parser :as selmer] 4 | [favicon] 5 | [page-views] 6 | [util :refer [->map]]) 7 | (:import (java.time LocalDate) 8 | (java.util UUID))) 9 | 10 | (defn get-env 11 | ([k] 12 | (or (System/getenv k) 13 | (let [msg (format "Missing env var: %s" k)] 14 | (throw (ex-info msg {:msg msg, :env-var k}))))) 15 | ([k default] 16 | (or (System/getenv k) default))) 17 | 18 | (def config {:aws-region (get-env "AWS_REGION" "eu-west-1") 19 | :views-table (get-env "VIEWS_TABLE") 20 | :num-days (util/->int (get-env "NUM_DAYS" "7")) 21 | :num-top-urls (util/->int (get-env "NUM_TOP_URLS" "10"))}) 22 | 23 | (def client (page-views/client config)) 24 | 25 | (defn serve-dashboard [{:keys [queryStringParameters] :as event}] 26 | (let [date (:date queryStringParameters) 27 | dates (if date 28 | [date] 29 | (->> (range (:num-days config)) 30 | (map #(str (.minusDays (LocalDate/now) %))))) 31 | date-label (or date (format "last %d days" (:num-days config))) 32 | all-views (mapcat #(page-views/get-views client %) dates) 33 | total-views (reduce + (map :views all-views)) 34 | top-urls (->> all-views 35 | (group-by :url) 36 | (map (fn [[url views]] 37 | [url (reduce + (map :views views))])) 38 | (sort-by second) 39 | reverse 40 | (take (:num-top-urls config)) 41 | (map-indexed (fn [i [url views]] 42 | (assoc (->map url views) :rank (inc i))))) 43 | chart-id (str "div-" (UUID/randomUUID)) 44 | chart-data (->> all-views 45 | (group-by :date) 46 | (map (fn [[date rows]] 47 | {:date date 48 | :views (reduce + (map :views rows))})) 49 | (sort-by :date)) 50 | chart-spec (json/generate-string 51 | {:$schema "https://vega.github.io/schema/vega-lite/v5.json" 52 | :data {:values chart-data} 53 | :mark {:type "bar"} 54 | :width "container" 55 | :height 300 56 | :encoding {:x {:field "date" 57 | :type "nominal" 58 | :axis {:labelAngle -45}} 59 | :y {:field "views" 60 | :type "quantitative"}}}) 61 | tmpl-vars (->map date-label 62 | total-views 63 | top-urls 64 | chart-id 65 | chart-spec)] 66 | (util/log "Rendering dashboard" tmpl-vars) 67 | {:statusCode 200 68 | :headers {"Content-Type" "text/html"} 69 | :body (selmer/render (slurp "index.html") 70 | tmpl-vars)})) 71 | 72 | (defn track-visit! [{:keys [queryStringParameters] :as event}] 73 | (let [{:keys [url]} queryStringParameters] 74 | (if url 75 | (let [date (str (LocalDate/now)) 76 | url (util/url-decode url)] 77 | (page-views/increment! client date url) 78 | {:statusCode 204}) 79 | (do 80 | (util/log "Missing required query param" {:param "url"}) 81 | {:statusCode 400 82 | :body "Missing required query param: url"})))) 83 | 84 | (defn handler [{:keys [requestContext] :as event} _context] 85 | (prn {:msg "Invoked with event" 86 | :data {:event event}}) 87 | (try 88 | (let [{:keys [method path]} (:http requestContext) 89 | _ (util/log "Request" (->map method path)) 90 | favicon-res (favicon/serve-favicon path) 91 | res (if favicon-res 92 | favicon-res 93 | (case [method path] 94 | ["GET" "/dashboard"] (serve-dashboard event) 95 | ["POST" "/track"] (track-visit! event) 96 | {:statusCode 404 97 | :headers {"Content-Type" "application/json"} 98 | :body (json/generate-string {:msg (format "Resource not found: %s" path)})}))] 99 | (util/log "Sending response" (dissoc res :body)) 100 | res) 101 | (catch Exception e 102 | (util/log "Caught exception" (ex-data e)) 103 | {:statusCode 500 104 | :headers {"Content-Type" "text/plain"} 105 | :body (ex-message e)}))) 106 | -------------------------------------------------------------------------------- /examples/site-analyser/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Site Analytics - Powered by Blambda! 25 | 26 | 27 | 38 |
39 |
40 | 48 |
49 |
50 | 53 |
54 |
55 |
56 |

Top URLs

57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {% for i in top-urls %} 67 | 68 | 69 | 70 | 71 | 72 | {% endfor %} 73 | 74 |
RankURLViews
{{i.rank}}{{i.url}}{{i.views}}
75 |
76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/site-analyser/src/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmglov/blambda/a7fc65aa77ab859219596774a55a7b54d6674b13/examples/site-analyser/src/mstile-150x150.png -------------------------------------------------------------------------------- /examples/site-analyser/src/page_views.clj: -------------------------------------------------------------------------------- 1 | (ns page-views 2 | (:require [com.grzm.awyeah.client.api :as aws] 3 | [util :refer [->map]])) 4 | 5 | (defn client [{:keys [aws-region] :as config}] 6 | (assoc config :dynamodb (aws/client {:api :dynamodb, :region aws-region}))) 7 | 8 | (defn validate-response [res] 9 | (when (:cognitect.anomalies/category res) 10 | (let [data (merge (select-keys res [:cognitect.anomalies/category]) 11 | {:err-msg (:Message res) 12 | :err-type (:__type res)})] 13 | (util/log "DynamoDB request failed" data) 14 | (throw (ex-info "DynamoDB request failed" data)))) 15 | res) 16 | 17 | (defn increment! [{:keys [dynamodb views-table] :as client} date url] 18 | (let [req {:TableName views-table 19 | :Key {:date {:S date} 20 | :url {:S url}} 21 | :UpdateExpression "ADD #views :increment" 22 | :ExpressionAttributeNames {"#views" "views"} 23 | :ExpressionAttributeValues {":increment" {:N "1"}} 24 | :ReturnValues "ALL_NEW"} 25 | _ (util/log "Incrementing page view counter" 26 | (->map date url req)) 27 | res (-> (aws/invoke dynamodb {:op :UpdateItem 28 | :request req}) 29 | validate-response) 30 | new-counter (-> res 31 | (get-in [:Attributes :views :N]) 32 | util/->int) 33 | ret (->map date url new-counter)] 34 | (util/log "Page view counter incremented" 35 | ret) 36 | ret)) 37 | 38 | (defn get-query-page [{:keys [dynamodb views-table] :as client} 39 | date 40 | {:keys [page-num LastEvaluatedKey] :as prev}] 41 | (when prev 42 | (util/log "Got page" prev)) 43 | (when (or (nil? prev) 44 | LastEvaluatedKey) 45 | (let [page-num (inc (or page-num 0)) 46 | req (merge 47 | {:TableName views-table 48 | :KeyConditionExpression "#date = :date" 49 | :ExpressionAttributeNames {"#date" "date"} 50 | :ExpressionAttributeValues {":date" {:S date}}} 51 | (when LastEvaluatedKey 52 | {:ExclusiveStartKey LastEvaluatedKey})) 53 | _ (util/log "Querying page views" (->map date page-num req)) 54 | res (-> (aws/invoke dynamodb {:op :Query 55 | :request req}) 56 | validate-response) 57 | _ (util/log "Got response" (->map res))] 58 | (assoc res :page-num page-num)))) 59 | 60 | (defn get-views [client date] 61 | (->> (iteration (partial get-query-page client date) 62 | :vf :Items) 63 | util/lazy-concat 64 | (map (fn [{:keys [views date url]}] 65 | {:date (:S date) 66 | :url (:S url) 67 | :views (util/->int (:N views))})))) 68 | -------------------------------------------------------------------------------- /examples/site-analyser/src/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 14 | 20 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/site-analyser/src/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /examples/site-analyser/src/util.clj: -------------------------------------------------------------------------------- 1 | (ns util 2 | (:import (java.net URLDecoder) 3 | (java.nio.charset StandardCharsets))) 4 | 5 | (defmacro ->map [& ks] 6 | (assert (every? symbol? ks)) 7 | (zipmap (map keyword ks) 8 | ks)) 9 | 10 | (defn ->int [s] 11 | (Integer/parseUnsignedInt s)) 12 | 13 | (defn url-decode [s] 14 | (URLDecoder/decode s StandardCharsets/UTF_8)) 15 | 16 | (defn lazy-concat [colls] 17 | (lazy-seq 18 | (when-first [c colls] 19 | (lazy-cat c (lazy-concat (rest colls)))))) 20 | 21 | (defn log 22 | ([msg] 23 | (log msg {})) 24 | ([msg data] 25 | (prn (assoc data :msg msg)))) 26 | -------------------------------------------------------------------------------- /examples/site-analyser/tf/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lambda_function_url" "lambda" { 2 | function_name = aws_lambda_function.lambda.function_name 3 | authorization_type = "NONE" 4 | } 5 | 6 | output "function_url" { 7 | value = aws_lambda_function_url.lambda.function_url 8 | } 9 | 10 | resource "aws_lambda_permission" "lambda" { 11 | action = "lambda:InvokeFunctionUrl" 12 | function_name = aws_lambda_function.lambda.function_name 13 | principal = "*" 14 | function_url_auth_type = "NONE" 15 | } 16 | 17 | resource "aws_dynamodb_table" "site_analyser" { 18 | name = "site-analyser-example" 19 | billing_mode = "PAY_PER_REQUEST" 20 | hash_key = "date" 21 | range_key = "url" 22 | 23 | attribute { 24 | name = "date" 25 | type = "S" 26 | } 27 | 28 | attribute { 29 | name = "url" 30 | type = "S" 31 | } 32 | } 33 | 34 | resource "aws_iam_role" "lambda" { 35 | name = "site-analyser-example-lambda" 36 | 37 | assume_role_policy = jsonencode({ 38 | Version = "2012-10-17" 39 | Statement = [ 40 | { 41 | Action = "sts:AssumeRole" 42 | Effect = "Allow" 43 | Principal = { 44 | Service = "lambda.amazonaws.com" 45 | } 46 | } 47 | ] 48 | }) 49 | } 50 | 51 | resource "aws_iam_policy" "lambda" { 52 | name = "site-analyser-example-lambda" 53 | 54 | policy = jsonencode({ 55 | Version = "2012-10-17" 56 | Statement = [ 57 | { 58 | Effect = "Allow" 59 | Action = [ 60 | "logs:CreateLogStream", 61 | "logs:PutLogEvents" 62 | ] 63 | Resource = "${aws_cloudwatch_log_group.lambda.arn}:*" 64 | }, 65 | { 66 | Effect = "Allow" 67 | Action = [ 68 | "dynamodb:Query", 69 | "dynamodb:UpdateItem", 70 | ] 71 | Resource = aws_dynamodb_table.site_analyser.arn 72 | } 73 | ] 74 | }) 75 | } 76 | 77 | resource "aws_iam_role_policy_attachment" "lambda" { 78 | role = aws_iam_role.lambda.name 79 | policy_arn = aws_iam_policy.lambda.arn 80 | } 81 | -------------------------------------------------------------------------------- /resources/blambda.tf: -------------------------------------------------------------------------------- 1 | variable "runtime_layer_name" {} 2 | {% if not skip-compatible-architectures %} 3 | variable "runtime_layer_compatible_architectures" {} 4 | {% endif %} 5 | variable "runtime_layer_compatible_runtimes" {} 6 | variable "runtime_layer_filename" {} 7 | {% if use-s3 %} 8 | variable "s3_bucket" {} 9 | variable "runtime_layer_s3_key" {} 10 | {% endif %} 11 | {% if deps-layer-name %} 12 | variable "deps_layer_name" {} 13 | {% if not skip-compatible-architectures %} 14 | variable "deps_layer_compatible_architectures" {} 15 | {% endif %} 16 | variable "deps_layer_compatible_runtimes" {} 17 | variable "deps_layer_filename" {} 18 | {% if use-s3 %} 19 | variable "deps_layer_s3_key" {} 20 | {% endif %} 21 | {% endif %} 22 | 23 | variable "lambda_name" {} 24 | variable "lambda_handler" {} 25 | variable "lambda_filename" {} 26 | variable "lambda_memory_size" {} 27 | variable "lambda_runtime" {} 28 | variable "lambda_architectures" {} 29 | {% if lambda-timeout %} 30 | variable "lambda_timeout" {} 31 | {% endif %} 32 | {% if use-s3 %} 33 | variable "lambda_s3_key" {} 34 | {% endif %} 35 | 36 | {% if use-s3 %} 37 | resource "aws_s3_bucket" "artifacts" { 38 | bucket = var.s3_bucket 39 | } 40 | 41 | resource "aws_s3_object" "lambda" { 42 | bucket = var.s3_bucket 43 | key = var.lambda_s3_key 44 | source = var.lambda_filename 45 | source_hash = filebase64sha256(var.lambda_filename) 46 | } 47 | {% endif %} 48 | 49 | module "runtime" { 50 | source = "./{{tf-module-dir}}" 51 | 52 | layer_name = var.runtime_layer_name 53 | {% if not skip-compatible-architectures %} 54 | compatible_architectures = var.runtime_layer_compatible_architectures 55 | {% endif %} 56 | compatible_runtimes = var.runtime_layer_compatible_runtimes 57 | filename = var.runtime_layer_filename 58 | {% if use-s3 %} 59 | s3_bucket = aws_s3_bucket.artifacts.bucket 60 | s3_key = var.runtime_layer_s3_key 61 | {% endif %} 62 | } 63 | 64 | {% if deps-layer-name %} 65 | module "deps" { 66 | source = "./{{tf-module-dir}}" 67 | 68 | layer_name = var.deps_layer_name 69 | {% if not skip-compatible-architectures %} 70 | compatible_architectures = var.deps_layer_compatible_architectures 71 | {% endif %} 72 | compatible_runtimes = var.deps_layer_compatible_runtimes 73 | filename = var.deps_layer_filename 74 | {% if use-s3 %} 75 | s3_bucket = aws_s3_bucket.artifacts.bucket 76 | s3_key = var.deps_layer_s3_key 77 | {% endif %} 78 | } 79 | {% endif %} 80 | 81 | resource "aws_lambda_function" "lambda" { 82 | depends_on = [aws_cloudwatch_log_group.lambda] 83 | 84 | function_name = var.lambda_name 85 | {% if lambda-iam-role %} 86 | role = "{{lambda-iam-role}}" 87 | {% else %} 88 | role = aws_iam_role.lambda.arn 89 | {% endif %} 90 | handler = var.lambda_handler 91 | memory_size = var.lambda_memory_size 92 | source_code_hash = filebase64sha256(var.lambda_filename) 93 | {% if use-s3 %} 94 | s3_bucket = aws_s3_object.lambda.bucket 95 | s3_key = aws_s3_object.lambda.key 96 | {% else %} 97 | filename = var.lambda_filename 98 | {% endif %} 99 | runtime = var.lambda_runtime 100 | architectures = var.lambda_architectures 101 | {% if lambda-timeout %} 102 | timeout = var.lambda_timeout 103 | {% endif %} 104 | layers = [ 105 | module.runtime.arn, 106 | {% if deps-layer-name %} 107 | module.deps.arn, 108 | {% endif %} 109 | ] 110 | {% if lambda-env-vars|length > 0 %} 111 | environment { 112 | variables = { 113 | {% for i in lambda-env-vars %} 114 | {{i.key}} = "{{i.val}}" 115 | {% endfor %} 116 | } 117 | } 118 | {% endif %} 119 | } 120 | 121 | resource "aws_cloudwatch_log_group" "lambda" { 122 | name = "/aws/lambda/${var.lambda_name}" 123 | } 124 | 125 | {% if not lambda-iam-role %} 126 | resource "aws_iam_role" "lambda" { 127 | name = var.lambda_name 128 | assume_role_policy = jsonencode({ 129 | Version = "2012-10-17" 130 | Statement = [ 131 | { 132 | Action = "sts:AssumeRole" 133 | Effect = "Allow" 134 | Principal = { 135 | Service = "lambda.amazonaws.com" 136 | } 137 | } 138 | ] 139 | }) 140 | } 141 | 142 | resource "aws_iam_policy" "lambda" { 143 | name = var.lambda_name 144 | policy = jsonencode({ 145 | Version = "2012-10-17" 146 | Statement = [ 147 | { 148 | Effect = "Allow" 149 | Action = [ 150 | "logs:CreateLogStream", 151 | "logs:PutLogEvents" 152 | ] 153 | Resource = "${aws_cloudwatch_log_group.lambda.arn}:*" 154 | } 155 | ] 156 | }) 157 | } 158 | 159 | resource "aws_iam_role_policy_attachment" "lambda" { 160 | role = aws_iam_role.lambda.name 161 | policy_arn = aws_iam_policy.lambda.arn 162 | } 163 | {% endif %} 164 | -------------------------------------------------------------------------------- /resources/blambda.tfvars: -------------------------------------------------------------------------------- 1 | runtime_layer_name = "{{runtime-layer-name}}" 2 | {% if not skip-compatible-architectures %} 3 | runtime_layer_compatible_architectures = [ 4 | {% for a in runtime-layer-compatible-architectures %} 5 | "{{a}}", 6 | {% endfor %} 7 | ] 8 | {% endif %} 9 | runtime_layer_compatible_runtimes = [ 10 | {% for r in runtime-layer-compatible-runtimes %} 11 | "{{r}}", 12 | {% endfor %} 13 | ] 14 | runtime_layer_filename = "{{runtime-layer-filename}}" 15 | {% if use-s3 %} 16 | s3_bucket = "{{s3-bucket}}" 17 | runtime_layer_s3_key = "{{runtime-layer-s3-key}}" 18 | {% endif %} 19 | 20 | {% if deps-layer-name %} 21 | deps_layer_name = "{{deps-layer-name}}" 22 | {% if not skip-compatible-architectures %} 23 | deps_layer_compatible_architectures = [ 24 | {% for a in deps-layer-compatible-architectures %} 25 | "{{a}}", 26 | {% endfor %} 27 | ] 28 | {% endif %} 29 | 30 | deps_layer_compatible_runtimes = [ 31 | {% for r in deps-layer-compatible-runtimes %} 32 | "{{r}}", 33 | {% endfor %} 34 | ] 35 | deps_layer_filename = "{{deps-layer-filename}}" 36 | {% if use-s3 %} 37 | deps_layer_s3_key = "{{deps-layer-s3-key}}" 38 | {% endif %} 39 | {% endif %} 40 | lambda_name = "{{lambda-name}}" 41 | lambda_handler = "{{lambda-handler}}" 42 | lambda_filename = "{{lambda-filename}}" 43 | lambda_memory_size = "{{lambda-memory-size}}" 44 | lambda_runtime = "{{lambda-runtime}}" 45 | {% if lambda-timeout %} 46 | lambda_timeout = {{lambda-timeout}} 47 | {% endif %} 48 | lambda_architectures = ["{{lambda-architecture}}"] 49 | {% if use-s3 %} 50 | lambda_s3_key = "{{lambda-s3-key}}" 51 | {% endif %} 52 | -------------------------------------------------------------------------------- /resources/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # For local debugging 6 | if [ -n "$LOCAL_LAYERS_DIR" ]; then 7 | LAYERS_DIR=$LOCAL_LAYERS_DIR 8 | DEPS_CLASSPATH_FILE="$LAYERS_DIR/deps-local-classpath" 9 | else 10 | LAYERS_DIR=/opt 11 | DEPS_CLASSPATH_FILE="$LAYERS_DIR/deps-classpath" 12 | fi 13 | 14 | 15 | CLASSPATH=$LAMBDA_TASK_ROOT 16 | if [ -e $DEPS_CLASSPATH_FILE ]; then 17 | CLASSPATH="$CLASSPATH:`cat $DEPS_CLASSPATH_FILE`" 18 | fi 19 | 20 | export BABASHKA_DISABLE_SIGNAL_HANDLERS="true" 21 | export BABASHKA_PODS_DIR=$LAYERS_DIR/.babashka/pods 22 | export GITLIBS=$LAYERS_DIR/gitlibs 23 | 24 | echo "Starting Babashka:" 25 | echo "$LAYERS_DIR/bb -cp $CLASSPATH $LAYERS_DIR/bootstrap.clj" 26 | 27 | PATH="$PATH:$LAYERS_DIR" $LAYERS_DIR/bb -cp $CLASSPATH $LAYERS_DIR/bootstrap.clj 28 | -------------------------------------------------------------------------------- /resources/bootstrap.clj: -------------------------------------------------------------------------------- 1 | ;; AWS Lambda runtime using babashka. 2 | ;; 3 | ;; The bootstrap shell script will run this 4 | 5 | (require '[babashka.http-client :as http] 6 | '[clojure.string :as str] 7 | '[cheshire.core :as cheshire]) 8 | 9 | (def handler-name (System/getenv "_HANDLER")) 10 | (println "Loading babashka lambda handler:" handler-name) 11 | 12 | (def runtime-api-url (str "http://" (System/getenv "AWS_LAMBDA_RUNTIME_API") "/2018-06-01/runtime/")) 13 | 14 | (defn throwable->error-body [t] 15 | {:errorMessage (.getMessage t) 16 | :errorType (-> t .getClass .getName) 17 | :stackTrace (mapv str (.getStackTrace t))}) 18 | 19 | ;; load handler 20 | (def handler 21 | (let [[handler-ns handler-fn] (str/split handler-name #"/")] 22 | (try 23 | (require (symbol handler-ns)) 24 | (resolve (symbol handler-ns handler-fn)) 25 | (catch Throwable t 26 | (println "Unable to run initialize handler fn " handler-fn "in namespace" handler-ns 27 | "\nthrow: " t) 28 | (http/post (str runtime-api-url "init/error") 29 | {:body (cheshire/encode 30 | (throwable->error-body t))}) 31 | nil)))) 32 | 33 | (when-not handler 34 | (http/post (str runtime-api-url "init/error") 35 | {:headers {"Lambda-Runtime-Function-Error-Type" "Runtime.NoSuchHandler"} 36 | :body (cheshire/encode {"error" (str handler-name " didn't resolve.")})})) 37 | 38 | ;; API says not to use timeout when getting next invocation, so make it a long one 39 | (def timeout-ms (* 1000 60 60 24)) 40 | 41 | (defn next-invocation 42 | "Get the next invocation, returns payload and fn to respond." 43 | [] 44 | (let [{:keys [headers body]} 45 | (http/get (str runtime-api-url "invocation/next") 46 | {:timeout timeout-ms}) 47 | id (get headers "lambda-runtime-aws-request-id")] 48 | {:event (cheshire/decode body keyword) 49 | :context headers 50 | :send-response! 51 | (fn [response] 52 | (http/post (str runtime-api-url "invocation/" id "/response") 53 | {:body (cheshire/encode response)})) 54 | :send-error! 55 | (fn [thrown] 56 | (http/post (str runtime-api-url "invocation/" id "/error") 57 | {:body (cheshire/encode 58 | (throwable->error-body thrown))}))})) 59 | 60 | (when handler 61 | (println "Starting babashka lambda event loop") 62 | (loop [{:keys [event context send-response! send-error!]} (next-invocation)] 63 | (try 64 | (let [response (handler event context)] 65 | (send-response! response)) 66 | (catch Throwable t 67 | (println "Error in executing handler" t) 68 | (send-error! t))) 69 | (recur (next-invocation)))) 70 | -------------------------------------------------------------------------------- /resources/lambda_layer.tf: -------------------------------------------------------------------------------- 1 | variable "layer_name" {} 2 | {% if not skip-compatible-architectures %} 3 | variable "compatible_architectures" {} 4 | {% endif %} 5 | variable "compatible_runtimes" {} 6 | variable "filename" {} 7 | {% if use-s3 %} 8 | variable "s3_bucket" {} 9 | variable "s3_key" {} 10 | {% endif %} 11 | 12 | resource "aws_lambda_layer_version" "layer" { 13 | layer_name = var.layer_name 14 | source_code_hash = filebase64sha256(var.filename) 15 | {% if not skip-compatible-architectures %} 16 | compatible_architectures = var.compatible_architectures 17 | {% endif %} 18 | compatible_runtimes = var.compatible_runtimes 19 | {% if use-s3 %} 20 | s3_bucket = aws_s3_object.object.bucket 21 | s3_key = aws_s3_object.object.key 22 | {% else %} 23 | filename = var.filename 24 | {% endif %} 25 | } 26 | 27 | {% if use-s3 %} 28 | resource "aws_s3_object" "object" { 29 | bucket = var.s3_bucket 30 | key = var.s3_key 31 | source = var.filename 32 | source_hash = filebase64sha256(var.filename) 33 | } 34 | {% endif %} 35 | 36 | output "arn" { 37 | value = aws_lambda_layer_version.layer.arn 38 | } 39 | -------------------------------------------------------------------------------- /src/blambda/api.clj: -------------------------------------------------------------------------------- 1 | (ns blambda.api 2 | (:require [babashka.deps :refer [clojure]] 3 | [babashka.http-client :as http] 4 | [babashka.fs :as fs] 5 | [babashka.pods :as pods] 6 | [babashka.process :refer [shell]] 7 | [blambda.internal :as lib] 8 | [clojure.edn :as edn] 9 | [clojure.java.io :as io] 10 | [clojure.string :as str])) 11 | 12 | (defn fetch-pods [{:keys [bb-arch source-dir work-dir] :as opts} pods] 13 | (let [home-dir (System/getProperty "user.home") 14 | os-arch (System/getProperty "os.arch")] 15 | (try 16 | (System/setProperty "user.home" work-dir) 17 | (System/setProperty "os.arch" (if (= bb-arch "arm64") "aarch64" "amd64")) 18 | (doseq [[pod {:keys [path version]}] pods] 19 | (if path 20 | (let [src-path (fs/file source-dir path) 21 | tgt-path (fs/file work-dir path)] 22 | (fs/copy src-path tgt-path)) 23 | (pods/load-pod pod version))) 24 | (finally 25 | (System/setProperty "user.home" home-dir) 26 | (System/setProperty "os.arch" os-arch))))) 27 | 28 | (defn build-deps-layer 29 | "Builds layer for dependencies" 30 | [{:keys [error deps-path target-dir work-dir] :as opts}] 31 | (let [deps-zipfile (lib/deps-zipfile opts)] 32 | (if (empty? (fs/modified-since deps-zipfile deps-path)) 33 | (println (format "\nNot rebuilding dependencies layer: no changes to %s since %s was last built" 34 | (str deps-path) (str deps-zipfile))) 35 | (do 36 | (println "\nBuilding dependencies layer:" (str deps-zipfile)) 37 | (doseq [dir [target-dir work-dir]] 38 | (fs/create-dirs dir)) 39 | (let [gitlibs-dir "gitlibs" 40 | m2-dir "m2-repo" 41 | pods-dir ".babashka" 42 | {:keys [deps pods]} (-> deps-path slurp edn/read-string)] 43 | (if (and (empty? deps) (empty? pods)) 44 | (println (format "\nNot building dependencies layer: no deps or pods listed in %s" 45 | (str deps-path))) 46 | (do 47 | (spit (fs/file work-dir "deps.edn") 48 | {:deps (or deps {}) 49 | :pods (or pods {}) 50 | :mvn/local-repo (str m2-dir)}) 51 | 52 | (let [classpath-file (fs/file work-dir "deps-classpath") 53 | local-classpath-file (fs/file work-dir "deps-local-classpath") 54 | deps-base-dir (str (fs/path (fs/cwd) work-dir)) 55 | classpath 56 | (with-out-str 57 | (clojure ["-Spath"] 58 | {:dir work-dir 59 | :env (assoc (into {} (System/getenv)) 60 | "GITLIBS" (str gitlibs-dir))})) 61 | deps-classpath (str/replace classpath deps-base-dir "/opt")] 62 | (println "Classpath before transforming:" classpath) 63 | (println "Classpath after transforming:" deps-classpath) 64 | (spit classpath-file deps-classpath) 65 | (spit local-classpath-file classpath) 66 | 67 | (when pods 68 | (fetch-pods opts pods)) 69 | 70 | (println "Compressing dependencies layer:" (str deps-zipfile)) 71 | (let [paths (concat [(fs/file-name gitlibs-dir) 72 | (fs/file-name m2-dir) 73 | (fs/file-name pods-dir) 74 | (fs/file-name classpath-file)] 75 | (->> pods 76 | (map (fn [[_ {:keys [path]}]] path)) 77 | (remove nil?)))] 78 | (apply shell {:dir work-dir} 79 | "zip -r" deps-zipfile 80 | paths)))))))))) 81 | 82 | (defn build-runtime-layer 83 | "Builds custom runtime layer" 84 | [{:keys [bb-arch bb-version target-dir work-dir] 85 | :as opts}] 86 | (let [runtime-zipfile (lib/runtime-zipfile opts) 87 | bb-filename (lib/bb-filename bb-version bb-arch) 88 | bb-url (lib/bb-url bb-version bb-filename) 89 | bb-tarball (format "%s/%s" work-dir bb-filename)] 90 | (if (and (fs/exists? bb-tarball) 91 | (empty? (fs/modified-since runtime-zipfile bb-tarball))) 92 | (println "\nNot rebuilding custom runtime layer; no changes to bb version or arch since last built") 93 | (do 94 | (println "\nBuilding custom runtime layer:" (str runtime-zipfile)) 95 | (doseq [dir [target-dir work-dir]] 96 | (fs/create-dirs dir)) 97 | 98 | (when-not (fs/exists? bb-tarball) 99 | (println "Downloading" bb-url) 100 | (io/copy 101 | (:body (http/get bb-url {:as :stream})) 102 | (io/file bb-tarball))) 103 | 104 | (println "Decompressing" bb-tarball "to" work-dir) 105 | (shell "tar -C" work-dir "-xzf" bb-tarball) 106 | 107 | (lib/copy-files! (assoc opts :resource? true) 108 | ["bootstrap" "bootstrap.clj"]) 109 | 110 | (println "Compressing custom runtime layer:" (str runtime-zipfile)) 111 | (shell {:dir work-dir} 112 | "zip" runtime-zipfile 113 | "bb" "bootstrap" "bootstrap.clj"))))) 114 | 115 | (defn build-lambda [{:keys [lambda-name source-dir source-files 116 | target-dir work-dir] :as opts}] 117 | (when (empty? source-files) 118 | (throw (ex-info "Missing source-files" 119 | {:type :blambda/error}))) 120 | (let [lambda-zipfile (lib/zipfile opts lambda-name)] 121 | (if (empty? (fs/modified-since lambda-zipfile 122 | (->> source-files 123 | (map (partial fs/file source-dir)) 124 | (cons "bb.edn")))) 125 | (println "\nNot rebuilding lambda artifact; no changes to source files since last built:" 126 | source-files) 127 | (do 128 | (println "\nBuilding lambda artifact:" (str lambda-zipfile)) 129 | (doseq [dir [target-dir work-dir]] 130 | (fs/create-dirs dir)) 131 | (lib/copy-files! opts source-files) 132 | (println "Compressing lambda:" (str lambda-zipfile)) 133 | (apply shell {:dir work-dir} 134 | "zip" lambda-zipfile source-files))))) 135 | 136 | (defn build-all [{:keys [deps-layer-name] :as opts}] 137 | (build-runtime-layer opts) 138 | (when deps-layer-name 139 | (build-deps-layer opts)) 140 | (build-lambda opts)) 141 | 142 | (defn clean 143 | "Deletes target and work directories" 144 | [{:keys [target-dir work-dir]}] 145 | (doseq [dir [target-dir work-dir]] 146 | (println "Removing directory:" dir) 147 | (fs/delete-tree dir))) 148 | -------------------------------------------------------------------------------- /src/blambda/api/terraform.clj: -------------------------------------------------------------------------------- 1 | (ns blambda.api.terraform 2 | (:require [babashka.fs :as fs] 3 | [babashka.process :refer [shell]] 4 | [blambda.internal :as lib] 5 | [clojure.java.io :as io] 6 | [clojure.string :as str] 7 | [selmer.parser :as selmer])) 8 | 9 | (defn tf-config-path [{:keys [target-dir tf-config-dir] :as opts} filename] 10 | (let [tf-dir (-> (fs/file target-dir tf-config-dir) fs/canonicalize)] 11 | (fs/file tf-dir filename))) 12 | 13 | (defn generate-module [opts] 14 | (selmer/render (slurp (io/resource "lambda_layer.tf")) opts)) 15 | 16 | (defn generate-vars 17 | [{:keys [s3-artifact-path target-dir use-s3 deps-layer-name 18 | skip-compatible-architectures] :as opts}] 19 | (let [runtime-zipfile (lib/runtime-zipfile opts) 20 | runtime-filename (fs/file-name runtime-zipfile) 21 | lambda-zipfile (lib/lambda-zipfile opts) 22 | lambda-filename (fs/file-name lambda-zipfile) 23 | deps-zipfile (when deps-layer-name (lib/deps-zipfile opts)) 24 | deps-filename (when deps-layer-name (fs/file-name deps-zipfile))] 25 | (selmer/render 26 | (slurp (io/resource "blambda.tfvars")) 27 | (merge opts 28 | (when-not skip-compatible-architectures 29 | {:runtime-layer-compatible-architectures (lib/runtime-layer-architectures opts)}) 30 | {:runtime-layer-compatible-runtimes (lib/runtime-layer-runtimes opts) 31 | :runtime-layer-filename runtime-zipfile 32 | :lambda-filename lambda-zipfile 33 | :lambda-architecture (first (lib/runtime-layer-architectures opts))} 34 | (when use-s3 35 | {:lambda-s3-key (lib/s3-artifact opts lambda-filename) 36 | :runtime-layer-s3-key (lib/s3-artifact opts runtime-filename)}) 37 | (when deps-layer-name 38 | (when-not skip-compatible-architectures 39 | {:deps-layer-compatible-architectures (lib/deps-layer-architectures opts)}) 40 | {:deps-layer-compatible-runtimes (lib/deps-layer-runtimes opts) 41 | :deps-layer-filename deps-zipfile}) 42 | (when (and deps-layer-name use-s3) 43 | {:deps-layer-s3-key (lib/s3-artifact opts deps-filename)}))))) 44 | 45 | (defn generate-config [{:keys [lambda-env-vars] :as opts}] 46 | (let [env-vars (->> lambda-env-vars 47 | (map #(let [[k v] (str/split % #"=")] 48 | {:key k 49 | :val v})))] 50 | (selmer/render (slurp (io/resource "blambda.tf")) 51 | (assoc opts :lambda-env-vars env-vars)))) 52 | 53 | (defn run-tf-cmd! [{:keys [tf-config-dir] :as opts} cmd] 54 | (let [config-file (tf-config-path opts "blambda.tf")] 55 | (when-not (fs/exists? config-file) 56 | (throw 57 | (ex-info 58 | (format "Missing Terraform config file %s; run `bb blambda terraform write-config`" 59 | (str config-file)) 60 | {:type :blambda/missing-file 61 | :filename (str config-file)}))) 62 | (shell {:dir (str (fs/parent config-file))} cmd))) 63 | 64 | (defn apply! [opts] 65 | (run-tf-cmd! opts "terraform init") 66 | (run-tf-cmd! opts "terraform apply")) 67 | 68 | (defn import-s3-bucket! [{:keys [s3-bucket] :as opts}] 69 | (run-tf-cmd! opts "terraform init") 70 | (run-tf-cmd! opts (format "terraform import aws_s3_bucket.artifacts %s" s3-bucket))) 71 | 72 | (defn write-config [{:keys [lambda-name tf-module-dir extra-tf-config target-dir] 73 | :as opts}] 74 | (let [opts (assoc opts 75 | :lambda-filename (format "%s.zip" lambda-name)) 76 | lambda-layer-config (generate-config opts) 77 | lambda-layer-vars (generate-vars opts) 78 | lambda-layer-module (generate-module opts) 79 | config-file (tf-config-path opts "blambda.tf") 80 | vars-file (tf-config-path opts "blambda.auto.tfvars") 81 | module-dir (tf-config-path opts tf-module-dir) 82 | module-file (tf-config-path opts (fs/file tf-module-dir "lambda_layer.tf"))] 83 | (when-not (empty? extra-tf-config) 84 | (fs/create-dirs target-dir) 85 | (doseq [f extra-tf-config 86 | :let [filename (fs/file-name f) 87 | target (fs/file target-dir filename)]] 88 | (println "Copying Terraform config" (str f)) 89 | (fs/delete-if-exists target) 90 | (fs/copy f target-dir))) 91 | (fs/create-dirs module-dir) 92 | (println "Writing lambda layer config:" (str config-file)) 93 | (spit config-file lambda-layer-config) 94 | (println "Writing lambda layer vars:" (str vars-file)) 95 | (spit vars-file lambda-layer-vars) 96 | (println "Writing lambda layers module:" (str module-file)) 97 | (spit module-file lambda-layer-module))) 98 | -------------------------------------------------------------------------------- /src/blambda/cli.clj: -------------------------------------------------------------------------------- 1 | (ns blambda.cli 2 | (:require [babashka.cli :as cli] 3 | [blambda.api :as api] 4 | [blambda.api.terraform :as api.terraform] 5 | [clojure.set :as set] 6 | [clojure.string :as str])) 7 | 8 | (def specs 9 | {:bb-arch 10 | {:cmds #{:build-runtime-layer :build-all :terraform-write-config} 11 | :desc "Architecture to target (use amd64 if you don't care)" 12 | :ref "" 13 | :default "amd64" 14 | :values #{"amd64" "arm64"}} 15 | 16 | :bb-version 17 | {:cmds #{:build-runtime-layer :build-all :terraform-write-config} 18 | :desc "Babashka version" 19 | :ref "" 20 | :default "1.3.186"} 21 | 22 | :deps-layer-name 23 | {:cmds #{:build-deps-layer :build-all :terraform-write-config} 24 | :desc "Name of dependencies layer in AWS" 25 | :ref ""} 26 | 27 | :deps-path 28 | {:cmds #{:build-deps-layer :build-all} 29 | :desc "Path to bb.edn or deps.edn containing lambda deps" 30 | :ref "" 31 | :default "src/bb.edn"} 32 | 33 | :extra-tf-config 34 | {:cmds #{:terraform-write-config} 35 | :desc "Filenames of additional Terraform files to include" 36 | :ref "" 37 | :coerce [] 38 | :default []} 39 | 40 | :lambda-handler 41 | {:cmds #{:terraform-write-config} 42 | :desc "Function used to handle requests (example: hello/handler)" 43 | :ref "" 44 | :require true} 45 | 46 | :lambda-env-vars 47 | {:cmds #{:terraform-write-config} 48 | :desc "Lambda environment variables, specified as key=val pairs" 49 | :ref "" 50 | :coerce [] 51 | :default []} 52 | 53 | :lambda-iam-role 54 | {:cmds #{:terraform-write-config} 55 | :desc "ARN of custom lambda role (use ${aws_iam_role.name.arn} if defining in your own TF file)" 56 | :ref ""} 57 | 58 | :lambda-memory-size 59 | {:cmds #{:terraform-write-config} 60 | :desc "Amount of memory to use, in MB" 61 | :ref "" 62 | :default "512"} 63 | 64 | :lambda-name 65 | {:cmds #{:build-lambda :build-all :terraform-write-config} 66 | :desc "Name of lambda function in AWS" 67 | :ref "" 68 | :require true} 69 | 70 | :lambda-runtime 71 | {:cmds #{:terraform-write-config} 72 | :desc "Identifier of the function's runtime (use provided or provided.al2023)" 73 | :ref "" 74 | :default "provided.al2023"} 75 | 76 | :lambda-timeout 77 | {:cmds #{:terraform-write-config} 78 | :desc "Time before the lambda function times out (in seconds)" 79 | :ref "" 80 | :coerce :integer} 81 | 82 | :runtime-layer-name 83 | {:cmds #{:build-runtime-layer :build-all :terraform-write-config} 84 | :desc "Name of custom runtime layer in AWS" 85 | :ref "" 86 | :default "blambda"} 87 | 88 | :s3-artifact-path 89 | {:cmds #{:terraform-write-config} 90 | :desc "Path in s3-bucket for artifacts (if using S3)" 91 | :ref ""} 92 | 93 | :s3-bucket 94 | {:cmds #{:terraform-write-config :terraform-import-artifacts-bucket} 95 | :desc "Bucket to use for S3 artifacts (if using S3)" 96 | :ref ""} 97 | 98 | :skip-compatible-architectures 99 | {:cmds #{:terraform-write-config} 100 | :desc "Skips generating compatible_architectures stanzas when not supported" 101 | :coerce :boolean} 102 | 103 | :source-dir 104 | {:cmds #{:build-deps-layer :build-lambda :build-all} 105 | :desc "Lambda source directory" 106 | :ref "" 107 | :default "src"} 108 | 109 | :source-files 110 | {:cmds #{:build-lambda :build-all} 111 | :desc "List of files to include in lambda artifact; relative to source-dir" 112 | :ref "file1 file2 ..." 113 | :require true 114 | :coerce []} 115 | 116 | :target-dir 117 | {:desc "Build output directory" 118 | :ref "" 119 | :default "target"} 120 | 121 | :tf-config-dir 122 | {:cmds #{:terraform-write-config :terraform-import-artifacts-bucket 123 | :terraform-apply} 124 | :desc "Directory to write Terraform config into, relative to target-dir" 125 | :ref "" 126 | :default "."} 127 | 128 | :tf-module-dir 129 | {:cmds #{:terraform-write-config} 130 | :desc "Directory to write lambda layer Terraform module into, relative to tf-config-dir" 131 | :ref "" 132 | :default "modules"} 133 | 134 | :use-s3 135 | {:cmds #{:terraform-write-config} 136 | :desc "If true, use S3 for artifacts when creating layers" 137 | :coerce :boolean} 138 | 139 | :work-dir 140 | {:desc "Working directory" 141 | :ref "" 142 | :default ".work"}}) 143 | 144 | (def global-opts #{:target-dir :work-dir}) 145 | 146 | (defn apply-defaults [default-opts spec] 147 | (->> spec 148 | (map (fn [[k v]] 149 | (if-let [default-val (default-opts k)] 150 | [k (assoc v :default default-val)] 151 | [k v]))) 152 | (into {}))) 153 | 154 | (defn mk-spec [default-opts cmd-name] 155 | (let [cmd-specs (->> specs 156 | (filter (fn [[_ {:keys [cmds]}]] (contains? cmds cmd-name))) 157 | (into {}))] 158 | (->> (select-keys specs global-opts) 159 | (merge cmd-specs) 160 | (apply-defaults default-opts)))) 161 | 162 | ;; TODO: handle sub-subcommands 163 | (defn ->subcommand-help [default-opts {:keys [cmd desc spec]}] 164 | (let [spec (apply dissoc spec global-opts)] 165 | (format "%s: %s\n%s" cmd desc 166 | (cli/format-opts {:spec 167 | (apply-defaults default-opts spec)})))) 168 | 169 | (defn print-stderr [msg] 170 | (binding [*out* *err*] 171 | (println msg))) 172 | 173 | (defn print-help [default-opts cmds] 174 | (println 175 | (format 176 | "Usage: bb blambda 177 | 178 | All subcommands support the options: 179 | 180 | %s 181 | 182 | Subcommands: 183 | 184 | %s" 185 | (cli/format-opts {:spec (select-keys specs global-opts)}) 186 | (->> cmds 187 | (map (partial ->subcommand-help default-opts)) 188 | (str/join "\n\n")))) 189 | (System/exit 0)) 190 | 191 | (defn print-command-help [cmd spec] 192 | (println 193 | (format "Usage: bb blambda %s \n\nOptions:\n%s" 194 | cmd (cli/format-opts {:spec spec})))) 195 | 196 | (defn error [{:keys [cmd spec]} msg] 197 | (println (format "%s\n" msg)) 198 | (print-command-help cmd spec) 199 | (System/exit 1)) 200 | 201 | (defn mk-cmd [default-opts {:keys [cmd spec] :as cmd-opts}] 202 | (merge 203 | (dissoc cmd-opts :cmd) 204 | {:cmds (if (vector? cmd) cmd [cmd]) 205 | :fn (fn [{:keys [opts]}] 206 | (when (:help opts) 207 | (print-command-help cmd spec) 208 | (System/exit 0)) 209 | (doseq [[opt {:keys [values]}] spec] 210 | (when (and values 211 | (not (contains? values (opts opt)))) 212 | (error {:cmd cmd, :spec spec} 213 | (format "Invalid value for --%s: %s\nValid values: %s" 214 | (name opt) (opts opt) (str/join ", " values))))) 215 | ((:fn cmd-opts) (assoc opts :error (partial error spec))))})) 216 | 217 | (defn mk-table [default-opts] 218 | (let [cmds 219 | [{:cmd "build-runtime-layer" 220 | :desc "Builds Blambda custom runtime layer" 221 | :fn api/build-runtime-layer 222 | :spec (mk-spec default-opts :build-runtime-layer)} 223 | {:cmd "build-deps-layer" 224 | :desc "Builds dependencies layer from bb.edn or deps.edn" 225 | :fn api/build-deps-layer 226 | :spec (mk-spec default-opts :build-deps-layer)} 227 | {:cmd "build-lambda" 228 | :desc "Builds lambda artifact" 229 | :fn api/build-lambda 230 | :spec (mk-spec default-opts :build-lambda)} 231 | {:cmd "build-all" 232 | :desc "Builds custom runtime, deps layer (if necessary), and lambda artifact" 233 | :fn api/build-all 234 | :spec (mk-spec default-opts :build-all)} 235 | {:cmd ["terraform" "write-config"] 236 | :desc "Writes Terraform config for Lambda layers" 237 | :fn api.terraform/write-config 238 | :spec (mk-spec default-opts :terraform-write-config)} 239 | {:cmd ["terraform" "apply"] 240 | :desc "Deploys runtime, deps layer, and lambda artifact" 241 | :fn api.terraform/apply! 242 | :spec (mk-spec default-opts :terraform-apply)} 243 | {:cmd ["terraform" "import-artifacts-bucket"] 244 | :desc "Imports existing S3 bucket for lambda artifacts" 245 | :fn api.terraform/import-s3-bucket! 246 | :spec (mk-spec default-opts :terraform-import-artifacts-bucket)} 247 | {:cmd "clean" 248 | :desc "Removes work and target folders" 249 | :fn api/clean 250 | :spec (mk-spec default-opts :clean)}]] 251 | (conj (mapv (partial mk-cmd default-opts) cmds) 252 | {:cmds [], :fn (fn [m] (print-help default-opts cmds))}))) 253 | 254 | (defn dispatch 255 | ([] 256 | (dispatch {})) 257 | ([default-opts & args] 258 | (try 259 | (cli/dispatch (mk-table default-opts) 260 | (or args 261 | (seq *command-line-args*))) 262 | (catch Exception e 263 | (let [err-type (:type (ex-data e))] 264 | (cond 265 | (contains? #{:blambda/error :org.babashka/cli} err-type) 266 | (do 267 | ;; TODO: print subcommand help here somehow 268 | (print-stderr (ex-message e)) 269 | (System/exit 1)) 270 | 271 | (= :babashka.process/error err-type) 272 | (let [{:keys [exit]} (ex-data e)] 273 | ;; Assume that the subprocess has already printed an error message 274 | (System/exit exit)) 275 | 276 | :else 277 | (throw e))))))) 278 | 279 | (defn -main [& args] 280 | (apply dispatch {} args)) 281 | -------------------------------------------------------------------------------- /src/blambda/internal.clj: -------------------------------------------------------------------------------- 1 | (ns blambda.internal 2 | (:require [babashka.fs :as fs] 3 | [clojure.java.io :as io] 4 | [clojure.string :as str])) 5 | 6 | (defn bb-filename [bb-version bb-arch] 7 | (format "babashka-%s-%s.tar.gz" 8 | bb-version 9 | (if (= "arm64" bb-arch) 10 | "linux-aarch64-static" 11 | "linux-amd64-static"))) 12 | 13 | (defn bb-url [bb-version filename] 14 | (format "https://github.com/babashka/%s/releases/download/v%s/%s" 15 | (if (str/includes? bb-version "SNAPSHOT") "babashka-dev-builds" "babashka") 16 | bb-version filename)) 17 | 18 | (defn zipfile [{:keys [target-dir]} layer-name] 19 | (fs/file (-> (fs/file target-dir) .getAbsolutePath) 20 | (format "%s.zip" layer-name))) 21 | 22 | (defn runtime-zipfile [{:keys [runtime-layer-name] :as opts}] 23 | (zipfile opts runtime-layer-name)) 24 | 25 | (defn lambda-zipfile [{:keys [lambda-name] :as opts}] 26 | (zipfile opts lambda-name)) 27 | 28 | (defn deps-zipfile [{:keys [deps-layer-name] :as opts}] 29 | (zipfile opts deps-layer-name)) 30 | 31 | (defn runtime-layer-architectures [{:keys [bb-arch]}] 32 | (if (= "amd64" bb-arch) 33 | ["x86_64"] 34 | ["arm64"])) 35 | 36 | (defn runtime-layer-runtimes [{:keys [bb-arch]}] 37 | (concat ["provided.al2023"] 38 | (when (= "amd64" bb-arch) 39 | ["provided"]))) 40 | 41 | (defn deps-layer-architectures [_opts] 42 | ["x86_64" "arm64"]) 43 | 44 | (defn deps-layer-runtimes [_opts] 45 | ["provided" "provided.al2023"]) 46 | 47 | (defn s3-artifact [{:keys [s3-artifact-path]} filename] 48 | (format "%s/%s" 49 | (str/replace s3-artifact-path #"/$" "") 50 | filename)) 51 | 52 | (defn copy-files! [{:keys [source-dir work-dir resource?] :as opts} 53 | filenames] 54 | (doseq [f filenames 55 | :let [source-file (cond 56 | resource? (io/resource f) 57 | source-dir (fs/file source-dir f) 58 | :else f) 59 | parent (if (and work-dir (fs/parent f)) (fs/file work-dir (fs/parent f)) (fs/parent f))]] 60 | (println "Adding file:" (str f)) 61 | (when parent 62 | (fs/create-dirs parent)) 63 | (fs/delete-if-exists (fs/file work-dir f)) 64 | (if parent 65 | (fs/copy source-file parent) 66 | (fs/copy source-file work-dir)))) 67 | --------------------------------------------------------------------------------