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.
Cloud Native Buildpacks – blog series
Part 1: Goodbye Dockerfile: Cloud Native Buildpacks with Paketo.io & layered jars for Spring Boot
Part 2: Cloud Native Buildpacks / Paketo.io in GitLab CI without Docker & pack CLI
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 he 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:
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.
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:
1mvn 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:
1$ mvn spring-boot:build-image 2... 3[INFO] --- spring-boot-maven-plugin:2.3.5.RELEASE:build-image (default-cli) @ spring-boot-buildpack --- 4[INFO] Building image 'docker.io/library/spring-boot-buildpack:0.0.1-SNAPSHOT' 5[INFO] 6[INFO] > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 100% 7[INFO] > Pulled builder image 'gcr.io/paketo-buildpacks/builder@sha256:2b3d585ed785ea2e4ecc89c35512c54f8d339f4ca09c1d445c51077ebe21cfaf' 8[INFO] > Pulling run image 'docker.io/paketobuildpacks/run:base-cnb' 100% 9[INFO] > Pulled run image 'paketobuildpacks/run@sha256:33d37fc9ba16e220f071805eaeed881a508ceee5c8909db5710aaed7e97e4fc2' 10[INFO] > Executing lifecycle version v0.9.3 11[INFO] > Using build cache volume 'pack-cache-604f3372716a.build' 12[INFO] 13[INFO] > Running creator 14[INFO] [creator] ===> DETECTING 15[INFO] [creator] 5 of 18 buildpacks participating 16[INFO] [creator] paketo-buildpacks/ca-certificates 1.0.1 17[INFO] [creator] paketo-buildpacks/bellsoft-liberica 5.2.1 18[INFO] [creator] paketo-buildpacks/executable-jar 3.1.3 19[INFO] [creator] paketo-buildpacks/dist-zip 2.2.2 20[INFO] [creator] paketo-buildpacks/spring-boot 3.5.0 21[INFO] [creator] ===> ANALYZING 22[INFO] [creator] Restoring metadata for "paketo-buildpacks/ca-certificates:helper" from app image 23[INFO] [creator] Restoring metadata for "paketo-buildpacks/bellsoft-liberica:helper" from app image 24[INFO] [creator] Restoring metadata for "paketo-buildpacks/bellsoft-liberica:java-security-properties" from app image 25[INFO] [creator] Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jre" from app image 26[INFO] [creator] Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jvmkill" from app image 27[INFO] [creator] Restoring metadata for "paketo-buildpacks/executable-jar:class-path" from app image 28[INFO] [creator] Restoring metadata for "paketo-buildpacks/spring-boot:helper" from app image 29[INFO] [creator] Restoring metadata for "paketo-buildpacks/spring-boot:spring-cloud-bindings" from app image 30[INFO] [creator] Restoring metadata for "paketo-buildpacks/spring-boot:web-application-type" from app image 31[INFO] [creator] ===> RESTORING 32[INFO] [creator] ===> BUILDING 33[INFO] [creator] 34[INFO] [creator] Paketo CA Certificates Buildpack 1.0.1 35[INFO] [creator] https://github.com/paketo-buildpacks/ca-certificates 36[INFO] [creator] Launch Helper: Reusing cached layer 37[INFO] [creator] 38[INFO] [creator] Paketo BellSoft Liberica Buildpack 5.2.1 39[INFO] [creator] https://github.com/paketo-buildpacks/bellsoft-liberica 40[INFO] [creator] Build Configuration: 41[INFO] [creator] $BP_JVM_VERSION 11.* the Java version 42[INFO] [creator] Launch Configuration: 43[INFO] [creator] $BPL_JVM_HEAD_ROOM 0 the headroom in memory calculation 44[INFO] [creator] $BPL_JVM_LOADED_CLASS_COUNT 35% of classes the number of loaded classes in memory calculation 45[INFO] [creator] $BPL_JVM_THREAD_COUNT 250 the number of threads in memory calculation 46[INFO] [creator] $JAVA_TOOL_OPTIONS the JVM launch flags 47[INFO] [creator] BellSoft Liberica JRE 11.0.9: Reusing cached layer 48[INFO] [creator] Launch Helper: Reusing cached layer 49[INFO] [creator] JVMKill Agent 1.16.0: Reusing cached layer 50[INFO] [creator] Java Security Properties: Reusing cached layer 51[INFO] [creator] 52[INFO] [creator] Paketo Executable JAR Buildpack 3.1.3 53[INFO] [creator] https://github.com/paketo-buildpacks/executable-jar 54[INFO] [creator] Process types: 55[INFO] [creator] executable-jar: java org.springframework.boot.loader.JarLauncher 56[INFO] [creator] task: java org.springframework.boot.loader.JarLauncher 57[INFO] [creator] web: java org.springframework.boot.loader.JarLauncher 58[INFO] [creator] 59[INFO] [creator] Paketo Spring Boot Buildpack 3.5.0 60[INFO] [creator] https://github.com/paketo-buildpacks/spring-boot 61[INFO] [creator] Launch Helper: Reusing cached layer 62[INFO] [creator] Web Application Type: Reusing cached layer 63[INFO] [creator] Spring Cloud Bindings 1.7.0: Reusing cached layer 64[INFO] [creator] Image labels: 65[INFO] [creator] org.opencontainers.image.title 66[INFO] [creator] org.opencontainers.image.version 67[INFO] [creator] org.springframework.boot.spring-configuration-metadata.json 68[INFO] [creator] org.springframework.boot.version 69[INFO] [creator] ===> EXPORTING 70[INFO] [creator] Reusing layer 'paketo-buildpacks/ca-certificates:helper' 71[INFO] [creator] Reusing layer 'paketo-buildpacks/bellsoft-liberica:helper' 72[INFO] [creator] Reusing layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties' 73[INFO] [creator] Reusing layer 'paketo-buildpacks/bellsoft-liberica:jre' 74[INFO] [creator] Reusing layer 'paketo-buildpacks/bellsoft-liberica:jvmkill' 75[INFO] [creator] Reusing layer 'paketo-buildpacks/executable-jar:class-path' 76[INFO] [creator] Reusing layer 'paketo-buildpacks/spring-boot:helper' 77[INFO] [creator] Reusing layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings' 78[INFO] [creator] Reusing layer 'paketo-buildpacks/spring-boot:web-application-type' 79[INFO] [creator] Reusing 1/1 app layer(s) 80[INFO] [creator] Reusing layer 'launcher' 81[INFO] [creator] Reusing layer 'config' 82[INFO] [creator] Adding label 'io.buildpacks.lifecycle.metadata' 83[INFO] [creator] Adding label 'io.buildpacks.build.metadata' 84[INFO] [creator] Adding label 'io.buildpacks.project.metadata' 85[INFO] [creator] Adding label 'org.opencontainers.image.title' 86[INFO] [creator] Adding label 'org.opencontainers.image.version' 87[INFO] [creator] Adding label 'org.springframework.boot.spring-configuration-metadata.json' 88[INFO] [creator] Adding label 'org.springframework.boot.version' 89[INFO] [creator] *** Images (d831d6a66f8e): 90[INFO] [creator] docker.io/library/spring-boot-buildpack:0.0.1-SNAPSHOT 91[INFO] 92[INFO] Successfully built image 'docker.io/library/spring-boot-buildpack:0.0.1-SNAPSHOT' 93[INFO] 94[INFO] ------------------------------------------------------------------------ 95[INFO] BUILD SUCCESS 96[INFO] ------------------------------------------------------------------------ 97[INFO] Total time: 25.936 s 98[INFO] Finished at: 2020-11-25T09:14:37+01:00 99[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:
1docker 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:
1diff: 2 # You can change the default files shown in the filetree (right pane). All diff types are shown by default. 3 hide: 4 - unmodified 5 6filetree: 7 # Show the file attributes next to the filetree 8 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):
):
1dive 408f3d59f38e
This should shift our console to a completely different view and presents us all the layers the Paketo build produced inside our image:
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:
1brew 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:
1$ pack suggest-builders 2 3Suggested builders: 4 Google: gcr.io/buildpacks/builder:v1 Ubuntu 18 base image with buildpacks for .NET, Go, Java, Node.js, and Python 5 Heroku: heroku/buildpacks:18 heroku-18 base image with buildpacks for Ruby, Java, Node.js, Python, Golang, & PHP 6 Paketo Buildpacks: paketobuildpacks/builder:base Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang 7 Paketo Buildpacks: paketobuildpacks/builder:full Ubuntu bionic base image with buildpacks for Java, .NET, NodeJS, Golang, PHP, HTTPD and NGINX 8 Paketo Buildpacks: paketobuildpacks/builder:tiny Tiny base image (bionic build image, distroless run image) with buildpacks for Golang 9 10Tip: Learn more about a specific builder with: 11 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:
1pack 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:
1$ docker images 2... 3paketobuildpacks/builder <none> 914aba170326 40 years ago 654MB 4pack.local/builder/axczkudrjk latest 69aeed7ad644 40 years ago 654MB 5spring-boot-buildpack latest b529a37599a6 40 years ago 259MB 6paketobuildpacks/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:
1unzip target/spring-boot-buildpack-0.0.1-SNAPSHOT.jar -d target/extractedjar
Now let’s take a look into the target/extractedjar
directory:
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 :
1<build> 2 <plugins> 3 <plugin> 4 <groupId>org.springframework.boot</groupId> 5 <artifactId>spring-boot-maven-plugin</artifactId> 6 <configuration> 7 <layers> 8 <enabled>true</enabled> 9 </layers> 10 </configuration> 11 </plugin> 12 </plugins> 13 </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:
1mvn 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:
1- "dependencies": 2 - "BOOT-INF/lib/" 3- "spring-boot-loader": 4 - "org/" 5- "snapshot-dependencies": 6- "application": 7 - "BOOT-INF/classes/" 8 - "BOOT-INF/classpath.idx" 9 - "BOOT-INF/layers.idx" 10 - "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:
1$ java -Djarmode=layertools -jar spring-boot-buildpack-0.0.1-SNAPSHOT.jar list 2 3dependencies 4spring-boot-loader 5snapshot-dependencies 6application
To extract each layer, we can also use the command line option with the extract
option:
1$ 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):
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:
1FROM adoptopenjdk:11-jre-hotspot as builder 2WORKDIR application 3ARG JAR_FILE=target/*.jar 4COPY ${JAR_FILE} application.jar 5RUN java -Djarmode=layertools -jar application.jar extract 6 7FROM adoptopenjdk:11-jre-hotspot 8WORKDIR application 9COPY --from=builder application/dependencies/ ./ 10COPY --from=builder application/spring-boot-loader/ ./ 11COPY --from=builder application/snapshot-dependencies/ ./ 12COPY --from=builder application/application/ ./ 13ENTRYPOINT ["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:
1docker build . --tag spring-boot-layered --file DockerfileThatsNotNeededUsingBuildpack
In the build output we then see the separate layers beeing created:
1... 2Step 8/12 : COPY --from=builder application/dependencies/ ./ 3 ---> 88bb8adaaca6 4Step 9/12 : COPY --from=builder application/spring-boot-loader/ ./ 5 ---> 3922891db128 6Step 10/12 : COPY --from=builder application/snapshot-dependencies/ ./ 7 ---> f139bcf5babb 8Step 11/12 : COPY --from=builder application/application/ ./ 9 ---> 5d02393d4fe2 10...
We can even further examine the created Docker image using our container inspection tool dive
:
1dive 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
:
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:
1$ mvn spring-boot:build-image 2... 3[INFO] [creator] Paketo Spring Boot Buildpack 3.5.0 4[INFO] [creator] https://github.com/paketo-buildpacks/spring-boot 5[INFO] [creator] Creating slices from layers index 6[INFO] [creator] dependencies 7[INFO] [creator] spring-boot-loader 8[INFO] [creator] snapshot-dependencies 9[INFO] [creator] application 10[INFO] [creator] Launch Helper: Reusing cached layer 11...
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:
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 …
More articles
fromJonas Hecht
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Jonas Hecht
Senior Solution Architect
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.