In diesem Beitrag setze ich GitOps mit Flux CI/CD in meinem lokalen Kubernetes-Cluster auf. Mein Ziel ist es, die im Cluster betriebenen Ressourcen mit deklarativ mit Hilfe von Konfigurationsdateien in einem Git-Repository zu pflegen. Der Cluster soll auf dem Stand gehalten werden, der im Repository mit Konfiguration dokumentiert ist. Dem gegenüber stünde, Änderungen imperativ einfach mit kubectl oder helm auf der Kommandozeile in den Cluster zu bringen.

Flux wird als Anwendung in den Cluster deployed. Von dort aus wird es regelmäßig den Inhalt des Git-Repositories prüfen und Änderungen an der Konfiguration umsetzen. Kubernetes-Ressourcen können dort beispielsweise YAML-Manifeste oder Helm-Charts sein.

Andere Beiträge

Dieser Beitrag ist Teil einer Reihe von Blogposts. Ich betreibe zuhause einen Kubernetes-Cluster, in dem ich ein paar Anwendungen wie Nextcloud, Jellyfin oder Vaultwarden betreibe.

Voraussetzungen

Der Kubernetes-Cluster muss bereits laufen und lokal müssen die Admin-Berechtigungen vorhanden sein. Die Konfiguration wird zu GitHub hochgeladen. Alternativ kann man dafür auch GitLab nehmen (hosted oder selfhosted) oder sonst irgendein Git-Repository. Ich verwende einfach GitHub.

Flux installieren

Zuerst wird ein GitHub Personal Access Token gebraucht - von der Classic-Sorte. Als Scope sind notwendig: repo (komplett, also der erste Haken) und admin:public_key (auch komplett).

Auf der lokalen Maschine wird das Kommandozeilentool für Flux gebraucht. Das kann auf vielfältige Weise installiert werden, beispielsweise mit Homebrew:

$ brew install fluxcd/tap/flux

Als nächstes ein paar grundlegende Checks machen:

$ flux check --pre
► checking prerequisites
✔ Kubernetes 1.25.4+k3s1 >=1.20.6-0
✔ prerequisites checks passed

Ist alles in Ordnung, kann mit dem Bootstrapping begonnen werden. Ist das GITHUB_REPO noch nicht vorhanden, wird es automatisch angelegt.

$ export GITHUB_USER=der_github_username
$ export GITHUB_TOKEN=das_github_personal_access_token_von_vorhin
$ export GITHUB_REPO=k8s
$ flux bootstrap github \
  --owner=$GITHUB_USER \
  --repository=$GITHUB_REPO \
  --branch=main \
  --path=./bootstrap \
  --personal

Dann passieren ein paar Dinge:

  • Das Repository wird angelegt, falls es noch nicht existiert.
  • Im Repository wird bootstrap/flux-system angelegt. Dort liegen die Bootstrapping-Resourcen.
  • Dem Repository bei GitHub wird ein öffentlicher SSH-Schlüssel als Deploy-Key hinzugefügt, mit dem sich Flux aus dem Cluster bei GitHub anmeldet
  • Im Cluster werden die notwendigen Ressourcen angelegt, darunter:
    • Controller für Helm, Kustomize.
    • CustomResourceDefinitions um Git Repositories, Helm Repositories und Charts sowie Kustomizations abzubilden.
    • Ein Secret mit dem privaten SSH-Schlüssel, mit dem sich Flux aus dem Cluster bei GitHub anmeldet.
    • Ein GitRepository, das den Abruf des Repositories regelt und eine anonyme Kustomization, die bootstrap/flux-system in diesem Repository einbindet. Dort findet sich die endgültige Kustomization, die wiederum rekursiv den ganzen bootstrap/-Ordner einbindet.

Als nächster Schritt sollte dann auch das Repository lokal ausgecheckt werden:

$ git clone [email protected]:$GITHUB_USER/$GITHUB_REPO
$ tree $GITHUB_REPO
k8s
└── bootstrap
    └── flux-system
        ├── gotk-components.yaml
        ├── gotk-sync.yaml
        └── kustomization.yaml

2 directories, 3 files

Grundlegende Struktur hinzufügen

Im Cluster können nun Anwendungen und weitere Ressourcen über eine Kustomization, ein GitRepository oder ein HelmChart installiert werden. Bevor es damit los geht, hilft eine Verzeichnisstruktur als Schema. Ein Beispiel könnte so aussehen:

$ tree $GITHUB_REPO
k8s
├── apps
│   └── exampleapp
│       └── helmchart-exampleapp.yaml
└── bootstrap
    ├── flux-system
    │   ├── gotk-components.yaml
    │   ├── gotk-sync.yaml
    │   └── kustomization.yaml
    ├── helmrepository
    │   └── helmrepository-example.yaml
    ├── kustomization
    │   └── kustomization-example.yaml
    └── namespace
        ├── namespace-exampe.yaml
        └── namespace-infra.yaml

Zur Begründung:

  • apps/ beinhaltet Unterordner mit der Konfiguration, die einzelne Anwendungen in den Cluster installiert. Hier im Beispiel als HelmChart. Die einzelnen Dateien in diesen Ordnern werden nur angewandt, wenn sie von einer Kustomization aus /bootstrap referenziert werden.
  • bootstrap/ wird von Flux bereits rekursiv angewandt. Dort können einfach Kubernetes-Manifeste als YAML-Dateien hinterlegt werden und Flux wird sie anlegen. Einzelne Apps konfiguriere ich dennoch nicht einfach dort sondern nehme den Umweg über eine Kustomization, die sich auf einen Ordner in /apps/ bezieht. So lässt sich eine App leicht aktivieren und deaktivieren.
    • helmrepository/ beinhaltet mehrfach nutzbare Helm Repositories für einzelne Helm Charts.
    • kustomization/ beinhaltet pro Anwendung eine Kustomization als Verweis auf einen Ordner in /apps/.
    • namespace/ legt unabhängig von den Anwendungen Namespaces an.

Kustomizations sind recht mächtige Konfigurationsmöglichkeiten für Kubernetes. In ihrer einfachsten Form referenzieren sie einfach andere Dateien, in denen Kubernetes-Manifeste abgelegt sind. Darüber hinaus lassen sich beispielsweise auch mehrstufige Deployments (dev, staging, prod) deklarativ abbilden. Das wäre analog zur Verwendung von Templating, was sich deklarativ allerdings schwieriger abbilden lässt. Für’s erste bleibe ich bei der einfachsten Form und setze sie als Verweise auf weitere Manifeste ein. Löscht man eine Kustomization aus /bootstrap/kustomization, wird die davon referenzierte Anwendung in /apps ebenfalls aus dem Cluster geworfen.

Umsetzung: Einen Namespace anlegen

Viel Theorie, nun etwas Praxis. Zum Test der Funktion wird ein Namespace infra angelegt.

$ cd $GITHUB_REPO
$ mkdir -p bootstrap/namespace
$ kubectl create --dry-run=client namespace infra --output yaml | tee -a bootstrap/namespace/namespace-infra.yaml
apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: null
  name: infra
spec: {}
status: {}

$ git add bootstrap/namespace/namespace-infra.yaml
$ git commit -m "create infra namespace"          
[main 313373] create infra  namespace
 1 file changed, 6 insertions(+)
 create mode 100644 bootstrap/namespace/namespace-infra.yaml
 $ git push
 Enumerating objects: 7, done.
 # […]
$ flux get kustomizations --watch
NAME            REVISION        SUSPENDED       READY   MESSAGE                        
flux-system     main/313373     False           True    Applied revision: main/313373

Mit dem letzten Befehl wird die Anwendung der Kustomizations überwacht. Bisher gibt es nur flux-system, die unter anderem den /bootstrap/namespace-Ordner überwacht. Sobald unter READY der Erfolg mit True angegeben wird, gibt es den Namespace:

$ kubectl get ns
NAME              STATUS   AGE
default           Active   65m
kube-system       Active   65m
kube-public       Active   65m
kube-node-lease   Active   65m
flux-system       Active   58m
infra             Active   2m

Mit diesem einfachen Beispiel sind die Grundlagen demonstriert. Die komplizierteren und nützlicheren Anwendungen folgen in den nächsten Artikeln.