Erstellung optimierter Docker-Images für Spring Boot-Anwendungen

Container sind das bevorzugte Mittel zur Verpackung von Anwendungen mit allen Software- und Betriebssystemabhängigkeiten und deren Bereitstellung in verschiedene Umgebungen.

In diesem Artikel werden verschiedene Methoden zur Containerisierung von Spring Boot-Anwendungen behandelt:

  • Erstellung eines Docker-Images mit einer Docker-Datei,
  • Erstellung eines OCI-Images aus dem Quellcode mit Cloud-Native Buildpack,
  • und Optimierung des Images zur Laufzeit durch Aufteilen von JAR-Teilen in verschiedene Schichten mit mehrstufigen Werkzeugen.

 Beispielcode

Dieser Artikel wird von einem funktionierenden Codebeispiel begleitet. auf GitHub .

Container-Terminologie

Wir beginnen mit der in diesem Artikel verwendeten Container-Terminologie:

  • Container-Image (Container image): eine Datei eines bestimmten Formats. Wir wandeln unsere Anwendung in ein Container-Image um, indem wir ein Build-Tool ausführen.
  • Container: eine ausführbare Instanz des Container-Images.
  • Container-Engine (Container engine): ein Demonprozess, der für das Starten des Containers verantwortlich ist.
  • Container-Host (Container host): der Host-Computer, auf dem die Container-Engine läuft.
  • Container-Registry (Container registry): ein gemeinsamer Speicherort, der zur Veröffentlichung und Verbreitung des Container-Images verwendet wird.
  • OCI-StandardOpen Container Initiative (OCI) — ist eine schlanke, offene Verwaltungsstruktur, die im Rahmen der Linux Foundation gegründet wurde. Die OCI-Image-Spezifikation definiert Branchenstandards für Container-Image-Formate und Laufzeitumgebungen, um sicherzustellen, dass alle Container-Engines Container-Images aus jedem Build-Tool ausführen können.

Um eine Anwendung in einen Container zu setzen, verpacken wir unsere Anwendung in ein Container-Image und veröffentlichen dieses Image in einer gemeinsamen Registry. Die Container-Laufzeitumgebung lädt dieses Image aus der Registry herunter, entpackt es und startet die Anwendung darin.

Version 2.3 von Spring Boot bietet Plugins zur Erstellung von OCI-Images.

Docker — die am häufigsten verwendete Container-Implementierung, und wir verwenden Docker in unseren Beispielen, daher beziehen sich alle nachfolgenden Hinweise auf Container in diesem Artikel auf Docker.

Erstellung eines Container-Images auf herkömmliche Weise

Es ist sehr einfach, Docker-Images für Spring Boot-Anwendungen zu erstellen, indem man einige Anweisungen in die Docker-Datei hinzufügt.

Zuerst erstellen wir eine ausführbare JAR-Datei und kopieren im Rahmen der Docker-Datei-Anweisungen die ausführbare JAR-Datei über das Basisimage JRE, nachdem die erforderlichen Einstellungen angewendet wurden.

Lassen Sie uns unsere Spring-Anwendung auf Spring Initializr mit Abhängigkeiten weblombokund actuator. Wir fügen auch einen Rest-Controller hinzu, um eine API mit GETMethode bereitzustellen.

Erstellen der Docker-Datei

Dann legen wir diese Anwendung in einen Container, indem wir hinzufügen Dockerfile:

FROM adoptopenjdk:11-jre-hotspot
ARG JAR_FILE=target\/ *.jar
COPY ${JAR_FILE} application.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","\/application.jar"]

Unsere Docker-Datei enthält das Basisimage von adoptopenjdk, über das wir unsere JAR-Datei kopieren und dann den Port öffnen, 8080der die Anfragen abhört.

Build der Anwendung

Zuerst müssen wir die Anwendung mit Maven oder Gradle erstellen. Hier verwenden wir Maven:

mvn clean package

Dadurch wird eine ausführbare JAR-Datei der Anwendung erstellt. Wir müssen diese ausführbare JAR in ein Docker-Image konvertieren, um im Docker-Engine zu arbeiten.

Container-Image erstellen

Dann legen wir diese ausführbare JAR-Datei in das Docker-Image, indem wir den Befehl docker buildaus dem Stammverzeichnis des Projekts, das die zuvor erstellte Docker-Datei enthält, ausführen:

docker build  -t usersignup:v1 .

Wir können unser Image in der Liste mit dem Befehl sehen:

docker images 

Das Ergebnis der Ausführung des obigen Befehls umfasst unser Image usersignupneben dem Basisimage, adoptopenjdk, das in unserer Docker-Datei angegeben ist.

REPOSITORY          TAG                 SIZE
usersignup          v1                  249MB
adoptopenjdk        11-jre-hotspot      229MB

Anzeigen der Schichten innerhalb des Container-Images

Lassen Sie uns die Stapel von Schichten innerhalb des Images betrachten. Wir werden Tool  dive verwenden, um diese Schichten anzusehen:

dive usersignup:v1

Hier sind einige Ergebnisse der Ausführung des Dive-Befehls: 

Erstellung optimierter Docker-Images für Spring Boot-Anwendungen

Wie wir sehen können, macht die Anwendungsebene einen erheblichen Teil der Größe des Images aus. Wir möchten die Größe dieser Schicht in den folgenden Abschnitten im Rahmen unserer Optimierung reduzieren.

Container-Image mit Buildpack erstellen

Buildpacks (Buildpacks) sind ein allgemeiner Begriff, der von verschiedenen „Platform as a Service“ (PAAS) -Angeboten verwendet wird, um ein Container-Image aus Quellcode zu erstellen. Es wurde 2011 von Heroku ins Leben gerufen und wird seitdem von Cloud Foundry, Google App Engine, Gitlab, Knative und einigen anderen übernommen.

Erstellung optimierter Docker-Images für Spring Boot-Anwendungen

Der Vorteil von Cloud-Buildpacks

Einer der Hauptvorteile der Verwendung von Buildpack zur Erstellung von Images ist, dass Mit Änderungen der Konfiguration des Images kann zentral (Builder) gesteuert und auf alle Anwendungen verteilt werden, die Builder verwenden.

Die Build-Pakete waren eng mit der Plattform verknüpft. Cloud-Native Buildpacks gewährleisten die Standardisierung zwischen Plattformen, indem sie das OCI-Image-Format unterstützen, das sicherstellt, dass das Image vom Docker-Engine ausgeführt werden kann.

Verwendung des Spring Boot-Plugins

Das Spring Boot-Plugin erstellt OCI-Images aus dem Quellcode mit Hilfe von Buildpack. Images werden unter Verwendung von bootBuildImageTask (Gradle) oder spring-boot:build-imageZielen (Maven) und einer lokal installierten Docker-Installation erstellt.

Wir können den Namen des Images, das für das Hochladen in die Docker-Registry erforderlich ist, angeben, indem wir den Namen in image tag:

org.springframework.boot
  spring-boot-maven-plugin
  
    
      docker.io/pratikdas/${project.artifactId}:v1

Lassen Sie uns Maven verwenden, um die build-imageZiel zur Erstellung der Anwendung und des Container-Images auszuführen. Momentan verwenden wir keine Docker-Dateien.

mvn spring-boot:build-image

Das Ergebnis wird etwa so aussehen:

[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Baue Image 'docker.io/pratikdas/usersignup:v1'
[INFO] 
[INFO]  > Ziehe Builder-Image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
.
.
.. [creator]     Hinzufügen des Labels 'org.springframework.boot.version'
.. [creator]     *** Images (c311fe74ec73):
.. [creator]           docker.io/pratikdas/usersignup:v1
[INFO] 
[INFO] Erfolgreich Image 'docker.io/pratikdas/usersignup:v1' gebaut

Aus der Ausgabe sehen wir, dass das paketo Cloud-Native Buildpackzur Erstellung eines funktionierenden OCI-Images verwendet wird. Wie zuvor können wir das Image, das als Docker-Image angegeben ist, anzeigen, indem wir den Befehl ausführen:

docker images 

Ausgabe:

REPOSITORY                             SIZE
paketobuildpacks/run                  84.3MB
gcr.io/paketo-buildpacks/builder      652MB
pratikdas/usersignup                  257MB

Erstellung eines Container-Images mit Jib

Jib ist ein Plugin zur Erstellung von Images von Google, das eine alternative Methode bietet, um ein Container-Image aus dem Quellcode zu erstellen.

Wir konfigurieren jib-maven-pluginin pom.xml:

      com.google.cloud.tools
        jib-maven-plugin
        2.5.2

Danach starten wir das Jib-Plugin mit dem Maven-Befehl, um die Anwendung zu bauen und das Container-Image zu erstellen. Wie zuvor verwenden wir hier keine Docker-Dateien:

mvn compile jib:build -Dimage=/usersignup:v1

Nach der Ausführung des oben genannten Maven-Befehls erhalten wir die folgende Ausgabe:

[INFO] Anwendung in pratikdas/usersignup:v1 kontainerisieren...
.
.
[INFO] Container-Eintrittspunkt auf [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication] gesetzt
[INFO] 
[INFO] Bild erstellt und als pratikdas/usersignup:v1 gepusht
[INFO] Aufgaben werden ausgeführt:
[INFO] [==============================] 100,0 % abgeschlossen

Die Ausgabedaten zeigen, dass das Container-Image erstellt und im Registry abgelegt wurde.

Motivationen und Methoden zur Erstellung optimierter Bilder

Wir haben zwei Hauptgründe für die Optimierung:

  • Leistung: Im Container-Orchestrierungssystem wird das Container-Image vom Image-Registry auf den Host gezogen, auf dem der Container-Engine läuft. Dieser Prozess wird als Scheduling bezeichnet. Das Herunterladen großer Images aus dem Registry führt zu längeren Scheduling-Zeiten in Container-Orchestrierungssystemen und längeren Build-Zeiten in CI-Pipelines.
  • Sicherheit: Große Images haben auch eine größere Angriffsfläche für Sicherheitsanfälligkeiten.

Ein Docker-Image besteht aus einem Stapel von Schichten, wobei jede Schicht eine Anweisung in unserem Dockerfile darstellt. Jede Schicht repräsentiert eine Delta-Änderung zur darunter liegenden Schicht. Wenn wir ein Docker-Image aus dem Registry abrufen, wird es schichtweise abgerufen und im Cache des Hosts gespeichert.

Spring Boot verwendet das "dicke JAR" als Standard-Paketformat. Wenn wir das dicke JAR betrachten, sehen wir, dass die Anwendung einen sehr kleinen Teil des gesamten JARs ausmacht. Dieser Teil ändert sich am häufigsten. Der verbleibende Teil besteht aus den Abhängigkeiten des Spring Framework. Die Optimierungsformel konzentriert sich auf die Isolierung der Anwendung auf einer separaten Ebene von den Abhängigkeiten des Spring Framework.

Die Schicht der Abhängigkeiten, die den größten Teil des dicken JARs bildet, wird nur einmal geladen und im Host-System zwischengespeichert.

Nur die dünne Schicht der Anwendung wird während der Anwendungsupdates und des Container-Schedulings abgerufen,

wie in diesem Diagramm gezeigt: In den folgenden Abschnitten werden wir sehen, wie diese optimierten Images für Spring Boot-Anwendungen erstellt werden.

Erstellung optimierter Docker-Images für Spring Boot-Anwendungen

Erstellung eines optimierten Container-Images für eine Spring Boot-Anwendung mit Buildpack

Spring Boot 2.3 unterstützt die Mehrschichtigkeit, indem Teile des dicken JARs in separate Schichten extrahiert werden. Die Standardfunktion der Schichtung ist deaktiviert und muss ausdrücklich mit dem Spring Boot Maven-Plugin aktiviert werden:

Spring Boot 2.3 unterstützt die Mehrschichtigkeit, indem Teile der fat JAR-Datei in separate Schichten extrahiert werden. Die Layering-Funktion ist standardmäßig deaktiviert und muss explizit mit dem Spring Boot Maven-Plugin aktiviert werden:

org.springframework.boot
  spring-boot-maven-plugin
  
    
      true

Wir werden diese Konfiguration verwenden, um unser Container-Image zuerst mit Buildpack und dann mit Docker in den folgenden Abschnitten zu erstellen.

Lass uns ausführen build-imagedas Maven-Ziel zum Erstellen des Container-Images:

mvn spring-boot:build-image

Wenn wir Dive starten, um die Schichten im resultierenden Image zu sehen, bemerken wir, dass die Anwendungsschicht (rot umrandet) im Vergleich zu dem, was wir im dicken JAR-Format erhalten haben, deutlich kleiner im Kilobyte-Bereich ist.

Erstellung optimierter Docker-Images für Spring Boot-Anwendungen

Erstellung eines optimierten Container-Images für eine Spring Boot-Anwendung mit Docker

Anstatt das Maven- oder Gradle-Plugin zu verwenden, können wir auch ein mehrschichtiges Docker-JAR mit einer Docker-Datei erstellen.

Wenn wir Docker verwenden, müssen wir zwei zusätzliche Schritte ausführen, um die Schichten zu extrahieren und in das endgültige Image zu kopieren.

Der Inhalt des erhaltenen JAR nach dem Erstellen mit Maven und aktivierter Schichtfunktion sieht folgendermaßen aus:

META-INF/
.
BOOT-INF/lib/
.
BOOT-INF/lib/spring-boot-jarmode-layertools-2.3.3.RELEASE.jar
BOOT-INF/classpath.idx
BOOT-INF/layers.idx

Die Ausgaben zeigen ein zusätzliches JAR mit dem Namen spring-boot-jarmode-layertoolsund layersfle.idxDatei. Diese zusätzliche JAR-Datei bietet die Möglichkeit der mehrschichtigen Verarbeitung, wie im nächsten Abschnitt beschrieben.

Extrahieren von Abhängigkeiten in separaten Schichten

Um die Schichten aus unserem mehrschichtigen JAR zu betrachten und zu extrahieren, verwenden wir die Systemvariable -Djarmode=layertoolsum das spring-boot-jarmode-layertoolsJAR anstelle der Anwendung auszuführen:

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar

Die Ausführung dieses Befehls gibt eine Ausgabe mit den verfügbaren Befehlsoptionen zurück:

Usage:
  java -Djarmode=layertools -jar usersignup-0.0.1-SNAPSHOT.jar

Available commands:
  list     List layers from the jar that can be extracted
  extract  Extracts layers from the jar for image creation
  help     Help about any command

Die Ausgabe zeigt die Befehle die Umwandlung von numerischen und textuellen Daten in die Formateextractund helphelpsollte standardmäßig sein. Lassen Sie uns den Befehl mit die Umwandlung von numerischen und textuellen Daten in die Formateder Option ausführen:

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
dependencies
spring-boot-loader
snapshot-dependencies
application

Wir sehen eine Liste von Abhängigkeiten, die als Schichten hinzugefügt werden können.

Standardmäßig Schichten:

Name der Schicht

Inhalt

dependencies

jede Abhängigkeit, deren Version kein SNAPSHOT enthält

spring-boot-loader

JAR-Ladeklassen

snapshot-dependencies

jede Abhängigkeit, deren Version ein SNAPSHOT enthält

application

Anwendungs- und Ressourcenklassen

Die Schichten sind in layers.idxder Datei in der Reihenfolge definiert, in der sie zum Docker-Image hinzugefügt werden sollen. Diese Schichten werden auf dem Host nach dem ersten Abruf zwischengespeichert, da sie sich nicht ändern. Nur die aktualisierte Anwendungsschicht wird auf den Host hochgeladen, was aufgrund der reduzierten Größe schneller erfolgt. .

Bau eines Images mit Abhängigkeiten, die in separate Schichten abgerufen werden

Wir werden das endgültige Image in zwei Phasen erstellen, indem wir eine Methode verwenden, die als Multistage-Build bezeichnet wird. In der ersten Phase rufen wir die Abhängigkeiten ab, und in der zweiten Phase kopieren wir die abgerufenen Abhängigkeiten in das endgültige Bild.

Lassen Sie uns unsere Docker-Datei für den Multistage-Build anpassen:

# the first stage of our build will extract the layers
FROM adoptopenjdk:14-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

# the second stage of our build will copy the extracted layers
FROM adoptopenjdk:14-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"]

Wir speichern diese Konfiguration in einer separaten Datei — Dockerfile2.

Wir erstellen das Docker-Image mit dem Befehl:

docker build -f Dockerfile2 -t usersignup:v1 .

Nach der Ausführung dieses Befehls erhalten wir folgende Ausgabe:

Sende Build-Kontext an Docker-Daemon  20,41 MB
Schritt 1/12 : FROM adoptopenjdk:14-jre-hotspot als Builder
14-jre-hotspot: Ziehen von library/adoptopenjdk
.
.
Erfolgreich gebaut a9ebf6970841
Erfolgreich getaggt userssignup:v1

Wir sehen, dass das Docker-Image mit einer Bild-ID erstellt und dann getaggt wird.

Schließlich führen wir den Dive-Befehl wie zuvor aus, um die Schichten innerhalb des generierten Docker-Images zu überprüfen. Wir können die Bild-ID oder das Tag als Eingabe für den Dive-Befehl angeben:

dive userssignup:v1

Wie aus der Ausgabe hervorgeht, benötigt die Schicht, die die Anwendung enthält, jetzt nur noch 11 KB, während die Abhängigkeiten in separaten Schichten zwischengespeichert werden. 

Erstellung optimierter Docker-Images für Spring Boot-Anwendungen

Abrufen interner Abhängigkeiten in separaten Schichten

Wir können die Größe der Anwendungsschicht weiter reduzieren, indem wir einige unserer benutzerdefinierten Abhängigkeiten in eine separate Schicht extrahieren, anstatt sie zusammen mit der Anwendung zu verpacken, indem wir sie in ymleiner Datei wie folgt deklarieren: layers.idx:

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

In dieser Datei layers.idxhaben wir eine benutzerdefinierte Abhängigkeit mit dem Namen io.myorghinzugefügt, die Abhängigkeiten der Organisation enthält, die aus einem gemeinsamen Repository abgerufen wurden.

Ausgabe

In diesem Artikel haben wir die Verwendung von Cloud-Native Buildpacks zur Erstellung eines Container-Images direkt aus dem Quellcode betrachtet. Dies ist eine Alternative zur Verwendung von Docker, um ein Container-Image auf herkömmliche Weise zu erstellen: Zuerst wird eine große JAR ausführbare Datei erstellt und dann in ein Container-Image verpackt, indem Anweisungen in der Docker-Datei angegeben werden.

Wir haben auch die Optimierung unseres Containers betrachtet, indem wir die Schicht-Funktion aktiviert haben, die Abhängigkeiten in separate Ebenen extrahiert, die auf dem Host zwischengespeichert werden, während die dünne Anwendungsschicht während der Planung in die Container-Ausführungsmechanismen geladen wird.

Den gesamten Quellcode, der im Artikel verwendet wurde, finden Sie unter Github .

Befehlsreferenz

Hier ist eine kurze Zusammenfassung der Befehle, die wir in diesem Artikel verwendet haben, zur schnellen Durchsicht.

Bereinigung des Kontextes:

docker system prune -a

Erstellen eines Container-Images mit einer Docker-Datei:

docker build -f  -t  .

Erstellen eines Container-Images aus dem Quellcode (ohne Dockerfile):

mvn spring-boot:build-image

Anzeigen der Abhängigkeitsschichten. Stellen Sie vor dem Erstellen der JAR-Datei der Anwendung sicher, dass die Schicht-Funktion im spring-boot-maven-plugin aktiviert ist:

java -Djarmode=layertools -jar application.jar list

Extrahieren der Abhängigkeitsschichten. Stellen Sie vor dem Erstellen der JAR-Datei der Anwendung sicher, dass die Schicht-Funktion im spring-boot-maven-plugin aktiviert ist:

 java -Djarmode=layertools -jar application.jar extract

Anzeigen der Liste der Container-Images

docker images

Anzeigen der Schichten innerhalb des Container-Images (stellen Sie sicher, dass das Tool für das Eintauchen installiert ist):

dive

Quelle: habr.com

60GB SSD 8Gb DDR4