CodeGym /Kurse /Spring Boot /bootBuildImage und buildpacks

bootBuildImage und buildpacks

Spring Boot
Level 25, Lektion 3
Verfügbar

1. Rolle von bootBuildImage im Kurs

Wenn Sie Spring Boot lernen, ist es sehr leicht, in die Falle zu tappen: „Packaging ist etwas für DevOps, ich möchte lieber den Controller fertigschreiben.“ In der Praxis ist Packaging eine Fortsetzung der Architektur. Wenn die Anwendung nicht mehr nur in der IDE lebt, brauchen Sie eine reproduzierbare Methode, um das Projekt in ein ausführbares Artefakt zu verwandeln. Und wenn das Artefakt in einer Containerumgebung leben soll, ist es wichtig, dass dieser Übergang nicht zu einer separaten manuellen Wissenschaft für jedes Projekt wird.

Die Schichten sind bereits sichtbar geworden: Wir können sie im Jar sehen und in Verzeichnisse aufteilen. Jetzt stellt sich eine andere logische Frage: Wer nutzt diese Struktur überhaupt, wenn das Jar in ein Container-Image umgewandelt wird?

Die Idee der heutigen Vorlesung ist einfach: Spring Boot zwingt Sie nicht, sofort ein Dockerfile „von Grund auf neu“ zu schreiben. Stattdessen bietet es einen plattformorientierten Weg: Bauen Sie ein Container-Image über Buildpacks, unter Verwendung unseres bereits bekannten Verpackungsmodells und der Schichten. Wir bleiben im Boot-Bereich: Wir studieren den Eingang (bootBuildImage), verstehen den Ausgang (fertiges Image), lesen Logs, aber tauchen nicht in tiefe Docker-Tuning, Netzwerkrichtlinien, Orchestrierung und andere Freuden des Erwachsenenlebens ein.

Um die Grenze zu fixieren, behalten Sie einen einfachen Gedanken. Wir lernen nicht, „im Container zu leben“. Wir lernen, ein containerfreundliches Artefakt zu erstellen und zu verstehen, wie Boot es weiterverpacken kann.

2. Cloud Native Buildpacks

Wenn man zum ersten Mal das Wort buildpacks hört, reagiert das Gehirn oft so: „Aha, schon wieder eine magische Blackbox.“ Aber eigentlich sind Buildpacks einfach eine sehr pragmatische Idee: Wenn wir eine typische Anwendung haben (z. B. Java + Jar), dann kann man den Build eines Container-Images als eine Reihe wiederverwendbarer Schritte beschreiben, nicht als einzigartiges Dockerfile für jedes Projekt.

Stellen Sie sich vor, ein Dockerfile ist wie ein Rezept, das Sie manuell schreiben: „Nimm ein Basis-Image“, „Kopiere das Jar“, „Konfiguriere ENTRYPOINT“, „Füge Benutzer hinzu“, „Setze Variablen“… Das ist alles in Ordnung, aber es gibt auch viele Stellen, an denen ein Anfänger Fehler macht und das Team später reparieren muss.

Buildpacks funktionieren anders. Sie sind wie ein „Autokoch“ (ja, klingt wie ein Küchengerät, das Sie nachts auf dem Marktplatz gekauft haben): Sie geben ihm die Anwendung, und es entscheidet nach den Plattformregeln, wie es gebaut wird. Im Inneren gibt es mehrere Schlüsselrollen, und es ist nützlich, sie zumindest konzeptionell zu kennen.

Erstens gibt es das builder image – das „Build-Image“. Darin leben die Werkzeuge, die Ihre Anwendung nach Buildpacks-Regeln in ein Image bauen können. Zweitens gibt es die buildpacks als Logik-Sets: „Wie erkenne ich ein Projekt?“ und „Wie baue ich es?“ Für Java bedeutet das normalerweise: erkennen, ob es ein Jar/Gradle/Maven ist, Java-Version bestimmen, Abhängigkeiten und Anwendung in Schichten legen, Start konfigurieren. Drittens gibt es das run image – eine minimale „Laufbahn“, auf der das Ergebnis dann ausgeführt wird (das endgültige Image zieht also normalerweise nicht die gesamte Build-Toolchain mit).

Wenn man ein sehr bodenständiges Schema möchte, sieht es etwa so aus:

flowchart TD
    A[Projekt catalog-service] --> B[bootJar: ausführbares jar]
    B --> C[bootBuildImage]
    C --> D["Builder (buildpacks)"]
    D --> E[Fertiges Container-Image]

Das Wesentliche: Buildpacks sind keine Bibliothek, keine Dependency in dependencies { ... }. Es ist ein Verpackungsmechanismus, der neben Ihrem Build-Zyklus lebt. Und Spring Boot als Plattform gibt Ihnen einen „Knopf“ für diesen Mechanismus – bootBuildImage.

3. Die Aufgabe bootBuildImage

Bevor Sie den Knopf drücken, ist es hilfreich zu verstehen, wer diesen Knopf hinzugefügt hat. Die Aufgabe bootBuildImage wird vom Spring Boot Gradle Plugin bereitgestellt – demselben, der uns bereits bootRun und bootJar beschert hat. Es ist also keine „externe Magie“, sondern eine logische Fortsetzung der Packaging-Geschichte, nur die nächste Ebene.

Überprüfen können Sie die Existenz der Aufgabe auf die übliche Weise – über Gradle:

# Suche die Aufgabe in der Liste aller verfügbaren Gradle-Aufgaben
./gradlew tasks --all | grep -n "bootBuildImage"

Oder „lesbarer“ – bitten Sie Gradle, über die Aufgabe zu berichten:

# Zeige Aufgabenbeschreibung und Hauptparameter
./gradlew help --task bootBuildImage

Nun zu einem wichtigen alltäglichen Punkt, der Studenten oft die Stimmung verdirbt: bootBuildImage verwendet fast immer einen lokalen Container-Runtime (meistens Docker). Wenn Docker/Podman nicht installiert oder nicht gestartet ist, schlägt die Aufgabe fehl – nicht, weil Sie ein schlechter Programmierer sind, sondern weil der Bau eines Images dieses Image irgendwo „ablegen“ muss.

Im Rahmen des Boot-Kurses machen wir daraus keine Tragödie. Man kann die Mechanik verstehen, Logs lesen und sogar die Aufgabe konfigurieren, ohne sich in einen Container-Ingenieur zu verwandeln. Aber wenn Sie die Aufgabe wirklich ausführen möchten, stellen Sie sicher, dass der Container-Runtime verfügbar ist.

Ein Mini-Check, maximal ohne „Docker-Kurs“ (einfach die Tatsache, ob der Befehl funktioniert):

# Überprüfen, ob Docker Engine verfügbar ist (ohne dass das Image nirgendwohin „exportiert“ werden kann)
docker version

Wenn der Befehl antwortet und Client-/Server-Versionen anzeigt – normalerweise ist alles okay, dann können Sie bootBuildImage ausprobieren.

4. Ausführen von bootBuildImage und Logs

Wenn Sie bootBuildImage zum ersten Mal ausführen, sieht es oft aus wie „ein Befehl, der etwas lange macht und viel schreibt“. Das ist normal: Der erste Durchlauf lädt in der Regel Builder-Images herunter und cacht Layer, daher kann es spürbar dauern. Wichtiger ist etwas anderes: Diese Ausgabe kann man lesen, und sie hat tatsächlich einen Sinn.

Der Basisbefehl ist:

# Container-Image über Buildpacks bauen
./gradlew bootBuildImage

Wenn Sie mehr Details wünschen (manchmal nützlich, um nicht zu denken „hängt es?“), können Sie --info hinzufügen:

# Detailliertes Log hinzufügen, um zu verstehen, in welchem Schritt sich der Build befindet
./gradlew bootBuildImage --info

Was normalerweise intern passiert, wenn man es ohne überflüssige Low-Level-Details, aber ehrlich beschreibt:

Zuerst stellt Gradle (und das Boot-Plugin) sicher, dass das endgültige Artefakt der Anwendung vorliegt. Normalerweise bedeutet das, dass ein Jar gebaut wird (im Grunde ist bootJar Teil der Kette). Dann übergibt Boot dieses Jar an den Buildpacks-Mechanismus. Buildpacks führen einen „Detect“ durch (erkennen, dass es sich um eine Java-Anwendung handelt, wie sie gestartet wird, welche Java-Version benötigt wird), danach bauen sie die Layer des zukünftigen Images, packen sie und „exportieren“ das fertige Image in den lokalen Container-Runtime.

Um in der Terminologie nicht verloren zu gehen, ist es hilfreich, eine einfache Dreiergruppe im Kopf zu behalten: Eingabe → Verarbeitung → Ausgabe.

Eingabe:  build/libs/<unser executable jar>
Schritt:  buildpacks (Detect → Layer-Bau → Verpackung)
Ausgabe:  container image (im lokalen docker images)

Das ist die wichtigste „entmystifizierende“ Übung. bootBuildImage ist kein Zauberknopf, sondern eine verständliche Pipeline: Es nimmt das Artefakt, durchläuft es mit einem standardisierten Verpackungsmechanismus und erstellt ein Image.

Wenn Sie das Ergebnis „mit den Augen eines Menschen“ sehen möchten, der nicht in die Details von Buildpacks eintaucht, können Sie einfach die Image-Liste anzeigen:

# Überprüfen, ob das Image tatsächlich in der lokalen Image-Liste erschienen ist
docker images | grep catalog-service

Ja, das ist schon ein Docker-Befehl, aber genau im minimal ausreichenden Umfang: um sicherzustellen, dass das Ergebnis tatsächlich vorhanden ist.

5. Layered Jar und Image-Layer

Jetzt ist es wichtig, das, was Sie bereits in den Lektionen 2–3 gemacht haben (Layered Jar), mit dem zu verbinden, was bootBuildImage tut. Sonst entsteht das Gefühl: „Wir haben Schichten gelernt, und dann kam bootBuildImage und hat alles auf seine Weise gemacht.“ Tatsächlich ist bootBuildImage eine Fortsetzung derselben Idee: Selten Veränderliches vom häufig Veränderlichen trennen.

Wenn Buildpacks ein Image bauen, versuchen sie, den Inhalt so zu verteilen, dass bei einem Neubau bereits vorhandene Layer wiederverwendet werden können. In der Praxis bedeutet das in etwa folgende Logik:

- Abhängigkeiten (Ihre Starter und alles, was transitiv kommt) ändern sich selten – daher ist es vorteilhaft, sie getrennt zu halten;
- Code und Ressourcen der Anwendung ändern sich häufig – also sind sie ein separater Layer;
- Dienstteile, die für den Start benötigt werden, sollten ebenfalls stabil sein und nicht wegen der Änderung eines einzigen Controllers neu gebaut werden.

Wenn Sie ein Layered Jar haben, enthält es bereits diese „Layer-Karte“ (layers.idx). Die Werkzeuge (einschließlich Buildpacks) können sie als Hinweis nutzen: wie genau die „Innereien“ der Anwendung in Teile aufgeteilt werden sollen, die separat gecacht werden sollten.

Hier geht es nicht um „Container-Philosophie“, sondern um einen sehr praktischen ingenieurtechnischen Vorteil. Stellen Sie sich vor, Sie ändern eine Methode in CourseCatalogController. Ohne Layer könnte der Image-Neubau eine Menge Bibliotheken neu übertragen und ablegen, obwohl sie sich nicht geändert haben. Mit Layern ist die Wahrscheinlichkeit viel größer, dass nur der application-Teil neu gebaut wird, während der Abhängigkeits-Layer wiederverwendet wird.

Das ist, als ob Sie jeden Tag einen Rucksack packen. Wenn Sie jedes Mal Laptop, Ladegerät, Dokumente, Erste-Hilfe-Set und 20 weitere Dinge neu einpacken, dauert das lange. Wenn Sie aber einen „festen Satz“ in einem Fach haben und nur „Butterbrot und T-Shirt“ austauschen – der Prozess wird drastisch schneller. Ja, die Analogie ist alltäglich, aber der Sinn ist genau derselbe.

6. Minimale Konfiguration von bootBuildImage

Standardmäßig kann bootBuildImage durchaus ohne Konfiguration arbeiten: Sie führen die Aufgabe aus, erhalten ein Image. Aber in einem Übungsprojekt ist es sinnvoll, eine kleine Einstellung vorzunehmen, die die Vorhersagbarkeit erhöht: den Image-Namen festlegen. Dann müssen Sie nicht jedes Mal raten, wie das Image heißt und welchen Tag die Version hat.

In Gradle sieht das in der Regel so aus – maximal kurz und verständlich:

// build.gradle.kts
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage

tasks.named<BootBuildImage>("bootBuildImage") {
    // Explizit den Image-Namen setzen, damit er leicht in `docker images` zu finden ist
    imageName.set("catalog-service:local")
}

Diese 2–3 Zeilen bringen einen sehr spürbaren Nutzen. Jetzt erwarten Sie nach dem Build ein Image mit einem bestimmten Namen, und es ist leicht zu finden und von anderen zu unterscheiden.

Wenn Sie einen etwas „realistischeren“ Namen wünschen, können Sie ihn an die Projektversion binden. Aber hier ist es wichtig, die Vorlesung nicht in eine Diskussion über Release-Strategien zu verwandeln. Es reicht zu verstehen, dass das Projekt in Gradle name und version hat und diese verwendet werden können.

Zum Beispiel so:

// build.gradle.kts
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage

tasks.named<BootBuildImage>("bootBuildImage") {
    // Praktisch für lokale Builds: Tag entspricht der Version Ihres Projekts
    imageName.set("${project.name}:${project.version}")
}

Ein weiterer Punkt, den es im Java-Kurs zu wissen lohnt: Das Image muss die richtige Java-Line enthalten. Meistens erkennen Boot und Buildpacks dies korrekt (besonders wenn Sie einen Java-Toolchain konfiguriert haben). Aber wenn Sie plötzlich sehen, dass während des Builds die falsche Version ausgewählt wird, gibt es einen Standardmechanismus von Buildpacks über Umgebungsvariablen. Im Übungsformat können Sie einfach sehen, dass dies möglich ist:

// build.gradle.kts
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage

tasks.named<BootBuildImage>("bootBuildImage") {
    // Buildpacks mitteilen, welche JVM-Linie beim Bau des Images verwendet werden soll
    environment.put("BP_JVM_VERSION", "25")
}

Beachten Sie den Stil: Wir haben keine einzige Dependency hinzugefügt, den Code der Anwendung nicht berührt, CatalogServiceApplication nicht umgeschrieben. Wir haben lediglich die Build-Aufgabe des Artefakts konfiguriert. Das ist der grundlegende „plattformorientierte“ Ansatz von Boot: Der Anwendungscode bleibt Anwendungscode, und das Packaging bleibt Packaging.

Wenn Sie danach ausführen:

# Bau des Images mit den bereits in build.gradle.kts gesetzten Parametern starten
./gradlew bootBuildImage

erhalten Sie ein Image mit dem erwarteten Namen und können zumindest seine Existenz über docker images prüfen. Alles andere – Start, Ports, Umgebungsvariablen, Dateisysteme – ist eine separate Welt, die wir in dieser Vorlesung bewusst nicht öffnen.

7. Typische Fehler bei der Arbeit mit bootBuildImage

Nach der schönen Theorie kommt fast immer der Moment: „Aber warum baut es bei mir nicht?“ Und das ist normal: bootBuildImage sitzt an der Grenze zwischen Java-Build und Container-Umgebung, und an Grenzen gibt es meist die meisten Überraschungen. Die gute Nachricht ist, dass die häufigsten Probleme recht typisch sind und man lernen kann, sie anhand der Symptome zu erkennen – ohne Panik und ohne das Gefühl „ich verstehe nichts“.

Fehler Nr. 1: Buildpacks als Abhängigkeit betrachten und versuchen, sie in dependencies { ... } hinzuzufügen.
Wenn Sie sich schon mal dabei ertappt haben, zu denken „Wo ist der Starter spring-boot-starter-buildpacks?“, dann sind Sie in guter Gesellschaft – das ist eine häufige Reaktion eines Anfängers. Buildpacks leben nicht im Classpath der Anwendung. Sie leben im Build-Prozess, und der Einstieg ist die Aufgabe bootBuildImage. Daher ist der richtige Ort für die Konfiguration die Gradle Task oder der springBoot-Block, nicht die Projekt-Dependencies.

Fehler Nr. 2: Erwarten, dass bootBuildImage bootJar ersetzt.
Manchmal scheint es: „Da ich jetzt ein Image gebaut habe, brauche ich das Jar nicht mehr.“ Tatsächlich stützt sich bootBuildImage fast immer auf das Artefakt der Anwendung (zumindest logisch), und das Jar bleibt Ihr grundlegendes, portables und überprüfbares Ergebnis. Selbst wenn Sie ein Image bauen, ist das Verständnis des executable Jar fundamental. Wenn das Jar nicht gebaut wird, wird auch das Image nicht „magisch“ gebaut.

Fehler Nr. 3: Docker/Container-Runtime ist nicht installiert oder nicht gestartet, und die Aufgabe schlägt fehl.
Das ist der häufigste „technische“ Fehlschlag. Aus Sicht von Boot ist alles in Ordnung: Es versucht ehrlich, ein Image zu bauen. Aber es hat keinen Ort, wo es es ablegen kann, wenn Sie keinen verfügbaren Runtime haben. Im Rahmen des Kurses ist die richtige Reaktion hier gelassen: Entweder den Runtime einschalten/installieren (wenn Sie dazu bereit sind), oder bootBuildImage als Überblicksmechanismus betrachten und sich auf Layered Jar und das Verständnis von Layern konzentrieren.

Fehler Nr. 4: Der erste Durchlauf ist „zu langsam“, und es scheint, als ob alles hängt.
Der erste Durchlauf zieht oft das Builder-Image und Caches, daher kann er merklich langsamer sein als die folgenden. Das ist nicht „Boot bremst“, sondern der normale Preis dafür, dass Sie zum ersten Mal die Build-Umgebung für Images erstellen. Wenn Sie etwas mehr Transparenz wünschen, verwenden Sie ./gradlew bootBuildImage --info und beobachten Sie einfach, dass der Prozess in Schritten voranschreitet und nicht hängt.

Fehler Nr. 5: Sofort versuchen, „alles zu konfigurieren“ – Builder, Run-Image, Registry, Download-Richtlinien, Proxy, Zertifikate …
Das ist ein typischer ingenieurtechnischer Drang nach Kontrolle (der gut ist!), aber zur falschen Zeit. Im Übungsprojekt ist es jetzt wertvoller, eine funktionierende Baseline zu bekommen und die Mechanik „Eingabe → Buildpacks → Ausgabe“ zu verstehen. Komplexe Konfigurationen werden fast immer in einer konkreten Infrastrukturumgebung und für konkrete Anforderungen benötigt, und ohne diese Anforderungen wird die Konfiguration zu Rätselraten und Copy-Paste.

1
Aufgabe
Spring Boot, Level 25, Lektion 3
Gesperrt
Image-Build mit festem Namen
Image-Build mit festem Namen
1
Aufgabe
Spring Boot, Level 25, Lektion 3
Gesperrt
Versionsbezeichnung des Docker-Images und kurze Build-Zusammenfassung
Versionsbezeichnung des Docker-Images und kurze Build-Zusammenfassung
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION