Im letzten Kapitel dieses Buchs wollen wir unser Blickfeld erweitern auf Anwendungsfälle von GitOps außerhalb des Kubernetes-Umfelds. Damit schließen wir gewissermaßen den Kreis und besinnen uns wieder auf das, was GitOps im Kern ausmacht: die vier Prinzipien. Wer GitOps umsetzen möchte, ist nämlich keineswegs dazu gezwungen, Kubernetes zu benutzen – die Prinzipien sind allgemeingültig und schließen keine Anwendungsplattform per se aus.
Dennoch haben wir bereits in Abschnitt 1.2.4 auf Seite 14 gesehen, warum GitOps sehr oft (und oftmals berechtigt) mit Kubernetes assoziiert wird: Einerseits liegt das in der Bauweise von Kubernetes begründet und andererseits in seiner Verbreitung: Die GitOps-Prinzipien 1, 3 und 4 legen ihren Fokus auf Deklarationen und kontinuierliche Mechanismen. Kubernetes wiederum ist als eine große Kontrollschleife implementiert, die beständig deklarierte Zustände zur Konvergenz führt.
Dieses Design begünstigt zum einen die Entstehung von GitOps-Operatoren auf Kubernetes-Basis. Zum anderen sorgt die weitläufige Verbreitung von Kubernetes ergänzend dafür, dass bevorzugt GitOps-Operatoren auf Kubernetes-Basis aufmerksam verfolgt und weiterentwickelt werden.
Wenn wir uns also mit GitOps außerhalb von Kubernetes beschäftigen, werden wir uns (zumindest im aktuellen Stand unserer Branche) immer in einem Spannungsfeld befinden, das sich zwischen diesen beiden Tatsachen auftut:
Wenn wir in dieses Thema einsteigen, könnten wir unmittelbar in die Technologie eintauchen und uns mit den momentan existierenden Tools auseinandersetzen, die GitOps außerhalb von Kubernetes anbieten (oder teilweise »GitOps für nicht nur Kubernetes«). Stattdessen wollen wir noch einmal die Prinzipien betrachten:
Unsere manuelle Verantwortung bleibt: Manifeste pflegen.
Im Großteil des Buches haben wir uns mit den Platzhirschen Flux und Argo CD beschäftigt. Diese Tools bieten uns die GitOps-Prinzipien 3 und 4 als Dienstleistung an. Die Verantwortung für die Umsetzung der Prinzipien 1 und 2 hingegen liegt immer auf unseren Schultern. Dazu gehören folgende Tätigkeiten:
Tool-Support können wir nur bei Prinzip 3 und 4 erwarten.
Auch wenn wir uns außerhalb von Kubernetes bewegen, wird kein Tool uns diese Verantwortung abnehmen können. GitOps-Operatoren kümmern sich hingegen um Tätigkeiten, die mit den anderen beiden Prinzipien zu tun haben:
Wir werden uns im weiteren Verlauf dieses Kapitels grob an der Reihenfolge der GitOps-Prinzipien orientieren:
Prinzip 2 ignorieren wir dabei aus folgenden Gründen: Der technische Aspekt der Versionierung spielt keine Rolle, weil er komplett entkoppelt ist von der Zielplattform (wir könnten Manifeste in Git oder Subversion speichern und es gäbe keinen Unterschied, ob wir auf Kubernetes oder eine andere Plattform deployen). Den inhaltlichen Aspekt der Unveränderlichkeit ignorieren wir ebenfalls, weil je nach Infrastructure-as-Code-Format die semantischen Feinheiten zu spezifisch für die jeweilige Plattform und die jeweilige Syntax sind.
Um GitOps implementieren zu können, müssen zwei grundlegende Voraussetzungen erfüllt sein:
Dateiformate, die diese Anforderung erfüllen, nennen wir im Folgenden Infrastructure-as-Code-Formate (IaC-Formate).
Diese Anwendung muss kein GitOps-Operator sein! Es kann auch einfach der native Interpreter des Dateiformats sein.
Ein Gegenbeispiel für ein IaC-Format ist ein Ansible-Playbook: Die einzelnen Tasks eines Ansible-Playbooks können zwar idempotent sein (das heißt, mehrfaches Ausführen führt zum selben Ergebnis), und man kann ein Playbook auch als Ansible-Modul umformulieren, sodass es nach außen eine deklarative Schnittstelle bietet. Dennoch werden die einzelnen Tasks des Playbooks in einer festen Reihenfolge ausgeführt, und dadurch ist das Grundprinzip von Ansible-Playbooks imperativ.
Bei einigen der nachfolgend aufgeführten Tools gibt es zwar Möglichkeiten, explizite Abhängigkeiten zwischen Ressourcen zu beschreiben und dadurch Einfluss auf die Reihenfolge von bestimmten Aktionen zu nehmen. Doch keines dieser Tools ist in seinem Grundaufbau imperativ.
Die momentane Auswahl an IaC-Formaten ist sehr begrenzt.
In Tabelle 12–1 auf Seite 337 haben wir diejenigen IaC-Formate aufgeführt, die wir bei unserer Recherche als deklarative Formate identifizieren konnten. Die Tabelle gibt auch hinsichtlich der Anwendungsfälle eine grobe Orientierung, sodass klarer ist, welches Tool welche Anforderungen ungefähr abdecken kann. Die letzte Spalte führt nur diejenigen GitOps-Operatoren für das jeweilige Dateiformat auf, die nicht auf Kubernetes angewiesen sind.
Explizit ausgelassen aus dieser Aufstellung haben wir alle Formate, die spezifisch nur für einen bestimmten Cloud-Provider nutzbar sind. Darunter fallen die folgenden:
Aus dieser Zusammenstellung der Tools leiten wir einige Erkenntnisse ab:
Tab. 12–1
IaC-Formate und abgedeckte Anwendungsfälle
Wir gehen weiter von GitOps-Prinzip 1 zu den Prinzipien 3 und 4. Im vorherigen Abschnitt haben wir bereits einige Tools gesehen, die als GitOps-Operatoren für bestimmte Dateiformate fungieren können.
Als Ergänzung dazu nennen wir an dieser Stelle weitere GitOps-Operatoren, die wir zusätzlich fanden und die nicht in unser bisheriges Schema passen:
Wir haben in Abschnitt 12.1 auf Seite 334 gesehen, dass ein GitOps-Operator für uns die Tätigkeiten übernimmt, die mit den Prinzipien 3 und 4 verbunden sind. Im Sinne der GitOps-Prinzipien ist damit eigentlich schon alles gesagt, und es ergeben sich keine weiteren Verantwortlichkeiten. Dennoch führen reife GitOps-Operatoren wie Flux und Argo CD einige weitere Tätigkeiten durch, die über die Grundanforderungen der Prinzipien 3 und 4 hinausgehen.
GitOps-Operatoren sollten mehr erfüllen als nur Prinzipien 3 und 4.
Einige dieser Features haben wir in den vergangenen Kapiteln dieses Buches bereits ausführlich behandelt. Aus unserer Sicht sind einige dieser Features essenziell wichtig für einen gesunden Anwendungsbetrieb auf GitOps-Basis. Wir identifizieren darunter besonders folgende Funktionalitäten:
Tab. 12–2
GitOps-Operatoren und Features
Pruning ist streng genommen ein essenzieller Teil von Prinzip 4, der kontinuierlichen Angleichung. Allerdings ist eine kontinuierliche Angleichung ohne Pruning deutlich einfacher zu implementieren, da man keine Rücksicht auf die Git-Historie nehmen muss. Deswegen führen wir Pruning als separates Feature auf.
In Tabelle 12–2 vergleichen wir die bisher identifizierten GitOps-Operatoren außerhalb von Kubernetes im Hinblick auf diese Features. Gerade bei Pruning, das grundlegend wichtig bei Prinzip 4 ist, zeigt sich ein sehr gemischtes Bild.
Bisher haben wir analysiert, welche GitOps-Operatoren uns außerhalb von Kubernetes zur Verfügung stehen. Ausgegangen sind wir dabei von deklarativen Dateiformaten, für die es bereits zumindest eine CLI gibt, mit der wir diese Deklarationen ausrollen können. Wir haben gesehen, dass es nicht für alle diese Dateiformate GitOps-Operatoren außerhalb von Kubernetes gibt oder dass vorhandene GitOps-Operatoren in ihren Funktionalitäten unzureichend sind. Wie gehen wir also vor, wenn wir den Punkt erreicht haben, an dem wir uns dazu entscheiden, unseren eigenen GitOps-Operator zu schreiben?
GitOps Engine und GitOps Toolkit können vielleicht als Starthilfe dienen.
Als hilfreiches Startmaterial für einen eigenen GitOps-Operator kann der Code von Argo CD und Flux dienen. Interessanterweise waren die Teams hinter Argo CD und Flux schon früh an einer Kollaboration interessiert, um ihre Anstrengungen zu bündeln. Im GitHub-Repository der GitOps Engine von Argo CD20 kann man die Entstehungsgeschichte nachverfolgen:
Die GitOps Engine wurde entwickelt, um langfristig eine gemeinsame Codebasis zu haben, auf der sowohl Argo CD als auch Flux ihre jeweiligen Anwendungen basieren würden. Der aufwendige Versuch vonseiten des Flux-Teams, die Codebasis von Flux v1 auf die GitOps Engine aufzubauen, war allerdings offenbar so komplex, dass er mit einer der Auslöser für den vollständigen Rewrite war, der zur heutigen Flux v2 führte. Aus unserer Sicht ist die Entwicklungsgeschwindigkeit hinter der GitOps Engine größtenteils verpufft; das letzte Release war im August 2022.
Flux hingegen hat im Gegensatz zu Argo CD mit seinem GitOps Toolkit21 eine sehr modulare Struktur mit verschiedenen Controllern22, die auch als einzelne Go-Packages importiert werden können. Dennoch rechnen sowohl die GitOps Engine von Argo CD als auch die Komponenten des GitOps Toolkit von Flux in allem mit Kubernetes-Ressourcen, sodass ein Umschreiben des Codes auf eine Situation ohne Kubernetes sehr aufwendig sein könnte. Wir haben bei einer kurzen Recherche auch einige kleine Module in den Ökosystemen von Rust und Python gefunden, die beim Bauen eines GitOps-Operators wertvolle Bausteine liefern können, aber die Auswahl ist ziemlich dünn.
Wenn wir anfangen, für einen spezifischen Anwendungsfall ein Softwareprodukt zu entwickeln (sei es ein GitOps-Operator oder eine beliebige Softwarelösung), dann wird dieses Produkt immer auf die ganz konkrete Problemstellung und den umgebenden Kontext angepasst sein. Deswegen würden mit Sicherheit die allermeisten unserer Versuche, allgemeingültige Ratschläge für das Entwickeln von GitOps-Operatoren zu erteilen, nichts nützen, weil die Annahmen dahinter im jeweiligen Anwendungsfall oft nicht zutreffen.
Stattdessen wollen wir von zwei konkreten Fällen aus unserer Beratungserfahrung erzählen, in denen wir uns entschieden haben, einen eigenen GitOps-Operator zu schreiben. Womöglich können diese praktischen Fälle, unsere Überlegungen dazu und die resultierenden Entscheidungen eine gute Hilfestellung dafür sein, wenn du in einem Szenario angelangt bist, wo du einen eigenen GitOps-Operator schreiben willst.
Container-Orchestrierung mit Docker Swarm
Im betreffenden Projekt waren wir mittendrin, dem Kunden dabei zu helfen, von einem langsameren Softwareentwicklungsprozess mit klaren Trennlinien zwischen Entwickelnden und Admins hineinzuwachsen in einen DevOps-naheren Prozess. Dabei spielten CI/CD und (zum ersten Mal in der Geschichte des Kunden) auch Container eine große Rolle. Der Kunde hatte sich im Rahmen einer Technologieauswahl gegen Kubernetes entschieden und wählte Docker Swarm als Plattform.
Wir waren also in der Lage, unsere Anwendungen als Container zu betreiben. Und für Docker Swarm stehen Docker-Stack-Manifeste als Format bereit, mit dem wir unsere Anwendungen deklarativ ausdrücken und in Config-Repos versionieren konnten. (Docker-Stack-Manifeste sind YAML-Dokumente, die auf Basis der Docker-Compose-Spezifikation23 geschrieben werden. Docker Swarm, das für Container-Orchestrierung auf mehreren Nodes gebaut ist, interpretiert dieselbe Spezifikation anders als Docker Compose, das nur für das Deployen auf einem einzelnen Host konzipiert ist.) Mit diesem Aufbau waren wir ganz grundsätzlich bereits an dem Punkt, dass wir die GitOps-Prinzipien 1 und 2 erfüllten.
Mehrere Teams, Config-Repos und Environments
Mehrere Teams von verschiedenen Dienstleistern waren für den Kunden tätig, und jedes Team nutzte ein oder mehrere Config-Repos, in denen die Docker-Stack-Manifeste mit ihren Workloads definiert waren. Für die Anwendungsentwicklung waren vier Environments vorgesehen (Dev, Test, Staging, Prod). Da die Unterschiede zwischen den Environments nur geringfügig waren, versuchten wir eine passende Struktur für die Manifeste zu finden, um Wiederholung von identischem Konfigurationscode zu vermeiden. Wir suchten nach einer dateibasierten Struktur ähnlich den Bases und Overlays von Kustomize24, konnten aber nichts finden.
Selbstgebautes Overlay-Rendering
Also bauten wir uns eine eigene behelfsmäßige Struktur aus Base- und Overlay-Manifesten. Die Overlay-Manifeste referenzierten ein oder mehrere Base-Manifeste über YAML-Kommentare mit einer bestimmten Syntax. Und so, wie man sich bei Kustomize mit dem Befehl kustomize build (oder kubectl kustomize) das Endergebnis eines Overlays ausgeben lassen kann, bauten wir uns einen Rendering-Befehl für das finale Docker-Stack-Overlay-Manifest: einen Shell-Einzeiler auf Basis von docker stack config25.
Plattformbetrieb mit Verantwortung für Host-Setup
Wir waren allerdings als Dienstleister nicht nur dafür zuständig, Anwendungsentwicklung für den Kunden zu betreiben. Als Teil der Digitalisierungsinitiative bauten wir zusätzlich eine Entwicklungsplattform auf und betrieben also auch Platform Engineering für den Kunden und die anderen Dienstleister. Dementsprechend waren wir auch dafür zuständig, ein gewisses Basis-Setup auf den virtuellen Maschinen durchzuführen, um den Betrieb der Docker Swarms in den verschiedenen Environments zu ermöglichen.
Eventgetriebenes CD als Anfang
Anfangs führten wir das Basis-Setup auf den VMs komplett manuell aus und hatten dafür ein Runbook mit Schritten, die man teilweise einfach in eine Shell kopieren konnte. Wir setzten nämlich größtenteils auf die GitOps-Möglichkeiten von Portainer in der Community Edition. Leider mussten wir feststellen, dass erst die Business Edition Prinzip 3 und 4 ermöglicht. Der Kunde hatte sich gegen die Business Edition entschieden, und in der Community Edition war ein Triggern der Angleichung nur per API-Aufruf möglich.
Also bewegten wir uns als Erstes zumindest in Richtung Continuous Deployment: Jedes Config-Repo bekam einen finalen CI-Schritt, der nach dem Durchlauf einer Commit-Pipeline die Angleichung triggerte. Damit waren wir an jenem typischen Zustand angekommen, wo wir immerhin Continuous Deployment vom Standardbranch jedes Config-Repos hatten.
Allerdings waren Prinzip 3 und 4 an diesem Punkt immer noch nicht erfüllt, weil diese Angleichung nicht kontinuierlich passierte, sondern eben nur eventbasiert bei einem Commit. Ebenso war das Einbinden der Config-Repos in Portainer selbst ein mühsamer Prozess, der sich nicht in Git abbilden ließ, sondern nur interaktiv in der UI möglich war.
Ein Ansible-Playbook als GitOps-Operator
Schließlich entschieden wir uns, in Richtung einer vollwertigen GitOps-Lösung zu arbeiten, um auch Prinzip 3 und 4 abzudecken. Wir starteten mit einem Ansible-Playbook. Bevor wir die Komponenten dieses Playbooks beleuchten, erläutern wir zuerst, was uns in dieser Situation dazu bewog, Ansible zu wählen:
Unser GitOps-Operator fing dann als sehr einfach gestricktes Playbook an, das nur folgende drei Schritte ausführte (einziger frei wählbarer Parameter war das Environment):
Der erste Schritt entspricht dem, was der Source-Controller bei Flux macht. Der zweite und dritte Schritt entsprechen wiederum eher dem Prinzip von Argo CD, das zuerst Manifeste rendert und dann anwendet.
Den ersten Schritt implementierten wir anhand eines Tasks auf Basis des Ansible-Moduls ansible.builtin.git28, den dritten Schritt mittels eines Tasks auf Basis des Ansible-Moduls community.docker.docker_stack29. Der zweite Schritt bestand aus mehreren Tasks, die alle aus Ansible-Modulen aus der Core-Collection stammen. Wir zeigen im Folgenden eine verkürzte Zusammenstellung des Playbooks, die alle drei Schritte zusammen zeigt:
Listing 12–1
Ansible-Playbook als GitOps-Operator
1
- name: Clone all config repos
2
loop: "{{ config_repos | dict2items }}"
3
ansible.builtin.git: # ...
4
5
- name: Find all overlay manifests
6
loop: "{{ config_repos | dict2items }}"
7
ansible.builtin.find: # ...
8
register: manifest_paths
9
10
- name: Render overlay manifests, save to register
11
loop: {{ manifest_paths.results # ...
12
environment:
13
manifest: "{{ item }}"
14
ansible.builtin.shell:
cmd: |-
16
set -o pipefail
17
grep -E "^\# docker-stack-bases:" "$manifest" \
18
| awk -F':' "{print \"-c \
19
$(printf "%s" "${manifest%overlays/*}")/\" \$NF}" \
20
| xargs | xargs -t -I {} sh -c \
21
"docker stack config \
22
--skip-interpolation {} -c $manifest"
23
register: manifests
24
25
- name: Create directories for manifests on host
26
loop: "{{ manifests.results }}"
27
hosts: managers
28
ansible.builtin.file: # ...
29
- name: Create manifest files on host from register
30
loop: "{{ manifests.results }}"
31
hosts: managers
32
ansible.builtin.copy: # ...
33
register: manifest_paths_host
34
35
- name: Deploy Stacks from created manifests
36
loop: "{{ manifest_paths_host.results }}"
37
community.docker.docker_stack:
38
compose: ["{{ item.dest }}"]
39
prune: true
40
state: present
41
# ...
Im Repository, in dem wir dieses Playbook verwalteten, erstellten wir pro Environment eine Scheduled Pipeline30, jede mit einer Frequenz von 15 Minuten. Durch das Ausführen im CI-Server hatten wir den Vorteil, dass grundlegendes Alerting per E-Mail bei Fehlschlägen des Playbooks automatisch aktiv war für alle Contributors des Repositories.
Secrets Management mit SOPS
Für Secrets Management bauten wir zusätzlich die Möglichkeit ein, bei jedem Overlay eine mit SOPS31 und age32 verschlüsselte Dotenv-Datei zu lagern. Aus der jeweils entschlüsselten Dotenv-Datei wurden alle Key-Value-Paare als Umgebungsvariablen geladen und beim Rendern (Schritt 2) in die Manifeste interpoliert und mit auf den Hosts gespeichert.
Eine anonymisierte Version dieses Playbooks stellen wir auf GitLab zur Verfügung: https://gitlab.com/gitops-book/docker-swarm-gitops-ansible
Viele Features für wenig Aufwand
Vergleichen wir dieses Playbook einmal mit der Auflistung von essenziellen Features in Abschnitt 12.4 auf Seite 338:
Relativ langsame Ausführung der Angleichung
Warum wählten wir eine Frequenz von nur 15 Minuten, wenn Tools wie Flux oder Argo CD deutlich geringere Frequenzen ermöglichen? Zum einen hatten wir bereits einige Minuten Versatz beim Start von Scheduled Pipelines in GitLab erlebt, sodass manchmal die nächste Pipeline nach 20 Minuten, manchmal bereits nach 10 Minuten eingeplant wurde. Zum anderen war die langsamste Ausführungsdauer einer Pipeline (je nach Environment) bereits bei fast zwei Minuten. Wir wollten eine Überlastung unseres CI-Servers vermeiden, falls sich zu viele Pipelines aufstauen würden. Durch Parallelisierung von Loops in unserem Playbook hätten wir die Performance des Pipeline-Durchlaufs wahrscheinlich noch etwas verbessern können.
Wir hatten unseren GitOps-Operator auch noch aus einem weiteren Grund als Ansible-Playbook angefangen: Da wir Teile der Infrastruktur bisher noch manuell verwalteten, sahen wir Potenzial darin, auch diese Aktivitäten über das Playbook zu automatisieren. Damit wären wir nicht nur in der Lage, den Rollout unserer Anwendungen über den GitOps-Operator abzuwickeln, sondern auch die Provisionierung und Wartung unserer Docker Swarms – ähnlich wie wir auch in Kapitel 11 auf Seite 291 gesehen haben, dass sich GitOps im Kubernetes-Umfeld hervorragend für die Verwaltung für Infrastruktur eignet.
Wir erweitern um Docker-Swarm-Tasks.
Leider hatten wir für die Verwaltung unserer Infrastruktur kein deklaratives IaC-Dateiformat zur Hand. Somit griffen wir zum Community-Modul community.docker.docker_swarm33 und bauten uns mit einer Handvoll Tasks auf Basis dieses Moduls eine Prozedur zum Aufsetzen unserer Basisinfrastruktur.
Durch das Hinzufügen dieser Tasks wurde aus unserem Playbook mehr als ein reiner GitOps-Operator – seine Zuständigkeiten wuchsen und wurden diverser. Wir verwalteten jetzt sowohl unsere Container-Workloads als auch unsere Infrastruktur über den GitOps-Operator, aber nur die Workloads verwalteten wir wirklich auf GitOps-Weise.
Mehr Stabilität und Geschwindigkeit
Bis unser selbstgebauter GitOps-Operator wirklich ausgereift war, dauerte es ein paar Wochen. Doch schon bald konnten wir uns freuen über die erhöhte Stabilität unserer Deployments, die höhere Geschwindigkeit beim Erstellen neuer Stacks und die massive Erleichterung beim Provisionieren und Verwalten aller Environments.
Asynchronität erhöht Komplexität für Entwickelnde.
Durch das Einbauen des GitOps-Operators führten wir allerdings auch Asynchronität in das Gesamtsystem ein (siehe Kapitel 7 auf Seite 197): Der grüne Haken am neuesten Commit im Config-Repo bedeutete nicht länger, dass die neuesten Änderungen bereits deployt sind. Stattdessen musste man in den Pipelines des GitOps-Operators nachschauen. Das führte zu erhöhter Komplexität aufseiten der Entwickelnden, wenn sie versuchten zu debuggen, wann ihre neueste Änderung ausgerollt würde. Glücklicherweise war durch die Zentralisierung in einem einzigen Repository der Aufwand bei der Umstellung nicht besonders hoch, und durch Training und Dokumentation konnten wir Stück für Stück allen Beteiligten hineinhelfen in die neue Betriebsweise.
Eine Erweiterung des Commit-Status, wie es Flux34 und Argo CD35 ermöglichen, wäre eine mögliche Verbesserung mit relativ geringem Aufwand: Der GitOps-Operator notiert sich nach dem Rollout den Commit des jeweiligen Config-Repos und fügt über die API des SCM dem jeweiligen Commit einen zusätzlichen, erfolgreichen Commit-Status hinzu.
Unter den gegebenen Umständen waren wir insgesamt sehr zufrieden: Wir hatten es mit vergleichsweise geringem Aufwand und einer relativ guten Codewartbarkeit geschafft, einen GitOps-Operator zu erstellen, der sowohl unsere Workloads als auch unsere Infrastruktur in einer akzeptablen Frequenz und einem ausreichenden Feature-Set auf grundlegende GitOps-Weise verwaltet.
Im Kontext von selbstgeschriebenen GitOps-Operatoren können wir noch eine weitere Geschichte erzählen. Sie spielt zwar im Kubernetes-Umfeld und ist damit in diesem Kapitel nicht maximal passend, aber einige Erkenntnisse aus diesem Projekt sind für den Einsatz von GitOps auch außerhalb von Kubernetes lehrreich.
In diesem Projekt ging es darum, Helm-Charts in einen verwalteten Cluster zu deployen. RBAC auf diesem Cluster war so konfiguriert, dass nur das Plattformteam Operatoren installieren konnte; einzelne Projekte bekamen nur vorkonfigurierte Namespaces. Wir versuchten zwar, auf ein Installieren von Flux oder Argo CD hinzuarbeiten, aber die Zeit drängte, und so mussten wir eine andere Lösung finden.
Glücklicherweise stießen wir auf Helmfile36, ein deklaratives Dateiformat zur Verwaltung von Helm-Releases, ähnlich den HelmRepositories und HelmReleases von Flux. Leider gab es noch keinen Operator dafür, um solche Helmfiles aus einem Git-Repo heraus kontinuierlich auszurollen.
Aber dank Kubernetes war es nicht schwierig, genau das in wenigen Stunden zu realisieren: Wir schrieben einen CronJob, der vereinfacht gesagt nur einen git clone und einen helmfile apply durchführt. Der Aufbau war ungefähr so:
Listing 12–2
Ein GitOps-Operator für Helmfile-Repos auf CronJob-Basis
1
apiVersion: batch/v1
2
kind: CronJob
3
metadata:
4
name: helmfile-gitops-agent
5
spec:
6
# Every 5 minutes
7
schedule: "*/5 * * * *"
8
jobTemplate:
9
spec:
10
template:
11
spec:
12
volumes:
13
- name: cloned-repo
14
emptyDir:
15
# Clone repo
16
initContainers:
17
- name: git-clone
18
image: alpine/git
19
volumeMounts: &mounts
20
- mountPath: /cloned-repo
21
name: cloned-repo
22
command:
23
- /bin/sh
24
- -c
25
- git clone GIT_REPO_URL /cloned-repo
26
# Apply manifests
27
containers:
28
- name: helmfile
29
image: ghcr.io/helmfile/helmfile
30
volumeMounts: *mounts
31
workingDir: /cloned-repo
32
command:
33
- /bin/sh
34
- -c
35
- helmfile apply
Wenn sich hinter der GIT_REPO_URL ein Config-Repo mit einem Helmfile verbirgt, dann erfüllt dieser kleine CronJob die vier GitOps-Prinzipien problemlos:
Der Schritt dazu, aus diesem CronJob auch ein öffentliches Helm-Chart zu machen, war nicht groß: https://artifacthub.io/packages/helm/helmfile-gitops-agent/helmfile-gitops-agent
Dieser GitOps-Operator ist natürlich von den Features her nicht zu vergleichen mit Flux oder Argo CD, aber er erfüllt die grundlegenden Voraussetzungen, um auch in rechtemäßig sehr beschränkten Clustern mithilfe von GitOps Ressourcen zu verwalten.
Ein Detail ist allerdings auffällig: Ein Helm-Release in einem Helmfile zu löschen benötigt zwei Schritte:
Ebenso passiert auch kein automatisches Pruning von Ressourcen, wenn der CronJob gelöscht wird.
Wir lernen von den beiden selbstgeschriebenen GitOps-Operatoren einige wichtige Dinge: