Container-Images: Abschied vom Dockerfile

Seite 3: Layered Jars mit Spring Boot

Inhaltsverzeichnis

Die wichtigsten Aspekte der Cloud Native Buildpacks, die den Einsatz von Dockerfile obsolet machen, sind damit behandelt. Beim Blick auf Spring Boot fällt aber ein noch recht neues Feature auf, das Erstellen von Containern betreffend: das Layered-Jars-Feature. Wie die Cloud Native Buildpacks zielt es auf eine möglichst effiziente Nutzung von Containern ab.

Um die Vorteile beider Welten sinnvoll miteinander zu integrieren, lohnt sich vorab ein genauerer Blick auf das in Spring Boot 2.3.x vorgestellte "layered jars"-Feature. Nach dem Entpacken der jar-Datei innerhalb des target-Verzeichnisses des Beispielprojekts erhält man Einblick in das Standard-Layout einer Spring-Boot-jar-Datei. Dazu muss vorab der Maven- oder Gradle-Build einmal erfolgreich durchgelaufen sein. Im Beispielprojekt sieht der Befehl zum Entpacken etwa so aus:

unzip target/spring-boot-buildpack-0.0.1-SNAPSHOT.jar -d target/extractedjar

Die jar-Datei enthält drei Verzeichnisse: BOOT-INF, META-INF und org (s. Abb.3). Der gesamte Anwendungscode in Form von class- und Konfigurations-Dateien befindet sich in BOOT-INF/classes. Das benachbarte Verzeichnis BOOT-INF/lib enthält alle abhängigen Pakete und Libraries. Egal ob in der SNAPSHOT- oder der jeweiligen Release-Version. Innerhalb von META-INF finden sich noch ergänzende Meta-Informationen zum Projekt. Zuletzt umfasst das Verzeichnis org/springframework/boot/loader alle notwendigen Klassen, die für den gewohnten Start einer Spring-Boot-Anwendung per java -jar-Befehl sorgen. All das ist bereits seit vielen Spring-Boot-Versionen unverändert.

Das Standard-Layout einer Spring Boot jar-Datei im Dateiexplorer (Abb. 3).

Seit Spring-Boot-Version 2.3.x lässt sich im spring-boot-maven-plugin explizit das neue „layered jars“-Feature aktivieren (in den Versionen ab 2.4.x ist es bereits die Default-Einstellung), über die folgende Konfiguration in der pom.xml:

<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<layers>
						<enabled>true</enabled>
					</layers>
				</configuration>
			</plugin>
		</plugins>
	</build>

Anschließend sollte per mvn clean package ein frischer Maven-Build gestartet werden. Nach dessen erfolgreichen Durchlauf lässt sich die gebaute Datei target/spring-boot-buildpack-0.0.1-SNAPSHOT.jar entpacken. Im Verzeichnis BOOT-INF enthält sie die Datei layers.idx, mit folgendem Inhalt:

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

Die Datei layers.idx dient als eine Art Blaupause für den Aufbau von Container-Images. Dabei werden verschiedene (Unter-)Verzeichnisse innerhalb des Spring-Boot-jars auf getrennte Container-Layer gemappt. Zusätzlich definiert die layers.idx eine Reihenfolge für die Container-Layer. Der erste Layer wird mit Dependencies deklariert und umfasst alle Releases aller Abhängigkeiten der Anwendung. Dieser Layer ändert sich in der Anwendungsentwicklung typischerweise nur selten. Der zweite Layer spring-boot-loader enthält alle notwendigen Klassen zum Starten der Spring-Boot-Anwendung. Erst im dritten Layer folgen dann die SNAPSHOT-Abhängigkeiten in snapshot-dependencies. Diese ändern sich innerhalb des Entwicklungsprozesses einer Anwendung in der Regel häufiger. Im letzten Layer application finden sich alle Dateien, die Entwicklerinnen und Entwickler selbst beisteuern – er weist daher auch in der Regel die meisten Änderungen auf. Denn jeder vom Entwicklungsteam in eine Continuous-Integration-Umgebung gepushte Commit führt ja zu neuen Container-Versionen. Demgegenüber kommt das Ändern beispielsweise der Hibernate- oder der Spring-Version deutlich seltener vor.

Um die Layer auch ohne wiederholtes Entpacken der jar-Dateien sichtbar zu machen, steht eine zusätzliche Erweiterung für die Kommandozeilenparameter des java -jar-Befehls bereit. Der Parameter -Djarmode=layertools offenbart die Details der Layer-Konfiguration:

$ java -Djarmode=layertools -jar target/spring-boot-buildpack-0.0.1-SNAPSHOT.jar list

dependencies
spring-boot-loader
snapshot-dependencies
application

Alle Layer sind dabei in der korrekten Reihenfolge aufgelistet. Neben der list- steht die extract-Funktion bereit, um die Inhalte sauber nach Layern getrennt zu extrahieren:

$ java -Djarmode=layertools -jar spring-boot-buildpack-0.0.1-SNAPSHOT.jar extract --destination extractedjar

Nach dem Ausführen des dargestellten Befehls stehen im Verzeichnis target/extractedjar alle Layer aufgeteilt in einzelne Verzeichnisse zur Verfügung (s. Abb. 4).

Alle Layer einer Spring Boot-Anwendung lassen sich als separate Verzeichnisse extrahieren (Abb. 4).

Damit die extrahierten Verzeichnisse jeweils einen eigenen Container-Layer repräsentieren, lässt sich ein simpler COPY-Befehl innerhalb eines Dockerfile nutzen. Wie in einem Spring-Blogbeitrag beschrieben, lässt sich zudem der neue Parameter -Djarmode=layertools ebenfalls innerhalb eines Dockerfile verwenden. Im folgenden Dockerfile dient der Parameter zum Extrahieren der Layer in einem Build-Container eines Docker-Multistage-Builds. Im zweiten Container werden dann die extrahierten Verzeichnisse per COPY-Befehl genutzt, um jeweils eigene Layer zu erzeugen:

FROM adoptopenjdk:11-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM adoptopenjdk:11-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

Der Build lässt sich auch mit dem Beispielprojekt auf GitHub, das die Datei DockerfileThatsNotNeededUsingBuildpacks enthält, ausführen:

docker build . --tag spring-boot-layered --file DockerfileThatsNotNeededUsingBuildpack

Die Anlage der separaten Layer lässt sich in der Log-Ausgabe nachvollziehen:

...
Step 8/12 : COPY --from=builder application/dependencies/ ./
 ---> 88bb8adaaca6
Step 9/12 : COPY --from=builder application/spring-boot-loader/ ./
 ---> 3922891db128
Step 10/12 : COPY --from=builder application/snapshot-dependencies/ ./
 ---> f139bcf5babb
Step 11/12 : COPY --from=builder application/application/ ./
 ---> 5d02393d4fe2
...

Beim Untersuchen des fertigen Container-Image mit dem Kommandozeilenwerkzeugs dive und dem Befehl dive spring-boot-layered lassen sich die Layer der Spring-Boot-Anwendung sichtbar machen. Sie erscheinen genauso, wie innerhalb der layers.idx definiert und wie im Dockerfile angewandt (s. Abb. 5).

Mit dive lassen sich die Layer der Spring Boot-Anwendung sichtbar machen (Abb. 5).

Idealerweise sollen Cloud Native Buildpacks das manuelle Schreiben von Dockerfiles unnötig machen, und beide Welten miteinander integrierbar sein. Beim Einsatz von Spring Boot 2.3.x genügt es dazu, das layered jars-Feature innerhalb der pom.xml aktiviert zu lassen (siehe Abschnitt "Layered Jars mit Spring Boot"). Ab Spring Boot 2.4.x ist das Feature bereits standardmäßig aktiv. Dann bedarf es lediglich eines neuerlichen Maven-Build mit mvn spring-boot:build-image. Alternativ kann pack CLI zum Einsatz kommen. Das Build-Log sollte in beiden Fällen einen Eintrag "Creating slices from layers index" im Abschnitt "Paketo Spring Boot Buildpack" enthalten:

$ mvn spring-boot:build-image
...
[INFO]     [creator]     Paketo Spring Boot Buildpack 3.5.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/spring-boot
[INFO]     [creator]       Creating slices from layers index
[INFO]     [creator]         dependencies
[INFO]     [creator]         spring-boot-loader
[INFO]     [creator]         snapshot-dependencies
[INFO]     [creator]         application
[INFO]     [creator]       Launch Helper: Reusing cached layer
...

Nach einem erfolgreichen Build lassen sich die Applikationslayer wieder mit dem Werkzeug dive betrachten (s. Abb. 6).

Nach einem erfolgreichen Build, lassen sich die Applikationslayer wieder mit dem Werkzeug dive betrachten (s. Abb. 6).

In dive zeigen sich die vier separaten Layer der Spring-Boot-Anwendung. Genau wie beim selbstgeschriebenen Dockerfile lässt sich auch innerhalb des Paketo-Build das Layered jars-Feature nutzen, um die Layer getrennt anzulegen. Abbildung 6 zeigt daher innerhalb des Application-Layers nur die .class- und Konfigurations-Dateien der Anwendung. Alle Abhängigkeiten und der spring-boot-loader finden sich in den anderen Layern.