Im vorherigen Beitrag habe ich Flux CI/CD in Kubernetes installiert und einen Namespace als erste Resource angelegt. Dass das Konzept funktioniert wäre damit belegt, besonders nützlich ist das aber noch nicht. In diesem Beitrag will ich eine erste Anwendung mit Hilfe von Helm und Flux in meinem Git-Repository konfigurieren und ausrollen.

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.

Theorie im Hintergrund: Wieso Helm verwenden?

Ressourcen in Kubernetes werden zumeist mit einer Vielzahl von Ressourcen konfiguriert und ausgerollt: Ein Deployment regelt, welche und wie viele Container laufen. ConfigMaps halten Konfiguration für die Anwendung vor. Ein Service macht die Sammlung der Container im Cluster erreichbar. Ein Ingress macht die Anwendung nach außen hin verfügbar, unter einer URL. Diese Resourcen laufen natürlich im Cluster, sind eigentlich aber eher Voraussetzungen für die Anwendung und nicht Teil der Infrastruktur. Läuft die Anwendung nicht, braucht man auch genau diese Ressourcen nicht. Es macht also Sinn, diese Ressourcen zusammen mit der Anwendung zu paketieren und zu versionieren. Noch mehr Sinn macht es, als Herausgeberin einer solchen Anwendung direkt die Kubernetes-Konfiguration mitzuliefern. Ein paar Dinge kann die Betreiberin eines Kubernetes-Clusters dann noch an die eigenen Gegebenheiten anpassen (Versionsnummern, FQDNs für URLs, Datenbankserver, etc) und ansonsten die vorgegebenen Rezepturen verwenden.

Der gebräuchlichste Ansatz dafür sind Helm Charts. Für viele Anwendungen, die sich in Clustern betreiben lassen, gibt es bereits diese fertigen Rezepte - Charts - zum Deployment. Über Values lassen sich noch eigene Parameter übergeben. Charts sind in Repositories organisiert. Zuallererst ist helm allerdings eine Kommandozeilen-Anwendung und das Installieren von Charts, die aus Repositories kommen, ist einfach ein Aufruf von helm install. Das ist nicht besonders deklarativ oder zuverlässig reproduzierbar.

Theorie im Hintergrund: Wie Helm deklarativ verwenden?

Um die Installation von Helm Charts auch im Git-Repository ordentlich konfigurieren zu können, liefert Flux CI/CD deswegen einen Operator mit. Ein Operator ist eine Anwendung, die im Cluster läuft und Cutom Resource Definitions umsetzt. In diesem Fall werden die neuen Ressourcen-Typen HelmRepository und HelmRelease eingeführt, die sich über Kubernetes-Manifeste definieren lassen. So kann in einem HelmRelease die Verwendung eines Charts wieder im Git-Repository deklarativ konfiguriert werden. Findet der Helm Operator dann eine Resource des Typs HelmRelease, startet er die Installation des Charts gemäß der damit verbundenen Konfiguration.

Installation von podinfo mit Helm

Nun der praktische Teil. Zum Testen der Installation mit Helm wird podinfo deployed. Das ist eine kleine Anwendung, gut geeignet für Hello World ohne große Abhängigkeiten.

Namespace als Ziel für das Deployment der Anwendung

Den Anfang macht der podinfo-Namespace, in den die Anwendung installiert werden soll. Um diesen automatisch anzulegen, genügt eine Datei bootstrap/namespace/namespace-podinfo.yaml:

---
apiVersion: v1
kind: Namespace
metadata:
  name: podinfo

Nachdem diese Datei im globalen bootstrap/-Verzeichnis liegt, dessen Inhalt automatisch in den Cluster gespielt wird, muss ansonsten nichts mehr zur Einrichtung eines Namespace vorgenommen werden.

HelmRepository als Quelle für Helm Charts

Die Installation der Anwendung erfolgt durch ein HelmRelease, dazu gleich mehr. Das Release bezieht sich auf ein Chart, das aus einem Repository kommt. In einem Repository können mehrere Charts vorhanden sein und ein und das selbe Chart kann mehrfach installiert werden, auch für verschiedene Anwendungen. Es macht daher Sinn, das HelmRepository global zu pflegen und nicht an die Anwendung zu koppeln. Deswegen gibt es im bootstrap/-Verzeichnis dafür einen eigenen Ordner und es bietet sich an, die HelmRepository-Resource nicht in den eben erstellten Namespace der Anwendung zu installieren.

Der Inhalt einer bootstrap/helmrepository/helmrepository-podinfo.yaml könnte also so aussehen:

---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmRepository
metadata:
  name: podinfo
  namespace: default
spec:
  interval: 15m
  url: https://stefanprodan.github.io/podinfo

Das Feld apiVersion hilft Kubernetes herauszufinden, wie diese Resource umgesetzt werden kann. In diesem Fall ist das keine native Resource von Kubernetes sondern kam erst mit Flux CI/CD dazu. Der name und der namespace beziehen sich hier natürlich auf das HelmRepository und nicht auf die Anwendung, die daraus später deployed wird! Die meisten HelmRepositories sind über eine URL erreichbar. Der Cluster wird alle 15 Minuten nachsehen, ob es im Repository Neuerungen gibt. Automatisch installiert werden diese allerdings nicht; das wäre eine Einstellung des HelmRelease.

HelmRelease zur Installation der Anwendung

Nun wird die Anwendung selbst konfiguriert. Dazu bekommt sie ein Verzeichnis, in das die notwendigen Manifeste alle zusammengesammelt werden. Damit das Verzeichnis von Flux CI/CD gefunden wird, bekommt die Anwendung im Anschluss noch ein Manifest für seine Kustomization.

Ich lege also das Verzeichnis apps/podinfo an und erstelle dort eine helmrelease-podinfo.yaml:

---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: podinfo
  namespace: podinfo
spec:
  chart:
    spec:
      chart: podinfo
      version: 6.x
      sourceRef:
        kind: HelmRepository
        name: podinfo
        namespace: default
  interval: 15m
  timeout: 5m
  releaseName: podinfo
  values:
    replicaCount: 1

Die Konfigurationsmöglichkeiten eines HelmRelease sind vielfältig. Zu bedenken ist, dass das HelmRelease diesmal in den podinfo-Namespace kommt und dass in der sourceRef als Quelle für das Release das HelmRepository von vorhin referenziert wird, was seinerseits aber im default-Namespace lebt.

Unter values können die Parameter für das Deployment des Helm Charts mitgegeben werden. Hier habe ich beispielsweise die Anzahl der zu startenden Container auf 1 gesetzt. Welche Parameter unterstützt werden, kommt auf das Helm Chart an und wird am besten jeweils der Dokumentation des Charts entnommen.

In diesem Fall bin ich nun mit einem einfachen HelmRelease als Resource ausgekommen. Hätte ich darüber hinaus noch z.B. eine ConfigMap gebraucht, hätte ich die einfach in das selbe Verzeichnis legen können und Flux CI/CD hätte sie auch von dort aus in den Cluster installiert.

Kustomization

Damit Flux CI/CD das HelmRelease findet, muss es noch in einer Kustomization referenziert werden. Dazu lege ich eine Datei im automatisch geladenen Bootstrapping-Ordner an - bootstrap/kustomization/kustomization-podinfo.yaml:

---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
  name: podinfo
  namespace: default
spec:
  interval: 15m
  path: "apps/podinfo"
  validation: server
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
    namespace: flux-system

Hier wird Flux CI-CD mitgeteilt, dass es eine Kustomization gibt, die podinfo heißt und deren Inhalt im GitRepository namens flux-system liegt. Dabei handelt es sich um die Kubernetes-Ressource, die sich auf das Git-Repository bezieht, mit welcher der Cluster konfiguriert wird. Das selbe Repository also, in dem die Kustomization auch liegt. Alle 15 Minuten wird dort im Pfad apps/podinfo/ nachgesehen und dessen Inhalt auf dem Cluster versucht anzuwenden (dry-run). Damit wird ermittelt, ob sich entweder im Cluster etwas getan hat, was so nicht im Git-Repository steht oder ob sich im Git-Repository etwas geändert hat, was im Cluster noch umzusetzen ist (drift detection). Ressourcen aus Manifesten, die im Git-Repository nicht mehr stehen, werden dabei auch aus dem Cluster entfernt (prune).

Wie wird das jetzt umgesetzt?

Beim periodischen Überprüfen dieser Kustomization wird Flux CI/CD also auf das HelmRelease stoßen und dieses im Cluster als Ressource anlegen. Der Vorgang, den Unterschied zwischen Git-Repository und Wirklichkeit im Cluster auszugleichen, heißt hier Reconciliation. Der Helm Controller stößt auf das HelmRelease und beginnt seinerseits mit einer Reconciliation, lädt das Helm Chart aus dem HelmRepository herunter und installiert es gemäß der Konfiguration im HelmRelease-Manifest.

Den Vorgang des Deployments kann man mit dem Kommandozeilentool von Flux beobachten. Für einen Überblick über alle von Flux verwalteten Ressourcen:

$ watch -n3 flux get all -A
NAMESPACE       NAME                            REVISION        SUSPENDED       READY   MESSAGE                                                                      
flux-system     gitrepository/flux-system       main/3782386    False           True    stored artifact for revision 'main/37823864bb3f1da405e4fe73bf304290c6e9cec4'

NAMESPACE       NAME                    REVISION                                                                SUSPENDED       READY   MESSAGE                                                                                         
default         helmrepository/podinfo  70c481e96b98984040c7150f644b77cc27baeebf8bbc7916ab40d5852297c8d3        False           True    stored artifact for revision '70c481e96b98984040c7150f644b77cc27baeebf8bbc7916ab40d5852297c8d3'

NAMESPACE       NAME                            REVISION        SUSPENDED       READY   MESSAGE                                     
default         helmchart/podinfo-podinfo       6.3.0           False           True    pulled 'podinfo' chart with version '6.3.0'

NAMESPACE       NAME                    REVISION        SUSPENDED       READY   MESSAGE                          
podinfo         helmrelease/podinfo     6.3.0           False           True    Release reconciliation succeeded

NAMESPACE       NAME                            REVISION        SUSPENDED       READY   MESSAGE                        
flux-system     kustomization/flux-system       main/3782386    False           True    Applied revision: main/3782386
default         kustomization/podinfo           main/3782386    False           True    Applied revision: main/3782386

Dabei ist zu beachten, dass hier nur erkannte Ressourcen beobachtet werden können! Hat man etwa keine Kustomization angelegt, die auf das HelmRelease verweist, weiß Flux CI/CD nichts davon und kann es hier auch nicht anzeigen.

Um zu sehen, was passiert, gibt es Logs von Flux CI/CD:

$ flux logs
info GitRepository/flux-system.flux-system - stored artifact for commit 'add podinfo helmrelease' 
info GitRepository/flux-system.flux-system - garbage collected 1 artifacts 
info GitRepository/flux-system.flux-system - no changes since last reconcilation: observed revision 'main/ca481dccca4b910e7375f7f3668c786deb541e2e' 
info GitRepository/flux-system.flux-system - stored artifact for commit 'add podinfo kustomization' 
info GitRepository/flux-system.flux-system - garbage collected 1 artifacts

Außerdem kann auch im Cluster beobachtet werden, was passiert:

$ kubectl get events -A
flux-system   12m         Normal    Progressing                  kustomization/flux-system                       Kustomization/default/podinfo created
flux-system   10m         Normal    NewArtifact                  gitrepository/flux-system                       stored artifact for commit 'add podinfo kustomization'
flux-system   10m         Normal    Progressing                  kustomization/flux-system                       Kustomization/default/podinfo configured
flux-system   10m         Normal    ReconciliationSucceeded      kustomization/flux-system                       Reconciliation finished in 419.014875ms, next run in 10m0s
default       10m         Normal    Progressing                  kustomization/podinfo                           HelmRelease/podinfo/podinfo created
default       10m         Normal    ReconciliationSucceeded      kustomization/podinfo                           Reconciliation finished in 48.9025ms, next run in 15m0s
podinfo       10m         Normal    info                         helmrelease/podinfo                             HelmChart 'default/podinfo-podinfo' is not ready
default       10m         Normal    ChartPullSucceeded           helmchart/podinfo-podinfo                       pulled 'podinfo' chart with version '6.3.0'
podinfo       10m         Normal    info                         helmrelease/podinfo                             Helm install has started
podinfo       10m         Normal    ScalingReplicaSet            deployment/podinfo                              Scaled up replica set podinfo-5b58b74576 to 1
podinfo       10m         Normal    SuccessfulCreate             replicaset/podinfo-5b58b74576                   Created pod: podinfo-5b58b74576-gd7px
podinfo       10m         Normal    info                         helmrelease/podinfo                             Helm install succeeded
podinfo       10m         Normal    Scheduled                    pod/podinfo-5b58b74576-gd7px                    Successfully assigned podinfo/podinfo-5b58b74576-gd7px to lima-rancher-desktop
podinfo       10m         Normal    Pulled                       pod/podinfo-5b58b74576-gd7px                    Container image "ghcr.io/stefanprodan/podinfo:6.3.0" already present on machine
podinfo       10m         Normal    Created                      pod/podinfo-5b58b74576-gd7px                    Created container podinfo
podinfo       10m         Normal    Started                      pod/podinfo-5b58b74576-gd7px                    Started container podinfo
flux-system   9m56s       Normal    GarbageCollectionSucceeded   gitrepository/flux-system                       garbage collected 1 artifacts
flux-system   8m          Normal    ReconciliationSucceeded      kustomization/flux-system                       Reconciliation finished in 489.568ms, next run in 10m0s
default       3m9s        Normal    ArtifactUpToDate             helmrepository/podinfo                          artifact up-to-date with remote revision: '70c481e96b98984040c7150f644b77cc27baeebf8bbc7916ab40d5852297c8d3'
flux-system   44s         Normal    GitOperationSucceeded        gitrepository/flux-system                       no changes since last reconcilation: observed revision 'main/37823864bb3f1da405e4fe73bf304290c6e9cec4'

Ausprobieren

Ist Flux CI/CD fertig mit Reconciling, sollte es ein paar Ressourcen im Cluster-Namespace podinfo geben:

$ kubectl get all -n podinfo
NAME                           READY   STATUS    RESTARTS   AGE
pod/podinfo-5b58b74576-gd7px   1/1     Running   0          1m

NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
service/podinfo   ClusterIP   10.43.244.205   <none>        9898/TCP,9999/TCP   1m

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/podinfo   1/1     1            1           1m

NAME                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/podinfo-5b58b74576   1         1         1       1m

Sind diese Ressourcen schlussendlich alle im Ready-Zustand, regelt ein Deployment mit Hilfe eines ReplicaSet, dass ein Pod läuft. Der Service sammelt alle Pods ein - in diesem Fall nur den einen. Der Service hat TCP-Ports offen und leitet Anfragen an diese Ports an die Pods weiter. Nun ist aber der Service nur mit einer ClusterIP erreichbar - nicht von außerhalb des Clusters. Zum einfachen Testen lässt sich der Port aber über kubectl auch lokal auf der eigenen Workstation erreichbar machen:

$ kubectl port-forward -n podinfo service/podinfo 9898:9898
Forwarding from 127.0.0.1:9898 -> 9898
Forwarding from [::1]:9898 -> 9898

Damit wird ein Port-Forwarding auf http://localhost:9898 eingerichtet, bei dem der Service mit Port 9898 ran geht. Ruft man diese Adresse im Browser auf, wird man von der Podinfo-Anwendung begrüßt.