Docker Buildkit. Let's Build it Fast

Buildkit: Schneller Container-Images bauen

3 Tipps, die deinen Builds Beine machen

Avatar von Andreas Rudat

Das Bauen von Container-Images ist nach wie vor ein zeitaufwendiges Unterfangen, mit dem sich niemand mehr rumschlagen möchte, wenn erstmal alles reibungslos funktioniert. Schaue ich als Dienstleister auf unterschiedlichste Projekte, sehe ich immer wieder, dass Kunden nach wie vor dieselben Build-Prozesse verwenden, wie sie seit der Multi-Stage „Revolution“ etabliert sind.

Die Gründe dafür sind vielfältig: „Keine Zeit“, fehlende Ressourcen, Betriebsblindheit oder die persönliche Einstellung „Funktioniert doch auch so“.

Daher möchte ich mit diesem Artikel zeigen, wie Images heute gebaut werden können. Der Fokus dabei soll auf Geschwindigkeit der Builds liegen, unabhängig vom Dockerfile.

Eine Einführung in Buildkit

docker build ist tot! Lang lebe docker buildx!

Andreas R., erfahrener Baumeister

Moment, was denn nun? Buildkit oder buildx?

Buildkit ist die eigentliche Engine, mit der wir den Container bauen. Damit löste Docker ab Version 23 den alten internen Builder ab. Aus Kompatibilitätsgründen ist er aber nach wie vor vorhanden.

Buildx hingegen ist ein Plugin für Docker, um Buildkit verwenden zu können. Damit ersetzen wir den üblichen Aufruf docker build mit docker buildx build. Der Einfachheit halber bieten viele Distributionen einen Patch bzw. Alias an, um wie gewohnt docker build nutzen zu können.

„Näheres dazu finden sie in der Dokumentation zum Betriebssystem ihres Vertrauens.“

Was kann Buildkit?

Buildkit kann die Build-Zeit durch Parallelisierung verkürzen. Vorausgesetzt, man hat ein entsprechendes Multi-Stage Setup.

Apropos: neben Multi-Stage ermöglicht Buildkit auch Multi-Platform Builds. Also die Möglichkeit, z. B. x86 und Arm zu bauen. Natürlich auch parallel.

Buildkit verfügt aber auch über erweiterte Caching-Möglichkeiten für den Bau von Images. So kann nicht nur der lokale Cache effizienter verwendet werden, sondern auch remote können Daten für den Bau einbezogen werden.

„Oh, damit kann man also Dinge sicher schneller machen?“

Ja, generell hat – je nach Setup – auch der Entwickler was davon. Dazu jedoch später mehr.

Time for craftership!

Es ist an der Zeit, dass wir uns ein wenig die Hände schmutzig machen. Also fangen wir mit einem einfachen Beispiel an …

Level 1: Wir parallelisieren den Bau

Zu Beginn gibt’s ein wenig Quellcode. So sieht unser Dockerfile aus:

FROM alpine AS p1
RUN echo p1 is building, time is date > /p1_time.txt && sleep 5

FROM alpine AS p2
RUN echo p2 is building, time is date > /p2_time.txt && sleep 5

FROM alpine AS p3
RUN echo p3 is building, time is date > /p3_time.txt && sleep 5

FROM p3 AS final-stage
COPY --from=p1 /p1_time.txt /
COPY --from=p2 /p2_time.txt /

ENTRYPOINT ["cat", "/p1_time.txt"]

Der Bau der einzelnen Stages ist ganz offensichtlich nicht voneinander abhängig. Lediglich die „Final-Stage“ benötigt die vorherigen Stages.

Testen wir doch auf die Schnelle, ob bei der Installation Buildkit aktiv ist. Wenn nicht, sollte die Ausgabe so aussehen wie in Abbildung 1. Mit dem Ergebnis, dass die Builds nicht zeitgleich gelaufen sind (Abb. 2).

Zeit für den Gegentest mit aktiviertem Buildkit: DOCKER_BUILDKIT=1 docker build ...

Und was sagt die Laufzeit?

Legacy Build: 18 Sekunden …

vs. Buildkit: 12 Sekunden

Ca. 6 Sekunden Unterschied bei einem 12MB-Image; na wenn sich das nicht schon lohnt? Den Benchmark habe ich 8 Mal mit der Option --no-Cache durchlaufen lassen, um das Ergebnis sicherstellen zu können.

Level 2: Remote Cache for the Win!

Ja, richtig gehört, wir können auch einen Cache aus der Entfernung nutzen. Genau genommen versucht der Builder nur die Layer aus dem Remote Image zu holen, die in unserem Lokalen unverändert sind. Es wird also nicht erst das gesamte Image heruntergeladen, das spart Zeit und Bandbreite.

Remote Cache? 6 Sekunden.

Zugegeben, hier ist ein wenig Pfusch am Bau. Ich habe in diesem Beispiel kein bestehendes Image, sondern ich muss sowieso das gesamte Remote Image laden. Dennoch ist es über diesen Weg schneller, als wenn ich ohne Remote Cache arbeite.

Der Parameter --Cache-to type=inline sorgt dafür, dass die Layer direkt im Ouput Image landen und nicht zusätzlich im Builder Cache. Außerdem werden nur die neu exportierten Zeilen in den Cache geschrieben. Dazu später mehr.

Ein Vorteil, der bei Remote Caches sofort ins Auge fällt, ist natürlich der, dass der Cache von überall verwendet werden kann. Egal ob ich ein Image für lokale Entwicklung bauen will oder in der CI.

Nun zum Nachteil dieser Cache-Variante: er kann nicht gut mit komplexen Multi-Stage Builds skalieren, da diese Cache-Option nicht zwischen output artifacts und Cache output differenzieren kann.

Zusammengefasst

  • Der Cache ist für jeden im Team verfügbar
  • Type inline verhält sich in erster Linie wie der Legacy Builder Cache und ergibt nur bei Single-Stage-Containern Sinn
  • Die Option --no-Cache stellt sicher, dass kein lokaler Cache verwendet wird
  • Wenn der Remote Cache verwendet wird, muss auch dieser aktualisiert werden: --Cache-from und --Cache-to. Mehr dazu im nächsten Abschnitt.
  • Zu beachten: Der Remote Cache ist natürlich immer von der Internet-Anbindung abhängig!

Level 3: Komplexe Builds cachen

Komplex bedeutet in diesem Fall sowohl Multi-Stage als auch Multi-Platform. Hier eignet sich der Type registry für die Docker Registry, es werden aber auch Caches von diversen Cloud- und Git-Hostern unterstützt.

Wichtig ist es, den Unterschied zu inline zu verstehen. In beiden Caching-Varianten kann der Remote Cache verwendet und gleichzeitig mit neuen Artifacts geupdated werden. Wie im ersten Beispiel jedoch schon angedeutet, werden nur die Layer exportiert, die auch am Ende im finalen Image sind.

Der Type registry hingegen exportiert alle Layer und Metainformationen in den Cache; also auch die, die nicht in das endgültige Image kommen. Somit kann der Builder dann auch bei Multistage die Daten besser unterscheiden und damit auch Layer zur Verfügung stellen, die evtl. in einem ähnlichen Image benötigt werden.

Kurzer Service-Hinweis: mode=[min|max]

min ist der Default und der Weg, wie der Builder Cache schon immer funktioniert hat. max ist der neue Mode, der alle Image-Informationen exportiert und damit die bereits erwähnten Vorteile mit sich bringt.

Ein Häppchen für die Massen!

Ein Beispiel, dass auch mit dem Type inline funktionieren würde:

docker buildx build -t foobar:latest \  
--Cache-from=type=registry,ref=foobar-build-Cache \  
--Cache-to=type=registry,ref=foobar-build-Cache,mode=max \  
--push

--Cache-from=type=registry,ref=foobar-build-Cache bedeutet: Nimm die Registry mit dem Image „foobar-build-Cache“ als Remote Cache für meinen aktuellen Build

--Cache-to=type=registry,ref=foobar-build-Cache,mode=max sagt: Schreibe das neue Cache Artifact in das Image „foobar-build-Cache“ der Registry.

Wenn ich dann in meiner Pipeline den eigentlichen „foobar“ Build baue, zieht Buildkit von Remote nur die Layer, die ich benötige. Das hält das Image schön klein und bringt es schnell in die Welt. Damit im nächsten Build aber wieder ein Abgleich gemacht werden kann, muss das aktuell gebaute Image wieder zur Verfügung gestellt werden, siehe --Cache-to.

Zusammengefasst

  • Für Multi-Stage ist mode=max mit Type registry ideal
  • Der Mode min exportiert nur die Layer aus dem finalen Stage (default), max exportiert alle Layer aus allen Stages.
  • Zu beachten:
    • Im Bestfall müssen in dem beschriebenen Szenario nur Metadaten/Referenzen hochgeladen werden, um das Cache Image zu aktualisieren
    • Funktioniert u. U. in der Pipeline nicht optimal und schafft neue Herausforderungen
      • Was passieren kann und wie man es behebt, verrate ich in einem meiner nächsten Artikel

Fazit

Docker entwickelt sich ständig weiter und bietet immer wieder neue Funktionen, um schneller und bessere Images zu produzieren.

Am Ende muss natürlich jedes Team selbst abwägen, ob der Aufwand die Mühe wert ist. Je größer die eigene Systemlandschaft ist, desto wahrscheinlicher ist es, dass es Sinn ergibt.

Let’s build it fast!

Avatar von Andreas Rudat

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.