Containerbau mit Kaniko
Die Automatisierung von Container-Imagebau-Prozessen startet in aller Regel mit dem direkten Portieren von docker build
in eine CI-Pipeline. Dieser Befehl ist an den Docker Daemon des Hosts gerichtet, welcher als Socket in die Pipeline Sandbox gemounted wird.
Problematisch ist das vor allem, weil es erlaubt, aus der Pipeline heraus beliebige Prozesse auf dem Host zu starten, ggf. ohne weitere Isolation von ihrem Umfeld. Das macht die CI Pipeline zum Sicherheitsrisiko. Umgehen lässt sich dieses Problem auf mehreren Wegen, welche alle mit ihren Vor- und Nachteilen daherkommen:
- Der Docker Daemon selbst lässt sich in einem Container ausführen („Docker in Docker“). Die Pipeline spricht dann nur mit einem containerisierten Docker Daemon, was mehr Isolation verspricht. Problem hierbei ist vor allem, dass der Daemon Container weitreichende Kernelprivilegien benötigt, um seine Arbeit (im Wesentlichen das Starten weiterer Prozesse) verrichten zu können. Solche privilegierten Container gelten ihrerseits selbst als Sicherheitsrisiko, da man aus ihnen ausbrechen könnte.
- Durch das Ersetzen von Docker durch Buildah lässt sich auf den Docker Daemon vollständig verzichten. Der Buildah Container selbst muss aber trotzdem privilegiert laufen, um Prozesse in den zu bauenden Containern starten zu können.
- Beliebige Abwandlungen beider vorheriger Varianten durch andere Tools oder Config (z.B. „rootless Docker in Docker“, Docker mit Userspace Remapping, …).
Beide Lösungen benötigen privilegierte Container, welche u.A. in Kubernetes nicht akzeptabel sind. Eine Lösung ohne dieses Problem bietet Kaniko. Kaniko lässt sich in Docker, wie auch in Kubernetes betreiben und bietet eine containerisierte Umgebung zum Imagebau, ohne weitreichende Kernel-Capabilities zu benötigen.
Das Konzept hinter Kaniko
Da ein Kaniko Container nicht privilegiert läuft, funktioniert der Image Bauprozess im Container etwas anders als in Docker. Wesentlicher Unterschied ist hier, dass alle Phasen des Baus (Aufbau des Basis Filesystems, isoliertes Ausführen von Befehlen, Snapshotting des Filesystems) im User- statt im Systemspace stattfinden. Von Anwendungsseite merkt man hiervon allerdings nichts.
Die einzigen Unterschiede in der Bedienung kommen daher, dass ein Kaniko Container zum einmaligen Gebrauch – also zum Bau eines einzigen Images – gedacht ist. Danach sollte im Idealfall ein neuer Kaniko Container gestartet werden, um wieder in einer neuen sauberen Umgebung zu starten, denn Kaniko löscht das Filesystem alter Container nicht automatisch. Alternativ gibt es zum „Aufräumen“ im laufenden Kaniko mittlerweile aber auch eine Möglichkeit (siehe später), um den Bau mehrerer Images in einem Container zu ermöglichen. Best Practice in alter Kubernetes-Manier bleibt aber weiterhin „eine Aufgabe – ein Pod“.
Verwendung
Bauen und Pushen eines Container Images mit Kaniko
Im Gegensatz zu Docker, Buildah, etc. ist das offizielle Kaniko Container Image komplett minimalistisch aufgebaut. Insbesondere beinhaltet es keine Shell, weswegen alle Vorbereitungen zum Containerbau (auch das Bereitstellen von Container Registry Credentials) außerhalb von Kaniko stattfinden müssen. Ein typischer Build Ablauf zum Pushen in eine Docker Registry sieht daher so aus:
- Erstellen einer Docker
config.json
mit Credentials via
cat
{
"auths"
: {
"$REGISTRY"
: {
"auth"
:
"$(echo -n $USER:$PASS | base64)"
}
}
}
EOF
- Starten eines Kaniko Containers, der diese Config unter
/kaniko/.docker/config.json
mountet. Der Build Context, einschließlich des Dockerfiles, muss ebenfalls unter/workspace
gemountet werden.
docker run -it --
rm
--name kaniko-build
-
v
"$WORKSPACE"
:
/workspace
-
v
dockerconfig.json:
/kaniko/
.docker
/config
.json:ro
gcr.io
/kaniko-project/executor
:latest
--dockerfile=
"${DOCKERFILE:-Dockerfile}"
--destination=
"$REGISTRY/$IMAGE:$TAG"
/kaniko/executor
.Am Ende des Builds pusht Kaniko das Containerimage direkt in die mittels --destination
angegebene Registry. Mehrfache Tags für das selbe Image, z.B. die Kombination Commit SHA, Commit Reference (Branch / Tag) und „latest“, sind über Merfachverwendung des --destination
Flags möglich.
Bau mehrerer Images im selben Kaniko Container
Nach dem Push endet der Kaniko-Executor Prozess und der Container stoppt. Im Container befinden sich jetzt noch Überreste des erstellten Filesystems, ein erneutes Ausführen von /kaniko/executor
kann (und wird!) fehlerhafte Images produzieren. Dementsprechend ist es empfehlenswert, für weitere Builds einen neuen, frischen Kaniko Container zu verwenden. Ist das keine Option, so erlaubt das Ausführen vom /kaniko/executor
mit dem Flag --cleanup
das anschließende „Aufräumen“ temporärer Dateien:
docker run -it --
rm
--name kaniko-build
-
v
"$WORKSPACE"
:
/workspace
-
v
dockerconfig.json:
/kaniko/
.docker
/config
.json:ro
gcr.io
/kaniko-project/executor
:latest
--dockerfile=
"${DOCKERFILE:-Dockerfile}"
--destination=
"$REGISTRY/$IMAGE:$TAG"
--cleanup
Dadurch dauert der Build Prozess ein bisschen länger, der Container wird aber wiederverwendbar.
Um nun auch noch mehrere Aufrufe des /kaniko/executors
in Folge (im selben Prozess) zu ermöglichen, bedarf es einer Shell. Diese ist zwar im minimalen Image nicht enthalten, in den „debug
“ Tags allerdings schon. Somit lässt sich dann im Kaniko Container mittels
docker run -it --
rm
--name kaniko-multi-build
-
v
"$WORKSPACE"
:
/workspace
-
v
dockerconfig.json:
/kaniko/
.docker
/config
.json:ro
-
v
build.sh:
/kaniko/build
.sh
--entrypoint
/kaniko/build
.sh
gcr.io
/kaniko-project/executor
:debug
#!/bin/sh /kaniko/executor --dockerfile= "Dockerfile1" --destination= "registry.io/image1:tag1" --cleanup /kaniko/executor --dockerfile= "Dockerfile2" --destination= "registry.io/image2:tag2" |
Verschiedene Build-Kontext-Ordner lassen sich dabei auch über das --context
Flag auswählen.
Best Practices
- Kaniko muss zwar nicht als privilegierter Container laufen, Root-Rechte im Container (
RunAsUser: 0
) sind aber trotzdem im Laufe des Containerbaus nötig, beispielsweise um Pakete zu installieren. Um dieses verbleibende Problem noch zu „entschärfen“, bedarf es beispielsweise User-Namespace Remappings. Diese stehen leider unter Kubernetes (noch) nicht zur Verfügung, weswegen für Kaniko ggf. bestehende Security Policies / Admission Rules aufgeweicht werden müssen. Kaniko Pods sollten daher in Kubernetes immer noch nach Möglichkeit auf gesonderten Nodes laufen, die sie sich nicht mit anderen Workloads teilen. - Wie oben beschrieben ist es durch die „debug“ Images möglich, im selben Kaniko Container mehrere Images zu bauen. Trotzdem bleibt ein neuer, frischer Container für jedes Image weiterhin Best Practice. Grund dafür ist die bessere Isolation der individuellen Build Prozesse.
- Die Abwesenheit einer Shell im Image erfordert etwas umzudenken. Soll etwa eine Pipeline eine dynamische Anzahl an Images bauen (z.B. für die Automatisierte Wartung von Basisimages), so ist erst eine Imageliste außerhalb des Build Containers (in einem Vorbereitungsschritt) zu erstellen. Danach muss die Pipeline, ausgehend von dieser Liste, pro Image einen Kaniko Container starten. In GitLab lässt sich das beispielsweise mit einer parallelen Job-Matrix oder, wenn die Anzahl der verschiedenen Image-Tags erst zur Laufzeit der Pipeline bekannt wird, mit dynamischen Child Pipelines realisieren. Diese Vorgehensweise hat neben der besseren Isolation auch noch einen weiteren Vorteil: Alle Build Prozesse laufen parallel ab, weswegen die Pipeline wesentlich schneller durchlaufen kann.
Sie benötigen Unterstützung oder Beratung bei der Umsetzung von IT-Automatisierungen oder Container-Plattformen? Wir beraten Sie gerne: info@atix.de
Pascal Fries
Neueste Artikel von Pascal Fries (alle ansehen)
- WebAssembly auf der Serverseite: Was ist WASI? - 5. Juni 2023
- Containerbau mit Kaniko - 1. April 2022