How To Use Buildpacks To Build Java Containers
This article will look under the hood of buildpacks to see how they operate and give tips on optimizing the default settings to reach better performance outcomes.
Do you still write lengthy Dockerfiles describing every step necessary to build a container image? Then, buildpacks come to your rescue! Developers simply feed them an application, buildpacks do their magic, and turn it into a fully functional container ready to be deployed on any cloud.
But how exactly does the magic happen? And what should you do if the resulting container performance doesn’t meet the business requirements?
This article will look under the hood of buildpacks to see how they operate and give tips on optimizing the default settings to reach better performance outcomes.
What Are Buildpacks?
A buildpack turns the application source code into a runnable production-ready container image. Buildpacks save time and effort for developers because there’s no need to configure the image and manually manage dependencies through a Dockerfile.
Heroku was the first company to develop buildpacks in 2011. Since then, many other companies (Cloud Foundry, Google, etc.) have adopted the concept. In 2018, Heroku partnered with Pivotal to create the Cloud Native Buildpacks project, encompassing modern standards and specifications for container images, such as the OCI format. The project is part of the Cloud Native Computing Foundation (CNCF).
Paketo buildpacks, which we will use in this article, is an open-source project backed by Cloud Foundry and sponsored by VMware. It implements Cloud Native Buildpacks specifications and supports the most popular languages, including Java. Containers produced with Paketo buildpacks can run on any cloud.
How Buildpacks Work
Buildpacks operate in two phases: detect and build.
1. The Detect Phase
During the detection phase, the buildpack analyzes the source code looking for indicators of whether or not it should be applied to the application. In other words, a group of buildpacks is tested against the source code, and the first group deemed fit for the code is selected for building the app. After the buildpack detects the necessary indicators, it returns a contract of what is required for creating an image and proceeds to the build phase.
2. The Build Phase
During the build phase, the buildpack transforms the codebase, fulfilling the contract requirements composed earlier. It provides the build-time and runtime environment, downloads necessary dependencies, compiles the code if needed, and sets the entry points and startup scripts.
Builders
A builder is a combination of components required for building a container image:
- Buildpacks, sets of executables that analyze the code and provide a plan for building and running the app;
- Stack consists of two images: the build image and the run image. The build image provides the built environment (a containerized environment where build packs are executed), the run image offers the environment for the application image during runtime;
- Lifecycle manages the buildpack execution and assembles the resulting artifact into a final image.
Therefore, one builder can automatically detect and build different applications.
Buildpacks Offer a Variety of JVMs — How to Choose?
Paketo buildpacks use Liberica JVM by default. Liberica is a HotSpot-based Java runtime supported by a major OpenJDK contributor and recommended by Spring. It provides JDK and JRE for all LTS versions (8, 11, 17), the current version, and Liberica Native Image Kit (NIK), a GraalVM-based utility for converting JVM-based apps into native images with an accelerated startup. Native images are beneficial when you need to avoid cold starts in AWS.
But the buildpacks support several Java distributions, which can be used instead of the default JVM:
- Adoptium
- Alibaba Dragonwell
- Amazon Corretto
- Azul Zulu
- BellSoft Liberica (default)
- Eclipse OpenJ9
- GraalVM
- Oracle JDK
- Microsoft OpenJDK
- SapMachine
If you want to switch JVMs, you have to keep in mind several nuances:
- Alibaba Dragonwell, Amazon Corretto, GraalVM, Oracle JDK, and Microsoft OpenJDK offer only JDK. The resulting container will be twice as big as the JRE-based one;
- Adoptium provides JDK and JRE for Java 8 and 11 and only JDK for Java 16+;
- Oracle JDK provides only Java 17.
Another important consideration: buildpacks facilitate and accelerate deployment, but if you are dissatisfied with container performance or seek to improve essential KPIs (throughput, latency, or memory consumption), you have to tune the JVM yourself. For more details, see the section Configuring the JVM below.
For instance, Eclipse OpenJ9 based on the OpenJ9 JVM may demonstrate better performance than HotSpot in some cases because HotSpot comes with default settings, and OpenJ9 is already tuned. Adding a few simple parameters will give you equal or superior performance with HotSpot.
How to Use Paketo Buildpacks
Let’s build a Java container utilizing a Paketo buildpack.
First, make sure Docker is up and running. If you don’t have it, follow these instructions to install Docker Desktop for your system.
The next step is to install pack CLI, a Command Line Interface maintained by Cloud Native Buildpack that can be used to work with buildpacks. Follow the guide to complete the installation for your platform (macOS, Linux, and Windows are supported). Pack is one of the several available tools. Spring Boot developers, for instance, can look into Spring Boot Maven Plugin or Spring Boot Gradle Plugin.
We will use Paketo sample applications, so run the following command:
git clone https://github.com/paketo-buildpacks/samples && cd sample
Alternatively, utilize your own demo app
Make Paketo Base builder the default builder:
pack config default-builder paketobuildpacks/builder:base
To build an image from source with Maven, run
pack build samples/java \
--path java/maven --env BP_JVM_VERSION=17
Java example images should return {"status":"UP"} from the actuator health endpoint:
docker run --rm --tty --publish 8080:8080 samples/jav
curl -s http://localhost:8080/actuator/health | jq .
It is also possible to build an image from a compiled artifact. The following archive formats are supported: executable JAR, WAR, or distribution ZIP.
To compile an executable JAR and build an image using pack, run
cd java/maven
./mvnw package
pack build samples/java \
--path ./target/demo-0.0.1-SNAPSHOT.jar
Extracting a Software Bill of Materials
Software supply chains consist of numerous libraries, tools, and processes used to develop and run applications. It is often hard to trace the origin of all software components in a software product, increasing the risk of nested vulnerabilities. A software bill of materials (SBOM) lists all library dependencies utilized to build a software artifact. It is similar to a traditional bill of materials, which summarizes the raw materials, parts, components, and exact quantities required to manufacture a product.
SBOMs enable the developers to monitor the version of software components, integrate security patches promptly, and keep vulnerable libraries out.
Buildpacks also enable the developers to see an SBOM for their image. Run the following command to extract the SBOM for the samples/java image built previously:
pack sbom download samples/java --output-dir /tmp/samples-java-sbom
After that, you can browse the folder. SBOMs are presented in JSON format. To list all .json files in the folder, run the following:
find /tmp/samples-java-sbom -name "*.json"
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_executable-jar/sbom.cdx.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_executable-jar/sbom.syft.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_spring-boot/helper/sbom.syft.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_spring-boot/spring-cloud-bindings/sbom.syft.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_bellsoft-liberica/jre/sbom.syft.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_bellsoft-liberica/helper/sbom.syft.json
/tmp/samples-java-sbom/layers/sbom/launch/sbom.legacy.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_ca-certificates/helper/sbom.syft.json
Now, you can open the file with any text editor. For instance, if you have Visual Studio Code installed, run the following:
code /tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_bellsoft-liberica/jre/sbom.syft.json
You will get the following output:
{
"Artifacts": [
{
"ID": "1f2d01eeb13b5894",
"Name": "BellSoft Liberica JRE",
"Version": "17.0.6",
"Type": "UnknownPackage",
"FoundBy": "libpak",
"Locations": [
{
"Path": "buildpack.toml"
}
],
"Licenses": [
"GPL-2.0 WITH Classpath-exception-2.0"
],
"Language": "",
"CPEs": [
"cpe:2.3:a:oracle:jre:17.0.6:*:*:*:*:*:*:*"
],
"PURL": "pkg:generic/[email protected]?arch=amd64"
}
],
"Source": {
"Type": "directory",
"Target": "/layers/paketo-buildpacks_bellsoft-liberica/jre"
},
"Descriptor": {
"Name": "syft",
"Version": "0.32.0"
},
"Schema": {
"Version": "1.1.0",
"URL": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json"
}
}
Configuring the JVM
The BellSoft Liberica Buildpack provides the newest patch updates of Java versions supported in the buildpack. The buildpack uses the latest LTS version by default. If you want to use another Java version, use the BP_JVM_VERSION environment variable. For instance, BP_JVM_VERSION=11 will install the newest release of Liberica JDK and JRE 11.
In addition, you can change the JDK type. The buildpack uses JDK at build-time and JRE at runtime. Specifying the BP_JVM_TYPE=JDK option will force the buildpack to use JDK at runtime.
The BP_JVM_JLINK_ENABLED option runs the jlink tool with Java 9+, which cuts out a custom JRE.
If you deploy a Java application to an application server, the buildpack uses Apache Tomcat by default. You can select another server (TomEE or Open Liberty). For instance, run the following command to switch to TomEE:
pack build samples/war -e BP_JAVA_APP_SERVER=tomee
You can configure JVM at runtime by using the JAVA_TOOL_OPTIONS environment variable. For instance, you can configure garbage collection, number of threads, memory limits, etc., to reach optimal performance for your specific needs:
docker run --rm --tty \
--env JAVA_TOOL_OPTIONS='-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40' \
--env BPL_JVM_THREAD_COUNT=100 \
samples/java
The whole list of JVM configuration options can be found on the Liberica Buildpack page.
Conclusion
As you can see, buildpacks are great automation tools saving developers time. But it would help if you used them wisely, or there’s a risk you will get a cat in the sack. Our general recommendation is to define the KPIs and adjust JVM settings accordingly.
What can you do if you are not happy with the size of the resulting image? After all, it’s not possible to change the base OS image utilized by buildpacks. One option is to migrate to the Native Image to optimize resource consumption. Another alternative is to manually build containers and switch to a smaller OS image, such as Alpine or Alpaquita Linux. The latter supports two libc implementations (optimized musl and glibc) and comes with numerous performance and security enhancements.
We Provide consulting, implementation, and management services on DevOps, DevSecOps, DataOps, Cloud, Automated Ops, Microservices, Infrastructure, and Security
Services offered by us: https://www.zippyops.com/services
Our Products: https://www.zippyops.com/products
Our Solutions: https://www.zippyops.com/solutions
For Demo, videos check out YouTube Playlist: https://www.youtube.com/watch?v=4FYvPooN_Tg&list=PLCJ3JpanNyCfXlHahZhYgJH9-rV6ouPro
If this seems interesting, please email us at [email protected] for a call.
Recent Comments
No comments
Leave a Comment
We will be happy to hear what you think about this post