GitOps mit Kubernetes
back

GitOps mit Kubernetes

27.09.2023

Vor einiger Zeit habe ich damit begonnen, den Workflow für mein Kubernetes-Cluster zu ändern. Statt YAML-Dateien und Helm-Charts manuell per Terminal anzuwenden, habe ich auf einen GitOps-Workflow umgestellt. Hierbei werden alle YAML-Dateien in einem Git-Repository abgelegt, und Änderungen im Repository werden von einer Anwendung im Kubernetes-Cluster gezogen und angewendet. Dieses Setup bietet mir folgende Vorteile:

  • Ich habe über das Repository einen einfachen Weg, Änderungen rückgängig zu machen, und eine Historie über alle meine Änderungen.
  • Änderungen können auch über andere Geräte mit Zugriff auf das Git-Repository durchgeführt werden, z.B. mein iPad, iPhone oder Arbeitscomputer.
  • Versionen von Container-Images können automatisch erhöht werden, wenn eine neue Version vorliegt, und über das Repository sehe ich, dass die Versionsnummer im Manifest aktualisiert wurde.
  • Änderungen werden gezogen und nicht gepusht, weshalb es nicht notwendig ist, das Cluster von außen erreichbar zu machen, um z.B. eine neue Version der Webseite zu deployen, was ich bis dahin mit GitLab CI gemacht habe.

Wie bereits erwähnt, lädt eine Anwendung im Cluster Änderungen vom Git-Repository und wendet diese an. Ich habe mich für die Anwendung Flux entschieden, welche diesen Job in meinem Cluster übernimmt. Ich kann nicht mehr genau sagen, warum ich mich für Flux und nicht z.B. für ArgoCD oder eine andere Anwendung entschieden habe, aber bisher bereue ich meine Entscheidung nicht und bin mit Flux und meinem Setup sehr zufrieden.

Installation von Flux

Es wird empfohlen, das Flux CLI Tool zu installieren, welches die Installation von Flux in einem Kubernetes Cluster vereinfacht. Die Installation des Flux CLI Tools ist in der Flux Dokumentation beschrieben.

Nach der Installation von Flux CLI kann man Flux mit Hilfe des Bootstrap-Befehls zum Cluster hinzufügen. Bevor man dies tut, sollte man sich überlegen, wo man das Git Repository hostet und ob man eventuell optionale Flux Komponenten installieren möchte.

Ich habe mich für Github als Repository-Hosting-Plattform entschieden, aber auch Github Enterprise und andere Anbieter werden unterstützt. Selbstgehostete Repositories können ebenfalls verwendet werden.

Bei der Installation von Flux habe ich zusätzlich zu den Flux Core Komponenten zwei weitere Komponenten installiert, um die Versionsnummern der Container-Images automatisch zu erhöhen. Wenn man die Option der automatischen Versionsnummern-Erhöhung nutzen möchte, ist es wichtig, dass der Benutzer, mit dem auf das Git-Repository zugegriffen wird, Schreibrechte hat.

Der Bootstrap Befehl sieht in meinem Fall wie folgt aus:

export GITHUB_USER=<your-username>export GITHUB_TOKEN=<your-token>flux bootstrap github \  --components-extra=image-reflector-controller,image-automation-controller \  --read-write-key \  --owner=$GITHUB_USER \  --repository=homelab \  --branch=main \  --personal \  --path=./

Der Bootstrap Befehl erstellt automatisch das Repository und legt die notwendigen Konfigurationen für die Verbindung zum Repository im Cluster an. Zusätzlich werden die benötigten Flux-Komponenten im Flux-System Namespace installiert.

Die Ordnerstruktur im erstellten Repository sieht wie folgt aus:

├── flux-system
│   ├── gotk-components.yaml
│   ├── gotk-sync.yaml
│   └── kustomization.yaml
├── apps
│   ├── base
│   ├── staging
│   └── production
├── infrastructure
│   ├── base
│   ├── staging
│   └── production
└── README.md

Die Datei gotk-sync.yaml im flux-system Ordner enthält eure Konfigurationen, wie beispielsweise die Informationen zu eurem Git-Repository und die Intervalle, in denen das Repository auf Änderungen geprüft werden soll. Meine Konfiguration sieht wie folgt aus:

# This manifest was generated by flux. DO NOT EDIT.---apiVersion: source.toolkit.fluxcd.io/v1beta2kind: GitRepositorymetadata:  name: flux-system  namespace: flux-systemspec:  interval: 1m0s  ref:    branch: main  secretRef:    name: flux-system  url: ssh://[email protected]/...---apiVersion: kustomize.toolkit.fluxcd.io/v1beta2kind: Kustomizationmetadata:  name: flux-system  namespace: flux-systemspec:  interval: 10m0s  path: ./  prune: true  sourceRef:    kind: GitRepository    name: flux-system

Auch wenn Flux den Kommentar “DO NOT EDIT.” hinzufügt, müssen wir später eine Änderung an dieser Konfiguration vornehmen.

Anwendungen deployen

Um eine Anwendung im Cluster über Flux zu installieren, müsst ihr nur die entsprechenden YAML-Dateien hinzufügen. Ich habe immer für jede Anwendung einen eigenen Ordner in der oben erwähnten Verzeichnisstruktur angelegt (in der Regel in apps → production). In dem Ordner habe ich dann die verschiedenen YAML-Dateien angelegt und diese wie bisher durchnummeriert, um sicherzustellen, dass sie in der richtigen Reihenfolge angewendet werden. Zum Beispiel:

├── flux-system
│   ├── gotk-components.yaml
│   ├── gotk-sync.yaml
│   └── kustomization.yaml
├── apps
│   ├── base
│   ├── staging
│   └── production
│       └── homepage
│           ├── 001-namespace.yaml
│           ├── 002-deployment.yaml
│           ├── 003-service.yaml
│           └── 004-ingress.yaml
├── infrastructure
│   ├── base
│   ├── staging
│   └── production
└── README.md

Nachdem die Änderungen commited und gepusht wurden, werden sie im Cluster in kürzester Zeit angewendet. Auf Wunsch können Benachrichtigungen über Flux per MS Teams, Slack oder Discord verschickt werden, wenn eine Änderung angewendet wurde. Ich lasse mich zum Beispiel über Discord benachrichtigen. Wie man die Notifications einrichtet, ist hier dokumentiert.

Secrets verschlüsseln

Im Gegensatz zu allen anderen Manifest-YAML-Dateien sollten die YAML-Dateien für Secrets nicht direkt im Git-Repository abgelegt werden, da die Gefahr besteht, dass die Secrets komprimiert werden. Um die Secrets dennoch zusammen mit den anderen Mainfest-Dateien abzulegen und die Vorteile von GitOps zu nutzen, kann SOPS von Mozilla verwendet werden.

SOPS ermöglicht die verschlüsselte Speicherung von Secrets im Git Repository und deren automatische Entschlüsselung innerhalb von Kubernetes-Clustern. Dazu muss ein Schlüsselpaar aus öffentlichem und privatem Schlüssel generiert werden. Der private Schlüssel wird dann als Secret im Kubernetes-Cluster hinterlegt, um YAML-Dateien zu entschlüsseln, die zuvor mit dem öffentlichen Schlüssel verschlüsselt wurden.

Flux liefert die dafür notwendigen Komponenten bereits mit und beschreibt in der Dokumentation verschiedene Möglichkeiten, SOPS einzusetzen. Ich verwende in meinem Kubernetes-Cluster eine Kombination aus SOPS und dem Verschlüsselungstool Age, einer moderneren Variante des ebenfalls unterstützten PGP.

Um SOPS und Age nutzen zu können, müssen diese zunächst auf dem Rechner installiert werden. Ich habe dazu den MacOS Package Manager brew verwendet:

# Installiere sops und agebrew install sops age

Nach der Installation muss ein neues Schlüsselpaar erzeugt werden, indem der folgende Befehl ausgeführt wird:

# Öffentlichen und privaten Schlüssel erstellenage-keygen -o keys.txt

Der Inhalt der generierten Datei keys.txt sollte wie folgt aussehen:

# created: 2023-09-27T13:18:54+02:00
# public key: age15qj3pa5mnf4un6yq5hpv0rxs2r8zfy84dk0c42ed2at7ca9mqgps9kxm69
AGE-SECRET-KEY-1AFE7AVFX4RKFRD8F8DCKMVUZEAY69PE0WA46L28VFTQTVUJHDCGS64DUFY

Der erzeugte Schlüssel muss nun im Kubernetes Cluster als Secret hinterlegt werden. Dazu kann dieser Befehl verwendet werden:

cat keys.txt |kubectl create secret generic sops-age \--namespace=flux-system \--from-file=age.agekey=/dev/stdin

Damit Flux das erstellte Secret zur Entschlüsselung verwendet, muss der verwendete Secret Name in der Flux-Konfiguration hinterlegt werden. Dazu muss in der Datei gotk-sync.yaml, die sich im Ordner Flux-System befindet, die Kustomization-Konfiguration um den Punkt decryption erweitert werden:

apiVersion: kustomize.toolkit.fluxcd.io/v1beta2kind: Kustomizationmetadata:  name: flux-system  namespace: flux-systemspec:  interval: 10m0s  path: ./  prune: true  sourceRef:    kind: GitRepository    name: flux-system  decryption:    provider: sops    secretRef:      name: sops-age

Flux ist nun in der Lage, verschlüsselte Secrets zu empfangen und sie Kubernetes in unverschlüsselter Form zur Verfügung zu stellen. Wie bereits erwähnt, werden diese zuerst lokal mit SOPS und Age verschlüsselt, bevor sie zum Git Repository hinzugefügt werden. Um nicht bei jeder Verschlüsselung angeben zu müssen, wo sich das Age-Schlüsselpaar befindet, kann man dieses in den Ordner von SOPS verschieben, wodurch SOPS es automatisch findet und verwendet. Dazu muss unter MacOS folgender Befehl ausgeführt werden:

mv keys.txt ~/Library/Application\ Support/sops/age/keys.txt

Anschließend kann man mit folgendem Befehl eine Secret YAML-Datei verschlüsseln:

sops --encrypt --encrypted-regex '^(data|stringData)$' --in-place my-scret.yaml

Dieser Befehl ersetzt den Inhalt der angegebenen Datei durch verschlüsselte Werte. Die YAML-Datei mit den verschlüsselten Werten kann nun zum Git-Repository hinzugefügt werden.

Die Verschlüsselung kann auf Wunsch auch lokal mit dem privaten Schlüssel rückgängig gemacht werden:

sops -d -i my-secret.yaml

Beispiel

Vorher

apiVersion: v1kind: Secretmetadata:  name: mysecrettype: Opaquedata:  username: YWRtaW4=  password: MWYyZDFlMmU2N2Rm

Nacher

apiVersion: v1kind: Secretmetadata:  name: mysecrettype: Opaquedata:  username: ENC[AES256_GCM,data:WLMHAA237HU=,iv:GU7dZ/AeUBWVQrgAhVp7Y6OTWzTTkBmBJhjmVS7AcNc=,tag:5LUIp2FRmlhg0s9qIPsE0A==,type:str]  password: ENC[AES256_GCM,data:QXbkpdThEmQ2kaUDP0MOqA==,iv:wANBzzC2vBLnBpfs5hT/Ah7F3nQdJK18Jd3tdQWM/Do=,tag:EtwXO+eDPX+14zxc5+m+UQ==,type:str]sops:  kms: []  gcp_kms: []  azure_kv: []  hc_vault: []  age:    - recipient: age1vcsluhx9fuqw2lvzar3xdj44qndag07f077jjn236dthhe56qqfses2nam      enc: |        -----BEGIN AGE ENCRYPTED FILE-----        YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5Y0dxZ1kvNFAxSXQ4R0hM        VlM2OW52NG5CMGQ1N2dYNjBmNStiTlhzZG1JCldRWjZTcy9yU3pmQUI3ZXBQN2hP        bWszQVFOMWZaWXNZOGIzVG5YOTlMcVUKLS0tIFRKaUIxcmRXR0xuclRZRS9tT3BQ        c1BUMms4Q09LRzVhUU43TzBZSEVYaTQKIEJiOe0CazOwY3QSo7/ZEdrw6QzkW3xt        DxPz7onVvMQf0j0TtjPsyz8T5KTw1GPUEdncilUzobqyNbNT/TOoCA==        -----END AGE ENCRYPTED FILE-----  lastmodified: "2023-09-27T11:25:36Z"  mac: ENC[AES256_GCM,data:th0xrQ+/oAG3vP/BtFDB0bj1O/o7rk72SOJ+wuZDqCkfnozan/I8ulqzczj8ujrsI5yhH80QRDZ26Qm29lhiqnvjDoR31Z6S0cwOgKlCzpzDzummJXQL/Vs/L5xJtrdShLUAGCvVLGaEejAeqFFyR/ver44Qa1AKGEenawgOsbk=,iv:YNpOPJpyZ7ttbeYZWiNl4U9VRF/KORIOw9uZ3B7v9j4=,tag:1gL+ZWFTlcURTjQkh2c8tg==,type:str]  pgp: []  encrypted_regex: ^(data|stringData)$  version: 3.7.3

Automatische Versionsupgrades

Einer der eingangs erwähnten Vorteile ist, dass Flux die Möglichkeit bietet, die Versionsnummer eines Docker Images automatisch zu aktualisieren, wenn eine neue Version verfügbar ist.

Die dafür notwendigen Flux-Komponenten habe ich bereits bei der Erstinstallation installiert. Konkret ermöglichten diese Optionen die automatische Aktualisierung der Image-Version:

# ...--components-extra=image-reflector-controller,image-automation-controller \--read-write-key \# ...

Damit Flux weiß, wie Änderungen in das Git-Repository geschrieben werden sollen, muss eine Konfigurationsdatei erstellt werden. In dieser Datei kann u.a. die Commit-Nachricht und der Autor konfiguriert werden oder in welchen Git Branch die Änderungen geschrieben werden sollen. Letzteres kann verwendet werden, wenn Änderungen an der Versionsnummer per Pull Request in den Main Branch übernommen werden sollen.

Die Konfigurationsdatei kann mit folgendem Flux CLI Befehl erzeugt werden:

flux create image update flux-system \
--git-repo-ref=flux-system \
--checkout-branch=main \
--push-branch=main \
--author-name=fluxcdbot \
[email protected] \
--commit-template="{{range .Updated.Images}}{{println .}}{{end}}" \
--export > flux-system/flux-system-automation.yaml

Man kann den Befehl nach Belieben anpassen oder man passt die erzeugte Konfigurationsdatei anschließend selbst an:

apiVersion: image.toolkit.fluxcd.io/v1beta1kind: ImageUpdateAutomationmetadata:  name: flux-system  namespace: flux-systemspec:  git:    commit:      author:        email: [email protected]        name: fluxcdbot      messageTemplate: "{{range .Updated.Images}}{{println .}}{{end}}"    push:      branch: main  interval: 5m0s  sourceRef:    kind: GitRepository    name: flux-system  update:    strategy: Setters

Damit die automatische Aktualisierung der Docker-Image-Version funktioniert, müssen zwei YAML-Dateien erstellt werden. Mit der ersten YAML Datei konfiguriert man das Image Repository und das Intervall, in dem geprüft werden soll, ob eine neue Version existiert. Diese YAML Datei kann entweder mit dem folgenden Befehl über das Flux CLI Tool generiert werden:

flux create image repository [Name] \--image=[Repository-Name] \--interval=[Intervall] \--export > [YAML-Pfad]# Beispiel mit n8nflux create image repository n8n \--image=n8nio/n8n \--interval=24h \--export > ./apps/production/n8n/001-image-repository.yaml

Oder man erstellt die YAML-Datei selbst und verwendet dieses Beispiel als Vorlage:

apiVersion: image.toolkit.fluxcd.io/v1beta1kind: ImageRepositorymetadata:  name: n8n  namespace: flux-systemspec:  image: n8nio/n8n  interval: 24h

Mit der zweiten YAML-Datei wird gesteuert, wann eine Versionsnummer aktualisiert werden soll: Wird semver für die Versionsnummer des Docker-Images verwendet, kann z.B. konfiguriert werden, dass nur Minor- und Patch-Versionen automatisch aktualisiert werden dürfen. Um diese YAML-Datei zu erzeugen, kann der folgende Befehl verwendet werden:

flux create image policy [Name] \--image-ref=[Name von zuvor erstellen Image Repository] \--select-semver=[Regel] \--export > [YAML-Pfad]# Beispiel mit n8nflux create image policy n8n \--image-ref=n8n \--select-semver=">=0.0.0" \--export > ./apps/production/n8n/001-image-policy.yaml

Oder man erstellt sie selbst:

apiVersion: image.toolkit.fluxcd.io/v1beta1kind: ImagePolicymetadata:  name: n8n  namespace: flux-systemspec:  imageRepositoryRef:    name: n8n  policy:    semver:      range: ">=0.0.0"

In der Flux-Dokumentation finden sich weitere Beispiele, wie man die Regel für semver setzen kann. Es sich auch Beispielregeln für andere Arten der Image-Versionierung in dieser Dokumentation.

Mit den beiden erstellten Dateien überprüft Flux nun das Docker Repository in dem definierten Intervall, ob neue Versionen verfügbar sind und ob diese für die automatische Aktualisierung der Docker Image Versionsnummer berücksichtigt werden sollen. Damit Flux weiß, an welcher Stelle die Versionsnummer aktualisiert werden soll, muss im letzten Schritt in der Deployment YAML der Anwendung auf die erstellte Image-Policy verwiesen werden.

Dazu wird ein Kommentar in die Zeile mit der Image-Referenz eingefügt, wie in diesem Beispiel:

//...containers:  - name: n8n    image: n8nio/n8n:0.226.0 # {"$imagepolicy": "flux-system:n8n"}//...

Wenn alles richtig konfiguriert wurde, sollte sich die Versionsnummer nun automatisch aktualisieren und man sieht im Git Repository die Commits von Flux für die Änderung der Versionsnummer in der Deployment YAML:

Version Update

Es empfiehlt sich, die vollständige Dokumentation zu diesem Thema zu lesen, da Flux hier noch weitere Optionen bietet, wie z.B. das Pausieren des automatischen Updates: Automate image updates to Git