Docker-Images in CI/CD-Pipelines bauen
Sie haben ein Dockerfile, das auf Ihrem Laptop einwandfrei funktioniert. Sie führen docker build aus, das Image wird gebaut, und Ihre Anwendung läuft. Aber wenn Sie dasselbe Dockerfile in eine Pipeline schieben, verhält sich plötzlich alles anders.
Der Build, der auf Ihrem Rechner zwei Minuten dauerte, braucht jetzt fünfzehn. Das Image, das gestern funktioniert hat, schlägt heute ohne ersichtlichen Grund fehl. Und wenn Sie für ein Rollback eine alte Version neu bauen müssen, verhält sich das resultierende Image anders als das Original.
Diese Probleme sind nicht zufällig. Sie entstehen durch die Kluft zwischen dem Bauen von Images auf einem Entwicklerrechner und dem Bauen in einer automatisierten Pipeline. Diese Kluft zu verstehen, ist der erste Schritt zur Behebung.
Was sich ändert, wenn Sie in eine Pipeline wechseln
Wenn Sie ein Image in einer Pipeline bauen, ändern sich drei Dinge grundlegend.
Erstens muss der Build auf einem fremden Rechner laufen. Dieser Rechner kann andere Ressourcen, andere Netzwerkbedingungen und andere Dateisysteme haben. Ihr Dockerfile muss unabhängig vom Ausführungsort funktionieren.
Zweitens muss die Pipeline das Image bei jeder Codeänderung neu bauen. Das ist nicht optional. Wenn Sie Neubauten überspringen, läuft Ihr Deployment mit veraltetem Code. Aber jedes Mal neu zu bauen bedeutet, dass jeder Commit den vollständigen Build-Prozess auslöst, und dieser Prozess muss schnell genug sein, um die Entwicklung nicht zu bremsen.
Drittens muss der Build reproduzierbar sein. Derselbe Quellcode muss dasselbe Image erzeugen, unabhängig davon, wann oder wo der Build läuft. Ohne Reproduzierbarkeit können Sie nicht darauf vertrauen, dass ein Rollback auf einen alten Commit exakt dasselbe Anwendungsverhalten wiederherstellt.
Das folgende Diagramm zeigt die typischen Phasen einer Docker-Build-Pipeline, vom Quellcode bis zum Registry-Push.
Kontrollieren Sie Ihren Build-Kontext
Der Build-Kontext ist der Ordner, den Sie an den Docker-Daemon senden, wenn Sie docker build ausführen. Auf Ihrem Laptop ist das normalerweise Ihr Projektordner. In einer Pipeline passiert dasselbe, aber die Folgen eines großen Kontexts sind schwerwiegender.
Jede Datei im Build-Kontext wird an den Docker-Daemon gesendet. Wenn Ihr Repository node_modules, Python-Virtualenvs oder kompilierte Binärdateien enthält, werden diese Dateien übertragen, obwohl sie für den Build nicht benötigt werden. Das verlangsamt jeden einzelnen Pipeline-Lauf.
Die Lösung ist eine .dockerignore-Datei. Sie funktioniert wie .gitignore, aber für Docker-Builds. Listen Sie alles auf, was für das Image nicht benötigt wird: Abhängigkeitsordner, Cache-Verzeichnisse, .git-Historie, Test-Fixtures und Dokumentation. Ein schlanker Build-Kontext bedeutet schnellere Builds und weniger Netzwerkverkehr.
Nutzen Sie den Cache optimal
Docker baut Images in Layern. Jede Anweisung in Ihrem Dockerfile erzeugt einen Layer. Beim Neubauen prüft Docker, ob sich ein Layer geändert hat. Ist ein Layer unverändert, verwendet Docker das gecachte Ergebnis des vorherigen Builds.
Dieser Caching-Mechanismus ist Ihr größter Verbündeter für schnelle Builds. Aber er funktioniert nur, wenn der Cache zwischen Pipeline-Läufen erhalten bleibt.
In der lokalen Entwicklung lebt der Cache auf Ihrem Rechner. In einer Pipeline verschwindet der Cache, wenn der Runner beendet wird, es sei denn, Sie speichern ihn explizit. Manche CI-Systeme bieten integriertes Docker-Layer-Caching. Wenn Ihres das nicht tut, müssen Sie es manuell konfigurieren oder akzeptieren, dass jeder Build bei null anfängt.
Selbst wenn das Caching funktioniert, bestimmt die Reihenfolge der Anweisungen in Ihrem Dockerfile, wie viel Cache Sie tatsächlich nutzen. Die goldene Regel lautet: Kopieren Sie zuerst die Dinge, die sich seltener ändern.
Für eine Node.js-Anwendung bedeutet das, package.json und package-lock.json vor dem restlichen Quellcode zu kopieren. Führen Sie npm install direkt nach dem Kopieren dieser Dateien aus. Kopieren Sie dann den Anwendungscode. Mit dieser Reihenfolge wird die Abhängigkeitsinstallation nur dann wiederholt, wenn sich Ihre Abhängigkeiten tatsächlich ändern, nicht wenn Sie eine einzelne Zeile Anwendungscode ändern.
Das gleiche Prinzip gilt für jede Sprache. Python-Projekte sollten zuerst requirements.txt oder pyproject.toml kopieren. Go-Projekte sollten zuerst go.mod und go.sum kopieren. Das Muster ist universell: Trennen Sie stabile Abhängigkeiten von sich änderndem Anwendungscode.
Verwenden Sie Build-Argumente für Flexibilität
Ihr Dockerfile sollte keine Werte hartcodieren, die sich zwischen Umgebungen ändern. Die Version eines Basis-Images, der Umgebungsname oder ein Zugriffstoken für eine private Registry sollten von außerhalb des Dockerfiles kommen.
Docker bietet ARG für diesen Zweck. Sie definieren einen Platzhalter in Ihrem Dockerfile, und die Pipeline füllt ihn während des Builds aus.
ARG BASE_IMAGE_VERSION=20.04
FROM ubuntu:${BASE_IMAGE_VERSION}
In Ihrer Pipeline übergeben Sie den tatsächlichen Wert:
docker build --build-arg BASE_IMAGE_VERSION=22.04 .
So bleibt Ihr Dockerfile generisch und wiederverwendbar. Ein Dockerfile kann Entwicklungs-, Staging- und Produktions-Builds ohne Duplizierung bedienen.
Stellen Sie reproduzierbare Builds sicher
Reproduzierbarkeit bedeutet, dass das Bauen desselben Quellcodes zu verschiedenen Zeiten dasselbe Image erzeugt. Ohne dies können Sie Ihrer Rollback-Strategie, Ihrem Audit-Trail oder Ihrem Security-Scanning nicht vertrauen.
Drei Dinge brechen die Reproduzierbarkeit typischerweise.
Erstens die Verwendung von latest-Tags für Basis-Images. Der latest-Tag ändert sich im Laufe der Zeit. Das heutige latest ist nicht das morgige latest. Fixieren Sie Ihr Basis-Image auf einen bestimmten Versionstag wie ubuntu:22.04 oder node:20-alpine.
Zweitens das Nicht-Fixieren von Abhängigkeitsversionen. Ihre package.json könnte einen Versionsbereich wie ^4.0.0 angeben. Dieser Bereich löst sich im Laufe der Zeit in verschiedene Versionen auf. Verwenden Sie Lock-Dateien wie package-lock.json oder yarn.lock, um exakte Versionen einzufrieren.
Drittens das Einbetten von Build-Zeitstempeln oder Build-Metadaten in das Image. Wenn Ihr Build-Prozess das aktuelle Datum in eine Datei innerhalb des Images schreibt, unterscheidet sich diese Datei zwischen Builds, selbst wenn der Quellcode identisch ist. Vermeiden Sie dies, es sei denn, Sie haben einen spezifischen operativen Grund dafür.
Speichern Sie das Image in einer Registry
Sobald die Pipeline das Image gebaut hat, braucht es einen Ort, an dem Server und Kubernetes-Cluster es abrufen können. Dieser Ort ist eine Container-Registry.
Ihre Pipeline sollte das Image mit aussagekräftigen Identifikatoren taggen. Ein gängiges Muster ist die Verwendung des Commit-SHA als primären Tag, zusammen mit zusätzlichen Tags für Branches oder semantische Versionen.
docker tag myapp:${COMMIT_SHA} registry.example.com/myapp:${COMMIT_SHA}
docker push registry.example.com/myapp:${COMMIT_SHA}
Dies gibt Ihnen einen permanenten Verweis auf jedes jemals gebaute Image. Sie können jederzeit zu einem beliebigen Commit zurückgehen und das exakte Image abrufen, das daraus gebaut wurde.
Praktische Checkliste
Bevor Sie Ihren Docker-Build in eine Pipeline schieben, überprüfen Sie diese Punkte:
.dockerignoreschließt Abhängigkeitsordner, Cache und.gitaus- Dockerfile kopiert Abhängigkeitsdateien vor dem Anwendungscode
- Basis-Image-Tags sind auf bestimmte Versionen fixiert, nicht auf
latest - Abhängigkeitsversionen sind mit Lock-Dateien fixiert
- Build-Argumente werden für umgebungsspezifische Werte verwendet
- Pipeline speichert den Docker-Layer-Cache zwischen Läufen
- Images werden mit Commit-SHA getaggt und in eine Registry gepusht
Was das für Ihre Pipeline bedeutet
Images in einer Pipeline zu bauen bedeutet nicht nur, docker build auf einem anderen Rechner auszuführen. Es geht darum, Ihr Dockerfile und Ihre Pipeline so zu gestalten, dass sie zusammenarbeiten. Ein gut strukturiertes Dockerfile, das die Layer-Reihenfolge und den Build-Kontext respektiert, wird schneller bauen. Eine Pipeline, die den Cache bewahrt und Images richtig taggt, liefert Ihnen zuverlässige, reproduzierbare Artefakte.
Das Image, das Sie heute bauen, sollte dasselbe Image sein, das Sie sechs Monate später aus demselben Commit neu bauen können. Diese Konsistenz macht Deployments vorhersagbar und Rollbacks sicher.