Skip to content

Commit

Permalink
Sample utilizing Spring Boot Layered JAR to make the build more frien…
Browse files Browse the repository at this point in the history
…dly for Docker build cache and to avoid rebuilding all the layers of image when just application code changes.
  • Loading branch information
mabrarov authored and rohanKanojia committed Mar 7, 2021
1 parent 2534a55 commit 0951e51
Show file tree
Hide file tree
Showing 12 changed files with 595 additions and 0 deletions.
53 changes: 53 additions & 0 deletions samples/build-cache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
## d-m-p sample utilizing docker build cache

This example shows:

1. How to use docker-maven-plugin together with [Spring Boot Layered JAR](https://spring.io/blog/2020/08/14/creating-efficient-docker-images-with-spring-boot-2-3) feature to utilize Docker build cache.
1. How to add files into the image with predefined permissions without [multi-stage build](https://docs.docker.com/develop/develop-images/multistage-build/).

It is a simple "Hello world" Spring Boot REST controller in the root context.

The [Dockerfile](image/src/main/docker/Dockerfile) is located in `image/src/main/docker`.

This example follows [reproducible builds](https://maven.apache.org/guides/mini/guide-reproducible-builds.html) approach
to generate archives which are friendly to the Docker build cache.

All content which needs to be added from the host into the image is packaged into archives of TAR format.
Content is separated based on the frequency of updates, so that changes in application code -
in [app/src/main/java/io/fabric8/dmp/samples/buildcache/Application.java](app/src/main/java/io/fabric8/dmp/samples/buildcache/Application.java) -
don't lead to modification of all archives, but impact just image/target/application.tar.

Generated archives are extracted inside image with `ADD` Dockefile directive adding the host content into the image with
desired UNIX file permissions, owner, group, creation and modification timestamps.

Dockerfile directives are ordered based on the frequency of updates, e.g. archive with application dependencies
(image/target/dependencies.tar) is added before archive with application code (image/target/application.tar) to avoid
creation of new image layer containing application dependencies (i.e. to take that layer from the Docker build cache)
when only application code changes.

When building the project multiple times, note that subsequent builds log "Using cache" messages, like:

```
$ mvn clean package
...
[INFO] DOCKER> Step 1 : FROM gcr.io/distroless/java-debian10
[INFO] DOCKER> ---> f6a5dc137f9b
[INFO] DOCKER> Step 2 : USER nonroot
[INFO] DOCKER> ---> Using cache
[INFO] DOCKER> ---> 26d64b9907e3
[INFO] DOCKER> Step 3 : ENTRYPOINT
[INFO] DOCKER> ---> Using cache
[INFO] DOCKER> ---> 6eb12980cd38
[INFO] DOCKER> Step 4 : WORKDIR "/app"
[INFO] DOCKER> ---> Using cache
[INFO] DOCKER> ---> d6a408147d7c
[INFO] DOCKER> Step 5 : CMD tini -e 130 -e 143 -- java org.springframework.boot.loader.JarLauncher
[INFO] DOCKER> ---> Using cache
[INFO] DOCKER> ---> 461505b7ade9
[INFO] DOCKER> Step 6 : ADD dependencies.tar /
[INFO] DOCKER> ---> Using cache
[INFO] DOCKER> ---> df87a8d003f3
[INFO] DOCKER> Step 7 : ADD spring-boot-loader.tar /
[INFO] DOCKER> ---> Using cache
[INFO] DOCKER> ---> 2de1bf3ac9ad
```
44 changes: 44 additions & 0 deletions samples/build-cache/app/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.fabric8.dmp.samples.build-cache</groupId>
<artifactId>parent</artifactId>
<version>0.34-SNAPSHOT</version>
</parent>

<artifactId>app</artifactId>

<properties>
<!-- Avoid docker build cache invalidation due to new commits -->
<project.build.outputTimestamp>1970-01-01T00:00:00Z</project.build.outputTimestamp>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<mainClass>io.fabric8.dmp.samples.buildcache.Application</mainClass>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.fabric8.dmp.samples.buildcache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class Application {

public static void main(final String[] args) {
SpringApplication.run(Application.class, args);
}

@RestController
public static class HelloController {

@RequestMapping("/")
public String greeting() {
return "Hello World!";
}
}
}
210 changes: 210 additions & 0 deletions samples/build-cache/image/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.fabric8.dmp.samples.build-cache</groupId>
<artifactId>parent</artifactId>
<version>0.34-SNAPSHOT</version>
</parent>

<artifactId>image</artifactId>
<packaging>pom</packaging>

<properties>
<!-- Avoid docker build cache invalidation due to new commits -->
<project.build.outputTimestamp>1970-01-01T00:00:00Z</project.build.outputTimestamp>

<tini.version>0.19.0</tini.version>
<tini.sha512>8053cc21a3a9bdd6042a495349d1856ae8d3b3e7664c9654198de0087af031f5d41139ec85a2f5d7d2febd22ec3f280767ff23b9d5f63d490584e2b7ad3c218c</tini.sha512>

<app-layers.dir>${project.build.directory}/app</app-layers.dir>
<app-classes.output.dir>app</app-classes.output.dir>

<image>fabric8/dmp-sample-build-cache-app:${project.version}</image>
</properties>

<dependencies>
<dependency>
<groupId>io.fabric8.dmp.samples.build-cache</groupId>
<artifactId>app</artifactId>
<exclusions>
<!-- Minimize class path for the java goal of Exec Maven Plugin -->
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<!-- Extracting layers from Spring Boot Layered JAR -->
<id>extract-app-layers</id>
<phase>generate-resources</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>org.springframework.boot.loader.JarLauncher</mainClass>
<arguments>
<argument>extract</argument>
<argument>--destination</argument>
<argument>${app-layers.dir}</argument>
</arguments>
<systemProperties>
<property>
<key>jarmode</key>
<value>layertools</value>
</property>
</systemProperties>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.googlecode.maven-download-plugin</groupId>
<artifactId>download-maven-plugin</artifactId>
<executions>
<execution>
<!--
tini (https://github.com/krallin/tini) to override Java exit code when stopping container,
because JVM returns non-zero exit code when stopped with SIGINT, SIGTERM or any other signal.
-->
<id>download-tini</id>
<phase>generate-resources</phase>
<goals>
<goal>wget</goal>
</goals>
<configuration>
<url>https://github.com/krallin/tini/releases/download/v${tini.version}/tini</url>
<outputFileName>tini</outputFileName>
<sha512>${tini.sha512}</sha512>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>build-dependencies-tar</id>
<phase>process-resources</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>dependencies</finalName>
<appendAssemblyId>false</appendAssemblyId>
<attach>false</attach>
<descriptors>
<descriptor>src/assembly/dependencies.xml</descriptor>
</descriptors>
</configuration>
</execution>
<execution>
<id>build-spring-boot-loader-tar</id>
<phase>process-resources</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>spring-boot-loader</finalName>
<appendAssemblyId>false</appendAssemblyId>
<attach>false</attach>
<descriptors>
<descriptor>src/assembly/spring-boot-loader.xml</descriptor>
</descriptors>
</configuration>
</execution>
<execution>
<id>build-snapshot-dependencies-tar</id>
<phase>process-resources</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>snapshot-dependencies</finalName>
<appendAssemblyId>false</appendAssemblyId>
<attach>false</attach>
<descriptors>
<descriptor>src/assembly/snapshot-dependencies.xml</descriptor>
</descriptors>
</configuration>
</execution>
<execution>
<id>build-application-tar</id>
<phase>process-resources</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>application</finalName>
<appendAssemblyId>false</appendAssemblyId>
<attach>false</attach>
<descriptors>
<descriptor>src/assembly/application.xml</descriptor>
</descriptors>
</configuration>
</execution>
<execution>
<!-- Prepare docker build context with Dockerfile and all archives used by it -->
<id>build-docker-context</id>
<phase>process-resources</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>context</finalName>
<appendAssemblyId>false</appendAssemblyId>
<attach>false</attach>
<descriptors>
<descriptor>src/assembly/docker-context.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<images>
<image>
<name>${image}</name>
<build>
<contextDir>${project.build.directory}/context</contextDir>
<!--
Don't remove image with the same name to reuse old image layers in case of reverting of changes.
Use "-D docker.cleanup=try" in the Maven command line if need to remove image with the same name.
-->
<cleanup>${docker.cleanup}</cleanup>
<!--
No need to filter Dockerfile - filtering is performed by Maven Assembly Plugin.
Refer to build-docker-context execution.
-->
<filter>false</filter>
</build>
</image>
</images>
</configuration>
<executions>
<execution>
<id>build-docker-image</id>
<goals>
<goal>build</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
19 changes: 19 additions & 0 deletions samples/build-cache/image/src/assembly/application.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
<id>application</id>
<formats>
<format>tar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<outputDirectory>${app-classes.output.dir}</outputDirectory>
<directory>${app-layers.dir}/application</directory>
<!-- Prevent modification of Java *.class files and other content which is considered a part of the application -->
<fileMode>0444</fileMode>
<!-- This directory defines Java class path, so don't allow creation of new files for security -->
<directoryMode>0555</directoryMode>
</fileSet>
</fileSets>
</assembly>
25 changes: 25 additions & 0 deletions samples/build-cache/image/src/assembly/dependencies.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
<id>dependencies</id>
<formats>
<format>tar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<outputDirectory>usr/bin</outputDirectory>
<directory>${project.build.directory}</directory>
<includes>
<include>tini</include>
</includes>
<fileMode>0555</fileMode>
</fileSet>
<fileSet>
<outputDirectory>${app-classes.output.dir}</outputDirectory>
<directory>${app-layers.dir}/dependencies</directory>
<fileMode>0444</fileMode>
<directoryMode>0555</directoryMode>
</fileSet>
</fileSets>
</assembly>

0 comments on commit 0951e51

Please sign in to comment.