Here is a very basic but complete example of a working shadow-cljs-based application that works as a standalone server uberjar, standalone executable server, and a lambda proxy extension using AWS API Gateway.
First install the necessary npm packages.
npm install
Compile clojurescript in dev mode to resources/public/app.js
clj -M:cljs watch :app
Now start a repl and run (gateway-example.main/start-server)
. This starts up
a basic web server with a ring handler stack. This stack servers a homepage and
the required resources. Note that some of these resources are in resources/public.
At this point you have a working server and you can change the text in webapp and it should hot-reload. We can do one better, however, in that we can have boot up a clojurescript repl.
When shadow-cljs booted up it listed a port - 8777. Connect to this with your IDE. You should see a prompt that looks like this:
shadow.user>
We can query shadow-cljs to find the running apps:
shadow.user> (shadow/active-builds)
#{:app}
And we can connect to an active build:
shadow.user> (shadow/repl :app)
To quit, type: :cljs/quit
[:selected :app]
cljs.user>
You can test this with an alert - (js/alert "hey")
.
- Compile clojurescript in release mode -
rm -rf resources/public/js/* && clj -M:cljs release app
This builds app.js, an all-in-one js that bundles everything we are using.
Now build the uberjar
clj -X:standalone-server
You should be able to run the server via
java -jar target/standalone.jar
Now that we have an uberjar we can compile a graal native executable with this functionality. The repo includes three scripts -
- scripts/get-graal - get a linux distribution of graal native compiled with java 11.
- scripts/activate-graal - get graal if user doesn't have it, then export environment
variable
GRAALVM_HOME
that indicates where graal is installed. Additionally update path such that the graal-installed java, javac, and various graalvm utilites are before anything else. - scripts/compile-standalone - Here is where the magic is. This command works for Linux but I imagine various things need to change for Mac. Ask in the Clojurians/graal slack channel for more information about this script; it packages resources and has additions for postgres and httpkit's ssl engine as well as enabling http,https support.
All you have to do at this point is run:
scripts/compile-standalone
Which should result in an executable compiled to target/standalone. Running this executable provides the server.
chrisn@chrisn-lt-01:~/dev/cnuernber/cljs-lambda-gateway-example/target$ ./standalone
08:26:33.039 [main] INFO gateway-example.main - Starting server on port 3000
08:26:33.041 [main] INFO gateway-example.main - Main function exiting-server still running
Here things start to get interesting. We first want to compile the proxy lambda and upload it to AWS. You will need credentials in your environment for this step.
AWS Lambda gives you as the developer an interesting option - a 'custom' lambda runtime is simply an executable script or file named 'bootstrap' in a zip file. This pares well with graal native but we do need bootstrap to be a script so we can set java runtime variables; namely -Xmx so we stay within the bounds of our default lambda memory requirements.
We have a simple build script - scripts/compile-proxy-lambda that builds an uberjar with the proxy-lambda function and packages it, along with our launch script into a zipfile named proxy-lambda.zip.
scripts/compile-proxy-lambda
Once this script completes you should see target/proxy-lambda.zip is created.
We now need to create an IAM role with the basic lambda execution permission that we have to attach to our lambda.
- In the AWS console, to to IAM.
- From the left menu, click on Roles.
- Create Role - Lambda - then click Next
- Find the
AWSLambdaBasicExecutionRole
policy and attach it. - Name your role and finish up. Get the arn - mine was
arn:aws:iam::801514925221:role/lambda-role
. It has one policy attached which is the policy listed above.
Now we can upload that script to AWS assuming you have the appropriate credentials in your environment.
aws lambda create-function --function-name proxy-lambda \
--zip-file fileb://target/proxy-lambda.zip --handler proxy.handler --runtime provided \
--role arn:aws:iam::801514925221:role/lambda-role
Successful output ends with:
...
"RevisionId": "e5764007-f018-4882-985f-dc8e408c9009",
"State": "Active",
"LastUpdateStatus": "Successful"
}
You should also be able to see your lambda in your console.
I wish I had the time to come up with a command line pathway to do this. The tricky part of that is the permissions; gateway needs specific access to your lambda which console does automagically and correctly. In any case, this part of our walkthrough now goes back to the console.
- In Console, got to API Gateway.
- Find Rest API and click Build.
- Click New API and fill out details ensuring you click Regional. I named mine
proxy-lambda-example
. - This brings you to your API management screen and here lie demons so hang in there.
- In the middle panel, click on the single '/' resource. Choose
Create Method
from the dropdown and a new dropdown appears. Select 'ANY'. - Now we configure our method to be a proxy lambda method. Choose 'Lambda' as the type and click the
Use Proxy Lambda Itegration
box and select our lambda method (proxy-lambda in my case) as the lambda function to run.
With that, you should see a test screen. We can test our method - click Test, select Get, and hit Test. At this point you should see your homepage in HTML form echoed back to you from the proxy lambda. New we need to setup additional routes with a wildcard so urls derived from our initial url also go through our lambda.
- In the middle pane click Actions.
- Click
Deploy API
, click 'create stage' name your stage. The other details are optional. - This brings to you Stages where near the top you see a test URL. Clicking this brings you to a blank page. Our HTML homepage loaded but our resources did not; you can verify this in the network tab of your browser dev tools where you see a bunch of 403 error codes.
That still is a large step further but of course we want our entire site to load which involves more than just hitting the base url.
- From the left pane go back to Resources.
- From Actions click
Create Resource
. - Click 'configure as proxy resource'
- Click 'Create Resource`.
- Configure it to use our proxy lambda function.
You should now have two methods configured under your base resource.
Now retest with your test url but make sure to add a '/' at the end. For example in my case https://izo5hfi5d8.execute-api.us-west-2.amazonaws.com/test returns the same 403 errors while https://izo5hfi5d8.execute-api.us-west-2.amazonaws.com/test/ returns the resources encoded in base-64. We fix that now.
There is one more bit of configuration we have to do to our API gateway to support base64 encoded resources which will be most of the file-based resources such as js files, css files, and images.
- In API Gateway, in leftmost pane click the first Settings. It is indented a bit.
- You should see a set of panels. A few are interesting but near the end is a panel
that named
Binary Media Types
. ClickAdd Binary Media Type
. - Type in
*/*
. This simply allows all media types to be binary. Our code in proxy_lambda.clj explicitly binary encodes any response bodies that are streams. - Save changes.
- Redeploy API.
A hard reload (Shift-F5) should reload your page and at this point you should see our nice welcome screen.
If you make a change to proxy-lambda and you want to update it, run 'compile-proxy-lambda to produce a new zip package and here is the command line to update it:
aws lambda update-function-code --function-name proxy-lambda \
--zip-file fileb://target/proxy-lambda.zip
It is instructive at this point to check the CloudWatch logs. Logs for our lambda are nicely done and since we are careful to write out JSON our logs are quite readable -- we can get into fancier ways to do structured logging that integrates with CloudWatch later. We can also enable Cloudwatch logs for API gateway if we so choose.
- HolyLambda - supports babashka-based deployments.
- graalvm-clojure - lots of examples of various projects.
- babashka - Compiled Clojure command line system with extensive extension system.
- avclj - In depth Clojure program using FFMpeg's shared libraries directly and compiling both an an executable and has an example of compiling Clojure to a library and calling it from C++.