9Imperativ eingreifen

Im Betrieb einer Anwendung kann es manuelle Tätigkeiten geben, die punktuell nötig sind und nicht oder nur sehr schwer mit den GitOps-Prinzipien in Einklang zu bringen sind. Dazu wird mindestens das Bootstrapping zählen (siehe Abschnitt 6.6.1 auf Seite 168). Es kann aber auch weitere Situationen geben, die imperatives Eingreifen erfordern. Darunter sehen wir uns die folgenden exemplarischen Situationen genauer an:

In solchen Fällen müssen wir möglicherweise Dinge tun, die sich außerhalb der Reichweite unseres deklarierten Zustandes befinden oder damit in Konflikt treten. Wir schauen uns dafür einige beispielhafte, typische Situationen an und wie wir in einem GitOps-Umfeld damit umgehen. In diesem Kapitel möchten wir uns besonders mit solchen Fällen befassen, die sich eher in einer Grauzone befinden.

9.1Eindeutig ausgeschlossene Aktionen

Manche Aktionen in einem Zielsystem werden sich immer mit GitOps vereinbaren lassen. Ein offensichtliches Beispiel sind alle lesenden Zugriffe, beispielsweise mit den kubectl-Verben get, describe, diff, logs oder top. Auch das Interagieren mit einem laufenden Container (beispielsweise mit den kubectl-Verben cp, exec oder port-forward) widerspricht den GitOps-Prinzipien nicht, solange der Zugriff rein lesend erfolgt.

Es gibt aber auch manuelle Eingriffe, die ganz eindeutig mit GitOps in Konflikt stehen:

Secrets nehmen bei diesen eindeutig konfliktierenden Eingriffen manchmal eine Sonderrolle ein (siehe Kapitel 5 auf Seite 97). Davon abgesehen ist im normalen Betrieb jede der eben genannten Aktionen eindeutig den GitOps-Prinzipien entgegengesetzt, und wir sollten solche Aktionen nur in gut überlegten Ausnahmefällen durchführen.

Bevor wir uns einigen konkreten Situationen zuwenden, in denen wir uns in einem gewissen Graubereich befinden, wollen wir untersuchen, was wir riskieren oder gewinnen können, wenn wir imperativ handeln statt in der GitOps-Arbeitsweise.

9.2Risiken und Chancen

Wenn wir grundsätzlich im Stil von GitOps arbeiten und dennoch gewisse Teile unseres Systems der Verwaltung des GitOps-Operators vorenthalten, dann sind diese Systemkomponenten gewissen Nachteilen oder Risiken unterworfen:

  1. geringere Stabilität: Wenn wir den Zielzustand der Komponente nicht in Git vorliegen haben und auch nicht von einem GitOps-Operator anwenden lassen, sind sowohl der Betrieb als auch eine Wiederherstellung im Fehlerfall erschwert (siehe Abschnitt 2.4 auf Seite 34 und Abschnitt 2.7 auf Seite 39).
  2. mangelnde Auditierbarkeit: Wenn wir Komponenten nicht vorrangig in Git verwalten, benötigen wir zusätzliches Tooling, um die Auditierbarkeit von Änderungen gewährleisten zu können.
  3. mehr Komplexität und geringere Sicherheit durch zusätzliche Schnittstellen: Wir können die Komponente nicht durch ein Interagieren mit Git verwalten, sondern brauchen eine zusätzliche Konfigurationsmöglichkeit (beispielsweise eine CLI). In manchen Fällen brauchen wir bei deren Benutzung auch Zugriff auf das Zielsystem, was geringere Sicherheit nach sich ziehen kann.
  4. aufwendigeres Aufräumen: Wenn wir Komponenten entfernen wollen, haben wir ohne GitOps keine hilfreichen Leitplanken und müssen einen Clean Up eigenständig im Auge behalten (siehe Abschnitt 2.5 auf Seite 35).
  5. (eventuell) mehr Abhängigkeit von CI und weniger Sicherheit: Für den Fall, dass wir die Verwaltung der Komponenten durch CI automatisieren, müssen wir dem CI-Server unter Umständen Zugriff auf das Zielsystem geben. Auch das kann geringere Sicherheit nach sich ziehen (siehe Abschnitt 2.10 auf Seite 45).

Allerdings gewinnen wir für diese von GitOps ausgenommenen Komponenten auch Vorteile. Manche der eben genannten Risiken können unter manchen Umständen (beispielsweise in Fehlerfällen und während Incidents) sogar Chancen sein:

  1. Höhere Geschwindigkeit: Wir können schneller agieren und bei Fehlern iterieren, als wenn wir für jede Änderung einen Commit machen müssten.
  2. Mehr Kontrolle durch spezialisierte Schnittstellen: Wenn wir nicht jede erdenkliche Aktion durch das Bearbeiten oder Löschen von Manifesten ausführen müssen, stehen uns möglicherweise mehr Werkzeuge zur Verfügung, mit denen wir gezielter das erreichen können, was wir wollen.
    Darunter kann beispielsweise das Einspielen eines Datenbank-Backups mittels der Cloud-Provider-CLI oder das Wiederherstellen einer früheren Version eines S3-Objekts fallen.
  3. Erleichtertes Debugging durch manuelles Aufräumen: Wir können (im besten Fall) leichter Debugging durchführen, weil die Ressourcen, mit denen wir umgehen, nicht automatisch aufgeräumt werden und wir dadurch mehr Zeit gewinnen, um relevante Ressourcen zu analysieren.

In den folgenden Abschnitten betrachten wir eine Handvoll beispielhafter Situationen, die teilweise ein Überschreiten der GitOps-Prinzipien erfordern können, damit wir die eben genannten Chancen ergreifen können.

9.3Einen Debug-Pod starten

Manchmal reicht es für Debugging nicht aus, wenn wir einfach nur eine Shell in einem Container mit kubectl exec starten. Gerade bei Netzwerkproblemen oder bei Distroless-Images kann es manchmal erforderlich sein, einen flüchtigen Container zu starten, der notwendige Analysewerkzeuge vorinstalliert hat.

In Kubernetes ist das über folgende Befehle möglich1:

Mit diesen Befehlen können wir also Container mit einem beliebigem Image starten, um Debugging zu betreiben. Für den alltäglichen Betrieb von Containern in Kubernetes ist das imperative Starten von Pods eine schlechte Idee, weil wir diese Pods dann nicht im Config-Repo verwalten können und weil wir auch keinen übergeordneten Pod-Controller zur Verfügung haben. Für kurzfristige Fehlersuche hingegen wären das Schreiben und Committen eines passenden Manifests vergleichsweise aufwendig.

Debug-Pods mit automatischem Clean Up sind oft vertretbar.

Meistens sind die Nachteile, die uns durch das manuelle Ausführen eines Debug-Pods entstehen, gering und verschmerzbar verglichen mit der erhöhten Geschwindigkeit, die wir gewinnen. Folgende Parameter für die Debugging-Befehle sind allerdings sehr hilfreich, um den Lebenszyklus des flüchtigen Pods sauber abzuwickeln:

In Listing 9–1 sehen wir Beispielbefehle für kubectl run und kubectl debug.

Listing 9–1
Ausführen eines Debug-Pods

1

# Run self-sufficient debug Pod:

2

kubectl run tmp-shell --rm -it \

3

--image nicolaka/netshoot:v0.11

4

# Run temporary container inside Pod "my-pod"

5

# with access to process namespace "my-container":

6

kubectl debug -it my-pod --target=my-container

7

--image nicolaka/netshoot:v0.11

9.4Ein Backup wiederherstellen

Wenn wir GitOps umsetzen, haben wir Manifeste und verwalten sie in Git. Ein Bestandteil dieser Manifeste kann ein Deployment-Manifest sein, mit dem wir unsere Container-Images betreiben. Dadurch, dass wir den konkreten Image-Tag in Git festhalten, wissen wir immer, zu welchem Zeitpunkt welcher Image-Tag ausgeführt wurde.

Rollbacks über Git-Reverts lassen sich schlecht mit State koppeln.

Das Ausrollen einer neuen Version kann auch einmal schiefgehen (wie beispielsweise in Abschnitt 2.9 auf Seite 43). Wenn wir uns für einen Rollback entscheiden, können wir die Anwendungsversion sehr einfach zurücksetzen, indem wir den letzten Commit reverten. Aber was ist mit dem State, unserem Datenbestand? Den können wir nicht mit einem Revert zurücksetzen, denn unseren State verwalten wir in aller Regel nicht mit GitOps.

Warum verwalten wir unseren Datenbestand nicht auch mit GitOps? Würden wir das tun, dann müssten wir konsequenterweise die kompletten Inhalte unserer Data-Stores in Git versionieren und praktisch Git als Datenbank verwenden. Es gibt zwar Projekte wie GitRows2, die Git als zeilenbasierten Data-Store nutzbar machen, aber die Performance ist deutlich schlechter als traditionelle Datenbanken. Außerdem laufen wir in die gleichen Probleme wie in Kapitel 5 auf Seite 97, wenn unsere Daten Secrets enthalten. Von der DSGVO wollen wir erst gar nicht anfangen. Generell lagert unser Datenbestand also außerhalb des GitOps-Zyklus. Das bedeutet dann aber auch, dass wir unsere Deklarationen und unseren State in aller Regel nicht zusammen in einem Rutsch (beispielsweise durch einen einzigen Commit) zurücksetzen können.

Viele Managed Offerings ermöglichen deklarative Restores.

Allerdings gibt es bei den verwalteten Datenbank-Angeboten öffentlicher Cloud-Provider oftmals eingebaute Features für Backups und Restores. In vielen Fällen gibt es dann deklarative Möglichkeiten, um per Commit in einem Config-Repo eine Datenbank zurückzusetzen auf einen vorherigen Stand. Bei AWS RDS ist das beispielsweise über Terra-form3 oder Crossplane4 möglich. Wenn wir bereits solche Angebote nutzen, können wir durch das Spezifieren eines bestimmten Snapshots ein Backup in unsere Datenbank einspielen.

Das kann bei einer AWS-RDS-Instanz, die mit Crossplane verwaltet wird, beispielhaft so aussehen:

Listing 9–2
Restore einer AWS-RDS-Instanz per Crossplane

1

apiVersion: database.aws.crossplane.io/v1beta1

2

kind: RDSInstance

3

metadata:

4

name: database

5

namespace: dev

6

spec:

7

forProvider:

8

# ...

9

restoreFrom:

10

source: PointInTime

11

pointInTime:

12

restoreTime: 2023-05-18T23:45:00Z

Wenn wir einen Git-Revert mit einer solchen Restore-Deklaration kombinieren, wird der Cloud-Provider im Hintergrund das Backup der Datenbank wiederherstellen, während beispielsweise in unserem Kubernetes-Cluster ein Deployment einen alten Image-Tag ausrollt. Beide Aktionen passieren zeitlich voneinander entkoppelt, und deshalb kann es hier zu Überlappungen kommen, während denen inkompatible Komponenten miteinander sprechen. Möglicherweise arbeitet beispielsweise eine zurückgerollte App (»alt«) mit den noch aktuellen Datenbankinhalten (»neu«) oder eine noch aktuelle App (»neu«) mit einer zurückgerollten Datenbank (»alt«), bevor schließlich beide Komponenten vollständig zurückgerollt sind.

In Szenarien, in denen das unerwünscht ist, könnten wir zuerst den deklarativen Restore zusammen mit einem Herunterskalieren des Deployments auf gar keine Replicas kombinieren und manuell warten, bis der Restore abgeschlossen ist, um erst anschließend das Deployment wieder hochzuskalieren. Das schließt dann zwar Downtime mit ein, aber wenn ein koordinierter, gemeinsamer Rollback sowieso nötig ist, lässt sich Downtime kaum vermeiden.

Mit CronJobs können wir Runbooks stärker automatisieren.

Die eben gezeigten deklarativen Herangehensweisen an Restores, gepaart mit GitOps, machen gemeinsame Rollbacks von Infrastruktur und State deutlich leichter. Aber was ist mit State, den wir bisher noch nicht per GitOps zurücksetzen können, weil es beispielsweise noch keine Controller dafür gibt? Auch dann, wenn wir einen GitOps-Revert nicht vollständig mit einem Data-Restore koppeln können, stehen uns oftmals mehr Möglichkeiten zur Automatisierung zur Verfügung, als wir intuitiv denken. In einem Kubernetes-Kontext können wir beispielsweise einen deaktivierten CronJob erstellen, den wir im Fall eines Restores manuell triggern.

Listing 9–3
Ein CronJob zum Einspielen eines MySQL-Backups

1

apiVersion: batch/v1

2

kind: CronJob

3

metadata:

4

name: restore-backup

5

spec:

6

schedule: "0 0 * * *" # dummy value

7

suspend: true

8

jobTemplate:

9

spec:

10

template:

11

spec:

12

restartPolicy: OnFailure

13

containers:

14

- name: mysql-restore

15

image: ubuntu:22.04

16

command:

17

- bash

18

- /opt/scripts/mysql-restore.sh

19

envFrom:

20

- configMapRef:

21

name: backup-restore

22

- secretRef:

23

name: aws-db-conn

24

volumeMounts:

25

- name: scripts

26

mountPath: /opt/scripts

27

volumes:

28

- name: scripts

29

configMap:

30

name: restore-scripts

Wie dieser beispielhafte CronJob im Detail arbeitet, ist für die Illustration weniger relevant. Das Entscheidende ist, dass der CronJob über .spec.suspend=true deaktiviert ist und wir bei Bedarf manuell per kubectl einen einmaligen Job aus diesem CronJob heraus triggern können. Der Befehl für dieses Triggern würde ungefähr folgendermaßen aussehen:

Listing 9–4
Triggern eines Jobs aus dem deaktivierten CronJob

1

# Generate unique Job name

2

backup_restore_job_name=restore-backup-$RANDOM

3

kubectl create job --from=cronjob/restore-backup \

4

$backup_restore_job_name || true

5

kubectl wait --for=condition=complete --timeout=40m \

6

job/$backup_restore_job_name

Sobald das Skript erfolgreich durchgelaufen ist, ist das Backup fertig eingespielt. Mit solchen deaktivierten CronJobs können wir bestehende Runbooks für Wiederherstellung (und auch tendenziell andere operative Runbooks) so weit wie möglich automatisieren.

Der deaktivierte CronJob ist in diesem Fall nur eine Art Template, aus dem wir imperativ bei Bedarf einen tatsächlichen Job erzeugen. Nur der CronJob selbst wird von GitOps verwaltet, der manuell erzeugte Job nicht (ähnlich wie bei einem Deployment und dessen Pods beziehungsweise ReplicaSets). Entsprechend müssen wir uns um das Aufräumen des erzeugten Jobs noch selbst kümmern (außer wir setzen einen Wert für .spec.jobTemplate.spec.ttlSecondsAfterFinished im CronJob). Allerdings kann ein manuelles Aufräumen gerade zu den Chancen zählen, die wir nutzen wollen, wenn wir manuell im Zielsystem hantieren und womöglich während eines Incidents explorativ noch herausfinden müssen, was alles zur Gesundung unserer Anwendung nötig ist.

Wenn Auditierbarkeit mehr zählt als Geschwindigkeit, dann ist ein Triggern des CronJobs im GitOps-Stil dennoch möglich in folgenden Schritten:

  1. die Inhalte des CronJob-Manifest in ein neues Manifest kopieren
  2. kind ändern von CronJob auf Job
  3. alle Felder direkt unterhalb von .spec (beispielsweise schedule oder suspend) entfernen, sodass nur noch .spec.jobTemplate übrig bleibt
  4. den Block .spec.jobTemplate.spec.template verschieben nach .spec.template
  5. den leeren Block .spec.jobTemplate löschen
  6. das neue Manifest committen und pushen
  7. das Durchlaufen des Runbooks abwarten
  8. das Job-Manifest löschen und die Löschung committen und pushen

9.5Ein Deployment neu starten

Wenn Container oder Kubernetes-Pods krank werden und nicht von alleine wieder in einen gesunden Zustand zurückfinden, ist ein Löschen und Neuerzeugen das Mittel der Wahl. Meistens reicht dafür das Löschen eines Pods mittels kubectl delete pod, weil meistens ein Pod-Controller wie ein Deployment, StatefulSet oder DaemonSet im Spiel ist, der umgehend einen neuen Pod erzeugen wird. Allerdings kann ein solch detailliertes Löschen von Pods zu Downtime führen, wenn der Pod-Controller konfiguriert ist, nur eine einzige Replica auszuführen (was der Standardwert ist).

Statt einzelne Pods zu löschen, kann man geschickter vorgehen und stattdessen den Pod-Controller anweisen, einen Neustart durchzuführen. Das ist mit kubectl rollout restart möglich. Wenn wir mit einem solchen Befehl einen Neustart triggern, werden im Hintergrund keine Pods gelöscht, sondern es wird nur in der Pod-Spezifikation der Ressource eine neue Annotation gesetzt. Dadurch verändert sich die Pod-Spezifikation und der Controller führt automatisch ein Upgrade durch, wodurch vorhandene Pods ersetzt werden durch neue. (Konkret wird unter dem YAML-Pfad .spec.template.metadata.annotations die Annotation kubectl.kubernetes.io/restartedAt auf einen aktuellen Zeitstempel gesetzt5.)

Imperative Restarts sind voll kompatibel mit GitOps.

In den meisten Fällen ist diese Annotation im committeten Manifest in Git nicht vorhanden. Deswegen gibt es für den GitOps-Operator an dieser Stelle nichts zu tun: Wenn er das Manifest aus dem Config-Repo erneut anwendet, hat sich am gewünschten Zielzustand nichts verändert, weil diese Annotation nicht Teil des gewünschten Zustands ist. Das bedeutet also, dass ein imperativer Neustart über die CLI und der GitOps-Lebenszyklus sich normalerweise überhaupt nicht in die Quere kommen und problemlos zusammen genutzt werden können.

GitOps-Operatoren ignorieren ungesetzte Felder.

Dies mag eine überraschende, aber sehr hilfreiche Erkenntnis über Kubernetes sein: Nicht jedes Kubernetes-Manifest muss bis ins letzte Detail ausformuliert sein, damit es erfolgreich angewandt werden kann. Es gibt natürlich bei fast jedem Ressourcentyp ein paar erforderliche Felder. Diese müssen wir in den Manifesten setzen, und entsprechend werden diese nach dem Commit definitiv vom GitOps-Operator überwacht. Aber die Controller und Mutating Admission Webhooks, die im Cluster tätig sind, können nach dem Apply des Manifests eine Vielzahl an Änderungen durchführen, die das Manifest bearbeiten und erweitern, sodass die Beschreibung der deployten Ressource im Cluster (beispielsweise über einen kubectl get) deutlich länger sein kann als das Manifest im Config-Repo. Aber nur diejenigen Felder des Manifests, die wir in Git committet haben, werden auch vom GitOps-Operator konsequent angewandt.

Deklarative Restarts sind möglich.

Auch hier gilt: Wenn maximale Auditierbarkeit wichtiger ist als Geschwindigkeit, dann können wir auch hier einen GitOps-konformen Weg wählen: Wir können eine Annotation an der Pod-Spezifikation eigenhändig im Manifest setzen und committen. Wir sollten dabei allerdings nicht dieselbe Annotation setzen wie Kubernetes (kubectl.kubernetes.io/restartedAt). Falls nämlich doch ein Neustart per CLI durchgeführt wird, passiert er womöglich doppelt. Denn zuerst wird die Annotation per CLI überschrieben durch einen aktuellen Wert, aber im nächsten Moment setzt der GitOps-Operator die Annotation wieder zurück auf den alten Wert; in der Zwischenzeit sind jedoch bereits zwei sequenzielle Neustarts im Gange.

9.6Ressourcen neu erzeugen

Imperativ gelöschte Ressourcen werden automatisch neu erzeugt.

Wir haben eben betrachtet, wie wir Pods neu erzeugen können in einem GitOps-Umfeld. Pods sind allerdings nicht die einzigen Ressourcen, die in einen ungesunden Zustand kommen können und bei denen ein Löschen und Neuerzeugen helfen kann. Wenn es sich bei den betreffenden Ressourcen nicht um Ressourcentypen handelt, die vom GitOps-Operator kommen (wenn es also beispielsweise um Ressourcen geht, die keine Argo-CD-Applications, Flux-HelmReleases oder Flux-Kustomizations sind), dann können wir diese kranken Ressourcen einfach löschen und der GitOps-Operator wird sie beim nächsten Angleichungsdurchlauf neu erzeugen. (Wir verwenden für Ressourcen aus Ressourcentypen, die vom GitOps-Operator kommen, im Folgenden den Begriff »GitOps-Ressourcen«.)

Wenn die zu löschenden Ressourcen allerdings doch GitOps-Ressourcen sind, dann ist mehr Vorsicht geboten. Denn in aller Regel werden diejenigen Ressourcen, die von einer Application, einem Helm-Release oder einer Kustomization überwacht werden, mit gelöscht, wenn die übergeordnete GitOps-Ressource gelöscht wird, und dadurch kann unerwünschte Downtime entstehen. (Die meisten GitOps-Operatoren verwenden Kubernetes-Finalizer6 für diese gesammelte Garbage Collection.)

Bevor wir also GitOps-Ressourcen löschen, sollten wir zuerst weniger invasive Alternativen ausprobieren. Bei Flux können wir über die Flux-CLI mit flux reconcile (beziehungsweise mit flux suspend und flux resume) feststeckende HelmReleases oder Kustomizations erneut anstoßen und im besten Fall ein Löschen umgehen. Bei Argo CD können wir über die UI einen erneuten Sync triggern oder über die CLI mit argocd app sync, wo uns noch mehr Parameter zur Verfügung stehen als in der UI.

GitOps-Ressourcen können durch Löschen und deaktivierte Finalizer sicher neu erzeugt werden.

Falls dieses Vorgehen nicht weiterhilft, können wir bei Argo CD eine Application löschen, ohne die zugehörigen Ressourcen zu löschen, indem wir den Finalizer auf der Application mit kubectl patch entfernen7 und anschließend die Application löschen und neu erzeugen. (Für ApplicationSets in Argo CD ist das auch möglich, erfordert jedoch zusätzliche Maßnahmen8.) Nachdem die Application dann neu erzeugt wurde, sollte automatisch auch wieder der Finalizer vorhanden sein.

Bei Flux-Ressourcen muss der Parameter .spec.prune auf false gesetzt und die Änderung in den Cluster synchronisiert werden, bevor die Ressource sicher entfernt und neu erzeugt werden kann. Anschließend kann das Pruning wieder aktiviert werden, damit die zugehörigen Ressourcen wieder von der Flux-Ressource vollständig überwacht werden. (Dieses Vorgehen ist auch dann angebracht, wenn man eine Flux-Ressource umbenennt9.)

9.7Ein Deployment skalieren

Langfristiges, statisches Skalieren geht per GitOps.

Über den Parameter .spec.replicas lassen sich Deployments und State-fulSets auf eine beliebige Anzahl Replicas skalieren. Dieser Parameter ist optional und nimmt standardmäßig den Wert 1 an. Wenn wir ein Deployment oder StatefulSet langfristig mit einer festen Anzahl skalieren wollen, sollten wir den Wert im Config-Repo direkt im Manifest setzen, damit er vom GitOps-Operator kontinuierlich angewandt wird. Wenn wir jedoch einen zusätzlichen Controller wie beispielsweise einen HorizontalPodAutoscaler verwenden, um auf Basis bestimmter Metriken innerhalb eines gewissen Spielraums automatisch den Replica-Wert zu setzen, dann sollten wir dieses Feld ungesetzt lassen und nur die Minimum- und Maximumwerte im HPA setzen. Hier sind sich Flux10 und Argo CD11 einig.

Für kurzfristige Eingriffe können wir die Angleichung pausieren.

Abgesehen von diesen langfristigen Skalierungen können wir aber auch in Situationen kommen (beispielsweise während Incidents), in denen wir sehr dynamisch manuell skalieren wollen, ohne dass die kontinuierliche Angleichung des GitOps-Operators unsere Änderungen ständig überschreibt, weil wir nicht über Commits gehen. Für solche Fälle können wir die Angleichung temporär pausieren und damit vollständig aushebeln.

Flux ermöglicht das auf seinen Ressourcen über den Parameter .spec.suspend: Setzen wir ihn auf false, wird die Angleichung pausiert. Bei einer Argo-CD-Application würden wir analog dazu den Parameter .spec.syncPolicy.automated.selfHeal auf false setzen, um die Angleichung zu pausieren12.

Wir können diese Einstellung entweder deklarativ über einen Commit ändern oder imperativ per CLI. Letzteres wird nur unter bestimmten Bedingungen funktionieren, denen wir uns im nächsten Abschnitt widmen. Flux ermöglicht das imperative Ändern mit flux suspend13 und bei Argo CD ist es ungefähr erreichbar mit argocd app set --sync-policy none14. Beide Befehle sind letztlich aber nur dünne Verpackungen um den Befehl kubectl patch. Wir zeigen direkt Beispiele für beide Anpassungen, wenn man sie mit kubectl statt über die Vendor-CLI ausführt:

Listing 9–5
Patchen von GitOps-Ressourcen zum Pausieren und Wiederaufnehmen der Angleichung

1

# Suspend a Flux HelmRelease

2

kubectl patch helmrelease backend --type=merge \

3

-p '{"spec":{"suspend":true}}'

4

# Resume a Flux HelmRelease

5

kubectl patch helmrelease backend --type=json \

6

-p '[{"op":"remove","path":"/spec/suspend"}]'

7

 

8

# Turn off self-heal on an Argo~CD Application

9

kubectl patch application backend --type=json \

10

-p '[{"op":"remove",

11

"path":"/spec/syncPolicy/automated/selfHeal"}]'

12

# Reactivate self-heal on an Argo~CD Application

13

kubectl patch application backend --type=merge \

14

-p '{"spec":{"syncPolicy":{"automated":

15

{"selfHeal":true}}}}'

Pausieren ist herausfordernder bei hierarchischen GitOps-Ressourcen.

Sehr häufig haben wir allerdings mehr als nur eine GitOps-Ressource im Spiel, und fast immer sind diese GitOps-Ressourcen hierarchisch verschachtelt (siehe Abschnitt 6.6.2 auf Seite 168). Das Pausieren einer untergeordneten Ressource ist dann zusätzlich erschwert: Wenn wir beispielsweise eine Flux-Kustomization A haben, die eine weitere Flux-Kustomization B einbindet, und wir wollen nur die Kustomization B pausieren, dann könnten wir versuchen, bei Kustomization B den Suspend zu aktivieren. Dieser imperative Patch per CLI würde aber sofort überschrieben werden, weil der Suspend-Wert von Kustomization B in Git anders committet ist. Analog tritt das Problem bei Argo CD auf mit dem Pattern »App of Apps«, wenn wir in einer der untergeordneten Applications den Self-Heal deaktivieren.

Wir können dann nur zwischen zwei Optionen wählen:

  1. Wir committen die Pausierung an der untergeordneten GitOpsRessource. Damit verlieren wir aber die Flexibilität, die wir uns durch das Pausieren eigentlich erhofft hatten.
  2. Wir pausieren die übergeordnete GitOps-Ressource und anschließend die untergeordnete. (Das wäre im obigen Flux-Beispiel zuerst Kustomization A, dann Kustomization B.) Dieses Vorgehen funktioniert, weil die Änderung an der untergeordneten Ressource dann nicht mehr ständig von der übergeordneten überschrieben wird. Damit bekommen wir mehr Flexibilität, aber im schlechtesten Fall pausieren wir mehr GitOps-Ressourcen als zwingend notwendig.

Bei flachen Hierarchien ist der zweite Weg derjenige mit dem besseren Kosten-Nutzen-Verhältnis.

Oft kann die konkrete Hierarchie von GitOps-Ressourcen eine Herausforderung sein. Wie können wir die am höchsten übergeordnete GitOps-Ressource finden (sozusagen die oberste Eltern-Ressource aller darunterliegenden Kind- und Kindeskind-Ressourcen)? Bei Flux kann uns der Befehl flux trace helfen: Am Ende der CLI-Ausgabe wird die hierarchisch oberste Ressource stehen, aus der die angegebene Ressource erzeugt wurde. In Argo CD können wir diese Ressource über die UI suchen oder über die CLI mit argocd app list15.

Allerdings sind bei großer Schachtelungstiefe Aufwand und Risiko der imperativen Änderungen an vielen Ressourcen hoch. Oft hat man als Endanwender auch gar nicht das Recht, die Eltern-Ressource zu verändern. Hier greifen wir dann doch zum Commit.

9.8Fazit

In diesem Kapitel haben wir gesehen, wie manuelle imperative Eingriffe und GitOps zueinander passen oder miteinander in Konflikt geraten können. An den Stellen, an denen Konflikte auftreten, müssen wir abwägen, ob die Vorteile von GitOps-freiem Vorgehen die Risiken überwiegen.

Viele imperative und von GitOps losgelöste Eingriffe sind ohne Weiteres möglich. Außerdem können wir oftmals Runbooks noch stärker automatisieren, wenn wir sie deklarativ verpacken (beispielsweise als deaktivierten Kubernetes-CronJob). Wenn wir jedoch in Ressourcen eingreifen wollen, die vom GitOps-Operator überwacht werden, müssen wir im Ernstfall die Angleichung pausieren.