sb-splitter-plugin
The plugin to disassemble spring boot jar to build layered docker image.
Motivation was this article
A wanted to automate splitting BOOR-INF/lib directory to application modules and real library modules.
Then I was not able to find docker plugin for maven which allows me to build layered image. To be honest all of them build layered image but layer with library modules was somehow different after each build. So docker do not want reuse them.
So I start to use maven exec plugin to call natural docker build. And this force me to use assembly plugin to build directory structure acceptable by docker to start build.
This comes to my plugin requirements
- disassemble spring boot jar
- copy some other resources (at minimum Dockerfile) to build directory structure for docker build
sb-splitter-plugin depends on native docker stack on building environment.
Ideas
Splitting SB jar
Idea is to split SB jar to parts which can be marked 'application' like and parts which can be marked 'library' like. 'library' parts are expected to be stable and it is useful to create separate docker layer with them so docker can reuse it.
Main problem is identify which jars from BOOT-INF/lib directory are stable libraries and which can be application dependencies.
The plugin uses two ways to identify 'application' jars.
- list names of those jars. (wildcards '*' and '?' can be used)
- specify package prefixes stored in those jars. (if your modules has packages like org.company.project.... you can specify prefix like org.company)
Assemble files for docker build
As docker build process has some expectations how resources are located relatively to Dockerfile. It is necessary to build a directory where Dockerfile and resources are located.
It can be done by many plugins to simplify this I add possibility to configure set of pair
- from file - to file
- from directory content - to directory content And the plugin copies specified files.
Docker build
The plugin doesn't build docker image. You can use some other plugin or use maven exec plugin to start layered build process. (see example)
Library classpath
In most of the cases classpath order of libraries are irrelevant. But maven knows 'right' order of dependencies. It is possible to generate and store classpath diring maven process and the plugin can use this text file to generate bash script which define environment variable with classpath.
The plugin configuration
Splitting
- property sbFile define location of SB file to be split. Default value is "target/${project.build.finalName}.jar"
- property destDir define directory where sbFile will be unzipped. Default value is "target/sb/"
- property sourceLibFolder define subdirectory of destDir where BOOT-INF/lib is located. Default value is "BOOT-INF/lib/"
- property destLibFolder define subdirectory of destDir where 'library' modules must be transferred. Default value is "BOOT-INF/lib/"
- property destAppFolder define subdirectory of destDir where 'application' modules must be transferred. Default value is "BOOT-INF/app/"
- property appModuleNames define list of application module names. (example: foo-application-1.9.jar)
- property appModulePackages define list package prefixes. Module is marked as 'application' is contain at least one package with specified prefix.(example: org.foo)
Generating classpath
- property cpFile define location of maven generated classpath. Default value is "BOOT-INF/classes/classpath.txt" The classpath file can be generated by maven-dependency-plugin.
- property cpScript define location of generated script file. Default value is "BOOT-INF/classpath.sh"
- property cpClassesPrefix define classes prefix in classpath. Default value is "./"
- property cpAppPrefix define app prefix in classpath. Default value is "app/"
- property cpLibPrefix define lib prefix in classpath. Default value is "lib/"
Copying files
- property copies define set of pairs from/to. They define which files are copied and where. If from is file to must be file. If from is directory to is also directory. (example: src/main/other-resources/assemblytarget/tmp/assembly)
The plugin usage example
Lets have SB application with following directory structure. The example can be simplified but in this way I can demonstrate more options
src/
main/
java/ ....
docker/
Dockerfile
docker-assembly/
start-application.sh
The Dockerfile epects that application lib, app, classes and assembly folders will be a direct subdirectories. This structure will be created in pom.xml file Each part is copied independently. So big lib part is separate level. The Dockerfile looks like
FROM openjdk:11-jre-slim
COPY ./lib /spring/lib
COPY ./app /spring/app
COPY ./classes /spring/classes
COPY ./assembly /spring
RUN chmod a+x /spring/start-application.sh
ENTRYPOINT /spring/start-application.sh
The start of application can be done directly by calling java. But in this way it is possible to demonstrate adding some other resources to unpacked jar. Important thing is calling classpath.sh script, which is generated by plugin and folloving usage od SBCP variable defined inside classpath.sh.
The start-application.sh looks like
#!/bin/bash
# do some stuff
# go to assemby directory
cd /spring
# call generated script to define SBCP variable
. ./classpath.sh
# start application
java -Djava.security.egd=file:/dev/./urandom -cp $SBCP sk.antons.test.TestApplication
# do some stuff
There will be sequence of plugins configured to get result.
- maven-dependency-plugin creates text file with classpath defined by maven project The file will be stored as resource in spring boot file. So after unzipping them it will be in BOOT-INF/classses folder. The sb-splitter-plugin uses it to create script which define classpath (This step is not mandatory)
- spring-boot-maven-plugin creates spring boot application jar
- sb-splitter-plugin unzip spring boot jar into target/sb folder. Jars from target/sb/BOOT-INF/lib, which contain package sk.antons are moved to target/sb/BOOT-INF/app. Then Dockerfile is copied into target/sb/BOOT-INF folder (It becomes root od docker building) and assembly folder is copied into target/sb/BOOT-INF/assembly
- docker-exec-plugin starts docker building process from target/sb/BOOT-INF folder The plugin usage looks like
<plugins>
<plugin> <!-- generates classpath.txt file -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>clpath</id>
<phase>compile</phase>
<goals>
<goal>build-classpath</goal>
</goals>
<configuration>
<outputFile>target/classes/classpath.txt</outputFile>
</configuration>
</execution>
</executions>
</plugin>
<plugin> <!-- generates spring boot jar -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin> <!-- split spring boot jar -->
<groupId>com.github.antonsjava</groupId>
<artifactId>sb-splitter-plugin</artifactId>
<version>1.3</version>
<executions>
<execution>
<goals>
<goal>split</goal>
</goals>
<configuration>
<cpScript>BOOT-INF/assembly/classpath.sh</cpScript>
<appModulePackages>
<appModulePackage>sk.antons</appModulePackage>
</appModulePackages>
<copies>
<copy>
<from>${basedir}/src/main/docker/Dockerfile</from>
<to>${project.build.directory}/sb/BOOT-INF/Dockerfile</to>
</copy>
<copy>
<from>${basedir}/src/main/docker-assembly</from>
<to>${project.build.directory}/sb/BOOT-INF/assembly</to>
</copy>
</copies>
</configuration>
</execution>
</executions>
</plugin>
<plugin> <!-- build docker image -->
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution>
<id>docker-package</id>
<phase>package</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>docker</executable>
<workingDirectory>${project.build.directory}/sb/BOOT-INF</workingDirectory>
<arguments>
<argument>build</argument>
<argument>${project.build.directory}/sb/BOOT-INF</argument>
<argument>-t</argument>
<argument>docker.sk.antons/test:${project.version}</argument>
</arguments>
</configuration>
</execution>
<execution> <!-- deploy docker image -->
<id>docker-deploy</id>
<phase>deploy</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>docker</executable>
<arguments>
<argument>push</argument>
<argument>docker.sk.antons/test:${project.version}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>