Erstellen optimierter Docker-Images für eine Spring Boot-Anwendung

Container sind zum bevorzugten Mittel geworden, um eine Anwendung mit all ihren Software- und Betriebssystemabhängigkeiten zu verpacken und sie dann in verschiedenen Umgebungen bereitzustellen.

In diesem Artikel werden verschiedene Möglichkeiten zum Containerisieren einer Spring Boot-Anwendung behandelt:

  • Erstellen eines Docker-Images mithilfe einer Docker-Datei,
  • Erstellen eines OCI-Images aus der Quelle mit Cloud-Native Buildpack,
  • und Bildoptimierung zur Laufzeit durch Aufteilung von JAR-Teilen in verschiedene Ebenen mithilfe von mehrschichtigen Tools.

 Codebeispiel

Dieser Artikel wird von einem funktionierenden Codebeispiel begleitet auf GitHub .

Container-Terminologie

Wir beginnen mit der im gesamten Artikel verwendeten Container-Terminologie:

  • Containerbild: eine Datei eines bestimmten Formats. Wir konvertieren unsere Anwendung in ein Container-Image, indem wir das Build-Tool ausführen.
  • Behälter: Eine ausführbare Instanz des Container-Images.
  • Containermotor: Der Daemon-Prozess, der für die Ausführung des Containers verantwortlich ist.
  • Container-Host: Der Hostcomputer, auf dem die Container-Engine ausgeführt wird.
  • Container-Registrierung: Der allgemeine Speicherort, der zum Veröffentlichen und Verteilen des Container-Images verwendet wird.
  • OCI-StandardOffene Container-Initiative (OCI) ist ein leichtes Open-Source-Management-Framework, das von der Linux Foundation entwickelt wurde. Die OCI-Image-Spezifikation definiert Industriestandards für Container-Image-Formate und die Laufzeit, um sicherzustellen, dass alle Container-Engines Container-Images ausführen können, die mit einem beliebigen Build-Tool erstellt wurden.

Um eine Anwendung zu containerisieren, packen wir unsere Anwendung in ein Container-Image und veröffentlichen dieses Image in der öffentlichen Registry. Die Containerlaufzeit ruft dieses Image aus der Registrierung ab, entpackt es und führt die darin enthaltene Anwendung aus.

Version 2.3 von Spring Boot bietet Plugins zum Erstellen von OCI-Images.

Docker ist die am häufigsten verwendete Containerimplementierung, und wir verwenden Docker in unseren Beispielen, daher beziehen sich alle nachfolgenden Containerreferenzen in diesem Artikel auf Docker.

Erstellen eines Container-Images auf traditionelle Weise

Das Erstellen von Docker-Images für Spring Boot-Anwendungen ist sehr einfach, indem Sie Ihrer Docker-Datei einige Anweisungen hinzufügen.

Wir erstellen zunächst ein ausführbares JAR und kopieren im Rahmen der Dockerfile-Anweisungen das ausführbare JAR auf das Basis-JRE-Image, nachdem wir die erforderlichen Anpassungen vorgenommen haben.

Lassen Sie uns unsere Frühlingsanwendung erstellen Frühlings-Initialisierung mit Abhängigkeiten weblombokи actuator. Wir fügen auch einen Rest-Controller hinzu, um eine API bereitzustellen GETMethode.

Erstellen einer Docker-Datei

Anschließend platzieren wir diese Anwendung in einem 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 ein Basis-Image von adoptopenjdk, darüber kopieren wir unsere JAR-Datei und öffnen dann den Port, 8080die auf Anfragen lauscht.

Anwendungsmontage

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

mvn clean package

Dadurch wird eine ausführbare JAR-Datei für die Anwendung erstellt. Wir müssen dieses ausführbare JAR in ein Docker-Image konvertieren, um es auf der Docker-Engine auszuführen.

Erstellen Sie ein Container-Image

Anschließend fügen wir diese ausführbare JAR-Datei in das Docker-Image ein, indem wir den Befehl ausführen docker buildaus dem Stammverzeichnis des Projekts, das die zuvor erstellte Docker-Datei enthält:

docker build  -t usersignup:v1 .

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

docker images 

Die Ausgabe des obigen Befehls enthält unser Bild usersignupzusammen mit dem Basisbild, adoptopenjdkin unserem Dockerfile angegeben.

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

Zeigen Sie Ebenen in einem Containerbild an

Schauen wir uns den Ebenenstapel im Bild an. Wir werden verwenden Werkzeug  tauchen, So zeigen Sie diese Ebenen an:

dive usersignup:v1

Hier ist ein Teil der Ausgabe des Dive-Befehls: 

Erstellen optimierter Docker-Images für eine Spring Boot-Anwendung

Wie wir sehen, macht die Anwendungsschicht einen erheblichen Teil der Bildgröße aus. Im Rahmen unserer Optimierung wollen wir in den folgenden Abschnitten die Größe dieser Ebene reduzieren.

Erstellen eines Container-Images mit Buildpack

Montagepakete (Baupakete) ist 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 eingeführt und seitdem von Cloud Foundry, Google App Engine, Gitlab, Knative und einigen anderen übernommen.

Erstellen optimierter Docker-Images für eine Spring Boot-Anwendung

Vorteil von Cloud Build-Paketen

Einer der Hauptvorteile der Verwendung von Buildpack zum Erstellen von Images besteht darin Änderungen an der Image-Konfiguration können zentral verwaltet werden (Builder) und an alle Anwendungen weitergegeben werden, die den Builder verwenden.

Die Buildpakete waren eng an die Plattform gebunden. Cloud-native Buildpacks sorgen für eine plattformübergreifende Standardisierung, indem sie das OCI-Image-Format unterstützen, das sicherstellt, dass das Image von der Docker-Engine ausgeführt werden kann.

Verwendung des Spring Boot-Plugins

Das Spring Boot-Plugin erstellt mithilfe von Buildpack OCI-Images aus dem Quellcode. Bilder werden mit erstellt bootBuildImageAufgaben (Gradle) bzw spring-boot:build-imageZiel (Maven) und lokale Docker-Installation.

Wir können den Namen des Images anpassen, das wir in die Docker-Registrierung übertragen müssen, indem wir den Namen in angeben image tag:

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <image>
      <name>docker.io/pratikdas/${project.artifactId}:v1</name>
    </image>
  </configuration>
</plugin>

Lassen Sie uns Maven zur Ausführung verwenden build-imageZiele für die Erstellung einer Anwendung und die Erstellung eines Container-Images. Wir verwenden derzeit keine Dockerfiles.

mvn spring-boot:build-image

Das Ergebnis wird in etwa so aussehen:

[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Building image 'docker.io/pratikdas/usersignup:v1'
[INFO] 
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
.
.
.. [creator]     Adding label 'org.springframework.boot.version'
.. [creator]     *** Images (c311fe74ec73):
.. [creator]           docker.io/pratikdas/usersignup:v1
[INFO] 
[INFO] Successfully built image 'docker.io/pratikdas/usersignup:v1'

Aus der Ausgabe sehen wir das paketo Cloud-Native buildpackWird zum Erstellen eines funktionierenden OCI-Images verwendet. Wie zuvor können wir das als Docker-Image aufgelistete Image sehen, indem wir den folgenden Befehl ausführen:

docker images 

Fazit:

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

Erstellen eines Container-Images mit Jib

Jib ist ein Bild-Authoring-Plugin von Google, das eine alternative Methode zum Erstellen eines Container-Images aus der Quelle bietet.

Konfigurieren jib-maven-pluginin pom.xml:

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.5.2</version>
      </plugin>

Als nächstes führen wir das Jib-Plugin mit dem Maven-Befehl aus, um die Anwendung zu erstellen und das Container-Image zu erstellen. Nach wie vor verwenden wir hier keine Dockerfiles:

mvn compile jib:build -Dimage=<docker registry name>/usersignup:v1

Nachdem wir den obigen Maven-Befehl ausgeführt haben, erhalten wir die folgende Ausgabe:

[INFO] Containerizing application to pratikdas/usersignup:v1...
.
.
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO] 
[INFO] Built and pushed image as pratikdas/usersignup:v1
[INFO] Executing tasks:
[INFO] [==============================] 100.0% complete

Die Ausgabe zeigt, dass das Container-Image erstellt und in der Registrierung platziert wurde.

Motivationen und Methoden zur Erstellung optimierter Bilder

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

  • Leistung: In einem Container-Orchestrierungssystem wird ein Container-Image aus der Image-Registrierung auf den Host gezogen, auf dem die Container-Engine ausgeführt wird. Dieser Vorgang wird als Planung bezeichnet. Das Abrufen großer Images aus der Registrierung führt zu langen Planungszeiten in Container-Orchestrierungssystemen und langen Build-Zeiten in CI-Pipelines.
  • Sicherheit: Große Bilder bieten auch einen großen Bereich für Schwachstellen.

Ein Docker-Image besteht aus einem Stapel von Ebenen, von denen jede eine Anweisung in unserer Docker-Datei darstellt. Jede Schicht stellt das Delta der Änderungen in der darunter liegenden Schicht dar. Wenn wir ein Docker-Image aus der Registrierung abrufen, wird es in Schichten abgerufen und auf dem Host zwischengespeichert.

Spring Boot verwendet „fettes JAR“ in als Standardverpackungsformat. Wenn wir uns ein dickes JAR ansehen, sehen wir, dass die Anwendung nur einen sehr kleinen Teil des gesamten JAR ausmacht. Dies ist der Teil, der sich am meisten ändert. Der Rest besteht aus Spring Framework-Abhängigkeiten.

Die Optimierungsformel konzentriert sich darauf, die Anwendung auf einer von Spring Framework-Abhängigkeiten getrennten Ebene zu isolieren.

Die Abhängigkeitsschicht, die den Großteil der dicken JAR-Datei ausmacht, wird nur einmal heruntergeladen und auf dem Hostsystem zwischengespeichert.

Während App-Updates und Containerplanung wird nur eine dünne Schicht der App abgerufen. wie in diesem Diagramm gezeigt:

Erstellen optimierter Docker-Images für eine Spring Boot-Anwendung

In den folgenden Abschnitten schauen wir uns an, wie man diese optimierten Images für eine Spring Boot-Anwendung erstellt.

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

Spring Boot 2.3 unterstützt Layering durch Extrahieren von Teilen einer dicken JAR-Datei in separate Ebenen. Die Layering-Funktion ist standardmäßig deaktiviert und muss explizit mit dem Spring Boot Maven-Plugin aktiviert werden:

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

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

Lass uns rennen build-imageMaven-Ziel zum Erstellen eines Container-Images:

mvn spring-boot:build-image

Wenn wir Dive ausführen, um die Ebenen im resultierenden Bild anzuzeigen, können wir sehen, dass die Anwendungsschicht (rot eingekreist) im Kilobyte-Bereich viel kleiner ist als das, was wir mit dem dicken JAR-Format erhalten haben:

Erstellen optimierter Docker-Images für eine Spring Boot-Anwendung

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

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

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

Der Inhalt der resultierenden JAR-Datei sieht nach der Erstellung mit Maven und aktiviertem Layering wie folgt 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 Ausgabe zeigt ein zusätzliches JAR mit dem Namen spring-boot-jarmode-layertoolsи layersfle.idxDatei. Diese zusätzliche JAR-Datei bietet Layering-Funktionen, wie im nächsten Abschnitt beschrieben.

Extrahieren Sie Abhängigkeiten auf separaten Ebenen

Um Ebenen aus unserem mehrschichtigen JAR anzuzeigen und zu extrahieren, verwenden wir die Systemeigenschaft -Djarmode=layertoolszu rennen spring-boot-jarmode-layertoolsJAR statt Anwendung:

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

Das Ausführen dieses Befehls erzeugt eine Ausgabe mit den verfügbaren Befehlsoptionen:

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 listextractи helpс helpdie Standardeinstellung sein. Lassen Sie uns den Befehl mit ausführen listMöglichkeit:

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 Ebenen hinzugefügt werden können.

Standardmäßig Ebenen:

Ebenenname

Inhalt

dependencies

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

spring-boot-loader

JAR-Loader-Klassen

snapshot-dependencies

jede Abhängigkeit, deren Version SNAPSHOT enthält

application

Anwendungsklassen und Ressourcen

Ebenen werden in definiert layers.idxDatei in der Reihenfolge, in der sie dem Docker-Image hinzugefügt werden sollen. Diese Ebenen werden nach dem ersten Abruf auf dem Host zwischengespeichert, da sie sich nicht ändern. Nur die aktualisierte Anwendungsschicht wird auf den Host heruntergeladen, was aufgrund der reduzierten Größe schneller ist .

Erstellen eines Images mit in separate Ebenen extrahierten Abhängigkeiten

Das endgültige Bild erstellen wir in zwei Schritten mit einer Methode namens mehrstufige Montage . Im ersten Schritt extrahieren wir die Abhängigkeiten und im zweiten Schritt kopieren wir die extrahierten Abhängigkeiten in die finale Datei.

Ändern wir unsere Docker-Datei für einen mehrstufigen Build:

# 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 .

Nachdem wir diesen Befehl ausgeführt haben, erhalten wir die folgende Ausgabe:

Sending build context to Docker daemon  20.41MB
Step 1/12 : FROM adoptopenjdk:14-jre-hotspot as builder
14-jre-hotspot: Pulling from library/adoptopenjdk
.
.
Successfully built a9ebf6970841
Successfully tagged userssignup:v1

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

Abschließend führen wir wie zuvor den Befehl „Dive“ aus, um die Ebenen im generierten Docker-Image zu überprüfen. Wir können eine Bild-ID oder ein Bild-Tag als Eingabe für den Dive-Befehl bereitstellen:

dive userssignup:v1

Wie Sie der Ausgabe entnehmen können, ist die Ebene, die die Anwendung enthält, jetzt nur noch 11 KB groß und die Abhängigkeiten werden in separaten Ebenen zwischengespeichert. 

Erstellen optimierter Docker-Images für eine Spring Boot-Anwendung

Extrahieren Sie interne Abhängigkeiten auf separaten Ebenen

Wir können die Größe der Anwendungsschicht weiter reduzieren, indem wir alle unsere benutzerdefinierten Abhängigkeiten in eine separate Ebene extrahieren, anstatt sie durch Deklaration in die Anwendung zu packen ymlähnliche Datei mit dem Namen 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.idxWir haben eine benutzerdefinierte Abhängigkeit mit dem Namen hinzugefügt: io.myorgEnthält Organisationsabhängigkeiten, die aus dem gemeinsam genutzten Repository abgerufen wurden.

Abschluss

In diesem Artikel haben wir uns mit der Verwendung von Cloud-Native Buildpacks befasst, um ein Container-Image direkt aus der Quelle zu erstellen. Dies ist eine Alternative zur Verwendung von Docker zum Erstellen eines Container-Images auf die übliche Weise: Zuerst wird eine dicke ausführbare JAR-Datei erstellt und dann in ein Container-Image gepackt, indem die Anweisungen in der Docker-Datei angegeben werden.

Wir haben auch darüber nachgedacht, unseren Container zu optimieren, indem wir eine Layering-Funktion eingefügt haben, die Abhängigkeiten in separate Layer extrahiert, die auf dem Host zwischengespeichert werden, und eine dünne Anwendungsschicht, die zur Planungszeit in die Ausführungs-Engines des Containers geladen wird.

Den gesamten im Artikel verwendeten Quellcode finden Sie unter Github .

Befehlsreferenz

Hier ist eine Zusammenfassung der Befehle, die wir in diesem Artikel verwendet haben, als Kurzreferenz.

Kontextklärung:

docker system prune -a

Erstellen eines Container-Images mit einer Docker-Datei:

docker build -f <Docker file name> -t <tag> .

Container-Image aus der Quelle erstellen (ohne Dockerfile):

mvn spring-boot:build-image

Abhängigkeitsebenen anzeigen. Stellen Sie vor dem Erstellen der Anwendungs-JAR-Datei sicher, dass die Layering-Funktion im Spring-Boot-Maven-Plugin aktiviert ist:

java -Djarmode=layertools -jar application.jar list

Abhängigkeitsschichten extrahieren. Stellen Sie vor dem Erstellen der Anwendungs-JAR-Datei sicher, dass die Layering-Funktion im Spring-Boot-Maven-Plugin aktiviert ist:

 java -Djarmode=layertools -jar application.jar extract

Anzeigen einer Liste von Containerbildern

docker images

Ansicht links im Containerbild (stellen Sie sicher, dass das Tauchtool installiert ist):

dive <image ID or image tag>

Source: habr.com