Buildpacks transformieren Applikations-Quellcode zu Images, welche man direkt in der Cloud laufen lassen kann. Dabei wird der Code untersucht um festzustellen, welche Abhängigkeiten gebraucht werden.
Buildpacks wurde erstmal 2011 von Heroku konzipiert. Seitdem wurde das Projekt von der Cloud Foundry und anderen Platform as a Service (PaaS) Providern adaptiert (Google App Engine, Gitlab, …).
Das Cloud Native Buildpacks Projekt wurde von Pyvotal und Heroku im Januar 2018 initiiert. Nur 9 Monate später, also im Oktoboer 2018 ist das Projekt der CNCF beigetreten. Ziel des Projekt ist es, das buildpack Ecosystem zu vereinheitlichen. Dazu gibt es den Platform-To-Build Vertrag, der genau definiert wurde und Erkenntnisse aus jahrelanger Erfahrung von Pyvotal und Heroku enthält.
Cloud Native Buildpacks unterstützen moderne Container Standards wie OCI (Open Container Initiative) und nutzen dabei immer die neuesten Fähigkeiten.
Komponenten
Wollen wir uns die wichtigsten Komponenten von Buildpacks anschauen.
Builder
Ein Builder ist ein Image, welche alle Komponenten beinhaltet um einen Build auszuführen. Ein Builder Image besteht aus einem Build Image, Lifecycle, buildpacks und sonstigen Files, welche zur Konfiguration notwendig sind.
Buildpack
Ein Buildpack ist eine Einheit, welche den Applikationscode betrachtet und daraus einen Plan formuliert, wie die Applikation ausgeführt wird. Dabei wird anhand vom Code der richtig Buildpack ermittelt und danach der Build mit allen notwendigen Installationen ausgeführt.
Lifecycle
Der Lifecycle ochestriert buildpack Ausführungen und führt dann die Artefakte zu dem App Image zusammen
Platform
Die Platform ist beispielsweise die Pack CLI oder im CICD Prozess ein Plugin, welche aus dem Lifecycle, Buildpack und dem Applikationscode das OCI Image erzeugt.
Bauen wir unsere erste App
Damit wir loslegen können, brauchen wir erstmal eine Platform, mit der wir unser OCI Image erzeugen können. Lokal können wir dazu einfach die Pack CLI nutzen. Die Installation der Pack CLI ist in verschiedenen Formen möglich und wird sehr gut auf der Pack CLI Seite beschrieben. Es gibt zum Beispiel ein Binary für Windows, Mac und die verschiedenen Linux-Distributionen, welche einfach über die entsprechenden Package-Manager installiert werden können.
Da wir Betriebssystem-unabhängig bleiben wollen, will ich hier das offizielle Docker Image nutzen.
Schauen wir uns im ersten Schritt an, welche Builder uns zur Verfügung stehen. Dies können wir mit dem folgenden Befehl erreichen.
|
|
Diese Builder können wir nun nutzen um unser OCI Image direkt aus dem Sourcecode unserer Applikation zu bauen. Ich erstelle also erstmal eine Go File mit dem folgenden Inhalt:
|
|
Wer sich mit Go nicht auskennt:
Wir starten hier einen einfachen HTTP Server auf Port 8080, der eine statische Ausgabe macht, sobald wir die URL aufrufen.
Nun können wir schon unser erstes Image aus dem Sourcecode erstellen. In der Konsole geben wir also folgendes ein:
|
|
Wir erhalten nun eine recht lange Ausgabe, welche wir uns im Detail mal anschauen wollen
|
|
Wir sehen hier, dass verschiedene Images heruntergeladen werden.
buildpacksio/pack → Das ist unsere Platform mit der wir zusammen mit dem Lifecycle, dem Buildpack und dem Applikationscode das OCI Image erzeugen können.
buildpacks/builder → Hier ist unser Build Image. Dieses Image wird genutzt um die Buildumgebung zu erstellen. In der Buildumgebung wird dann der Lifecycle und die Buildpacks ausgeführt.
buildpacks/gcp/run → Das Run Image ist das Base Image für unser Applikations Image.
Build-Image und Run-Image nennt man auch Stack. Man braucht diese immer gemeinsam um ein Image zu erstellen
Als nächstes folgt der Lifecycle. Wir sehen in unserer Ausgabe folgendes:
|
|
Der Lifecycle besteht wie wir sehen aus:
DETECT(1): Hier werden passende Buildpacks gefunden, welche während der Buildphase genutzt werden
ANALYZE(7): Hier werden Files wiederhergestellt, welche die Build- und Export-Phase optimieren können
RESTORE(10): Hier werden Layer vom Cache wiederhergestellt
BUILD(12): Hier wird der Applikationscode in ein lauffähiges Artefakt transformiert, welches in einen Container eingepackt werden kann.
EXPORT(29): Hier wird das OCI Image erstellt
Nachdem unser Image gebaut wurde, können wir dies einfach mit dem docker run Befehl starten
|
|
Unser eigenes buildpack
Nun haben wir gesehen, wie man mit einem bereits bestehenden buildpack seine Applikation zu einem Image transformieren kann. Wollen wir uns nun anschauen, wie man ein eigenes buildpack schreiben kann. Dies ist beispielsweise nützlich, wenn man den Buildprozess modifizieren will. Dazu installieren wir uns erstmal die Pack CLI Binary.
|
|
Danach erstellen wir uns die erforderlichen Files für ein eigenes Buildpack. Das Projekt hat danach die folgende Struktur:

In die Dateien fügen wir nun folgendes ein:
|
|
|
|
|
|
Anschliessend müssen wir die beiden Files im bin Ordner noch ausführbar machen.
|
|
Um unser Buildpack zu testen, müssen wir den buildpack gegen unsere go Applikation laufen lassen. Dazu führen wir in der CLI folgendes aus.
|
|
Nun builden wir unsere Applikation mit unserem Buildpack.
|
|
Wie wir sehen können schlägt der Build erstmal fehl, da wir in unserem detect Skript erstmal nur einen Fehler zurückgeben. Wollen wir dies nun so anpassen, dass wir bei einem Go File keinen Fehler mehr erhalten.
|
|
Wenn wir unsere Applikation nun wieder builden erhalten wir die folgende Ausgabe:
|
|
Nun müssen wir uns noch unser Build-Skript schreiben um die Applikation zu bauen. Das fertige Skript sieht so aus.
|
|
Wollen wir uns das Skript mal genauer unter die Lupe nehmen.
Schritte 1-5:
Wir erstellen uns den Layer für die go Installation und machen es für den Build verfügbar.
Schritt 6:
Wir bauen unsere Applikation und erzeugen das fertige Binary
Schritt 7:
Wir müssen unserer Applikation ein default Start Kommando übergeben. Wir können hier mehrere Prozesse angeben, wenn wir verschiedene Entrypoints haben (Beispielsweise könnte hier noch ein asynchroner Task laufen). Der “web” Prozess ist der aktuell der Default Prozess.
Nun können wir unseren Build wieder ausführen und sollten keine Fehler mehr erhalten.
|
|
Umgebungsvariablen
Nun da wir unser eigenes Buildpack haben, können wir beispielsweise Umgebungsvariablen an unseren Buildprozess übergeben. Schauen wir uns das an einem kleinen Beispiel an. Nehmen wir an, wir müssten den Port der Applikation im Buildprozes konfigurieren können.
Wir ändern also in unserer Applikation die Initialisierung des Ports folgendermassen ab:
|
|
Wir lesen also den Port aus der Umgebungsvariable PORT aus, welche an unsere Go-Applikation übergeben wird.
Danach müssen wir unser build File modifizieren, damit wir auf die Umgebungsvariablen Zugriff haben.
Wir fügen folgendes unter Schritt 1 “GET ARGS” ein:
|
|
und ändern den Build Befehl folgendermassen ab
|
|
Nun können wir beim Bauen des Image den Port als Umgebungsvariable mitgeben. Wir führen dazu den build folgendermassen aus:
|
|
Der Http-Server sollte jetzt auf Port 8081 statt Port 8080 laufen.
Den gesamten Quellcode findet in unserem Github: https://github.com/b-nova/buildpacks-go-sample
Ausblick
Cloud Native Buildpacks sind ein sehr mächtiges und einfaches Mittel, wie man seinen Quellcode schnell in ein Image transformieren kann. Wir haben heute einen kleinen Ausblick gesehen, wie man sein eigenes buildpack erstellen kann um den Build anzupassen. Ich werde mir in den nächsten Tagen noch Tekton in Verbindung mit Cloud Native Buildpacks anschauen. Ob es aber einen weiteren Blogbeitrag dazu gibt ist noch nicht sicher.
Was aber sicher ist, ist die Tatsache, dass wir bei b-nova uns weiterhin mit interessanten Themen rund um die Themen Cloud, GitOps und DevOps auseinandersetzen werden. Stay tuned.
Stefan Welsch – Manitu, Pionier, Stuntman, Mentor. Als Gründer von b-nova ist Stefan immer auf der Suche nach neuen und vielversprechenden Entwicklungsfeldern. Er ist durch und durch Pragmatiker und schreibt daher auch am liebsten Beiträge die sich möglichst nahe an 'real-world' Szenarien anlehnen.