12GitOps außerhalb von Kubernetes

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:

  1. Die GitOps-Prinzipien sind nicht an Kubernetes gebunden und gelten grundsätzlich.
  2. Jede Umsetzung der GitOps-Prinzipien außerhalb von Kubernetes ist momentan deutlich limitierter, weniger ausgereift und bietet weniger Features als GitOps-Operatoren auf Kubernetes.

12.1Aus den GitOps-Prinzipien folgende Verantwortlichkeiten

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:

  1. deklarative Manifeste schreiben und pflegen (Prinzip 1)
  2. diese Manifeste versioniert lagern (Prinzip 2)
  3. die Inhalte der Manifeste in einer unveränderlichen Weise formulieren (Prinzip 2)

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:

  1. die Manifeste kontinuierlich beziehen (Prinzip 3)
  2. den tatsächlichen Systemzustand kontinuierlich angleichen an den deklarativ beschriebenen Zielzustand (Prinzip 4)

Wir werden uns im weiteren Verlauf dieses Kapitels grob an der Reihenfolge der GitOps-Prinzipien orientieren:

  1. Prinzip 1:
    1. Welche Formate für Infrastructure as Code gibt es momentan (außer Kubernetes-Manifesten)? (Wir können nur solche Ressourcen mit GitOps verwalten, für die es deklarative Ausdrucksformen gibt.)
    2. Wie sieht die technische Unterstützung aus, wenn wir diese Formate im GitOps-Stil verwalten wollen?
  2. Prinzip 3 und 4:
    1. Welche GitOps-Operatoren gibt es außerhalb von Kubernetes?
    2. Welche Optionen habe ich, wenn es keine vorgefertigten GitOps-Operatoren für meine Situation gibt?

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.

12.2Infrastructure-as-Code-Formate

Um GitOps implementieren zu können, müssen zwei grundlegende Voraussetzungen erfüllt sein:

  1. Für die Ressourcen, die wir verwalten wollen, muss ein Dateiformat existieren, mit dem wir diese Ressourcen deklarativ ausdrücken können.

    Dateiformate, die diese Anforderung erfüllen, nennen wir im Folgenden Infrastructure-as-Code-Formate (IaC-Formate).

  2. Für das Dateiformat muss eine Anwendung existieren, die diese Deklarationen ausrollen kann.

    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

image

12.3Weitere GitOps-Operatoren

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:

  1. Ignite18 von Weaveworks, den ursprünglichen Autoren von FluxCD, verwaltet Firecracker-VMs auf GitOps-Weise. Als Dateiformat kommen Kubernetes-artige Manifeste zum Einsatz. Auch wenn das Einsatzgebiet sehr limitiert und das Projekt noch nicht über das Alpha-Stadium hinaus ist, begrüßen wir den »GitOps first«-Ansatz dieses Tools sehr.
  2. PipeCD19 ist ein CNCF-Sandbox-Projekt und fällt in diesem Kapitel in eine gewisse Grauzone: Die Control Plane des Tools läuft zwar in einem Kubernetes-Cluster, aber der eigentliche Operator piped, der effektiv die Angleichung ausführt, kann auf verschiedensten Plattformen laufen (beispielsweise auch ohne Container Runtime unmittelbar auf VMs). Quellformate, die deployt werden können, sind Kubernetes-Manifeste (auch Helm-Charts), Terraform-Dateien und Definitionen für AWS ECS Tasks/Services, AWS Lambdas und Anwendungen in Google Cloud Run.

12.4Features von GitOps-Operatoren

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:

  1. Bezug: Manifeste kontinuierlich beziehen (beispielsweise als git clone)
  2. Angleichung: neue und veränderte Manifeste ausrollen (beispielsweise als kubectl apply oder docker stack up)
  3. Pruning: in Git entfernte Ressourcen im System entfernen
  4. Secrets Management: in Git committete Secrets entschlüsseln oder Referenzen auf einen Secrets-Store auflösen (siehe Kapitel 5 auf Seite 97)
  5. Alerting: Benachrichtigungen verschicken (siehe Kapitel 8 auf Seite 229)

Tab. 12–2
GitOps-Operatoren und Features

image

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.

12.5Einen eigenen GitOps-Operator bauen

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.

12.6Eigene GitOps-Operatoren aus der Praxis

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.

12.6.1Docker Swarm und Ansible

Problemstellung und Kontext

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.

Erste Schritte hin zu CD

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.

Vollwertigeres GitOps mit Ansible-Playbook

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:

  1. Erfahrung: Einige unserer Teammitglieder hatten Erfahrung mit Ansible aus früheren Projekten, teilweise auch mit Python. YAML als relativ zugängliches Dateiformat für die Playbooks erzielte ebenfalls Pluspunkte.
  2. Infrastruktur: Wir hatten bereits SSH-Zugriff auf die VMs, und Ansible ist für SSH geschaffen.
  3. Idempotente Tasks: Ansible-Playbooks als Ganzes sind zwar ihrer Natur nach sequenziell und deshalb grundsätzlich imperativ (also nicht deklarativ), aber die einzelnen Tasks eines Playbooks sind als idempotente Aktionen gedacht26. (Das mehrfache Ausführen eines Tasks soll also immer zum selben Endresultat führen.)
  4. Großes Ökosystem: Die Community hinter Ansible hat einen umfassenden Katalog an fertig nutzbaren (und oftmals idempotenten) Tasks erstellt. Ansible Galaxy27 beherbergt über 30.000 sogenannte Collections. Unter diesen wurden wir schnell fündig, um unseren GitOps-Operator zusammenzubauen.

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):

  1. alle Config-Repos klonen
  2. alle Overlays (für das aktuell gewählte Environment) aus den Config-Repos rendern und als Dateien auf den Hosts speichern
  3. die Overlays ausrollen (mit docker stack up, ähnlich einem kubectl apply)

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:

15

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:

  1. Feature Bezug ist erfüllt: Manifeste werden kontinuierlich bezogen.
  2. Feature Angleichung ist erfüllt: Neue und veränderte Manifeste werden ausgerollt (über ein Modul, das vermutlich einen docker stack up --prune ausführt).
  3. Feature Pruning ist teilweise erfüllt: Docker-Services, die in Stack-Manifest entfernt werden, werden aufgeräumt. Wenn aber ganze Overlays in Git gelöscht werden, werden die entsprechenden Stacks nicht gelöscht. Eine simple Erweiterung für Stack-Pruning könnte so aussehen:
  4. Feature Secrets Management ist erfüllt: Das jeweilige Config-Repo dient als Secret-Store und lagert Dotenv-Dateien, die mit SOPS und age verschlüsselt wurden. Während des Rollouts werden Secrets entschlüsselt, interpoliert und in die gerenderten Manifeste geschrieben.
  5. Feature Alerting ist erfüllt: Benachrichtigungen im Fehlerfall werden verschickt.

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.

Infrastruktur mitverwalten

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.

Auswirkungen

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.

12.6.2Helmfile

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:

  1. Der Systemzustand wird deklarativ durch Helmfiles beschrieben.
  2. Die Manifeste lagern in Git.
  3. Der Init-Container bezieht die Manifeste kontinuierlich.
  4. Der Haupt-Container rollt die Manifeste kontinuierlich aus.

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:

  1. Man muss zuerst die Einstellung installed=false setzen und eine Reconciliation abwarten.
  2. Erst danach kann man das Release selbst aus dem Helmfile entfernen.

Ebenso passiert auch kein automatisches Pruning von Ressourcen, wenn der CronJob gelöscht wird.

12.6.3Lektionen

Wir lernen von den beiden selbstgeschriebenen GitOps-Operatoren einige wichtige Dinge:

  1. Ohne IaC-Format haben wir verloren: Bei dem Ansible-Operator war es Docker-Compose, bei dem zweiten Operator war es Helmfile. Falls für einen speziellen Use Case kein deklaratives Format vorhanden sein sollte, kann der Aufwand für das Konzipieren und Erstellen des entsprechenden Toolings so hoch sein, dass GitOps nicht praktikabel ist.
  2. Nicht jedes IaC-Format ist gleich gut geeignet für GitOps: Gerade beim Thema Pruning sind viele Tools und Formate bisher noch im Hintertreffen und nicht mit erstklassigem GitOps-Support ausgerüstet.
  3. Kubernetes ist selbst ohne Operatoren ein hervorragendes GitOps-Substrat: Mit wenigen Handgriffen einen GitOps-Operator zu bauen ist mit Kubernetes nicht schwer.