Working with Red Hat Fuse 7 on Spring Boot is straightforward. In my field experience, I have seen many development (a.k.a. integrator) teams moving to Fuse 7 on Spring Boot for their new integration platforms on Red Hat OpenShift Container Platform (well aligned with agile integration).
Lately, however, I have also seen some teams worried about the size of the final images and the deployment pipeline. In most cases, they had developed a set of common libraries or frameworks to extend or to homogenize the final integration projects. All the cases have the same result:
- Several dependencies copied in each integration project
- Always replacing the container images with the latest fat JAR (including the same dependencies) in each build pipeline
Spring Boot is usually packaged as "fat JARS" that contain all runtime dependencies. Although this is quite convenient, because you only need a JRE and a single JAR to run the application, in a container environment such as Red Hat OpenShift, you have to build a full container image to deploy your application.
Single application layer vs. multiple application layer
A typical container image with Fuse 7 on Spring Boot application has the following structure:
Basically, the image has only two big layers:
- Fuse 7 on Spring Boot Application: Fat JAR created by Fuse 7 with Spring Boot and all its dependencies
- Java Runtime: Base image providing the JRE and other libraries, tools, and so on.
Every time the application is built with this structure, we are wasting the storage of that layer. Basically, the container image build process will not reuse the cache of that layer because we are adding a new fat JAR, which may be very similar to the previous one because normally we are doing small changes in our application.
One of the best practices of Dockerfiles is reducing the number of layers; however, in our case, we should also minimize the size of the layers we are changing between each build. Applications developed with Fuse 7 on Spring Boot will change a few things (Camel Routes, Beans, etc.); however, the dependencies used are basically the same for each build process.
To optimize the storage and increase the build process and the deployment phase, we need to change the default structure. The new structure should be similar to:
This new structure is based on three layers:
- Application: This only has the final application. The components could be changed several times, but the layer is small because it only includes the items needed for the application (Apache Camel context basically).
- Spring Boot and dependencies: Any dependency or library needed by the application will be managed in this layer. It is bigger than the previous one, but this layer will change only if we apply changes in the dependency tree of the application.
- Java Runtime: Base image providing the JRE and other libraries, tools, and so on.
To achieve this, there are other strategies, such as Docker multistage build; however, this article is based in Maven and its life cycle.
The main steps are:
- To not package the application as Spring Boot does (i.e., not use spring-boot-maven-plugin)
- Use Maven plugins to copy any runtime dependency needed by the application in one place
- Use Maven plugins to build a simple jar file with the classes provided by the application. This new application will include a MANIFEST.MF file with:
- Main class name
- Class-Path entry to locate any dependency needed by the application.
- Build container image using a Dockerfile aligned with the new layer structure:
- From a base image providing OS and Java Runtime
- Copy any dependency needed by the application into a one place
- Copy application file (JAR file) in a place to be executed
Show me the code
There is a single project developed in GitHub to show how to implement this strategy easily. This project includes two different Maven profiles to:
- Build a Fat JAR file (fuse7-sb-fatjar): Typical Fuse 7 on Spring Boot Application
- Build a Thin JAR file (fuse7-sb-thinjar): Fuse 7 on Spring Boot application using the new structure
In both cases, the docker-maven-plugin is used to build the container image. Also the base image to build the image is the same, the official image provided by Red Hat for Fuse 7 Spring Boot applications (fuse7/fuse-java-openshift:1.2).
The profile to build a fat JAR is:
<profile> <id>fuse7-sb-fatjar</id> <activation> <activeByDefault>false</activeByDefault> </activation> <build> <defaultGoal>spring-boot:run</defaultGoal> <plugins> <plugin> <groupId>org.jboss.redhat-fuse</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> <!-- Build Docker Image --> <plugin> <groupId>io.fabric8</groupId> <artifactId>docker-maven-plugin</artifactId> <executions> <!-- Build Docker Image at Maven package phase --> <execution> <id>docker</id> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <images> <image> <name>fuse7-sb-sample-fatjar:${project.version}</name> <build> <from>registry.access.redhat.com/fuse7/fuse-java-openshift:1.2</from> <assembly> <basedir>/deployments</basedir> <descriptorRef>artifact</descriptorRef> </assembly> </build> </image> </images> </configuration> </plugin> </plugins> </build> </profile>
To build the project as usual with Fuse 7 on Spring Boot application:
$ mvn clean package -Pfuse7-sb-fatjar
The container image and its structure could be inspected as:
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE fuse7-sb-sample-fatjar 1.0.0-SNAPSHOT 413ec90066b2 2 minutes ago 472MB $ docker image history fuse7-sb-sample-fatjar:1.0.0-SNAPSHOT IMAGE CREATED CREATED BY SIZE 413ec90066b2 3 minutes ago /bin/sh -c #(nop) COPY dir:bce1849c62a66a19b… 22.5MB 3acce9532a02 2 months ago 29.7MB <missing> 2 months ago 204MB <missing> 2 months ago 12.6MB <missing> 2 months ago 2.92kB <missing> 2 months ago 203MB
Application layer consumes 22.5 MB of storage.
The profile to build a thin JAR is:
<profile> <id>fuse7-sb-thinjar</id> <activation> <activeByDefault>false</activeByDefault> </activation> <build> <plugins> <!-- Generate a simple JAR file with ClassPath --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> <includeScope>runtime</includeScope> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>org.jboss.fuse7.samples.Application</mainClass> </manifest> </archive> </configuration> </plugin> <!-- Build Docker Image --> <plugin> <groupId>io.fabric8</groupId> <artifactId>docker-maven-plugin</artifactId> <executions> <!-- Build Docker Image at Maven package phase --> <execution> <id>docker</id> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <images> <image> <name>fuse7-sb-sample-thinjar:${project.version}</name> <build> <dockerFile>${project.basedir}/Dockerfile</dockerFile> </build> </image> </images> </configuration> </plugin> </plugins> </build> </profile>
This profile uses a new Dockerfile to define the new structure.
FROM registry.access.redhat.com/fuse7/fuse-java-openshift:1.2 # Fuse 7, Spring Boot and Third Party Dependencies COPY target/lib /deployments/lib # Application COPY target/*.jar /deployments
To build the project with the new structure:
$ mvn clean package -Pfuse7-sb-thinjar
The container image and its structure could be inspected as:
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE fuse7-sb-sample-fatjar 1.0.0-SNAPSHOT 413ec90066b2 2 minutes ago 472MB fuse7-sb-sample-thinjar 1.0.0-SNAPSHOT f3bf83f4ac28 About a minute ago 472MB $ docker image history fuse7-sb-sample-thinjar:1.0.0-SNAPSHOT IMAGE CREATED CREATED BY SIZE f3bf83f4ac28 3 minutes ago /bin/sh -c #(nop) COPY file:8e8b28eec64eeef0… 5.93kB 8f85633e632f 6 weeks ago /bin/sh -c #(nop) COPY dir:9371efeba146b0c49… 22.4MB 3acce9532a02 2 months ago 29.7MB <missing> 2 months ago 204MB <missing> 2 months ago 12.6MB <missing> 2 months ago 2.92kB <missing> 2 months ago 203M
Application layer only consumes around 6 kB of storage.
Conclusions
Comparing both container images we could conclude:
- Both images have the same final size (472 MB)
- No changes or effects during application execution
- After each build using the fat JAR application, the layer with the application is replaced completely (around 22 MB)
- After each build using the thin JAR, only the application layer is replaced completely (around 6 kB). Docker cache layer is applied for dependencies.
So, if you are worried about the size of your Fuse 7 on Spring Boot applications, there are various alternatives and strategies to help you optimize it.
Last updated: April 24, 2019