Container-Images: Abschied vom Dockerfile

Seite 2: Von 0 auf 100 mit Paketo.io

Inhaltsverzeichnis

Das auf der Website prangende Versprechen des Cloud-Native-Buildpacks-Projekts lässt aufhorchen:

(Bild: Buildpacks.io)

"Transform your application source code into [container] images that can run on any cloud."

Entwicklerinnen und Entwickler sollen mit dem Werkzeug beliebigen Anwendungscode zu Container-Images zusammenbauen können, die zudem in jeder Cloud-Umgebung lauffähig sind. Tatsächlich scheinen Cloud Native Buildpacks dieser Herausforderung gewachsen. Die Übersicht der Features macht deutlich, dass alle mit der manuellen Pflege von Dockerfiles verbundenen Probleme damit der Vergangenheit angehören. Darüber hinaus werden viele weitere Themen zum Erstellen zeitgemäßer Container-Images auf Basis des OCI-Image-Formats adressiert. So kommen Nutzer in den Genuss von Optimierungen, die sie nur mit hohem Aufwand im eigenen Projekt hätten implementieren können.

Ein zentraler Vorteil der Cloud Native Buildpacks ist die Unterstützung des sogenannten Rebasing bereits gebauter Container-Images. Damit lassen sich neuere Basis-Images, die beispielsweise aktuelle Patches und Updates auf Betriebssystemebene mitbringen, automatisch auf existierende Application-Images anwenden. Und das, ohne sie neu bauen zu müssen – in großen Microservices-Architekturen ist das ein Killer-Feature. Denn das kontinuierliche Updaten vieler Container ist sonst eine Mammutaufgabe, für die es aber kaum Alternativen gibt.

Neben dem Rebasing kümmert sich die Buildpacks-Implementierung von Paketo aber auch um weitere Aspekte wie erweitertes Caching, minimale Applikations-Images sowie den strikten Fokus auf Reproduzierbarkeit – genauso, wie es die Spezifikation der Cloud Native Buildpacks vorschreibt. Auch sind standardmäßig die Build- von den Runtime-Images entkoppelt. Damit werden die eigenen Container-Images fit für den Einsatz in Produktion und in Cloud-nativen Umgebungen wie Kubernetes. Der größte Vorteil der Nutzung von Buildpacks ist aber, dass man sich mit keinem dieser Themen als Entwickler im Detail beschäftigen muss. Alles wird "out of the box" mitgeliefert.

Paketo unterstützt eine Vielzahl von Programmiersprachen – darunter .NET Core, Go, Node.js, Java, Ruby und PHP, und immer neue kommen hinzu. Spezielle Buildpacks für weitverbreitete Frameworks zu diesen Sprachen liegen ebenfalls vor, beispielsweise für Spring Boot. Sollte ein Framework noch fehlen, lässt sich dennoch mit einer eigenen Implementierung starten. Es dürfte nur eine Frage der Zeit sein, bis das entsprechende Buildpack zur Verfügung steht.

Nachdem die Cloud Native Computing Foundation das Buildpacks-Projekt in den Status "CNCF incubating" befördert hat, stehen alle relevanten Cloud-Anbieter im Zugzwang, die Unterstützung für Cloud Native Buildpacks anzukündigen. Google hatte Unterstützung innerhalb der Google Cloud Platform im Oktober 2020 öffentlich gemacht. Azure bietet die "Preview"-Unterstützung für Cloud Native Buildpacks sogar bereits seit Oktober 2019 an.

Wie können Entwicklerinnen und Entwickler Cloud Native Buildpacks in eigenen Projekten nutzen? Liegt der Fokus auf JVM-basierten Sprachen, eröffnet sich eine breite Palette spezifischer Buildpacks. Ausgehend vom Buildpack für Gradle über das Scala SBT Buildpack bis zum Maven Buildpack sind alle verbreiteten Build-Tools abgedeckt. Darüber hinaus findet sich ein spezifisches für ausführbare Jar-Dateien sowie für Anwendungen, die auf Apache Tomcat basieren. Zudem lassen sich mehrere Buildpacks auch für ein und dieselbe Anwendung gleichzeitig nutzen.

Anhand von Spring Boot lässt sich eine noch stärkere Integration der Buildpacks in einen Anwendungs-Stack zeigen. Innerhalb dieses Frameworks gestaltet sich die Integration von Paketo.io in den üblichen Build-Prozess mit Maven oder Gradle für den Nutzer vollständig transparent, wie das beispielhafte Erstellen einer einfachen Spring-Boot-Anwendung über start.spring.io verdeutlicht (s. Abb. 1).

Eine Spring Boot-Beispielanwendung ist mithilfe von start.spring.io schnell erstellt (Abb. 1).

Alternativ lässt sich auch eine bestehende Spring-Boot-Anwendung nutzen, so lange eine aktuelle Spring Boot-Version zum Einsatz kommt – im Rahmen dieses Artikels ist es Version 2.4.4. Für das spätere vereinfachte Testen der Anwendung im Container kann zudem das Implementieren eines simplen REST-Service sinnvoll sein. Der im Folgenden verwendete Code liegt in einem Repository des Autors auf GitHub frei zugänglich bereit.

Für Spring-Boot-Nutzer ist die Integration der Buildpacks bereits abgeschlossen, da aktuelle Versionen direkt auf Paketo.io aufsetzen. Ist in der verwendeten Umgebung eine aktuelle Docker-Installation vorhanden, lässt sich der Maven-Build direkt mit dem Befehl mvn spring-boot:build-image starten. Das neue Maven-Goal build-image wurde extra für die Integration der Cloud Native Buildpacks eingeführt. Gradle-Nutzer verwenden den Befehl gradle bootBuildImage. Ist der Build gestartet, erscheint im Build-Log die neue Phase build-image. Zugunsten der Lesbarkeit sind im Folgenden die Details leicht gekürzt:

$ mvn spring-boot:build-image
...
[INFO] --- spring-boot-maven-plugin:2.3.7.RELEASE:build-image (default-cli) @ spring-boot-buildpack ---
[INFO] Building image 'docker.io/library/spring-boot-buildpack:0.0.1-SNAPSHOT'
[INFO]
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 100%
[INFO]  > Pulled builder image 'gcr.io/paketo-buildpacks/builder@sha256:2b3d585ed785ea2e4ecc89c35512c54f8d339f4ca09c1d445c51077ebe21cfaf'
[INFO]  > Pulling run image 'docker.io/paketobuildpacks/run:base-cnb' 100%
[INFO]  > Pulled run image 'paketobuildpacks/run@sha256:33d37fc9ba16e220f071805eaeed881a508ceee5c8909db5710aaed7e97e4fc2'
[INFO]  > Executing lifecycle version v0.10.2
[INFO]  > Using build cache volume 'pack-cache-604f3372716a.build'
[INFO]
[INFO]  > Running creator
[INFO]     [creator]     ===> DETECTING
[INFO]     [creator]     5 of 18 buildpacks participating
[INFO]     [creator]     paketo-buildpacks/ca-certificates   2.1.0
[INFO]     [creator]     paketo-buildpacks/bellsoft-liberica 7.1.0
[INFO]     [creator]     paketo-buildpacks/executable-jar    5.0.0
[INFO]     [creator]     paketo-buildpacks/dist-zip          4.0.0
[INFO]     [creator]     paketo-buildpacks/spring-boot       4.1.0
[INFO]     [creator]     ===> ANALYZING
[INFO]     [creator]     Restoring metadata for "paketo-buildpacks/ca-certificates:helper" from app image
...
[INFO]     [creator]     ===> RESTORING
[INFO]     [creator]     ===> BUILDING
[INFO]     [creator]
[INFO]     [creator]     Paketo CA Certificates Buildpack 2.1.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/ca-certificates
[INFO]     [creator]
[INFO]     [creator]     Paketo BellSoft Liberica Buildpack 7.1.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/bellsoft-liberica
...
[INFO]     [creator]     Paketo Executable JAR Buildpack 5.0.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/executable-jar
...
[INFO]     [creator]     Paketo Spring Boot Buildpack 4.1.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/spring-boot
[INFO]     [creator]       Launch Helper: Reusing cached layer
[INFO]     [creator]       Web Application Type: Reusing cached layer
[INFO]     [creator]       Spring Cloud Bindings 1.7.0: Reusing cached layer
[INFO]     [creator]       Image labels:
[INFO]     [creator]         org.opencontainers.image.title
[INFO]     [creator]         org.opencontainers.image.version
[INFO]     [creator]         org.springframework.boot.spring-configuration-metadata.json
[INFO]     [creator]         org.springframework.boot.version
[INFO]     [creator]     ===> EXPORTING
...
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/spring-boot:helper'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/spring-boot:web-application-type'
[INFO]     [creator]     Reusing 1/1 app layer(s)
[INFO]     [creator]     Reusing layer 'launcher'
[INFO]     [creator]     Reusing layer 'config'
[INFO]     [creator]     Adding label 'io.buildpacks.lifecycle.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.build.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.project.metadata'
[INFO]     [creator]     Adding label 'org.opencontainers.image.title'
[INFO]     [creator]     Adding label 'org.opencontainers.image.version'
[INFO]     [creator]     Adding label 'org.springframework.boot.spring-configuration-metadata.json'
[INFO]     [creator]     Adding label 'org.springframework.boot.version'
[INFO]     [creator]     *** Images (d831d6a66f8e):
[INFO]     [creator]           docker.io/library/spring-boot-buildpack:0.0.1-SNAPSHOT
[INFO]
[INFO] Successfully built image 'docker.io/library/spring-boot-buildpack:0.0.1-SNAPSHOT'
...

Um den Ablauf eines vollständigen Builds einer Spring-Boot-Anwendung mit Cloud Native Buildpacks und Paketo.io nachvollziehbar zu machen, steht ein Asciicast bereit. Nach anfänglichem Herunterladen einiger Images startet der Build in eine mit creator gekennzeichnete Phase. Die vorliegende Anwendung wird in den Abschnitten DETECTING und ANALYZING analysiert und anhand der Ergebnisse passende Buildpacks für die Anwendung identifiziert, die ihr später zu einem lauffähigen Container-Image verhelfen. Im vorliegenden Fall kommen fünf von 18 Buildpacks infrage. Das Buildpack paketo-buildpacks/bellsoft-liberica:jre bringt beispielsweise die Java-Runtime mit und paketo-buildpacks/executable-jar sorgt für die spätere Ausführbarkeit der Spring-Boot-Anwendung. Die Paketo-Dokumentation zu Spring Boot bietet einen detaillierten Einblick in den Build-Prozess.

Sobald das Build-Log einen Output wie Successfully built image 'docker.io/library/spring-boot-buildpack:0.0.1-SNAPSHOT enthält, steht lokal auch schon ein Container-Image zur Nutzung bereit. Mit dem zusätzlichen Parameter -Dspring-boot.build-image.publish=true lässt sich das Image sofort in jede Registry pushen. Die notwendige Konfiguration der Credentials erfolgt innerhalb des spring-boot-maven-plugin. Damit steht alles bereit, um die Beispielanwendung im Container auszuführen. Der folgende Befehl startet den Container lokal und bietet über ein simples Port-Binding Zugriff auf den REST-Service:

docker run -p 8080:8080 spring-boot-buildpack:0.0.1-SNAPSHOT

Im Browser lässt sich die Anwendung nun unter der Adresse http://localhost:8080 aufrufen.

Um mehr Einblick in die vom Build-Prozess erzeugten Container-Images zu bekommen, bietet sich das unscheinbare Kommandozeilen-Werkzeug dive an. Es kommt recht einfach daher, hat es aber faustdick hinter den Ohren. Unter macOS ist es schnell per brew install dive installiert (Anleitungen für andere Betriebssysteme finden sich auf GitHub). Um die effiziente Nutzung des Tools sicherzustellen, sollten Entwicklerinnen und Entwickler vor dem ersten Öffnen die Konfigurationsdatei .dive.yaml im Home-Verzeichnis des Nutzers mit dem folgenden Inhalt anlegen:

diff:
  # You can change the default files shown in the filetree (right pane). All diff types are shown by default.
  hide:
    - unmodified

filetree:
  # Show the file attributes next to the filetree
  show-attributes: false

Mit dieser Konfiguration startet dive immer so, dass Datei-Attribute ausgeblendet und nur die pro Layer hinzugefügten Dateien in der Detailansicht zu sehen sind. Diese Einstellungen verschaffen einen deutlich besseren Einblick in die Details der Container-Images und helfen, sich mit dem Tool schnell vertraut zu machen. In den meisten Fällen sollten sie ausreichen, die dive-Dokumentation hält aber noch mehr Möglichkeiten parat.

Für den Einsatz von dive müssen Nutzer nur die Image-ID des gerade per Maven oder Gradle gebauten Container-Image aus dem Build-Log fischen. Die entscheidende Zeile sieht etwa so aus: [creator] *** Images (408f3d59f38e):. Mit der Image-ID 408f3d59f38e lässt sich dann in die Tiefen des Container-Image abtauchen:

dive 408f3d59f38e

Nach Ausführung des Befehls füllt sich die gesamte Terminal-Ansicht mit den durch den Build-Prozess und Paketo erstellten Layern des Container-Images (s. Abb. 2.).

Das Kommandozeilen-Werkzeug dive gibt Einblicke in die Layer des Container-Image (Abb. 2).

Die Maven- und Gradle-Plug-ins bilden lediglich eine Art Wrapper um den eigentlichen Paketo-Build. Daher lassen sich Paketo-Builds auch direkt ausführen. Dafür steht das wichtige Werkzeug pack CLI zur Verfügung. Es stellt auch die gängige Methode dar, Projekte auf Basis anderer Sprachen mit Cloud Native Buildpacks zu Containern zu bauen. Die Installation pack CLI gelingt unter macOS mit dem Befehl brew install buildpacks/tap/pack. Um das Tool auch auf anderen Betriebssystemen lokal nutzen zu können, finden sich passende Anleitungen in der Dokumentation.

Sobald pack CLI installiert ist, verschafft der Befehl pack builder suggest einen Überblick über die verfügbaren Buildpack-Builder, die bereits die Cloud Native Buildpack Specification implementieren:

$ pack builder suggest

Suggested builders:
	Google:                gcr.io/buildpacks/builder:v1      Ubuntu 18 base image with buildpacks for .NET, Go, Java, Node.js, and Python
	Heroku:                heroku/buildpacks:18              heroku-18 base image with buildpacks for Ruby, Java, Node.js, Python, Golang, & PHP
	Paketo Buildpacks:     paketobuildpacks/builder:base     Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang
	Paketo Buildpacks:     paketobuildpacks/builder:full     Ubuntu bionic base image with buildpacks for Java, .NET, NodeJS, Golang, PHP, HTTPD and NGINX
	Paketo Buildpacks:     paketobuildpacks/builder:tiny     Tiny base image (bionic build image, distroless run image) with buildpacks for Golang

Tip: Learn more about a specific builder with:
	pack inspect-builder <builder-image>

Ein Default-Builder lässt sich mithilfe des Befehls pack set-default-builder festlegen. Aber auch ohne diesen Standard lässt sich eine Spring-Boot-Anwendung mit dem gleichen "pack CLI"-Befehl bauen, wie ihn das Maven- oder Gradle-Plug-in ausführen würden:

pack build spring-boot-buildpack --path . --builder paketobuildpacks/builder:base

Nichts anderes löst auch mvn spring-boot:build-image aus. Dafür leuchten nun aber in der Kommandozeile deutlich schönere Farben. Auch hier lohnt der Blick in ein entsprechendes Asciicast, das die Ausführung des Befehls im Video zeigt.

Sobald die Logausgabe etwas wie Successfully built image spring-boot-buildpack ausspuckt, ist der Build mit pack CLI erfolgreich abgeschlossen – und das ganz ohne eigenes Dockerfile.

Die Paketo Buildpacks lassen sich umfassend konfigurieren. Die Version des Java Development Kit (JDK) oder auch eine spezielle Maven-Einstellung lassen sich einfach per Umgebungsvariablen festlegen. Darüber hinaus ist es möglich, die Datei buildpack.yml innerhalb des Wurzelverzeichnisses des eigenen Projekts zu nutzen, um darin die gleichen Konfigurationen vorzunehmen, die auch mit Umgebungsvariablen möglich sind.

Credentials, die beispielsweise für private Binär-Repositories oder auch Application Performance Management Server nötig sind, lassen sich über sogenannte Bindings ebenfalls den Buildpacks bekannt machen und sicher in die Images übernehmen. Bindings decken zudem auch seltenere Anforderungen ab, wie das Konfigurieren eigener Download-URIs, wenn ein Zugriff aus abgeschotteten Umgebungen nicht möglich ist. Will beispielsweise das für die Bereitstellung des JDKs genutzte Buildpack bellsoft-liberica eigentlich gern die Zip-Datei des JDK von GitHub herunterladen, scheitert es regelmäßig an Zugriffsbeschränkungen.

Zu Irritationen könnten die Altersangaben von Container-Images führen, die auftauchen, wenn Entwicklerinnen und Entwickler den Paketo-Build völlig unverändert mit dem Befehl docker images ausführen. Dann erscheinen Images, die angeblich 40 Jahre alt sind:

$ docker images
...
paketobuildpacks/builder                  <none>                  914aba170326        40 years ago        654MB
pack.local/builder/axczkudrjk             latest                  69aeed7ad644        40 years ago        654MB
spring-boot-buildpack                     latest                  b529a37599a6        40 years ago        259MB
paketobuildpacks/builder                  base                    1435430a71b7        40 years ago        558MB

Dieses Phänomen ist von den Paketo-Entwicklern allerdings beabsichtigt. Die fixen Zeitstempel sind notwendig, um hundertprozentig nachvollziehbare Builds zu erstellen. Ohne sie würden sich die Hashes der Docker-Images nach jedem Build ändern, selbst wenn der eigentliche Inhalt des Containers unverändert bleibt. Auch Googles jib nutzt das gleiche Konzept. Detailliertere Informationen zu dem Thema bietet ein Blogartikel.