Spring Boot 3 application on AWS Lambda - Part 12 Develop application with GraalVM Native Image
Vadym Kazulkin
Posted on November 25, 2024
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 the examples how to deploy pure Lambda function written in Java without the usage of any frameworks this way. We can re-use many introduced concepts, but there will be some differences dealing with Spring Boot GraalVM Native Image Support. In 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 re-use 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
In order to build GraalVM Native Image we'll need to do the following:
- Setup m5.large AWS Cloud9 EC2 instance. you can od 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 its version 3.0.
In order to our sample application to run as GraalVM Native Image we need to declare all classes which objects will be instantiated per reflection. These classes needed to be known by 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 which 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 custom RuntimeHintsRegistrar and allowed 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 hints. All the reflection and hint stuff comes from Spring Boot AOT support and package org.springframework.aot.hint.
Lambda Custom Runtime
In order to deploy the Lambda function as custom runtime we need to package everything into the the file with .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 how to invoke 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 native profile there:
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
....
</profile>
</profiles>
As a part of 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.
Default name of the built GraalVM Native Image is exact the name of the artifact in pom.xml which is:
<artifactId>aws-spring-boot-3.2-graalvm-native-image</artifactId>
In order 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 Lambda Custom Runtime by using 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
@ImportRuntimeHints(ApplicationConfiguration.ApplicationRuntimeHintsRegistrar.class)
Conclusion
In this article we took a look into how to write and deploy Lambda function as 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.
Posted on November 25, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.