What we’ll be doing:

In this article I’ll show you how to build a GraalVM native-image to be deployed on AWS Lambda to massively decrease your cold start times.

What’s the result?

Java AWS Lambda Function with near 200 milliseconds cold start.

Some of the instructions for this article came from following instructions from Quarkus here - however this article explains the steps I needed to take to get working and hope it helps you too.

Important note on building the image for AWS Lambda

The Lambda runtime is Linux based (amd64 based architecture) - and the native-image is a binary file that does NOT start the JVM so the binary must be built on the same platform as the target runtime. If you build the binary on a Non-Linux OS and try to run it in Lambda you’ll see this error:

{
  "errorMessage": "RequestId: <reqid> Error: Runtime exited with error: exit status 127",
  "errorType": "Runtime.ExitError"
}

Alright now lets get started :)

Create your project using Quarkus Maven archetype

We first need to get a template project that comes with Quarkus dependencies and with a vanilla structure.

mvn archetype:generate \
       -DarchetypeGroupId=io.quarkus \
       -DarchetypeArtifactId=quarkus-amazon-lambda-archetype \
       -DarchetypeVersion=1.13.7.Final

Building locally with Docker

As mentioned already - if you’re not building your project on a Linux based host already you will need to use Docker. These commands can also be put into your CI pipeline :).

You can use the below Dockerfile to build your application.

FROM maven:3.6.3-openjdk-11-slim

RUN apt-get update -qq && apt-get install -y -qq build-essential
RUN apt-get install -y -qq libz-dev zlib1g-dev python3-pip

ARG MAVEN_OPTS="-Dmaven.repo.local=.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"

ARG MAVEN_CLI_OPTS="--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"

ARG CI_PROJECT_DIR=$PWD

ARG GVM=21.1.0

COPY ./pom.xml ./pom.xml
COPY src ./src/

RUN curl -L https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-${GVM}/graalvm-ce-java11-linux-amd64-${GVM}.tar.gz > graalvm-ce-java11-linux-amd64-${GVM}.tar.gz

RUN tar -zxf graalvm-ce-java11-linux-amd64-${GVM}.tar.gz -C ${CI_PROJECT_DIR}/

RUN ${CI_PROJECT_DIR}/graalvm-ce-java11-${GVM}/bin/gu install native-image

RUN export GRAALVM_HOME=${CI_PROJECT_DIR}/graalvm-ce-java11-${GVM}

ENV PATH=${CI_PROJECT_DIR}/graalvm-ce-java11-${GVM}/bin:${PATH}

RUN mvn package -Pnative

Run the below command to build

docker build -t alex/quarkus-test .

Copy the deployable artifact to your host system

docker create -ti --name dummy alex/quarkus-test:latest bash
docker cp dummy:/target/function.zip function.zip
docker rm -f dummy

Now you can take your function.zip and deploy it too AWS Lambda. Skip to the Deploying to Lambda section of the article where we handle that!

Building locally without Docker

Note that deploying the artifact when built locally witout Docker will only work if you are running on Linux. That being said it could be good to experiment and also to try to test out running the native image before you try to deploy it to AWS.

Download the Dependencies for GraalVM and native-image

I found it easiest to install GraalVM from a CLI tool called jabba the link is here.

Once you installed Jabba take a look to see what available versions of GraalVM based JVM’s are by listing with

jabba ls-remote

You should see some prefixed with graalvm-ce-java pick the java version you’d like to work with and run:

jabba install <version you chose>
jabba use <version you chose>

Verify your java version was set correctly

$ java -version
openjdk version "1.8.0_282"
OpenJDK Runtime Environment (build 1.8.0_282-b07)
OpenJDK 64-Bit Server VM GraalVM CE 21.0.0.2 (build 25.282-b07-jvmci-21.0-b06, mixed mode)

Check to make sure JAVA_HOME is also set to the GraalVM one.

echo $JAVA_HOME

If the output mentions GraalVM you should be good! In some cases the path shown could be a pointer or symlink to a different java home.

Install native-image using the GraalVM install tool

gu install native-image

If you encounter errors in this step or the following Maven build steps then it’s possible you should probably set your JAVA_HOME. Otherwise you can skip the next section.

Optional setup with JAVA_HOME

You may need to tell Maven which version of java to use in case it is referencing a different version.

  1. First get the version with jabba
$ jabba ls
graalvm-ce-java8@21.0.0 
  1. Ask jabba where it is installed.
$ jabba which --home graalvm-ce-java8@21.0.0
> /Users/alex/.jabba/jdk/graalvm-ce-java8@21.0.0/Contents/Home
  1. Copy that path and set the $JAVA_HOME to that path if it is not set yet.
JAVA_HOME=/Users/alex/.jabba/jdk/graalvm-ce-java8@21.0.0/Contents/Home

Build the GraalVM project

Unzip the file and you will see a demo project. At the CLI do a cd demo you should now be inside the project.

You’ll need to set some of the command line and environment variable parameters. For brevity - you can see those MAVEN_ prefixed ones in the Dockerfile. Set those in your shell.

To build a native image you can now tell Maven the packaging type.

mvn package -Pnative

If the build is successful you should be able to navigate to the build artifacts under the /target directory. There you’ll find the jar file and also a function.zip artifact with your binary file inside!

Deploying to Lambda

The Quarkus documentation I had mentioned before has some helper scripts on deploying the lambda - I found these to be okay but I wanted to do myself to understand the minimum viable steps necessary to get it up and running.

I know some people aren’t comfortable with the AWS CLI (sometimes I’m not either!) so I’m showing from the console here how to deploy the function.

Create a Lambda function and upload the binary

your image

Make sure to specify to choose the option

“Provide your own bootstrap on Amazon Linux 2”

Add the zip file you built function.zip by clicking

“Upload from .zip file”

Set the handler

io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest

According to the Quarkus documentation:

Do not change the handler switch. This must be hardcoded to io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest. This handler bootstraps Quarkus and wraps your actual handler so that injection can be performed.

Test your function!

Trigger a test event (configure depending on what your handler input expects)

your image

Here was the Lambda cold start stats :)

your image

Note the Init duration and compare this with another java project running on Lambda.

Next steps

Now that you’ve got a simple application that can build + run on AWS Lambda you will probably need to actually make it do something. To get you started I suggest reading this Quarkus documentation on some common pitfalls that arise. Good luck!