Goodbye Dockerfile: Cloud Native Buildpacks with Paketo.io & layered jars for Spring Boot

No Comments

Containers are industry standard today. But how often do we try to write our own Dockerfiles again and again? Cloud Native Buildpacks with Paketo.io are here to free us from this burden! No matter what language you use. And if it’s Spring Boot, you also get layered jars included.

Loving Dockerfiles …

I remember the first time I was able to use Docker in a customer project. My colleague Marco and I were really excited about this lightweight way of virtualization where we could finally “bake” every dependency of our software into code! I guess this was such a huge step forward compared to the times before where we hasseled with all the miseries that occur when you’ll not able to really manage every dependency as a developer! A Java application, for example, is bound to a specific version of the JVM. And the JVM installation (especially using custom enterprise certificates) is bound to a specific OS version. And so on.

So we really fell in love with containers! But there was also a gut feeling that we were maybe missing something because we were blind in one eye. This had to do with the operations side of things. Hardening a Docker container for the odds in production was not really a thing we focussed on back then. We simply didn’t have the time to do that in our project. And we didn’t even know that we should prioritize on that.

… is not the full story!

The second part of the problem was our Continous Integration pipelines. Being so overwhelmed by the benefits containers gave us and occupied with “Dockerizing” everything we could get our hands on, we neglected to take a deeper look into our application-specific Dockerfiles. If you’re in love with the Spring programming model, you may also know the spring.io guide on how to use Docker with your Spring Boot app. The Dockerfile back then simply used an OpenJDK image, added the executable jar file and defined an ENTRYPOINT which was able to fire up our Spring Boot app exactly as we were used to without using Docker (I said “back then”, because this guide also evolved over time).

As this is a straightforward approach, it misses some points that should be done using Docker in Day 2 scenarios. For example, we should switch to a non-root user when running our app. And using a fat JAR inside a container is also not the best idea. Our application consists of parts that are more likely to change than others! The application code will change much more frequently than the Spring Boot version we’re defining inside our pom.xml or build.gradle files. And I guess we also don’t change the Hibernate version ten times a day 🙂 So we maybe should treat these parts of our application differently. And we really should use separate Docker image layers for those parts in order to speed up our Continous Integration pipelines.

Phew! All those “we should” things! And these are only an extract. Simply give Google a go with hardening Dockerfiles for production.
This led to a problem in nearly every project: We needed to focus on things the business sees exactly zero value for at first sight. But we can’t ignore these aspects, since they lead to security issues and long-running CI pipelines that our developers need to wait on endlessly.

The rise of buildpacks

There are already many great approaches to parts of the problems mentioned above. You may already have heard or even used tools like spotify/docker-maven-plugin (which is now developed as dockerfile-maven), fabric8io/docker-maven-plugin or Google’s Jib (there’s also a great post about the latter written by some colleagues of mine). Being great choices for many problems, they didn’t feel like a standard thing to me personally. As a consequence I saw many projects stick to their Dockerfiles.

But then I attended this year’s SpringOne 2020. One topic that went through literally every talk was Cloud Native Buildpacks (CNB). Throughout the conference, nearly every speaker used them. And I got a bit confused on that, since Buildpacks weren’t introduced until the end of the second day. But then finally Ben Hale raised the veil with his Spring to Image talk. And he even apologized for being quite late in the schedule, but he really made up for it with his great talk. 🙂

Invented by Heroku in 2011, the concept of buildpacks was broadly adopted by CloudFoundry, Google App Engine, GitLab, Knative, Deis, and more. The concept seemed somthing that was meant to stay. And finally in 2018 Pivotal and Heroku joined forces to initiate the Cloud Native Buildpacks project which sandboxed in the CNCF the same year. Using the knowledge of the many years of experience with buildpacks, the CloudFoundry buildpack engineering team created the Paketo.io project which is based on former CloudFoundry Buildpacks. Here’s a small sketchnote I created to get a clearer picture, trying to illustrate the history of buildpacks together with the launch dates of some relevant tools:

Sketchnote outlining a brief history of Cloud Native Buildpacks

Just a few days ago the CNCF Technical Oversight Committee (TOC) promoted Cloud Native Buildpacks from Sandbox to Incubation. You can already guess what that means: It’s time to take a look!

Speed up developer productivity with Cloud Native Buildpacks & Paketo.io

The Cloud Native Buildpacks docs tell us what we can expect:

Transform your application source code into images that can run on any cloud.

And that’s really the TLDR;. Addressing all the shortcomings of writing your own Dockerfiles we already discussed, the project adds many more topics you maybe didn’t even know you should focus on. For example, Cloud Native Buildpacks embrace modern container standards like the OCI image format and enable cross-repository blob mounting and image layer “rebasing”. They aim to bring advanced caching, multi-language support, minimal app images and reproducibility to our images without forcing us to care of all this ourselves.

Cloud Native Computing Foundation (CNCF) logo together with Cloud Native Buildpacks and Paketo.io logos

Logo sources: CNCF logo, Buildpacks logo, Paketo.io logo

And what about the Paketo.io thingy? Well, that’s “simply” the implementation of the Cloud Native Buildpack interface specification for a wide variety of languages. No matter if you want to use .Net Core, Go, Node.js, Java, Ruby or PHP – you don’t need to write a Dockerfile anymore.

And starting with the announcement that Cloud Native Buildpacks are now CNCF incubating, you will for sure be able to run your application on every cloud infrastructure you’d like to. Google started with the announced support in 10/2020 based on the CNCF buildpacks v3 specification. And it’s no hard guesswork that all the other cloud vendors will follow soon!

Building Spring Boot apps using Cloud Native Buildpacks & Paketo.io

So how do we use Cloud Native Buildpacks in our projects? Focussing on JVM-based languages, you’ll soon realize that there are many buildpacks waiting to handle your specific use cases. Ranging from the Gradle buildpack, the Scala SBT buildpack to the Maven buildpack, we can also find buildpacks capable of running Executable JARs or even Apache Tomcat-based war file deployments.

As a frequent Spring Boot user, I was really amazed to find out about a specific Spring Boot buildpack also. And as Ben Hale stated in his SpringOne talk, we don’t even need to know anything about buildpacks in order to get started with them! All we have to do is to create a Spring Boot application skeleton using start.spring.io – or simply upgrade an existing Spring Boot application to the latest 2.3.x parent version like 2.3.5.RELEASE (we will upgrade to 2.4.x in a moment – it’s simply for didactic purposes 🙂 ). Starting with a skeleton, we should add some code, e.g. by building a reactive web app using Spring Webflux as I did in my post about Spring’s GraalVM integration. If you’re looking for some example code, there’s also a project waiting for you on GitHub.

That’s all. Now, using a new Maven goal, we can issue a Cloud Native Buildpack-enabled build right out of the box. Simply run:

mvn spring-boot:build-image

I also prepared a small asciicast to demonstrate what’s happening thereafter:

As you may notice, a standard Maven build is started. But after compiling and testing, the build-image phase becomes interesting! All the buildpack magic kicks in here:

$ mvn spring-boot:build-image
...
[INFO] --- spring-boot-maven-plugin:2.3.5.RELEASE:build-image (default-cli) @ spring-boot-buildpack ---
[INFO] Building image 'docker.io/library/spring-boot-buildpack:0.0.1-SNAPSHOT'
[INFO]
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 100%
[INFO]  > Pulled builder image 'gcr.io/paketo-buildpacks/builder@sha256:2b3d585ed785ea2e4ecc89c35512c54f8d339f4ca09c1d445c51077ebe21cfaf'
[INFO]  > Pulling run image 'docker.io/paketobuildpacks/run:base-cnb' 100%
[INFO]  > Pulled run image 'paketobuildpacks/run@sha256:33d37fc9ba16e220f071805eaeed881a508ceee5c8909db5710aaed7e97e4fc2'
[INFO]  > Executing lifecycle version v0.9.3
[INFO]  > Using build cache volume 'pack-cache-604f3372716a.build'
[INFO]
[INFO]  > Running creator
[INFO]     [creator]     ===> DETECTING
[INFO]     [creator]     5 of 18 buildpacks participating
[INFO]     [creator]     paketo-buildpacks/ca-certificates   1.0.1
[INFO]     [creator]     paketo-buildpacks/bellsoft-liberica 5.2.1
[INFO]     [creator]     paketo-buildpacks/executable-jar    3.1.3
[INFO]     [creator]     paketo-buildpacks/dist-zip          2.2.2
[INFO]     [creator]     paketo-buildpacks/spring-boot       3.5.0
[INFO]     [creator]     ===> ANALYZING
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/ca-certificates:helper" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/bellsoft-liberica:helper" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/bellsoft-liberica:java-security-properties" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jre" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jvmkill" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/executable-jar:class-path" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/spring-boot:helper" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/spring-boot:spring-cloud-bindings" from app image
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/spring-boot:web-application-type" from app image
[INFO]     [creator]     ===> RESTORING
[INFO]     [creator]     ===> BUILDING
[INFO]     [creator]
[INFO]     [creator]     Paketo CA Certificates Buildpack 1.0.1
[INFO]     [creator]       https://github.com/paketo-buildpacks/ca-certificates
[INFO]     [creator]       Launch Helper: Reusing cached layer
[INFO]     [creator]
[INFO]     [creator]     Paketo BellSoft Liberica Buildpack 5.2.1
[INFO]     [creator]       https://github.com/paketo-buildpacks/bellsoft-liberica
[INFO]     [creator]       Build Configuration:
[INFO]     [creator]         $BP_JVM_VERSION              11.*            the Java version
[INFO]     [creator]       Launch Configuration:
[INFO]     [creator]         $BPL_JVM_HEAD_ROOM           0               the headroom in memory calculation
[INFO]     [creator]         $BPL_JVM_LOADED_CLASS_COUNT  35% of classes  the number of loaded classes in memory calculation
[INFO]     [creator]         $BPL_JVM_THREAD_COUNT        250             the number of threads in memory calculation
[INFO]     [creator]         $JAVA_TOOL_OPTIONS                           the JVM launch flags
[INFO]     [creator]       BellSoft Liberica JRE 11.0.9: Reusing cached layer
[INFO]     [creator]       Launch Helper: Reusing cached layer
[INFO]     [creator]       JVMKill Agent 1.16.0: Reusing cached layer
[INFO]     [creator]       Java Security Properties: Reusing cached layer
[INFO]     [creator]
[INFO]     [creator]     Paketo Executable JAR Buildpack 3.1.3
[INFO]     [creator]       https://github.com/paketo-buildpacks/executable-jar
[INFO]     [creator]       Process types:
[INFO]     [creator]         executable-jar: java org.springframework.boot.loader.JarLauncher
[INFO]     [creator]         task:           java org.springframework.boot.loader.JarLauncher
[INFO]     [creator]         web:            java org.springframework.boot.loader.JarLauncher
[INFO]     [creator]
[INFO]     [creator]     Paketo Spring Boot Buildpack 3.5.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/spring-boot
[INFO]     [creator]       Launch Helper: Reusing cached layer
[INFO]     [creator]       Web Application Type: Reusing cached layer
[INFO]     [creator]       Spring Cloud Bindings 1.7.0: Reusing cached layer
[INFO]     [creator]       Image labels:
[INFO]     [creator]         org.opencontainers.image.title
[INFO]     [creator]         org.opencontainers.image.version
[INFO]     [creator]         org.springframework.boot.spring-configuration-metadata.json
[INFO]     [creator]         org.springframework.boot.version
[INFO]     [creator]     ===> EXPORTING
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/ca-certificates:helper'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:helper'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:jre'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:jvmkill'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/executable-jar:class-path'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/spring-boot:helper'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/spring-boot:web-application-type'
[INFO]     [creator]     Reusing 1/1 app layer(s)
[INFO]     [creator]     Reusing layer 'launcher'
[INFO]     [creator]     Reusing layer 'config'
[INFO]     [creator]     Adding label 'io.buildpacks.lifecycle.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.build.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.project.metadata'
[INFO]     [creator]     Adding label 'org.opencontainers.image.title'
[INFO]     [creator]     Adding label 'org.opencontainers.image.version'
[INFO]     [creator]     Adding label 'org.springframework.boot.spring-configuration-metadata.json'
[INFO]     [creator]     Adding label 'org.springframework.boot.version'
[INFO]     [creator]     *** Images (d831d6a66f8e):
[INFO]     [creator]           docker.io/library/spring-boot-buildpack:0.0.1-SNAPSHOT
[INFO]
[INFO] Successfully built image 'docker.io/library/spring-boot-buildpack:0.0.1-SNAPSHOT'
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  25.936 s
[INFO] Finished at: 2020-11-25T09:14:37+01:00
[INFO] ------------------------------------------------------------------------

After some builder images are pulled, the creator takes over. It starts by DETECTING and ANALYZING the given application and identifies multiple build packs that are needed to successfully package the application into a Docker image. You heard right: Not one buildpack is used on its own, but a whole bunch of them. In our case the creator tells us that 5 of 17 buildpacks [are] participating.

For example, there’s paketo-buildpacks/bellsoft-liberica:jre to bring in a JRE, since we have a Java app here. And there’s paketo-buildpacks/executable-jar since the resulting application is an executable JAR. Also, there are a few paketo-buildpacks/spring-boot-x build packs specifically for our Spring Boot application. For more details on how the Spring Boot buildpacks are organized, you can take a look into the Paketo docs.

But these are all details you don’t even need to know when using buildpacks with Spring Boot! After a successful Maven build containing something like Successfully built image 'docker.io/library/spring-boot-buildpack:0.0.1-SNAPSHOT' were already able to run our application with:

docker run -p 8080:8080 spring-boot-buildpack:0.0.1-SNAPSHOT

That’s all we have to do in order to run our app inside a container. To access it, simply open your browser and point it to http://localhost:8080. Right now Paketo needs a running Docker installation on your machine, so be sure to have Docker running before starting your build.

Let’s “dive” into our new image

To get a better feeling about what’s going on inside our Docker images, there’s a great tool that was also used quite heavily throughout SpringOne 2020: It’s called dive and its a simple but powerful command line tool to gain better insights of our container images. On a Mac, simply install it with brew install dive (or take a look into the docs for other OSses).

In order to ensure a great user experience with dive, I recommend you to first create a .dive.yaml inside your home directory containing the following lines:

diff:
  # You can change the default files shown in the filetree (right pane). All diff types are shown by default.
  hide:
    - unmodified

filetree:
  # Show the file attributes next to the filetree
  show-attributes: false

With this configuration in place, dive will always start with the default to hide file attributes and unmodified files of each layer. This will enable a much a much better overview of the contents of our images and it helps you get comfortable with the tool more quickly. There are even more tweaks in the docs – but that should be a good starting point. And by the way, this is also the configuration most speakers used at SpringOne 2020 – but it took me a while to wrap my head around that. 🙂

Now having dive readily installed & configured, we can use it together with the id of our recently build image (simply take a look into the Maven build log and watch out for something like [creator] *** Images (408f3d59f38e):):

dive 408f3d59f38e

This should shift our console to a completely different view and presents us all the layers the Paketo build produced inside our image:

dive container layers without layered jars feature

Using Paketo pack CLI directly

You may have already guessed it: the Maven goal spring-boot:build-image is only a convenience wrapper for the Paketo build. We can also issue Paketo build using the so-called pack CLI directly. And that’s also the way to use Paketo for just every language we want to use Cloud Native Buildpacks with. In order to install pack CLI simply use your package manager of choice. On a Mac this is:

brew install buildpacks/tap/pack

Now having pack CLI installed, we can take a look at the variety of buildpacks which are already available right now. Therefore run:

$ pack suggest-builders
 
Suggested builders:
	Google:                gcr.io/buildpacks/builder:v1      Ubuntu 18 base image with buildpacks for .NET, Go, Java, Node.js, and Python
	Heroku:                heroku/buildpacks:18              heroku-18 base image with buildpacks for Ruby, Java, Node.js, Python, Golang, & PHP
	Paketo Buildpacks:     paketobuildpacks/builder:base     Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang
	Paketo Buildpacks:     paketobuildpacks/builder:full     Ubuntu bionic base image with buildpacks for Java, .NET, NodeJS, Golang, PHP, HTTPD and NGINX
	Paketo Buildpacks:     paketobuildpacks/builder:tiny     Tiny base image (bionic build image, distroless run image) with buildpacks for Golang
 
Tip: Learn more about a specific builder with:
	pack inspect-builder <builder-image>

Using the pack set-default-builder command, we can even configure a default builder to use for every pack command. But even without defining a default we can use pack CLI in the same way the spring-boot-maven-plugin does. Therefore we need to simply execute:

pack build spring-boot-buildpack --path . --builder paketobuildpacks/builder:base

This will do exactly the same build we already issued using Maven. But now we have much more beautiful colors 🙂 You may convince yourself by taking a look at the following asciicast:

A Successfully built image spring-boot-buildpack at the end of the output indicates that we just built another image from our application, using only one command – no Dockerfile needed anymore!

Configuring Paketo and why the images are 40 years old …

There are plenty ways on how to configure Paketo builds. For example, if you want to change the JDK version used to build your application or want to change Maven settings, you can simply use environment variables for that. Or you can use a buildpack.yml inside the root of your project to change build-time parameters.

There are also some buildpacks that accept credentials or other secrets to use them at build or runtime. These can include the access to private artifact repositories or APM servers. Therefore Paketo Buildpacks use so-called bindings to include this kind of configuration into the build process – or later at runtime. And finally there are even Procfiles to override Buildpack-provided types, as you may already know from using Heroku.

But even if you don’t change anything about the default Paketo configuration, you might still wonder about your images that seem to be 40 years old. Simply run a docker images command to get an overview of the build images:

$ docker images
...
paketobuildpacks/builder                  <none>                  914aba170326        40 years ago        654MB
pack.local/builder/axczkudrjk             latest                  69aeed7ad644        40 years ago        654MB
spring-boot-buildpack                     latest                  b529a37599a6        40 years ago        259MB
paketobuildpacks/builder                  base                    1435430a71b7        40 years ago        558MB

So why is that? These are fixed timestamps and they are simply needed in order to be able to provide 100% reproducible builds. There’s a great post about the why available here (Thanks coldfinger for clarifying this one on StackOverflow!) and it’s not only used by Paketo, but also by Google’s Jib and Google’s ko. Long story short: without fixed timestamps, the hashes of the Docker images would differ every time you issue a build. And if the hash changes, it wouldn’t be clear whether something changed inside the image or not. For more details, also check out the Reproducible Builds project.

Layered jars for Spring Boot apps

We could stop here, because we already reached our goal of using Cloud Native Buildpacks to build our application images. But being a heavy Spring Boot user, you may already have heard of the layered jars feature. So what is it all about? And how does it fit into Cloud Native Buildpacks?

Let’s start by taking a look at the layered jars feature first. It was introduced with Spring Boot 2.3.x already. To better grasp the meaning of the feature, we should think of a standard Spring Boot JAR. Therefore, simply unzip the jar file inside the target directory after a successful Maven build. Using the example project on GitHub, the command is:

unzip target/spring-boot-buildpack-0.0.1-SNAPSHOT.jar -d target/extractedjar

Now let’s take a look into the target/extractedjar directory:

a normal Spring Boot app's jar layout

There are three main directories: BOOT-INF, META-INF and org. Our application class files reside in BOOT-INF/classes and BOOT-INF/lib inherits all the application dependencies. The directory org/springframework/boot/loader then finally contains the Spring Boot loader magic that is needed to make our executable app work. So nothing new here for the moment.

And now we’re approaching the point where I urged you to start with Spring Boot 2.3.x at the beginning. Since using 2.3.x we are able to explicitly activate the layered jars feature by configuring it inside the spring-boot-maven-plugin in our pom.xml:

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<layers>
						<enabled>true</enabled>
					</layers>
				</configuration>
			</plugin>
		</plugins>
	</build>

From Spring Boot 2.4.x on, the layered jars feature already became the default behavior. Having the layered jar feature enabled, we should run a fresh:

mvn clean package

Unzipping the resulting JAR file target/spring-boot-buildpack-0.0.1-SNAPSHOT.jar again you will notice a new file inside the BOOT-INF directory which is called layers.idx. It looks like this:

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

The layers.idx file is a blueprint for how our Docker image layers should look in order to match different requirements. Therefore it assigns our JAR file’s directories to layer names and implements an order for them. Our dependencies define the first layer since they are likely not to change that often. The second layer spring-boot-loader inherits all Spring Boot loader classes and also shouldn’t change all too much. Our snapshot-dependencies then make for a more variable part and create the third layer. Finally our application’s class files and properties are likely to change quite a lot! So they reside in the last layer called application.

To easily view the layers, there’s a new command line extension (or system property) -Djarmode=layertools for us. Simply cd into the target directory and run:

$ java -Djarmode=layertools -jar spring-boot-buildpack-0.0.1-SNAPSHOT.jar list
 
dependencies
spring-boot-loader
snapshot-dependencies
application

To extract each layer, we can also use the command line option with the extract option:

$ java -Djarmode=layertools -jar spring-boot-buildpack-0.0.1-SNAPSHOT.jar extract --destination extractedjar

Now inside the target/extractedjar directory you should find four folders that represent the separate layers (that will be created from them later):

the extracted jar layers of our Spring Boot app

Using Layered jars inside custom Dockerfiles

Every one of those extracted directories could be used to create a separate layer inside a Docker image by using the COPY command. Phil Webb outlined this in his spring.io post already, where he crafts a Dockerfile that runs the java -Djarmode=layertools -jar command in the first build container and then uses the extracted directories to create separate Docker layers from them:

FROM adoptopenjdk:11-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
 
FROM adoptopenjdk:11-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

After cloning the example project on GitHub, you can run the Docker build if you want using the DockerfileThatsNotNeededUsingBuildpacks via:

docker build . --tag spring-boot-layered --file DockerfileThatsNotNeededUsingBuildpack

In the build output we then see the separate layers beeing created:

...
Step 8/12 : COPY --from=builder application/dependencies/ ./
 ---> 88bb8adaaca6
Step 9/12 : COPY --from=builder application/spring-boot-loader/ ./
 ---> 3922891db128
Step 10/12 : COPY --from=builder application/snapshot-dependencies/ ./
 ---> f139bcf5babb
Step 11/12 : COPY --from=builder application/application/ ./
 ---> 5d02393d4fe2
...

We can even further examine the created Docker image using our container inspection tool dive:

dive spring-boot-layered

Using dive we see the Spring Boot layered jars feature immediately in action since the four layers have been created as defined in the layers.idx file and our Dockerfile:

dive Docker image inspection showing layers

Buildpacks with Spring Boot layered jars

But wait! Wasn’t this post supposed to be about using Cloud Native Buildpacks that should free us from the burden of maintaining our own Dockerfiles? The cool thing is: we can combine the power of Cloud Native Buildpacks with the Spring Boot layered jars feature! All we have to do is to keep the layered jars feature activated inside our pom.xml – or to simply switch to Spring Boot 2.4.x. And I’am really greatful for the fast help I received by Ben Hale when I found a bug in Paketo, which was triggered by a general change in the buildpacks/lifecycle umbrella project.

That means all we have to do is to run another Maven build via mvn spring-boot:build-image (or pack CLI if you want nicer colors. 🙂 ). The build log should now show a new part called Creating slices from layers index inside the Paketo Spring Boot Buildpack output:

$ mvn spring-boot:build-image
...
[INFO]     [creator]     Paketo Spring Boot Buildpack 3.5.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/spring-boot
[INFO]     [creator]       Creating slices from layers index
[INFO]     [creator]         dependencies
[INFO]     [creator]         spring-boot-loader
[INFO]     [creator]         snapshot-dependencies
[INFO]     [creator]         application
[INFO]     [creator]       Launch Helper: Reusing cached layer
...

After doing our buildpack-powered build, you should find the latest image id like *** Images (4c26dc7b3fa3) at the end of the log. Now use that to dive 4c26dc7b3fa3 into the build image again:

dive Paketo generated layers using Spring Boot layered jars feature

As you can see, there’s not only one big layer for our Spring Boot app! Now there are four layers – right as we would expect when using the layered jars feature. Looking at the screenshot, you should see the application layer that only contains our class files and properties. All dependencies and the spring-boot-loader reside in earlier layers. 🙂

Cloud Native Buildpacks are here to stay

I really like to write my own Dockerfiles. But getting them ready for production can be tedious and distract from focussing on the business problems we’d like to solve. Waiting for our CI server to complete our container-based builds is also annoying (and is one of the biggest challenges when building CI/CD pipelines). So it’s great to see CNCF now promoting Cloud Native Buildpacks (CNB) to incubating, since the underlying concept has already been proven in many cloud environments for years. And the specific CNB specification has what it takes to standardize how we describe and build our containers that eventually will be able to run everywhere. I think that’s a huge thing! And I can’t wait for the support announcements of the remaining cloud vendors. 🙂

If you’re a Spring fan like my, it’s even better so see how seamlessly integrated Cloud Native Buildpacks are already part of the default Spring build process. You have to do exactly nothing. Just use a current Spring Boot version (e.g. using start.spring.io) and fire a mvn spring-boot:build-image command. That’s all. I really like that convention-over-configuration approach since you can dig into the details and configure whatever you like. And as Paketo.io Buildpacks are developed using Go, you can issue a pull request to an existing buildpack – or even create your own based on a common lifecycle. And finally the integration of Spring Boot’s layered jars feature puts the cherry on top. Now only a small layer containing our application sources and property files is changed when we issue a new build – all the other layers are simply reused.

I’d really like to hear about your experiences with Cloud Native Buildpacks! And I’am looking forward to the things to come. Particularly, the GraalVM Buildpack and how it could be used to build Native Images from Spring Boot Apps is something I’d like to check out …

Jonas Hecht

After falling in love with Spring, Jonas also developed an interest in all container- and infrastructure-related topics. Now he focuses on bringing methods like Test-driven Development and Continuous Integration into the world of infrastructure code. He founded the codecentric branch in Erfurt/Thuringia and is involved in the local community, organizing the Java User Group Thüringen, DevOps Thüringen, and IoT Thüringen meetups. He loves to write blog posts & give lectures at Thuringian universities. Spare time is reserved for his family and mountain biking.

Comment

Your email address will not be published. Required fields are marked *