Introduction
In one of the previous articles, we introduced GraalVM and its Native Image capabilities. In this article, we'll re-use our Spring Boot application based on Spring Cloud Function and its AWS Adapter, which we introduced in the article Develop application with Spring Cloud Function AWS, and adjust it to be deployable as a Lambda Custom Runtime with GraalVM Native Image. Similarly, you can re-use the Spring Boot 3 application introduced in the article Develop application with AWS Serverless Java Containerand make it deployable as a Lambda Custom Runtime with GraalVM Native Image yourself as well. In the article How to develop and deploy Lambda function with custom runtime, we gave a general introduction to this topic with examples of how to deploy a pure Lambda function written in Java without the usage of any frameworks this way. We can reuse many introduced concepts, but there will be some differences dealing with Spring Boot GraalVM Native Image Support. At the time of writing this article and doing the measurements, I used Spring Boot 3.2. version for all my experiments to have comparable results. To use the newer version of Spring Boot (i.e., 3.3), it will maybe be enough to update the version in the pom.xml.
How to write and deploy AWS Lambda as Custom Runtime with GraalVM Native Image using Spring Cloud Function and Spring Boot 3
For the sake of explanation, we'll reuse our sample application and use Oracle GraalVM 22 runtime for our Lambda functions (or you can instead install the newest GraalVM version available). Our sample Spring Boot application is based on Spring Cloud Function, which we introduced in the article Develop application with Spring Cloud Function AWS.
In this application, we'll create and retrieve products and use DynamoDB as the NoSQL database. You can find the DynamoProductDao implementation here. We also put Amazon API Gateway in front of it as defined in AWS SAM template.
General Setup
To build GraalVM Native Image, we'll need to do the following:
- Setup m5.large AWS Cloud9 EC2 instance. You can, of course, use your own Linux environment.
- Install SDKMAN
curl -s "https://get.sdkman.io" | bash
source "/home/ec2-user/.sdkman/bin/sdkman-init.sh"
- Install the latest GraalVM version, for example with
sdk install java 22.0.1-graal
- Install Native Image
sudo yum install gcc glibc-devel zlib-devel
sudo dnf install gcc glibc-devel zlib-devel libstdc++-static
- Install Maven capable of building with the installed GraalVM version. We need a Maven version capable of dealing with the Java 21 or higher version of the source code. For example :
wget https://mirrors.estointernet.in/apache/maven/maven-3/3.8.5/binaries/apache-maven-3.8.5-bin.tar.gz tar -xvf apache-maven-3.8.5-bin.tar.gz sudo mv apache-maven-3.8.5 /opt/
M2_HOME='/opt/apache-maven-3.8.5'
PATH="$M2_HOME/bin:$PATH"
export PATH
If this mirror becomes unavailable, please use another one available for your operating system. On Amazon Linux, you can also execute yum install apache-maven.
Making sample application GraalVM Native Image capable
Spring Boot 3 offers direct Spring Boot GraalVM Native Image Support since version 3.0.
For our sample application to run as a GraalVM Native Image, we need to declare all classes whose objects will be instantiated by reflection. These classes needed to be known by the AOT compiler at compile time. We can do it either in reflect.json as explained in the previous article or do it with Spring AOT support in the Spring Boot ApplicationConfiguration.
With
@RegisterReflectionForBinding({DateTime.class, APIGatewayProxyRequestEvent.class, HashSet.class,
APIGatewayProxyRequestEvent.ProxyRequestContext.class, APIGatewayProxyRequestEvent.RequestIdentity.class,
Product.class, Products.class})
we declared classes that will be instantiated per reflection during the runtime, and with
public static class ApplicationRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection().
registerType(Product.class,
PUBLIC_FIELDS,INVOKE_PUBLIC_METHODS,INVOKE_PUBLIC_CONSTRUCTORS
).
registerType(Products.class,
PUBLIC_FIELDS, INVOKE_PUBLIC_METHODS, INVOKE_PUBLIC_CONSTRUCTORS
);
}
}
we implemented a custom RuntimeHintsRegistrar and allowed it to invoke public methods, set public fields, and invoke constructors on our entity objects as explained in the article. When we need to import our custom ApplicationRuntimeHintsRegistrar as a runtime hint. All the reflection and hint stuff comes from Spring Boot AOT support and package org.springframework.aot.hint.
This registrar needs to be imported in the ApplicationConfiguration class with the annotation @ImportRuntimeHints like this :
@ImportRuntimeHints(ApplicationConfiguration.ApplicationRuntimeHintsRegistrar.class)
Lambda Custom Runtime
To deploy the Lambda function as custom runtime, we need to package everything into a file with a .zip extension, which includes the file with the name bootstrap. This file can either be the GraalVM Native Image, in our case, or contain instructions on how to invoke the GraalVM Native Image placed in another file. Let's explore it.
Building GraalVM Native Image
We'll build GraalVM Native image through plugins and profile defined in pom.xml.
First of all, we define the native profile there:
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
....
</profile>
</profiles>
As part of the profile, we need to perform Spring AOT, which is a process that analyzes your application at build-time and generates an optimized version of it. It is a mandatory step to run a Spring ApplicationContext in a native image. To configure our application to use this feature, we need to define the plugin in the build section of the pom.xml, add an execution for the process-aot goal as recommended by Spring Boot AOT maven plugin and shown in the following example:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>process-aot</id>
<goals>
<goal>process-aot</goal>
</goals>
</execution>
</executions>
</plugin>
As the final step, we need to define another plugin in the build section like this:
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<mainClass>software.amazonaws.Application</mainClass>
<buildArgs>
--enable-url-protocols=http
-H:+AddAllCharsets
</buildArgs>
</configuration>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
We use native-image-maven-plugin from org.graalvm.buildtools tools and execute native-image in the package phase. This plugin requires the definition of the main class, which is in our software.amazonaws.Application (Spring Boot class annotated with @SpringBootApplication). We can also optionally add additional build arguments for GraalVM Native Image.
The default name of the built GraalVM Native Image is exactly the name of the artifact in pom.xml, which is:
<artifactId>aws-spring-boot-3.2-graalvm-native-image</artifactId>
To zip the built GraaVM Native Image as function.zip required by Lambda Custom Runtime, we use the maven-assembly plugin in the build section:
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>native-zip</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<inherited>false</inherited>
</execution>
</executions>
<configuration>
<finalName>function</finalName>
<descriptors>
<descriptor>src/assembly/native.xml</descriptor>
</descriptors>
</configuration>
</plugin>
The finalName we define as function and id as native-zip. We also include native.xml assembly. This assembly defines file format as zip (the complete file name will be ${finalName}-${id}.zip, in our case function-native-zip.zip) and adds the previously built GraalVM Native Image with the name aws-spring-boot-3.2-graalvm-native-image and adds the already defined bootstrap which invokes the GraalVM Native Image :
#!/bin/sh
cd ${LAMBDA_TASK_ROOT:-.}
./ aws-spring-boot-3.2-graalvm-native-image
In the end, we have to build GraalVM Native Image packaged as a zip file, which can be deployed as a Lambda Custom Runtime by using the native profile defined in pom.xml with :
mvn clean package -Pnative
Deploying GraalVM Native Image as a Lambda Custom Runtime
In the AWS SAM template we define the Lambda runtime as provided.al2023, which is the newest version of the custom runtime) and provide the path to the previously built GraalVM Native Image function-native-zip.zip.
Globals:
Function:
CodeUri: target/function-native-zip.zip
Runtime: provided.al2023
Now we are ready to deploy our application with
sam deploy -g
Conclusion
In this article, we took a look at how to write and deploy a Lambda function as a Custom Runtime with GraalVM Native Image with Spring Cloud Function (AWS) and using Spring Boot 3 version.
In the next article of the series, we'll measure the cold and warm start times for this sample application.
Update from March 3, 2025: new measurements with Spring Boot 3.4 and Spring Cloud Function AWS as GraalVM Native Image published in the article Spring Boot 3.4 application on AWS Lambda- Part 6 GraalVM Native Image with Spring Cloud Function AWS.

Top comments (0)