DEV Community

Vadym Kazulkin for AWS Heroes

Posted on

Spring Boot 3 application on AWS Lambda - Part 12 Develop application with GraalVM Native Image

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.

Image description

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"
Enter fullscreen mode Exit fullscreen mode
sdk install java 22.0.1-graal
Enter fullscreen mode Exit fullscreen mode
sudo yum install gcc glibc-devel zlib-devel 
sudo dnf install gcc glibc-devel zlib-devel libstdc++-static
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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
     );
   }
 }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now we are ready to deploy our application with

sam deploy -g
Enter fullscreen mode Exit fullscreen mode
@ImportRuntimeHints(ApplicationConfiguration.ApplicationRuntimeHintsRegistrar.class)
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)