AWS Lambda support many build in runtimes. But we also have a possibility to create custom runtimes to run functions in a language that is not yet supported by Lambda service or when we need to do some extra optimization (i.e. reduce startup time or memory footprint of functions build for JVM). Today we will learn how to build a very simple runtime focusing mainly on steps required to do so.
Custom runtime
Every runtime is executed on Amazon Linux or Amazon Linux 2 operating system. In case of JVM, functions need at least a Java Runtime Environment (JRE) to run a jar
but it won’t be available out of the box on mentioned systems. Thankfully starting from Java 9 a jlink
tool was introduced which creates a customized JRE that contains only these modules that are required by your own module - this way JRE can be part of the deployment package.
To create a runtime we need a bootstrap
file to be present in the root of a deployment package. This file is an entry point for the custom runtime and will be called by Lambda as part of init
phase.
Source: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html
Deployment package
So far we know that custom runtime will require a bootstrap
file and custom JRE. Since we will be working with (SAM CLI) we can use the possibility to customize build
command. This can be done by providing Metadata
resource attribute with a BuildMethod: makefile
entry
Resources:
NaiveJavaRuntimeFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: NaiveJavaRuntime
Handler: do.not.apply
Runtime: provided
MemorySize: 128
Metadata:
BuildMethod: makefile
By convention sam build
will look for build-NaiveJavaRuntimeFunction
target in a Makefile
.
build-NaiveJavaRuntimeFunction:
@ mvn clean package
@ chmod +x ./target/classes/bootstrap
@ cp -rf ./target/classes/bootstrap $(ARTIFACTS_DIR)
@ jlink --module-path ./target/naive-java-runtime.jar:$(JAVA_HOME)/jmods \
--add-modules naive.java.runtime \
--output ./target/dist \
--launcher executable=naive.java.runtime/io.github.ajurasz.naivejavaruntime.Bootstrap \
--compress 2 --no-header-files --no-man-pages --strip-debug
@ cp -rf ./target/dist $(ARTIFACTS_DIR)
Here’s what is happening:
- Build package using maven
- Change
bootstrap
file permission - Copy
bootstrap
file (which is part of project resources) toARTIFACTS_DIR
- Build custom JRE. Notice
--launcher
option which will create an executable file (dist/bin/executable
) which when called will run the main method - Copy output created by
jlink
toARTIFACTS_DIR
At the end ARTIFACTS_DIR
will look like this:
.
├── bootstrap
└── dist
├── bin
│ ├── executable
│ ├── java
│ └── keytool
├── conf
│ ├── net.properties
│ └── security
├── legal
│ ├── java.base
│ └── java.net.http
├── lib
│ ├── classlist
│ ├── jexec
│ ├── jli
│ ├── jrt-fs.jar
│ ├── jspawnhelper
│ ├── jvm.cfg
│ ├── libjava.so
│ ├── libjimage.so
│ ├── libjsig.so
│ ├── libnet.so
│ ├── libnio.so
│ ├── libverify.so
│ ├── libzip.so
│ ├── modules
│ ├── security
│ ├── server
│ └── tzdb.dat
└── release
When the function was deployed we can give it a try in Lambda console.
Runtime
Beside initialization, custom runtime is also responsible for processing tasks consisting of:
- Getting an event
- Propagating the tracing header
- Creating a context object
- Invoking the function handler
- Handling the response
- Handling errors
- Cleanup
My naive implementation meets just three of these tasks. To support all of them it would require much more development and then the maintenance of that code. But there is a better alternative to reinventing the wheel - taking advantage of very impressive Quarkus module. Except that it provides an implementation for all processing tasks it also gives the possibility to build a native image which additionally lower memory footprint and accelerates initialization times. All this can be done with small adjustments to pom.xml.
When the function was deployed we can give it a try in Lambda console.
Please notice time difference between these two functions, native executable is 11 times faster. If it comes to deployment package size it’s 30% smaller (24 MB instead of 34 MB).
Source code can be found here.