Creating Spring Boot container in a minute with Cloud Native Buildpacks for Azure Container Platform

Jay Lee
9 min readMar 19, 2022

From the day I start using nice technology called “Kubernetes”, creating containers right has been always a cumbersome job that I can’t avoid for everyday work. Especially the fact that back in the day I needed to keep switching mode between ‘cf push’(I still ❤️ Cloud Foundry) and ‘Dockerfile’, I never get to like writing ‘Dockerfile’ at all. Then Cloud Native Buildpack came into the world in 2018 and eventually becomes CNCF incubating project, and this was like dream come true. No more manual writing Dockerfile, and most importantly ‘buildpack experiences’ finally land on Kubernetes world. If you’re a former user of Cloud Foundry or Heroku, you should be able to resonate well.

This article is for those of you who are not familiar with Cloud Native Buildpack. I’d like to show you ‘buildpack experiences’ and how it can make developers' life more productive by saving your effort building production-ready container images.

Prerequisite

  1. pack CLI
  2. Docker desktop
  3. Spring Boot project

Getting started with Cloud Native Buildpack(CNB)

Putting CNB in one sentence, CNB takes source code and builds it into container images without writing Dockerfile. It does way more than just containerization, but let’s save it for now. CNB supports not only Java but also other languages like .NET, Go, Java, Node.js, Python, PHP, Ruby, etc. It can also containerize your static contents like Javascript with Apache HTTPD, and Nginx.

NOTE: In this article, I will mainly focus on getting started, so I will save deep technical details for the next article.

Before we go into more details, let’s just try it and see what happens. You could simply use mvn spring-boot:build-image but we won’t do it here for the sake of understanding the basics of CNB.

Look at the command below, I tell pack to build eggboy/springboot:0.0.1 container image. It will take a fair bit of time as it will download maven dependencies for the first time.

$ ls
HELP.md mvnw mvnw.cmd pom.xml src
$ pack build eggboy/springboot:0.0.1 --builder paketobuildpacks/builder:base
.....
===> ANALYZING
Previous image with name "eggboy/springboot:0.0.1" not found
===> DETECTING
8 of 20 buildpacks participating
paketo-buildpacks/ca-certificates 3.1.0
paketo-buildpacks/bellsoft-liberica 9.2.0
paketo-buildpacks/syft 1.10.0
paketo-buildpacks/maven 6.4.1
paketo-buildpacks/executable-jar 6.1.0
paketo-buildpacks/apache-tomcat 7.2.0
paketo-buildpacks/dist-zip 5.2.0
paketo-buildpacks/spring-boot 5.8.0
===> RESTORING
===> BUILDING
....

If you’re inquisitive enough, it makes you wonder how on earth buildpack knows if it’s a Java project specifically a maven project based on what I said earlier that CNB supports other languages. CNB has an internal DETECT phase to find out what languages the source project is based on, then line up the right buildpacks for your project. You will see how that works in detail in the next article.

docker run our new image eggboy/springboot:0.0.1 .

$ docker run -p 8080:8080 eggboy/springboot:0.0.1
Setting Active Processor Count to 6
WARNING: Unable to convert memory limit "max" from path "/sys/fs/cgroup/memory.max" as int: memory size "max" does not match pattern "^([\\d]+)([kmgtKMGT]?)$"
Calculating JVM memory based on 1514764K available memory
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx917453K -XX:MaxMetaspaceSize=85310K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 1514764K, Thread Count: 250, Loaded Class Count: 12648, Headroom: 0%)

Enabling Java Native Memory Tracking
Adding 128 container CA certificates to JVM truststore
Spring Cloud Bindings Enabled
Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_bellsoft-liberica/java-security-properties/java-security.properties -XX:+ExitOnOutOfMemoryError -XX:ActiveProcessorCount=6 -XX:MaxDirectMemorySize=10M -Xmx917453K -XX:MaxMetaspaceSize=85310K -XX:ReservedCodeCacheSize=240M -Xss1M -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -Dorg.springframework.cloud.bindings.boot.enable=true

Some notable messages are worth explaining.

  1. Somehow, the container image built by CNB tries to be smart to calculate the right Heap and Metaspaces sizes.
  2. Adding CA certificates to JVM truststore
  3. XX settings are auto-configured somehow.

At this moment, you should realize that CNB does more than just containerization. It acts smart to configure the JAVA_OPTS using kind of heuristics by looking at available compute resources, CPU, and memory. Let’s try one more thing. This time, run the image with two environment variables, BPL_JMX_ENABLED=true and BPL_JMX_PORT=9999 that hints to enable JMX and open JMX port as 9999.

$ docker run --env BPL_JMX_ENABLED=true --env BPL_JMX_PORT=9999 -p 9999:9999 -p 8080:8080 eggboy/springboot:0.0.1
...
Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_bellsoft-liberica/java-security-properties/java-security.properties -XX:+ExitOnOutOfMemoryError -XX:ActiveProcessorCount=6 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.rmi.port=9999 -XX:MaxDirectMemorySize=10M -Xmx928413K -XX:MaxMetaspaceSize=85310K -XX:ReservedCodeCacheSize=240M -Xss1M -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -Dorg.springframework.cloud.bindings.boot.enable=true

You can see that JMX-related options appeared automatically. This is what I meant by ‘buildpack experiences’. Buildpack can make developers so much more productive by freeing them from dealing with Dockerfiles, optimal Java settings, etc.

Before we move on to the next topic, let’s revisit the message we saw from pack. From the previous message as seen below, it seems that buildpack is not just single but multiple of them that are participating in the building process. There can be numerous different buildpacks to contribute to container image.

paketo-buildpacks/ca-certificates   3.1.0
paketo-buildpacks/bellsoft-liberica 9.2.0
paketo-buildpacks/syft 1.10.0
paketo-buildpacks/maven 6.4.1
paketo-buildpacks/executable-jar 6.1.0
paketo-buildpacks/apache-tomcat 7.2.0
paketo-buildpacks/dist-zip 5.2.0
paketo-buildpacks/spring-boot 5.8.0

For example, paketo-buildpacks/ca-certificates is described as “The Paketo CA Certificates Buildpack is a Cloud Native Buildpack that adds CA certificates to the system truststore at build and runtime.” paketo-buildpacks/maven is described as “The Paketo Maven Buildpack is a Cloud Native Buildpack that builds Maven-based applications from source”. Each buildpack has its role to play while building and running container images.

Building images for Azure Container Platform

We have briefly touched upon the very basics of using CNB. Now, we will take a step further to build images to run on Azure Container Platforms like Azure Kubernetes Service, Azure Container Instances, Azure App Service, and Azure Container Apps. Azure Spring Cloud is embedded with Tanzu build service which is using CNB under the hood.

We will build an image this time with Microsoft OpenJDK instead of bellsoft-liberica JDK. But before that, Why do you wanna use Microsoft build of OpenJDK over other OpenJDK distros? First and foremost, supportability. For Azure customers, running Microsoft Build of OpenJDK on Azure, Azure Stack, Azure Arc with active Azure support plans will get the customer service just like any other Azure product. If you have hit the JDK bug, for example, you can raise the support case through a standard Azure support channel just like what you do with other Azure services. This should give you enough assurance to run your production Java workload on Azure.

Let’s build it. Supply paketo-buildpacks/java-azure as a buildpack so that CNB knows what to do.

$ pack build eggboy/springboot:0.0.1 --builder paketobuildpacks/builder:base --buildpack paketo-buildpacks/java-azure
...
===> DETECTING
8 of 20 buildpacks participating
paketo-buildpacks/ca-certificates 3.1.0
paketo-buildpacks/microsoft-openjdk 2.2.0
paketo-buildpacks/syft 1.10.0
paketo-buildpacks/maven 6.4.1
paketo-buildpacks/executable-jar 6.1.0
paketo-buildpacks/apache-tomcat 7.2.0
paketo-buildpacks/dist-zip 5.2.0
paketo-buildpacks/spring-boot 5.8.0
...

Once it’s completed, run and verify it.

$ docker run -p 8080:8080 eggboy/springboot:0.0.1
...
$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f32c63654e05 eggboy/springboot:0.0.1 "/cnb/process/web" About a minute ago Up About a minute 0.0.0.0:8080->8080/tcp, 0.0.0.0:9999->9999/tcp reverent_bohr
$ docker exec -it f32c63654e05 /bin/bashcnb@f32c63654e05:/workspace$ /layers/paketo-buildpacks_microsoft-openjdk/jdk/bin/java -version
openjdk version "11.0.14" 2022-01-18 LTS
OpenJDK Runtime Environment Microsoft-30257 (build 11.0.14+9-LTS)
OpenJDK 64-Bit Server VM Microsoft-30257 (build 11.0.14+9-LTS, mixed mode)

One more nice ‘buildpack experience’ that we’re missing here. We see that the default version of JDK is 11. but what if we want JDK17? We can easily make it happen using one of the configurations of java-azure buildpack.

$ pack build eggboy/springboot:0.0.1 --builder paketobuildpacks/builder:base --buildpack paketo-buildpacks/java-azure --env BP_JVM_VERSION=17
...
Microsoft OpenJDK 17.0.2: Contributing to layer
Downloading from https://aka.ms/download-jdk/microsoft-jdk-17.0.2.8.1-linux-x64.tar.gz
Verifying checksum
Expanding to /layers/paketo-buildpacks_microsoft-openjdk/jdk
...

Azure Application Insights Buildpack

We have covered enough about building images. Let’s shift the gear to look at the application monitoring. For those of you who are not familiar with Application Insights, it’s an Azure native Application Performance Monitoring service that can monitor .NET, Node.js, Java, Javascript, and Python.

App Insights offers an instrumentation-based monitoring agent meaning you don’t have to change your code at all. Agent auto collect requests like servlet, JMS, Netty/Webflux, etc, and also downstream services like JDBC, Kafka, MongoDB, Redis, Cassandra, etc. All you need to do is to inject the agent jar file and configure it with JVM args, and of course, CNB can reduce this toil for us. We will use Paketo Azure Application Insights Buildpack to configure the agent inside a container.

Using buildpack will look different this time. App insights buildpack uses Binding which is based on volume. As it relies on volume, we should create a volume structure. Create appinsights folder and create a file type with contents of ApplicationInsights . This tells CNB that we need binding for ApplicationInsights buildpack.

$ ls
HELP.md mvnw mvnw.cmd pom.xml src
$ mkdir appinsights
$ cd appinsights
$ echo "ApplicationInsights" > type
$ cd ..
$ ls
HELP.md appinsights mvnw mvnw.cmd pom.xml src

Then run pack with --volume to supply the newly created file type .

$ pack build eggboy/springboot:0.0.1 --builder paketobuildpacks/builder:base --buildpack paketo-buildpacks/java-azure --volume "$(pwd)/appinsights:/platform/bindings/application-insights"...9 of 20 buildpacks participating
paketo-buildpacks/ca-certificates 3.1.0
paketo-buildpacks/microsoft-openjdk 2.2.0
paketo-buildpacks/syft 1.10.0
paketo-buildpacks/maven 6.4.1
paketo-buildpacks/executable-jar 6.1.0
paketo-buildpacks/apache-tomcat 7.2.0
paketo-buildpacks/dist-zip 5.2.0
paketo-buildpacks/spring-boot 5.8.0
paketo-buildpacks/azure-application-insights 5.3.2
...Paketo Azure Application Insights Buildpack 5.3.2
https://github.com/paketo-buildpacks/azure-application-insights
Azure Application Insights Java Agent 3.2.8: Contributing to layer
Downloading from https://github.com/microsoft/ApplicationInsights-Java/releases/download/3.2.8/applicationinsights-agent-3.2.8.jar
Verifying checksum
Copying to /layers/paketo-buildpacks_azure-application-insights/azure-application-insights-java
Writing env.launch/JAVA_TOOL_OPTIONS.append
Writing env.launch/JAVA_TOOL_OPTIONS.deli
...$ docker run -p 8080:8080 eggboy/springboot:0.0.1...
Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_microsoft-openjdk/java-security-properties/java-security.properties -XX:+ExitOnOutOfMemoryError -javaagent:/layers/paketo-buildpacks_azure-application-insights/azure-application-insights-java/applicationinsights-agent-3.2.8.jar -XX:ActiveProcessorCount=6 -XX:MaxDirectMemorySize=10M -Xmx941095K -XX:MaxMetaspaceSize=97352K -XX:ReservedCodeCacheSize=240M -Xss1M -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -Dorg.springframework.cloud.bindings.boot.enable=true
2022-03-19 02:04:02.558Z ERROR c.m.applicationinsights.agent -
*************************
ApplicationInsights Java Agent 3.2.8 failed to start (PID 1)
*************************
Description:
No connection string or instrumentation key provided
Action:
Please provide connection string or instrumentation key.
...

The new image fails to run complaining ‘No connection string or instrumentation key provided’. App Insights agent expects a piece of connection information from the APPLICATIONINSIGHTS_CONNECTION_STRING environment variable so that it can send metrics to the right endpoint. APPLICATIONINSIGHTS_CONNECTION_STRING can be obtained from the Azure portal. To distinguish between different applications, specify the role name in APPLICATIONINSIGHTS_ROLE_NAME. Run the image again with two env variables this time.

$ docker run -p 8080:8080 --env APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=[REDACTED];IngestionEndpoint=https://southeastasia-0.in.applicationinsights.azure.com/" --env APPLICATIONINSIGHTS_ROLE_NAME=springboot eggboy/springboot:0.0.1
Setting Active Processor Count to 6
WARNING: Unable to convert memory limit "max" from path "/sys/fs/cgroup/memory.max" as int: memory size "max" does not match pattern "^([\\d]+)([kmgtKMGT]?)$"
Calculating JVM memory based on 1524092K available memory
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx914739K -XX:MaxMetaspaceSize=97352K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 1524092K, Thread Count: 250, Loaded Class Count: 14774, Headroom: 0%)
Enabling Java Native Memory Tracking
Adding 128 container CA certificates to JVM truststore
Spring Cloud Bindings Enabled
Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_microsoft-openjdk/java-security-properties/java-security.properties -XX:+ExitOnOutOfMemoryError -javaagent:/layers/paketo-buildpacks_azure-application-insights/azure-application-insights-java/applicationinsights-agent-3.2.8.jar -XX:ActiveProcessorCount=6 -XX:MaxDirectMemorySize=10M -Xmx914739K -XX:MaxMetaspaceSize=97352K -XX:ReservedCodeCacheSize=240M -Xss1M -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -Dorg.springframework.cloud.bindings.boot.enable=true
2022-03-19 02:40:33.081Z INFO c.m.applicationinsights.agent - ApplicationInsights Java Agent 3.2.8 started successfully (PID 1)
2022-03-19 02:40:33.086Z INFO c.m.applicationinsights.agent - Java version: 11.0.14, vendor: Microsoft, home: /layers/paketo-buildpacks_microsoft-openjdk/jdk
Performance metrics
End to End transaction details
Live metrics of requests, CPU, etc

Using CNB in Github Action

Github action provides buildpacks/github-actions/setup-pack that can set up pack CLI in the pipeline. Here is the sample snippet for reference.

    - name: Install pack CLIs including pack and yq
uses: buildpacks/github-actions/setup-pack@v4.1.0
with:
pack-version: 0.24.0
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASS }}
- name: Set the version
run: |
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Build app with pack CLI
run: |
pack build index.docker.io/eggboy/springboot:${{ env.VERSION }} \
--builder paketobuildpacks/builder:base \
--cache-image index.docker.io/eggboy/springboot:${{ env.VERSION }} \
--volume "$(pwd)/appinsights:/platform/bindings/application-insights" \
--publish

az acr pack build

The Azure CLI command az acr pack build uses the pack CLI tool, from Buildpacks, to build an app and push its image into an Azure container registry. This is still in the preview that has a few limitations, especially around specifying the buildpacks. But, it could be handy for some simple use cases where you just need a quick way to build images from source code without Docker installed on the laptop.

$ az acr pack build --registry acrjay --image acrjay.azurecr.io/containerapp-build:0.0.1 --builder paketobuildpacks/builder:base .

Conclusion

I hope ‘buildpack experiences’ resonate with you by now after quick getting started hands-on experiences. Cloud Native Buildpack page has a nice summary of comparisons between different tools in the market.

I will go deep on the concepts CNB put in place for producing images, and how we could customize them further for more complex use cases in the next article. Till then, Stay safe!

If you like my article, please leave some claps here or maybe even start following me. Thanks!

--

--

Jay Lee

Cloud Native Enthusiast. Java, Spring, Python, Golang, Kubernetes.