Beliebte Suchanfragen
//

Sieben Wege um Kaniko beim Bau von Container-Images zu ersetzen

11.9.2025 | 18 Minuten Lesezeit

In modernen CI-Pipelines hat man sich weitgehend von spezialisierten, individuell konfigurierten Build-Servern verabschiedet zugunsten reproduzierbarer, durch Code definierter Umgebungen. In GitLab CI und anderen gängigen Plattformen wird dies durch einen Container-basierten Ansatz umgesetzt, bei dem jeder CI-Job in einem eigenen Linux-Container ausgeführt wird. Dies sorgt für eine saubere, reproduzierbare und portable Build-Umgebung, was einen großen Gewinn an Konsistenz bedeutet.

Knifflig wird es jedoch, wenn man innerhalb dieser CI-Job-Container Container-Images für die eigene Anwendung erstellen möchte. Kaniko, ein ursprünglich von Google veröffentlichtes Open-Source-Projekt, ist seit geraumer Zeit eine gängige Lösung für dieses Problem. Da Kaniko jedoch kürzlich offiziell abgekündigt wurde, sind viele Teams auf der Suche nach Alternativen.

In diesem Artikel werden wir sieben Alternativen zu Kaniko für Container-basierte CI-Jobs erkunden, wobei der Schwerpunkt auf GitLab CI liegt.

Hintergrund: Abwägungen bei der Container-Isolierung

Warum sind Image-Builds überhaupt so ein großes Thema?

In einer Container-basierten CI-Umgebung sollten sich Jobs nicht gegenseitig beeinträchtigen. Umso mehr sollten Job-Container nicht in der Lage sein, ihre Host-Maschine zu modifizieren: Aufgrund ihrer zentralen Rolle ermöglichen CI-Server direkte Supply-Chain-Angriffe und sind ein attraktives Ziel für Angreifer. Auch wenn dieses Risiko auf geteilten CI-Servern besonders groß ist, ist eine Isolierung zwischen Jobs und dem Host-System auch auf projektspezifischen Servern erstrebenswert. Das zu gewährleisten, ist die Aufgabe der Container-Laufzeitumgebung (typischerweise Docker, containerd oder CRI-O).

Um Container-Images zu erstellen, müssen wir (zumindest bis zu einem gewissen Grad) einen weiteren Container innerhalb des CI-Job-Containers starten. Dies liegt vor allem an den verschiedenen Features des Linux-Kernels, auf denen Container basieren. Ich werde in diesem Blogpost nicht näher auf diese eingehen. Wenn du dich dafür interessierst, empfehle ich folgende Ressourcen:

Es ist relativ einfach, die Isolierung zwischen dem CI-Job-Container und dem zu erstellenden Image wegzulassen – genau das macht Kaniko. Aus Sicherheitssicht ist dies kein allzu großes Problem, da der Job-Container nur vorübergehend existiert und beide in den meisten Fällen aus derselben Codebasis stammen. Es ist gleichermaßen einfach, die Isolierung zwischen dem Host und dem CI-Job-Container aufzugeben. Letzteres sollte jedoch vermieden werden, da es Angriffe auf den Host durch einen einzelnen CI-Job ermöglicht. Die gleichzeitige Beibehaltung beider Ebenen der Isolierung ist möglich, erfordert jedoch einen gewissen Aufwand.

Grafik zur Schachtelung und Isolierung zwischen CI-Job und Image-Build mit Kaniko, privileged Containern und dem IdealzielKaniko bietet keine Isolierung zwischen dem CI-Job-Container und dem gebauten Image. Andere Optionen isolieren stattdessen nicht zwischen dem Host und dem CI-Job-Container. Im Idealfall möchten wir beide Ebenen intakt lassen.

Wie das in der Praxis funktioniert, werden wir später in diesem Blogpost erfahren. Aber schauen wir uns zunächst einige einfachere Möglichkeiten an.

Option 1: Kaniko weiter verwenden

OK, das ist zwar keine wirkliche Alternative zu Kaniko, aber wir sollten diese Option nicht vorschnell abschreiben.

Zwar ist das ursprüngliche Image von Google (gcr.io/kaniko-project/executor) nach wie vor verfügbar und wird wahrscheinlich so schnell nicht verschwinden, dennoch ist es keine gute Idee, kritische Build-Schritte auf nicht mehr gewarteter Software aufzubauen.

Glücklicherweise ist Chainguard eingesprungen und entwickelt nun einen Fork von Kaniko. Der Haken daran ist, dass die Entwicklung zwar offen erfolgt und Source-Releases bereitgestellt werden, Binär-Releases und Container-Images jedoch nur zahlenden Kunden zugänglich sind.

Alternativ kann man selbst ein Image erstellen oder nach inoffiziellen, von der Community bereitgestellten Images suchen. GitLab plant derzeit, von ihnen erstellte Images bereitzustellen. Darüber hinaus gibt es mindestens einen weiteren, von der Community gepflegten Fork von Kaniko, der Container-Images kostenlos zur Verfügung stellt.

Was mich persönlich angeht, war ich schon seit geraumer Zeit unzufrieden mit Kaniko, noch bevor Google die Weiterentwicklung eingestellt hat. Vielleicht werden nun, da Chainguard die Entwicklung übernommen hat, einige seit Langem bestehende und grundlegende Bugs behoben. Dennoch werde ich das Gefühl nicht los, dass es sich um ein Tool handelt, dessen Zeit vorbei ist.

Option 2: Privilegierte Container oder gemountete Sockets

Der Vollständigkeit halber wollen wir auch Möglichkeiten erwähnen, bei denen keine Isolierung zwischen dem Host und dem CI-Job-Container besteht. Obwohl dies wahrscheinlich die ältesten Methoden zum Erstellen von Container-Images innerhalb von CI-Containern sind, sind sie nach wie vor recht verbreitet und werden in der GitLab-Doku empfohlen.

Entweder man startet einen Docker-in-Docker Job-Container („dind“) im privileged-Modus, wodurch dieser faktisch die volle Kontrolle über den Host erhält. Oder man mountet den Docker-Socket (/var/run/docker.sock) in den Job-Container. Auch wenn die letztere Option auf den ersten Blick sicherer erscheinen mag, bedeutet sie, dass der Job-Container beliebige andere Container starten kann, einschließlich solcher im privileged-Modus. Letztendlich gibt auch das die volle Kontrolle über den Host.

Wer meiner Argumentation von oben gefolgt ist, wird nicht überrascht sein, dass ich das aus der Sicherheitsperspektive für inakzeptabel halte.

Grafik zu Container-Verschachtelung und Isolierung mit privileged Containern und einem gemounteten Docker-Socket

Option 3: Buildah mit chroot-Isolation

Buildah ist ein Tool von Red Hat zum Erstellen von Container-Images. Es ist eng verzahnt mit Podman und dem übrigen Container-Ökosystem von Red Hat. Tatsächlich wird es intern verwendet, wenn man podman build ausführt, kann aber auch als eigenständiges Tool genutzt werden.

Was Buildah für uns besonders interessant macht, ist die Unterstützung verschiedener Isolationsmodi: Man kann den Grad der Isolation zwischen dem gebauten Image und seiner Umgebung (in unserem Fall also dem CI-Job-Container) über das Flag --isolation oder die Umgebungsvariable BUILDAH_ISOLATION anpassen.

Der Modus mit der geringsten Isolation in Buildah wird als chroot-Isolation bezeichnet. Laut der Manpage bedeutet dies, dass sich Buildah verhält wie folgt:

reusing the host's control group, network, IPC, and PID namespaces, and creating private mount and UTS namespaces, and creating user namespaces only when they're required for ID mapping

Anders als bei Kaniko müssen wir sicherstellen, dass der Container in der Lage ist, Mounts durchzuführen und User-Namespaces zu erstellen. Andererseits stellen procfs-Maskierung und schreibgeschützte cgroups im chroot-Isolationsmodus kein Problem dar.

Zum Laufen bringen mit Docker

Was ist tatsächlich erforderlich, um Buildah mit chroot-Isolation in der Praxis zum Laufen zu bringen? Betrachten wir zunächst den Fall des GitLab Docker Executors.

Standardmäßig wendet Docker ein seccomp-Profil an, das benötigte Syscalls wie mount() und unshare() unterbindet. Die einfachste Möglichkeit, dies zu umgehen, besteht darin, die seccomp-Filterung in der config.toml-Datei des GitLab Runners vollständig zu deaktivieren:

1[runners.docker]
2# ...
3security_opt= ["seccomp=unconfined"]

Die sauberere Methode besteht darin, ein benutzerdefiniertes seccomp-Profil bereitzustellen, das zusätzlich zu den Standardeinstellungen nur die erforderlichen Systemaufrufe zulässt. Ich habe eine angepasste Version des Standardprofils von Docker erstellt und stelle sie hier zur Verfügung. Beachte, dass man es, um es über den GitLab Runner bereitzustellen, als langen, einzeiligen JSON-String inline in der config.toml angeben muss:

1[runners.docker]
2# ...
3security_opt = ['seccomp={"defaultAction": ...

Unter Debian und Ubuntu wird Docker in der Regel durch AppArmor im „enforce“-Modus zusätzlich eingeschränkt. Andere Distributionen wenden womöglich SELinux-Regeln auf ähnliche Weise an. AppArmor und SELinux sind zusätzliche Ebenen, die in unserem Fall auch einige erforderliche Systemaufrufe verbieten. Wir können mit ihnen ähnlich wie mit seccomp umgehen: Entweder deaktivieren wir sie oder stellen benutzerdefinierte Profile bereit. Um AppArmor zu deaktivieren, fügt man die Option "apparmor:unconfined" zur config.toml hinzu:

1[runners.docker]
2# ...
3security_opt= ["seccomp=unconfined", "apparmor=unconfined"]

Genau wie für seccomp stelle ich eine angepasste Version von Dockers Standard-AppArmor-Profil bereit. Profile werden unter /etc/apparmor.d gespeichert und einmalig geladen über:

1apparmor_parser -r -W '/etc/apparmor.d/<profile-name>'

Anschließend referenziert man das Profil aus der config.toml wie folgt:

1[runners.docker]
2# ...
3security_opt= ["apparmor=<profile-name>"]

Für dieses minimale Beispiel muss Docker selbst nicht mit User-Namespaces ausgeführt werden. Buildah setzt jedoch intern einen User-Namespace auf. Damit das funktioniert, müssen unprivilegierte User die Berechtigung zum Erstellen von User-Namespaces haben. Dies sollte bei den meisten modernen Distributionen der Fall sein, aber beachte, dass neuere Versionen von Debian und Ubuntu AppArmor-Einschränkungen dafür eingeführt haben. Sowohl das Deaktivieren von AppArmor als auch die Verwendung meines benutzerdefinierten Profils lösen dieses Problem angemessen.

Sowohl seccomp als auch AppArmor sind Defense-in-Depth-Mechanismen, die wir durch unsere Anpassungen deaktiviert oder zumindest ein Stück weit abgeschwächt haben. Daher empfehle ich, dies zu kompensieren, indem Docker selbst mit User-Namespaces ausgeführt wird. Dazu kannst du den Anweisungen aus der offiziellen Docker-Dokumentation folgen. Der Rest der Konfiguration sollte weiterhin wie bisher funktionieren.

Leider erfordern alle Einstellungen – seccomp, AppArmor/SELinux und User-Namespaces – Anpassungen an der Konfiguration des Runners oder des CI-Hosts. Das heißt, dass man sie nicht als einfacher Nutzer von GitLab CI anwenden kann, sondern in der Lage sein muss, den eigenen GitLab Runner zu administrieren.

Das heißt auch, dass alle Änderungen für alle Jobs auf dem jeweiligen Runner gelten, nicht nur für den Build von Container-Images. Du kannst natürlich einen speziellen Runner einrichten und ihn mithilfe von Tags für Image-Builds verwenden. Die Sicherheitsvorteile sind jedoch überschaubar, wenn dieser Runner weiterhin von denselben Projekten und Entwickler*innen genutzt werden kann.

Zum Laufen bringen mit Kubernetes

Mit dem GitLab Kubernetes Executor ähnelt der allgemeine Ansatz dem für Docker. Da reines (vanilla) Kubernetes keine seccomp- oder AppArmor-Profile anwendet, ist die Wahrscheinlichkeit groß, dass Buildah mit chroot-Isolation von Haus aus funktioniert. Beispielsweise läuft es in einem Amazon EKS-Cluster mit Standardoptionen einfach so.

Einschränkungen können bestehen, wenn du einen gehärteten Kubernetes-Cluster hast und deine Möglichkeiten, diese als Cluster-Tenant zu überschreiben (z. B. durch das Setzen eines benutzerdefinierten seccomp-Profils) können durch Pod Security Standards oder einen Admission Controller wie Kyverno oder OPA Gatekeeper eingeschränkt sein. Das bedeutet, dass je nach deiner genauen Konfiguration möglicherweise einige Konfigurationsänderungen auf Ebene des Kubernetes-Clusters erforderlich sind.

Optionen für den GitLab Runner auf Kubernetes werden in der Regel nicht direkt in einer config.toml-Datei festgelegt, sondern über Helm-Values. Schauen wir uns zum Beispiel an, wie man ein eigenes seccomp-Profil auf Pods anwendet, die über GitLab CI gestartet werden. Bei Kubernetes werden Profile aus JSON-Dateien im Verzeichnis /var/lib/kubelet/seccomp auf dem Dateisystem des Nodes geladen. Man würde dies zur Helm-Datei values.yml hinzufügen:

1runners:
2  config: |
3    [[runners]]
4      environment = ["FF_USE_ADVANCED_POD_SPEC_CONFIGURATION=true"]
5
6      [runners.kubernetes]
7        namespace = "{{ default .Release.Namespace .Values.runners.jobNamespace }}"
8        image = "alpine"
9
10        # Siehe https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3904#note_1322875439
11        [[runners.kubernetes.pod_spec]]
12        patch = '''
13          containers:
14            - name: build
15              securityContext:
16                seccompProfile:
17                  type: Localhost
18                  localhostProfile: <profile-name>.json
19        '''
20        patch_type = "strategic"

Was User-Namespaces betrifft, gelten dieselben Überlegungen wie beim Docker-Runner-Executor: Buildah nutzt diese intern, weshalb auf den Cluster-Nodes unprivilegierte User-Namespaces verfügbar sein müssen. Neuere Versionen von Kubernetes unterstützen zudem die Ausführung des Buildah-Pods selbst in einem User-Namespace. Dies für Image-Builds zum Laufen zu bringen, kann jedoch eine knifflige Angelegenheit sein. Wir werden das unten bei Option 4 diskutieren.

Der CI-Job

Wie sieht ein GitLab-CI-Job für Buildah mit chroot-Isolation nun konkret aus?

In unserem Beispiel verwenden wir das offizielle Container-Image quay.io/buildah/stable. Du kannst natürlich auch jedes andere Image verwenden und Buildah beispielsweise über die Paketverwaltung installieren. Ein Nachteil des offiziellen Images ist, dass es im Gegensatz zu den allgemeinen Standardeinstellungen von Buildah bereits für die Verwendung von fuse-overlayfs vorkonfiguriert ist.

Ich nehme an, dass dies hauptsächlich historische Gründe hat, da Fuse-Overlays früher in User-Namespaces unterstützt wurden, reguläres OverlayFS hingegen nicht. Seit Linux-Kernel 5.13 ist dies jedoch nicht mehr der Fall, sodass es heutzutage kaum noch Gründe gibt, sich mit den Komplikationen der FUSE-Einrichtung auseinanderzusetzen. Andere Anleitungen, wie beispielsweise GitLabs Buildah-Tutorial für rootless, empfehlen die Verwendung des VFS Storage-Treibers von Buildah, was jedoch zu einer geringeren Performance und einer höheren Festplattennutzung führen kann.

Der einfachste Weg, den Container dazu zu bringen, OverlayFS zu verwenden, besteht darin, die Optionen mount_program = und mountopt = aus /etc/containers/storage.conf auszukommentieren. Einschließlich dieser Änderung sieht eine vollständige .gitlab-ci.yml-Datei wie folgt aus:

1stages:
2  - build
3
4build_image:
5  stage: build
6  image: quay.io/buildah/stable
7  before_script:
8    - buildah login -u "$CI_REGISTRY_USER" --password "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
9    - sed -i 's/^mount_program =/#&/' /etc/containers/storage.conf
10    - sed -i 's/^mountopt =/#&/' /etc/containers/storage.conf
11  script:
12    - buildah build --isolation chroot -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
13    - buildah push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

Alles in allem ist Buildah mit chroot-Isolation eine attraktive Option, die heute vielen Nutzer*innen offensteht. Je nach Runner-Setup funktioniert es möglicherweise sofort oder erfordert Anpassungen an der Runner-Konfiguration, der Host-Maschine oder dem Kubernetes-Cluster. Nun, da wir Buildah zum Laufen gebracht haben, warum sollten wir dann bei der chroot-Isolation bleiben? Können wir noch mehr Isolation zwischen dem CI-Job-Container und dem gebauten Image erreichen?

Option 4: Buildah mit rootless-Isolation

Eine Isolationsstufe über von chroot-Isolation hinaus bietet Buildah rootless-Isolation. Im Vergleich zur chroot-Isolation erhalten wir nun auch private IPC- und PID-Namespaces für das gebaute Image.

Ein separater PID-Namespace erfordert das Mounten eines neuen procfs, was bedeutet, dass das von Container-Laufzeitumgebungen durchgeführte Shadowing von /proc zu einem Problem wird. Kurz gesagt ist es dem CI-Job-Container nicht gestattet, ein neues procfs zu mounten, da er von vornherein keinen Zugriff auf das vollständige /proc hat.

Im Allgemeinen können wir dieses Verhalten für Docker deaktivieren, indem wir Container mit --security-opt systempaths=unconfined starten. Ohne die Maskierung wird die Aktivierung von User-Namespaces dringend empfohlen, um nicht-isolierte privilegierte Operationen innerhalb des Image-Builds zu verhindern.

Leider gibt es derzeit keine Möglichkeit, den GitLab Docker Executor anzuweisen, Job-Container mit der Option systempaths=unconfined zu starten. Dies ist als GitLab Runner Issue #36810 bekannt. Das bedeutet, dass wir derzeit die rootless-Isolation mit dem Docker Executor nicht nutzen können.

Für den Kubernetes Executor kann das proc-Masking über die values.yml-Datei von Helm deaktiviert werden. Die vollständigen Runner-Einstellungen, einschließlich aktivierter User-Namespaces und des oben genannten seccomp-Profils, sehen dann wie folgt aus:

1runners:
2  config: |
3    [[runners]]
4      environment = ["FF_USE_ADVANCED_POD_SPEC_CONFIGURATION=true"]
5      builds_dir = "/tmp/builds"
6
7      [runners.kubernetes]
8        namespace = "{{ default .Release.Namespace .Values.runners.jobNamespace }}"
9        image = "alpine"
10
11        # Siehe https://docs.gitlab.com/runner/executors/kubernetes/#user-namespaces
12        privileged = false
13        allowPrivilegeEscalation = false
14        logs_base_dir = "/tmp"
15        scripts_base_dir = "/tmp"
16
17        [[runners.kubernetes.pod_spec]]
18        name = "hostUsers"
19        patch = '''
20          [{"op": "add", "path": "/hostUsers", "value": false}]
21        '''
22        patch_type = "json"
23
24        # Siehe https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3904#note_1322875439
25        [[runners.kubernetes.pod_spec]]
26        patch = '''
27          containers:
28            - name: build
29              securityContext:
30                procMount: Unmasked
31                seccompProfile:
32                  type: Localhost
33                  localhostProfile: <profile-name>.json
34        '''
35        patch_type = "strategic"

Die genauen Schritte hängen stark von den Komponenten deines Kubernetes-Clusters, deren Konfiguration und dem Umfang an Hardening-Maßnahmen ab.

Zu bedenken sind unter anderem folgende Aspekte:

  • Du benötigst mindestens Kubernetes 1.33, damit User-Namespaces allgemein verfügbar sind.
  • Da Kubernetes für tmpfs ID-mapped mounts verwendet, benötigst du außerdem mindestens Linux-Kernel 6.3.
  • Mit dem containerd-CRI ist mindestens containerd 2.0 erforderlich.
  • Die Interaktion zwischen Kubernetes, containerd und User-Namespaces führt zu einem ungelösten Spezialfall, der das vom quay.io/buildah-Image definierte Volume /var/lib/containers betrifft.

Der letztgenannte Teil lässt sich beheben, indem man die Volumes manuell über eine zusätzliche Konfigurationsanweisung in der values.yml einbindet. Dies passt eine generische Runner-Konfiguration jedoch speziell für die Verwendung von Buildah an:

1runners:
2  config: |
3        # ...
4        # (Siehe oben)
5
6        # Workaround für https://github.com/containerd/containerd/discussions/12212
7        # weil: "'overlay' is not supported over overlayfs"
8        # Siehe auch: https://github.com/containers/image_build/blob/943ddba/buildah/Containerfile
9        [[runners.kubernetes.volumes.empty_dir]]
10        name = "var-lib-containers"
11        mount_path = "/var/lib/containers"
12
13        [[runners.kubernetes.volumes.empty_dir]]
14        name = "home-local-share-containers"
15        mount_path = "/home/build/.local/share/containers"

rootless-Isolation bietet zwar verstärkte Isolierung, ist aber noch aufwendiger einzurichten als chroot-Isolation. Ich würde vorschlagen, zunächst chroot-Isolation zum Laufen zu bringen und diese dann, sofern es deine Umgebung zulässt, auf rootless zu steigern.

Option 5: BuildKit im rootless-Modus

In vielerlei Hinsicht ist BuildKit für Docker das, was Buildah für Podman ist. In der Tat wird BuildKit heutzutage intern aufgerufen, wenn man docker build aufruft. Obwohl BuildKit weniger als Buildah darauf ausgelegt ist, als einzelne Anwendung aufgerufen zu werden, kann es dennoch eigenständig ausgeführt werden.

Der rootless-Modus von BuildKit verhält sich standardmäßig ähnlich wie die rootless-Isolation von Buildah. PID-Namespaces können optional über das Flag --oci-worker-no-process-sandbox deaktiviert werden, wodurch das Verhalten eher dem von Buildah mit chroot-Isolation angeglichen wird.

Die Anforderungen entsprechen denen für Buildah (wie in Option 3): seccomp und AppArmor / SELinux müssen deaktiviert oder angepasst werden. User-Namespaces für den BuildKit-Container selbst können optional verwendet werden. Ohne --oci-worker-no-process-sandbox muss das Proc-Masking deaktiviert werden (wie in Option 4).

Basierend auf dem Beispiel aus der GitLab-Dokumentation können wir eine .gitlab-ci.yml-Datei für BuildKit wie folgt erstellen:

1stages:
2  - build
3
4build_image:
5  stage: build
6  image: moby/buildkit:rootless
7  variables:
8    BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
9  before_script:
10    - mkdir -p ~/.docker
11    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > ~/.docker/config.json
12  script:
13    - |
14      buildctl-daemonless.sh build \
15        --frontend dockerfile.v0 \
16        --local context=. \
17        --local dockerfile=. \
18        --output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA,push=true

Wenn ich mich recht erinnere, habe ich BuildKit mit dieser Konfiguration in der Vergangenheit erfolgreich verwendet. Bei meinen jüngsten Tests blieb der BuildKit-Build jedoch immer hängen, nachdem die Meldung „running server on /run/user/1000/buildkit/buildkitd.sock“ ausgegeben wurde. Da dies jedoch auch in einem privileged-Container passierte, gehe ich davon aus, dass es nichts mit der Isolierung zu tun hat.

Wenn man die Ursache für dieses Problem finden kann und, aus welchen Gründen auch immer, Buildah nicht verwenden möchte, könnte BuildKit eine geeignete Alternative sein.

Option 6: Virtuelle Maschinen

Nachdem wir uns nun eingehend mit den Interna von Containern und Isolation befasst haben, wollen wir einen Schritt zurücktreten und uns fragen, warum all dieser Aufwand überhaupt notwendig ist. Bisher haben wir versucht, das Problem aus einer Perspektive innerhalb des Container-Ökosystems zu lösen. Aber was, wenn wir einen Schritt zurücktreten und die Prämisse hinterfragen? Schließlich umgehen GitHub Actions und andere CI-Plattformen das Problem vollständig, indem sie vollständige Virtuelle Maschinen anstelle von Containern verwenden. Könnten wir dasselbe mit GitLab CI tun?

Damit dies funktioniert, bräuchten wir einen GitLab CI Executor, der pro Build-Job eine kurzlebige VM startet. Ich würde davon ausgehen, dass dies auf einer Virtualisierungstechnologie wie libvirt, VMware oder OpenStack oder sogar einem Cloud-Dienst wie Amazon EC2 aufbauen würde.

Die Option, die diesen Anforderungen am nächsten kommt, ist die GitLab-Doku zur Verwendung von libvirt mit dem Custom Executor. Diese Lösung nutzt eine überraschend einfache Sammlung von Shell-Skripten, um VMs mit libvirt zu starten. Ich kann die Produktionsreife nicht einschätzen, daher wäre ich an Erfahrungsberichten aus der Praxis interessiert. Es sollte klar sein, dass dies nicht als einfacher Nutzer von GitLab CI umsetzbar ist, sondern erhebliche Änderungen an der Runner-Infrastruktur erfordert.

Option 7: Fortgeschrittene Container-Laufzeitumgebungen

Geht man noch einen Schritt weiter, gibt es einige Projekte, die das Deployment-Modell von Docker und Kubernetes mit der Isolation virtueller Maschinen kombinieren. Bei richtiger Umsetzung könnte dies beispielsweise die Ausführung privilegierter Container ermöglichen, die dennoch vom Host-Rechner isoliert sind.

Diese Projekte lassen sich grob in zwei Kategorien einteilen: Solche, die Hardware-basierte Virtualisierung voll ausnutzen, und solche, die bei einer Softwarelösung bleiben.

In der Kategorie der Hardware-basierten Lösungen ist Kata Containers ein weit verbreitetes Projekt. Kata startet pro Container bzw. Pod eine leichtgewichtige VM, die diesen vom Host-System isoliert. Es kann als Container-Laufzeitumgebung in containerd integriert werden und somit innerhalb von Kubernetes verwendet werden.

Firecracker ist eine von AWS entwickelte Open-Source-Technologie, die das Konzept der Mikro-VMs zur Isolierung von Containern populär gemacht hat. So bildet sie beispielsweise die Grundlage für AWS Lambda. Während das firecracker-containerd-Projekt noch nicht bereit für den produktiven Einsatz mit Kubernetes zu sein scheint, kann Firecracker als zugrunde liegender Hypervisor in Kata Containers verwendet werden.

gVisor von Google ist eine softwarebasierte Lösung, die zwischen den Container und den Host-Kernel geschaltet wird. Sie fängt alle Systemaufrufe ab und implementiert eine wesentliche Teilmenge der Kernel-API im Userspace. Ähnlich wie Kata Containers lässt sich gVisor in containerd integrieren und ist zudem auf der Google Kubernetes Engine als GKE Sandbox verfügbar.

Schließlich verspricht sysbox, durch eine ausgeklügelte Kombination aus Features des Linux-Kernels und eigens entwickeltem Code alle Arten von Container-Workloads zu ermöglichen, einschließlich verschachtelter Container. Tatsächlich steht sysbox vor denselben Herausforderungen, die wir oben besprochen haben, und basiert auf vielen der gleichen Technologien: User-Namespaces, ID-mapped mounts und strategische Mounts von Volumes, um OverlayFS auf OverlayFS zu vermeiden. Was sysbox auszeichnet, ist die Virtualisierung von procfs und sysfs, um Masking-Probleme zu vermeiden. Das Unternehmen hinter sysbox wurde 2022 von Docker übernommen, und Docker bietet dessen Funktionen kommerziell als Enhanced Container Isolation an.

All diese fortgeschrittenen Container-Laufzeitumgebungen bieten leistungsstarke Funktionen für die Erstellung von Container-Images. Ich habe ihre Praxistauglichkeit und Kompatibilität mit GitLab CI allerdings noch nicht bewertet.

Auch wenn diese Optionen möglicherweise erhebliche Investitionen erfordern würden, sind sie solide und legen großen Wert auf Sicherheit. Wenn Sicherheit für dich oberste Priorität hat und du eine dedizierte CI-Plattform aufbaust, die über Jahre hinweg Bestand haben soll, könnte es sich lohnen, sie näher zu untersuchen. Lass mich wissen, wenn du Erfahrungen damit hast, sie zum Laufen zu bringen!

Bonusoption: Cloud Native Buildpacks

Update (2025-10-07): Seit der ersten Veröffentlichung dieses Artikels hat mein Kollege Jonas Hecht eine aktuelle Anleitung zur Verwendung von Cloud Native Buildpacks (CNBs) mit GitLab CI veröffentlicht.

CNBs sind eine deklarative Alternative zu Dockerfiles. Das bedeutet, dass man bestehende Dockerfiles ersetzen muss, aber die Alternative könnte einfacher sein und sich gut in deine Software-Build-Tools integrieren lassen.

Nach meinem Verständnis ähnelt der Build-Prozess für CNBs mit Paketo.io auf GitLab CI dem von Kaniko, ohne Isolation zwischen dem CI-Job-Container und dem gebauten Image. Für eine vollständige, praktische Anleitung empfehle ich die Lektüre von Jonas’ Artikel.

Fazit

Wir haben eine Vielzahl von Optionen umfassend beleuchtet, von der (vorläufigen) Beibehaltung von Kaniko bis hin zu einer kompletten Neugestaltung deiner CI-Runner-Infrastruktur. Jede Option bringt unterschiedliche Vor- und Nachteile mit sich und unterscheidet sich hinsichtlich der Einfachheit ihrer Umsetzung.

Was für dich am besten funktioniert, hängt stark von deiner bestehenden Infrastruktur und dem Grad der Kontrolle ab, den du über deine CI-Umgebung hast. Um eine praktische Orientierungshilfe zu geben:

  • Wenn du eine schnelle Lösung benötigst und bereit bist, etwas Geld auszugeben oder einem Community-Fork zu vertrauen, bleibe bei Kaniko (Option 1).
  • Wenn du vollen Admin-Zugriff auf deine eigenen Runner hast, verwende Buildah mit chroot-Isolation (Option 3).
  • Wenn deine Runner auf einem topaktuellen Kubernetes-Cluster laufen, kannst du auch Buildah mit rootless-Isolation in Betracht ziehen (Option 4).
  • Wenn du einen geteilten Runner ohne Admin-Rechte nutzt, ist es am besten, weiterhin ein Kaniko-Image zu verwenden (Option 1) und darauf zu hoffen, dass die anderen Optionen in Zukunft auf geteilten Plattformen besser verfügbar werden.

Vielen Dank an Laura Spork und Simon Ruderich für ihr wertvolles Feedback zu einer früheren Version dieses Artikels.

Beitrag teilen

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//
Jetzt für unseren Newsletter anmelden

Alles Wissenswerte auf einen Klick:
Unser Newsletter bietet dir die Möglichkeit, dich ohne großen Aufwand über die aktuellen Themen bei codecentric zu informieren.