Container-Images einfach erstellt mit Ko
10. Oktober 2022
0 Min. LesezeitIn einem früheren Artikel habe ich beschrieben, wie — und warum — Sie das Tool Jib der Google Open Source Group zum Erstellen von Container-Images für Ihre Java-Anwendung nutzen können. Jib erstellt schlanke, JVM-basierte, OCI-konforme Images, die Best Practices entsprechen – ganz ohne Container-Runtime wie Docker. Außerdem müssen Sie damit keine Dockerfiles schreiben und verwalten. Was aber, wenn Sie Go-Anwendungen entwickeln? Dafür gibt es ein weiteres Open-Source-Tool für Go, das ähnlich funktioniert: Ko.
Hinweis: Während der Erstellung dieses Artikels wurde das Ko-Projekt aus einem Google gehörenden GitHub-Repository in eine eigene Organisation auf oberster Ebene namens „ko-build“ migriert. Wir haben den Wortlaut dieses Beitrags entsprechend angepasst. Verweise auf „Google Ko“ finden sich jedoch möglicherweise noch eine Weile online und in Modulnamen. Sie können sicher sein: Es handelt sich um dasselbe Projekt. Herzlichen Glückwunsch an das Ko-Projekt zu diesem Erfolg, der die Migration ermöglicht hat!
In diesem Artikel erfahren Sie, wie Sie mit Ko Container-Images ohne Dockerfiles erstellen, SBOMs generieren und Kubernetes integrieren.
Was ist Ko?
Ko ist ein einzelnes Binärprogramm und Kommandozeilentool, das in Ihrem Entwicklungsprozess dort eingesetzt werden kann, wo Sie heute den go-Compiler ausführen. Neben dem Kompilieren Ihrer Anwendung erstellt es ein extrem schlankes Container-Image, in dem Ihre Anwendung installiert ist. Wie Jib überträgt Ko das Image je nach Konfiguration und Ausführung in eine Registry oder legt es im lokalen Docker-Image-Cache ab.
Ko bietet außerdem einige praktische Funktionen rund um die Erstellung einer Software-Stückliste (SBOM) sowie die Kubernetes-Integration, die iterative Entwicklungs- und Bereitstellungsprozesse erheblich vereinfachen.
Welche Probleme soll Ko lösen?
Viele Programmiererinnen und Programmierer sind unabhängig von der verwendeten Sprache noch neu beim Erstellen von Containern und haben häufig Fragen zum Erstellen von Images, zum Beispiel:
Welches Basis-Image sollte ich verwenden, und entspricht es den Richtlinien meiner Organisation?
Wie kombiniere ich Befehle am besten, um überflüssige Image-Layer zu vermeiden?
Wie viel von meiner Anwendung muss ich in das Image kopieren, damit sie ausgeführt werden kann?
Welche Tools muss ich beherrschen, um das Image zu erstellen? (Docker? Buildah? BuildKit?)
Muss ich gemäß den Anforderungen meiner Organisation bestimmte Standardannotationen einfügen?
Anwendungsarchitektinnen und -architekten sowie Teamleitungen möchten außerdem dafür sorgen, dass ihre Teams Governance-Vorgaben und Standards einfach einhalten können. Einheitliche Vorgehensweisen aufrechtzuerhalten, kann jedoch schwierig sein, wenn jedes Team eigene, organisch gewachsene Dockerfile-Muster hat.
Auch Sicherheitsteams achten genau darauf, was in ein Image gelangt und welche Tools bei seiner Erstellung zum Einsatz kommen. Wird beispielsweise die Docker-Engine – oder der docker.sock des Host-Computers – für einen CI-Build-Node zugänglich gemacht, können Build-Umgebungen auf diesen Nodes erhöhte Zugriffsrechte erhalten.
„Der [Docker-]Daemon … kann weit mehr als Images erstellen und mit Registries interagieren. Ohne zusätzliche Sicherheitstools kann jeder Benutzer, der auf diesem Computer einen Docker-Build auslösen kann, auch einen Docker-Run ausführen, um jeden beliebigen Befehl auf dem Computer auszuführen … Er kann also nicht nur beliebige Befehle ausführen, sondern es ist auch schwierig nachzuverfolgen, wer für eine schädliche Aktion verantwortlich war, wenn er diese Berechtigung dafür nutzt.“
— Container Security von Liz Rice, Kapitel 6
Andererseits kann die Einführung neuer Build-Tools komplex sein und den Lernaufwand für die Verantwortlichen der Build-Systeme erhöhen. Ko soll all diese Herausforderungen angehen und zugleich Entwicklerinnen und Entwicklern eine bessere Nutzererfahrung bieten.
Images mit und ohne Ko erstellen
Um zu zeigen, wie Ko die oben genannten Herausforderungen angeht, sehen wir uns zunächst an, wie Ko im Vergleich zu herkömmlichen Go- und Docker-Build-Schritten abschneidet.
Images ohne Ko erstellen
In diesem Beispiel erstellen wir die klassische Go-Tutorial-Anwendung: eine „Hello World“-Web-App.
1. Erstellen Sie in einem leeren Verzeichnis die folgende Datei mit dem Namen hello.go:
2. Wir erstellen die Binärdatei direkt in unserem Dockerfile – das ist ein gängiges Muster:
Dies ist ein Multi-Stage-Build. Die erste Stage mit dem Namen build basiert auf dem offiziellen golang-Image. In dieser Stage kopieren wir den Inhalt unserer Anwendung (derzeit nur die Datei hello.go) in das Image unter /go/src und führen einfach das Tool go build mit den Parametern aus, die zum Erstellen einer statischen Binärdatei erforderlich sind.
In der zweiten Stage verwenden wir das Basis-Image static:nonroot des Google-Distroless-Projekts, legen nonroot als Standardbenutzer fest, kopieren die Binärdatei hello aus der Stage build in das Stammverzeichnis, hinterlegen Metadaten zum freizugebenden Port und legen fest, dass beim Start des Containers standardmäßig ./hello ausgeführt wird.
Warum zwei Stages?
Es ist üblich, Ihre Anwendung im Dockerfile zu erstellen. So stellen Sie sicher, dass sowohl auf dem Rechner einer Entwicklerin oder eines Entwicklers als auch bei automatisierten Builds die richtige Compiler-Version verwendet wird. Ein häufiger Fehler ist jedoch, dasselbe Image bereitzustellen, in dem der Build stattgefunden hat. Das ist ein Sicherheitsrisiko, da das bereitgestellte Image dann nicht nur unnötige Tools wie den Compiler, sondern auch den Quellcode Ihrer Anwendung enthält. Kann ein Angreifer eine Schwachstelle ausnutzen oder eine Kopie Ihres Images erhalten, stehen ihm viele Informationen und Tools zur Verfügung, um seinen Angriff auszuweiten. Mit dem Basis-Image distroless/static:nonroot erhalten wir ein äußerst minimales Dateisystem mit einem Nicht-Root-Standardbenutzer.
3. Nachdem unser Dockerfile fertig ist, können wir mit dem Befehl docker build ein lokal zwischengespeichertes Image erstellen und mit einem Tag versehen:
Hinweis: Ihre Ausgabe kann etwas anders aussehen – je nachdem, welche Docker-Version Sie verwenden und ob BuildKit-Erweiterungen aktiviert sind.
4. Damit andere dieses Image verwenden können, müssen wir es in ein Registry-Repository übertragen. In diesem Beispiel betreibe ich auf meinem Rechner ein lokales Repository auf Port 5000, und zwar über das registry:2-Image von DockerHub. (Deshalb habe ich dem Image das Präfix localhost:5000/ gegeben.)
Mit Docker lässt sich ein Image ganz einfach über den Befehl docker push in eine Registry übertragen:
Hinweis: Bei einer verwalteten Registry müsste ich mich zuerst mit docker login anmelden.
5. Zum Ausführen unseres Images können wir schließlich docker run oder ein entsprechendes kubectl-Deployment verwenden. Der Einfachheit halber nutzen wir die erste Möglichkeit:
Anschließend können wir die Anwendung mit einem einfachen curl-Befehl testen:
Die gerade ausgeführten Schritte könnten visualisiert etwa so aussehen:

Gehen wir nun zum Anfang zurück und sehen uns an, wie das mit Ko funktioniert.
Images mit Ko erstellen
1. Wir beginnen mit derselben Datei hello.go in einem leeren Verzeichnis:
2. Als Nächstes speichern wir die Registry für unser Image in einer Umgebungsvariable und führen ko build aus:
Dieser Befehl:
kompilierte unsere Anwendung zu einer statischen Binärdatei,
packte sie in ein korrekt aufgebautes Image und
übertrug es in meine Registry.
Für all das waren weder ein Dockerfile noch eine Container-Runtime erforderlich. Außerdem sehen Sie in der Ausgabe Hinweise darauf, dass eine SBOM erstellt und veröffentlicht wurde. Darauf kommen wir später zurück.
Hinweis:Der Image-Tag enthält hier einen MD5-Hash, der standardmäßig aus dem Importpfad Ihrer Go-Anwendung gebildet wird. Da wir in diesem Beispiel kein vollständiges Go-Modul haben, wird der Hash einfach aus hello.go abgeleitet. Weitere Informationen zu Tags finden Sie in der Ko-Dokumentation.
3. Nun führen wir das Image mit docker run aus:
… und testen es mit curl:
Die Ko-Pipeline könnte visualisiert etwa so aussehen:

Indem Ko die aufwendige Image-Erstellung und das Übertragen übernimmt, konnten wir die Anzahl der erforderlichen Schritte, die Abhängigkeit von einer Container-Runtime zur Build-Zeit sowie den mit Dockerfiles verbundenen Aufwand für Fachwissen und Wartung reduzieren.
Sinnvolle Standardeinstellungen, deterministische Anpassungen
Ko-Nutzerinnen und -Nutzer profitieren von den gemeinsam definierten Best Practices der Open-Source-Community. Was aber, wenn Ihre Organisation eigene Standards hat, die davon abweichen? Hier helfen die Kommandozeilenoptionen von Ko und/oder die Konfigurationsdatei ko.yaml.
Benutzerdefiniertes Basis-Image
Angenommen, Ihr Unternehmen schreibt ein bestimmtes Image als Basis für alle Go-Anwendungen vor. Fügen Sie einfach in einer Datei ko.yaml im Stammverzeichnis die Zeile defaultBaseImage: ein und geben Sie dort den gewünschten Image-Namen an.
Go-Compiler-Flags
Um die Go-Compiler-Option -ldflags wie in unserem ursprünglichen Beispiel ausdrücklich anzugeben, fügen Sie der Datei ko.yaml einfach einen Eintrag builds: mit den passenden Einstellungen hinzu, wie in der Ko-Dokumentation beschrieben.
Docker-Labels
Docker-Labels – auch OCI-Annotationen genannt – ermöglichen es, einem Image Metadaten hinzuzufügen. Diese können beispielsweise auf das Quell-Repository, den CI-Build, der das Image erstellt hat, oder andere Daten verweisen, die Ihr Team hinterlegen möchte. Weitere Informationen zu Image-Labels und ihrer Verwendung finden Sie in meinem Blogbeitrag Docker-Labels: Wie und wann Sie sie verwenden sollten.
Ko unterstützt das Hinzufügen von Labels über das Kommandozeilen-Flag --image-label:
Hinweis: Zum Zeitpunkt der Veröffentlichung dieses Artikels lassen sich Image-Labels noch nicht in der Konfigurationsdatei .ko.yaml festlegen. Für diese Funktion gibt es jedoch bereits ein offenes Issue.
Weitere interessante Ko-Funktionen
SBOMs für Images
Wie oben zu sehen, erstellt Ko automatisch eine SBOM für Ihr neues Container-Image und überträgt sie. Standardmäßig wird das SPDX-Format verwendet. Sie können aber auch CycloneDX wählen, indem Sie in der Kommandozeile das Flag --sbom=cyclonedx übergeben. Mit --sbom=none lässt sich die Funktion deaktivieren. Diese und weitere Konfigurationsdetails finden Sie in der Ko-Dokumentation.
Eine ausführliche Erläuterung von SBOMs und ihrer Bedeutung für eine sichere Supply Chain würde den Rahmen dieses Artikels sprengen. Wenn Sie mehr erfahren möchten, lesen Sie SBOMs für die Sicherheit von Open-Source-Supply-Chains erstellen.
Kubernetes-Integration
Wenn Sie Ihr Image in einer Kubernetes-Clusterumgebung testen müssen, kennen Sie wahrscheinlich den Aufwand eines sich wiederholenden Ablaufs mit Schritten wie diesen:
Mein Image erstellen
Es in meine Registry übertragen (oder anderweitig in den Kubernetes-Cluster laden)
Mein Deployment-YAML mit dem neuen Image-Tag aktualisieren
Mit
kubectlerneut bereitstellen
Ko vereinfacht das Ganze, indem es all diese Schritte mit einem einzigen Befehl automatisiert: ko apply. Ersetzen Sie im Deployment-YAML den Image-Tag durch einen speziell formatierten ko://-Tag. Mit ko apply werden dann automatisch alle oben genannten Schritte ausgeführt und die Bereitstellung erfolgt in dem Cluster, der im aktuellen Kontext Ihrer kubectl-Konfiguration festgelegt ist.
Stellen Sie sich zum Beispiel vor, Sie haben eine Sandbox oder einen lokal ausgeführten Kubernetes-Cluster und möchten Ihre Arbeit schrittweise bereitstellen und testen. Hier ist ein Deployment-Manifest, mit dem sich unsere „hello“-Anwendung dort bereitstellen lässt:
Sie sehen, dass die Zeile image: den Eintrag ko:// enthält, gefolgt vom Pfad zum Go-Modul unserer Anwendung auf GitHub.
Nun führen wir einfach ko apply aus und übergeben die YAML-Datei mit dem Flag -f, wie wir es auch mit kubectl tun würden:
Jetzt können wir unsere Anwendung testen, eine Änderung vornehmen und ko apply -f myfile.yaml beliebig oft erneut ausführen. Ko übernimmt dabei die ganze Arbeit für Sie!
Wie zu erwarten, gibt es weitere Kubernetes-Befehle, die Sie wahrscheinlich benötigen, zum Beispiel:
delete, um Ihre Deployments zu entfernen, undresolve, um YAML zu generieren, das Sie ankubectloder andere Tools übergeben können.
Die vollständige Dokumentation finden Sie unter https://github.com/ko-build/ko#kubernetes-integration.
Herausforderungen bei der Verwendung von Ko
Wir haben uns mit den Gründen befasst, warum Sie Ko in Ihre Workflows integrieren möchten. Bevor Sie loslegen, sprechen wir jedoch über einige Herausforderungen, die bei der Verwendung von Ko auftreten können.
Plattformübergreifende Aspekte
Wenn Sie auf einem Rechner ohne Linux entwickeln, kann das Fehlen einer Container-Runtime die Arbeit in mancher Hinsicht erschweren. Mit einem containerbasierten Build-Image können Sie sowohl die Build-Tools als auch das Betriebssystem samt Bibliotheken in einem gemeinsamen Basis-Build-Image bündeln. So spielt es praktisch keine Rolle mehr, ob die Plattform Ihrer Workstation der Ihrer Produktionsumgebung entspricht.
Ein Beispiel: Der Go- und Docker-Teil des obigen Beispiels funktioniert auf so gut wie jeder Plattform. Denn unabhängig davon, wo Sie den Build erstellen, stellt das Basis-Image golang die erforderlichen Debian-basierten glibc-Bibliotheken bereit, mit denen der Go-Builder die statische Binärdatei erstellt. Wenn Sie hingegen beispielsweise versuchen, das Ko-Beispiel auf einem MacOS-Rechner mit denselben ldflags-Optionen auszuführen, erhalten Sie Fehlermeldungen zu fehlenden Bibliotheken, die für die statische Verknüpfung erforderlich sind.
Der Grund dafür ist, dass MacOS/Darwin nicht die erforderlichen Bibliotheken für die Cross-Kompilierung einer statischen Linux-Binärdatei enthält – zumindest zum Zeitpunkt, an dem ich dies schreibe. Ohne Apple zu sehr herausgreifen zu wollen: Ähnliche Probleme können auch bei unterschiedlichen CPU-Architekturen sowie auf Linux- und Windows-WLS-Workstations auftreten.
Verlust von Fachkenntnissen
Die Fähigkeiten und das Wissen, die für die Erstellung und Pflege hochwertiger Container-Images erforderlich sind, sind wertvoll. Wenn Tools den Bedarf an diesen Kenntnissen abstrahieren oder beseitigen, kann es schwieriger werden, Probleme zu beheben, wenn etwas schiefgeht. Je weniger Teammitglieder Container-Technologien verstehen, desto stärker ist das Team bei der Fehlersuche und Wartung zur Laufzeit auf die wenigen Personen mit entsprechender Erfahrung angewiesen. Im Extremfall kann das zu Burnout, Fluktuation und/oder Ausfällen führen.
Nachlässigkeit bei der Sicherheit
Wenn die Erstellung von Container-Images vollständig automatisiert wird und nicht mehr bei den Entwicklern liegt, kann die Einhaltung von Prozessen wie dem Scannen von Images auf Schwachstellen leiden. Schwachstellen in nicht aktualisierten Images, Paketen, Bibliotheken usw. können unbemerkt eindringen und Ihre Anwendung Angriffen aussetzen. Stellen Sie daher sicher, dass solche Scans weiterhin auf andere automatisierte Weise durchgeführt werden, zum Beispiel durch:
Build-Skripte oder Makefiles
Git-Hooks
CI-Build-Schritte
Falls Sie noch keine Image-Scans durchführen: Snyk bietet kostenlose Scans von Container-Images, die Schwachstellen darin finden und einfache, umsetzbare Empfehlungen zu ihrer Behebung liefern.
Container-Images vereinfachen
Ko und andere alternative Tools zur Image-Erstellung können die Erstellung von Container-Images für Ihre Go-Anwendungen erheblich vereinfachen und Ihren Teams helfen, Images effizienter und einheitlicher zu erstellen. Besonders Ihre CI-Tools profitieren davon: Sie müssen Ihrer Build-Umgebung keine Container-Runtime mehr bereitstellen und reduzieren so Angriffsvektoren, die durch privilegierte Zugriffe in Ihrem SDLC entstehen. Unabhängig davon, ob Sie ein Build-Tool wie Ko verwenden: Stellen Sie sicher, dass Ihre Teams mit Container-Technologien und den Sicherheitsrisiken der Prozesse und Tools vertraut sind, mit denen sie ihre Images erstellen.
Starten Sie mit Capture the Flag
Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.
