Spring Native and Serverless with Spring Boot apps on Google Cloud!
This blog is part 3 of the 5-part Spring Boot on Google Cloud series. Read Part 1 and Part 2 if you missed it previously.
Before we dive into what is Spring Native and how to deliver that, we will understand the basic concepts quickly.
JIT (Just In Time) Compilation
We all know that Java supports its signature capability — being portable (WORA) — (“Write once, Run Anywhere”) by compiling the .java source files into .class files, in bytecode at compile time, which then is interpreted by the JVM (Java Virtual Machine) on any machine that we wish to run the code. To avoid the cost involved in interpreting bytecode, JVM compiles the frequently invoked functions / code into native code at run-time which is what we fondly refer to as the JIT (Just In Time) Compilation.
Why am I telling you this?
‘Cos, as opposed to JIT, Spring Native supports an approach called AOT (Ahead of Time) Compilation. It means, all reachable code is converted into a native executable at compile-time itself. This executable contains application classes, dependencies, libraries and native code.
Hmm, is it better than JIT?
It’s a trade-off. With JIT, you get portability but with AOT, we trade that for memory efficiency, reduced application startup time. Also you have to remember that, since code is converted into native executable at compile-time, you have to ensure all dependencies like loading a file, are made available at build time.
So, where would I use this?
Applications that require instant startup, applications that are running in highly memory constrained environment, applications that require faster and efficient deployment by packaging into lightweight containers.
Ok, get back to Spring Native
Spring Native is that which enables the process of converting Spring applications into native executables using some open sourced utilities. It is interesting and important to note that this process, at compile-time, grabs all your reachable code (functions) and converts them into native images, and hence is memory-intensive and also lengthy.
… And what is that Open-Sourced utility Spring Native employs?
GraalVM (General Recursive Applicative and Algorithmic Language Virtual Machine) is a high performing open-sourced JDK distribution written for Java (and JVM languages), Ruby, JavaScript, Python etc. What it does for Spring Native is that, it provides a Native Image Builder to build native code from your Java applications, package it with the VM into a native executable!
Alright, get to business
Spring Native is fully integrated into Spring Boot 3.0. However in this hands-on blog, you will learn how to achieve Spring Native in Spring Boot 2 application and deploy it on Cloud Run (Google Cloud’s Serverless Deployment Service) in only 5 steps!!!
We are using Spring Boot 2.7.1, Spring-Native Dependency 0.12.1 (experimental) and Maven build. Since it is experimental version, the difference here is we will include the artifacts manually in pom.xml as the experimental artifacts were not made available in Maven’s central repo.
I hope you have already familiarized yourself with Google Cloud console in part 1 of the series, if not quickly read that short one up, so you are aware of how to launch Google Cloud Shell Terminal and execute the rest of the steps in this blog.
Step 1 — Bootstrap your Spring Boot application
Run the below command in Cloud Shell Terminal:
curl https://start.spring.io/starter.tgz -d dependencies=web -d baseDir=ntv-boot-app -d javaVersion=11 -d bootVersion=2.7.1 -d type=maven-project | tar -xzvf -
This will create a project ntv-boot-app in your Cloud Shell machine with the dependencies in pom.xml and source in com / example / demo / DemoApplication java file.
Add the following code in a new file index.html in the path ntv-boot-app/src/main/resources/static/
<html>
<body>
WELCOME to Spring Native Implementation!
</body>
</html>
Compile the code and run the application by executing the following commands in the Cloud Shell terminal:
./mvnw package
mvn spring-boot:run
Go to Web Preview on the top right corner of your Cloud Shell terminal to view the result like this:
Step 2 — Build a regular image for this application
To build an image for this application, run the following command in Cloud Shell terminal
mvn spring-boot:build-image
This creates the docker image and you can see the steps involved in it in your terminal, like creating a jar, using Paketo builder to pull the necessary libraries and frameworks and finally the built docker image and the time it took. In this case, execute the command below to view the details
docker images demo
As you can notice below, an image of size 262 MB is created in the demo repository.
****@cloudshell:~/ntv-boot-app (***)$ docker images demo
REPOSITORY TAG IMAGE ID SIZE
demo 0.0.1-SNAPSHOT e94231ea6a7f 262MB
Now let’s compare this regular Spring Boot image with its Spring-Native counterpart.
Step 3— Make this Spring Boot App Spring-Native
To make this a Spring Native app, start by including the following dependency in the pom.xml
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.12.1</version>
</dependency>
Include the repositories for the experimental artifacts in the pom.xml file
<repositories>
<repository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
Next, a) update the Spring Boot Maven Plugin to use the Paketo build pack with Native Image configuration and b) Add the AOT (Ahead Of Time) Maven Plugin to pom.xml. Make sure your pom.xml’s <build> segment looks like this:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>0.12.1</version>
<executions>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Step 4 — Build the Spring-Native Image
Run the following command from the Cloud Shell terminal:
mvn spring-boot:build-image
You will notice that the build took more time than regular build and took up extra memory at compile time. But on the bright side, you will see that the size of the native image is far less compared to the regular image. You can check this by running the docker images demo again on Cloud Shell:
docker images demo
As you can notice below, an image of size 86.1MB is created in the demo repository.
****@cloudshell:~/ntv-boot-app (***)$ docker images demo
REPOSITORY TAG IMAGE ID SIZE
demo 0.0.1-SNAPSHOT 0a9227545d6b 86.1MB
From 262MB to 86.1MB is a huge reduction.
Step 5— Deploy the Spring-Native App in Cloud Run
To deploy the spring native image to Cloud Run, we need to 1) Create a repository in Artifact Registry 2) Tag the spring native image 3) Push it into the AR repository 4) Deploy in Cloud Run
In Google Cloud Console, search for Artifact Registry and when it opens the Artifact Registry console, click Create Repository button as highlighted in the following image:
Select the repository name, format, location as “spring-native-repo”, “Docker” and “us-central1” respectively as seen below:
Click CREATE and the repository is created. On the console you will see the repositories in your Artifact Registry. Select the one we just created and on the right pane under Permissions tab, click ADD PRINCIPAL to make sure you’re authenticated to push your image into this repository:
Click ADD PRINCIPAL and enter your user in the New Principals and in Assign Roles select Artifcat Registry and Artifact Registry Writer:
The next step is to tag and push our native image to the repository. In all the commands below, replace <<YOUR_PROJECT_ID>> with your current working project id.
Tag:
docker tag demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/<<YOUR_PROJECT_ID>>/spring-native-repo/sn-image:first
Push:
docker push us-central1-docker.pkg.dev/<<YOUR_PROJECT_ID>>/spring-native-repo/sn-image:first
Finally, to deploy the app in Cloud Run, execute the below command from Cloud Shell terminal:
gcloud run deploy spring-native-app --image us-central1-docker.pkg.dev/<<YOUR_PROJECT_ID>>/spring-native-repo/sn-image:first --platform managed --region us-central1 --allow-unauthenticated
Once deployed, you should the service endpoint in your terminal:
Once you click the deployed app URL, you would see that the native-image application started up really quickly compared to the regular image!
Conclusion
I hope this blog was helpful in explaining the concepts of Spring Native, Ahead Of Time compilation and usage of GraalVM in building native images that help achieve faster startup time, smaller images and efficient deployments.
This blog is part of my Spring Boot on Google Cloud series. Read Part 1 and Part 2 if you missed it previously!