Optimizing Red Hat Fuse 7 Spring Boot container images

Optimizing Red Hat Fuse 7 Spring Boot container images

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:

Fuse 7 Container Image as Fat JAR

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:

Fuse 7 Container Image as Thin JAR

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

Everything you need to grow your career.

With your free Red Hat Developer program membership, unlock our library of cheat sheets and ebooks on next-generation application development.

SIGN UP

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.

To learn more, visit our Linux containers or microservices pages.

Join Red Hat Developer and get access to handy cheat sheets, free books, and product downloads that can help you with your microservices and container application development.

Download and learn more about Red Hat Fuse, a modular, cloud-ready architecture. Fuse is Java EE 7 certified and features high-availability clustering, distributed caching, messaging, transactions, and a full web services stack.

For more information about Red Hat OpenShift and other related topics, visit: OpenShift, OpenShift Online.

Share