GitOps mit Helmfile und Kyverno: Unser Deployment-Workflow
Zurück zum Blog

GitOps mit Helmfile und Kyverno: Unser Deployment-Workflow

7 Min. Lesezeit
Lesen auf English

Zwei Werkzeuge, ein Workflow

In den letzten Jahren haben wir in separaten Artikeln beschrieben, wie wir Helmfile für deklaratives Helm-Management einsetzen, wie wir Kyverno als Policy Engine nutzen, und wie sich beides in größeren Setups bewährt -- von Multi-Cluster-Deployments mit Helmfile bis zu Kyverno im Produktiveinsatz. Was bisher fehlte, war der Artikel, der zeigt, wie diese Bausteine in unserem täglichen Deployment-Workflow zusammenspielen.

Der Kern unseres Ansatzes ist einfach: Git ist die einzige Quelle der Wahrheit. Jede Änderung an der Infrastruktur -- ob ein neues Deployment, eine geänderte Konfiguration oder eine aktualisierte Policy -- durchläuft einen Pull Request, wird von Kyverno validiert und von Helmfile auf die Cluster angewendet. Kein manuelles kubectl apply, kein helm install auf der Kommandozeile.

Git als Single Source of Truth

Unser Infrastruktur-Repository ist seit Mitte 2023 die zentrale Stelle für alles, was auf unseren Kubernetes-Clustern läuft. Die Verzeichnisstruktur folgt dem Muster, das wir im Multi-Cluster-Artikel beschrieben haben, erweitert um ein dediziertes Verzeichnis für Kyverno-Policies:

infra/
├── helmfile.yaml
├── environments/
│   ├── dev/
│   │   ├── values.yaml
│   │   └── secrets.yaml.enc
│   ├── staging/
│   │   └── values.yaml
│   └── prod/
│       ├── values.yaml
│       └── secrets.yaml.enc
├── policies/
│   ├── base/
│   │   ├── require-labels.yaml
│   │   ├── restrict-image-registries.yaml
│   │   └── disallow-privileged.yaml
│   └── prod/
│       ├── require-resource-limits.yaml
│       └── restrict-nodeport.yaml
└── releases/
    ├── cert-manager.yaml
    ├── ingress-nginx.yaml
    └── kyverno.yaml

Jede Änderung in diesem Repository erzeugt einen Pull Request. Niemand pushed direkt auf main. Das ist keine Konvention, sondern über Branch-Protection-Rules in Gitea erzwungen.

Helmfile mit integriertem Kyverno

Unser helmfile.yaml verwaltet Kyverno als reguläres Helm Release -- genauso wie jeden anderen Infrastruktur-Service. Das stellt sicher, dass Kyverno selbst denselben deklarativen Lifecycle durchläuft wie alles andere:

repositories:
  - name: kyverno
    url: https://kyverno.github.io/kyverno/

environments:
  dev:
    values:
      - environments/dev/values.yaml
  staging:
    values:
      - environments/staging/values.yaml
  prod:
    values:
      - environments/prod/values.yaml
    secrets:
      - environments/prod/secrets.yaml.enc

releases:
  - name: kyverno
    namespace: kyverno
    chart: kyverno/kyverno
    version: 3.3.4
    values:
      - releases/kyverno.yaml
      - replicaCount: {{ .Values | getOrNil "kyverno.replicas" | default 1 }}

  - name: kyverno-policies
    namespace: kyverno
    chart: kyverno/kyverno-policies
    version: 3.3.4
    needs:
      - kyverno/kyverno
    values:
      - validationFailureAction: {{ .Values | getOrNil "kyverno.validationAction" | default "Audit" }}

Der entscheidende Punkt ist die umgebungsabhängige validationFailureAction. In Dev steht sie auf Audit -- Verstöße werden geloggt, aber nicht blockiert. In Staging und Production steht sie auf Enforce. Das gibt Entwicklern in Dev die Freiheit zu experimentieren, während in höheren Umgebungen Policies konsequent durchgesetzt werden.

Zusätzlich deployen wir unsere eigenen Policies als separate Kyverno-Ressourcen. Deren YAML-Dateien liegen im policies/-Verzeichnis und werden über ein eigenes Helmfile-Release angewendet, das einen lokalen Chart nutzt.

Pre-Deploy Policy Checks in der CI

Die eigentliche Stärke des Workflows liegt nicht im Cluster, sondern in der CI-Pipeline. Bevor Helmfile überhaupt etwas anwendet, prüft die Kyverno CLI alle generierten Manifeste gegen unsere Policies. Das fängt Verstöße ab, bevor sie den Cluster erreichen.

Unsere Gitea-Actions-Pipeline für das Infrastruktur-Repository sieht so aus:

name: Deploy Infrastructure
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install tools
        run: |
          # Helmfile
          curl -fsSL -o helmfile.tar.gz \
            https://github.com/helmfile/helmfile/releases/download/v0.169.2/helmfile_0.169.2_linux_amd64.tar.gz
          tar xzf helmfile.tar.gz && mv helmfile /usr/local/bin/
          # Kyverno CLI
          curl -fsSL -o kyverno-cli.tar.gz \
            https://github.com/kyverno/kyverno/releases/download/v1.13.2/kyverno-cli_v1.13.2_linux_x86_64.tar.gz
          tar xzf kyverno-cli.tar.gz && mv kyverno /usr/local/bin/kyverno-cli

      - name: Template manifests
        run: |
          helmfile -e ${{ env.TARGET_ENV }} template > /tmp/rendered-manifests.yaml

      - name: Validate against policies
        run: |
          kyverno-cli apply policies/base/ \
            --resource /tmp/rendered-manifests.yaml \
            --detailed-results
          if [ "${{ env.TARGET_ENV }}" = "prod" ]; then
            kyverno-cli apply policies/prod/ \
              --resource /tmp/rendered-manifests.yaml \
              --detailed-results
          fi

  diff:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    needs: validate
    steps:
      - uses: actions/checkout@v4

      - name: Helmfile diff
        run: |
          helmfile -e ${{ env.TARGET_ENV }} diff --context 3
        env:
          KUBECONFIG: ${{ secrets.KUBECONFIG }}

  apply:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    needs: validate
    steps:
      - uses: actions/checkout@v4

      - name: Helmfile apply
        run: |
          helmfile -e ${{ env.TARGET_ENV }} apply --context 3
        env:
          KUBECONFIG: ${{ secrets.KUBECONFIG }}

Der Ablauf ist dreistufig. Zuerst rendert helmfile template alle Manifeste, ohne sie anzuwenden. Dann prüft kyverno-cli apply diese Manifeste gegen unsere Policies -- vollständig offline, ohne Cluster-Zugriff. Erst wenn die Validierung bestanden ist, führt die Pipeline entweder helmfile diff (bei Pull Requests) oder helmfile apply (bei Merges auf main) aus.

Policy-Verstöße im Pull Request

Wenn die Kyverno-Validierung fehlschlägt, sieht der Entwickler das direkt im Pull Request. Die CI-Pipeline scheitert, und die Ausgabe zeigt präzise, welche Policy verletzt wurde und in welchem Manifest:

Policy: require-labels
Rule: check-team-label
Result: FAIL
Resource: apps/v1/Deployment/api-gateway
Message: label 'app.kubernetes.io/team' is required

Das ist ein bewusster Shift-Left-Ansatz. Statt dass Kyverno den Admission Request im Cluster ablehnt und der Entwickler erst beim Deployment-Versuch davon erfährt, scheitert der PR-Check bereits Minuten nach dem Push. Die Feedback-Schleife ist kurz, und die Korrektur findet im selben PR statt.

In der Praxis hat sich gezeigt, dass die meisten Verstöße in den ersten Wochen nach Einführung einer neuen Policy auftreten. Danach hat das Team die Regeln internalisiert, und die CI-Checks sind eher ein Sicherheitsnetz als eine Hürde.

Environment Promotion: Von Dev nach Production

Unser Promotion-Modell folgt einem klaren Pfad: Dev, dann Staging, dann Production. Dabei ändern sich nicht die Manifeste selbst, sondern die umgebungsspezifischen Values in den Helmfile-Environments.

Ein typischer Ablauf für ein neues Service-Deployment sieht so aus:

  1. Dev: Der Entwickler erstellt einen PR, der das neue Release in helmfile.yaml hinzufügt und Dev-Values konfiguriert. Die CI rendert und validiert. Nach dem Merge deployt die Pipeline auf den Dev-Cluster.

  2. Staging: Ein zweiter PR fügt die Staging-Values hinzu oder aktualisiert die Image-Version. Die gleiche Validierung läuft, diesmal mit Enforce-Policies. Nach dem Merge geht das Release auf Staging.

  3. Production: Ein dritter PR mit den Prod-Values. Zusätzlich greifen die Prod-spezifischen Policies -- zum Beispiel die Anforderung, dass Resource Limits gesetzt sein müssen oder dass nur Images aus unserer internen Registry erlaubt sind.

Jeder Schritt ist ein separater, reviewbarer PR. Es gibt keinen automatischen Promote-Mechanismus von Dev nach Staging. Das ist Absicht: Wir wollen, dass ein Mensch den Übergang explizit auslöst und bestätigt. Automatisierung innerhalb einer Umgebung ja, automatische Promotion zwischen Umgebungen nein.

Kyverno als letzte Verteidigungslinie

Auch wenn die CI-Validierung den Großteil der Verstöße abfängt, läuft Kyverno im Cluster im Enforce-Modus als letzte Verteidigungslinie. Das ist relevant für zwei Szenarien: Erstens, wenn jemand doch einmal manuell etwas auf den Cluster anwendet -- was in Notfallsituationen vorkommen kann. Zweitens, wenn sich Policies ändern, nachdem Manifeste bereits validiert und gemerged wurden.

Wie in unserem Artikel zu Kyverno in Produktion beschrieben, nutzen wir Policy Exceptions für Ausnahmen, die technisch begründet und zeitlich begrenzt sind. Diese Exceptions liegen ebenfalls im Git-Repository und durchlaufen denselben Review-Prozess.

Erfahrungen nach einem Jahr

Wir setzen diesen Workflow in seiner jetzigen Form seit Anfang 2024 ein. Vorher hatten wir Helmfile und Kyverno zwar einzeln im Einsatz -- das haben wir seit den Anfängen mit Helm 3 schrittweise aufgebaut -- aber die Integration in eine kohärente GitOps-Pipeline war ein eigenes Projekt.

Die wichtigsten Erkenntnisse:

Helmfile template ist der Schlüssel. Die Möglichkeit, alle Manifeste zu rendern, ohne sie anzuwenden, macht die gesamte Pre-Validation erst möglich. Ohne diesen Schritt müsste man sich auf die Cluster-seitige Admission Control verlassen, und die Feedback-Schleife wäre deutlich länger.

Policy-Versionen gehören ins Helmfile. Wir pinnen die Kyverno-Charts-Version genauso wie jede andere Abhängigkeit. Ein unbeabsichtigtes Policy-Update, das plötzlich Deployments blockiert, ist das Letzte, was man in Production will.

Die Kyverno CLI muss dieselbe Version haben wie der Cluster. Wir hatten einen Fall, in dem die CLI-Version neuer war als der installierte Kyverno und eine Policy-Syntax akzeptiert hat, die der Admission Controller im Cluster noch nicht kannte. Seitdem pinnen wir beide Versionen gemeinsam.

Weniger Policies, strenger durchgesetzt. Wir haben anfangs zu viele Policies gleichzeitig eingeführt. Das hat zu einer Flut von CI-Fehlern geführt und die Akzeptanz im Team gesenkt. Inzwischen führen wir neue Policies erst im Audit-Modus ein, kommunizieren sie, und schalten sie nach zwei bis vier Wochen auf Enforce.

Helmfile vs. Argo CD

Nicht verschweigen wollen wir, dass wir in einigen neueren Setups Argo CD statt Helmfile einsetzen. Argo CD bringt einen Pull-basierten GitOps-Ansatz mit: Es überwacht das Git-Repository kontinuierlich und synchronisiert den Cluster-Zustand automatisch. Das hat Vorteile -- insbesondere die sofortige Erkennung von Drift zwischen Git und Cluster. Der hier beschriebene Helmfile-Workflow ist Push-basiert: Die CI-Pipeline wendet Änderungen aktiv an.

Beide Ansätze funktionieren gut mit Kyverno. Argo CD synchronisiert Manifeste über den Kubernetes-API-Server, und Kyverno validiert sie dort als Admission Controller -- unabhängig davon, woher das Manifest kommt. Für uns ist die Wahl zwischen Helmfile und Argo CD keine Glaubensfrage, sondern eine projektspezifische Entscheidung. Helmfile glänzt bei bestehenden Setups mit vielen Helm-Charts und komplexen Values-Hierarchien. Argo CD eignet sich besser für Umgebungen, in denen kontinuierliche Synchronisation und eine grafische Übersicht gewünscht sind.

Fazit

Der Workflow ist kein kompliziertes Setup. Es sind letztlich zwei Werkzeuge -- Helmfile (oder alternativ Argo CD) und Kyverno -- die über Git als zentrale Wahrheitsquelle verbunden werden. Die eigentliche Arbeit liegt darin, Disziplin durchzuhalten: Alles geht durch Git, alles wird validiert, und manuelle Eingriffe bleiben die dokumentierte Ausnahme. Das ist kein perfektes System, aber es ist eines, das wir verstehen, warten können und das zuverlässig funktioniert.

Teilen

Patrick Hütter

Geschrieben von

Patrick Hütter

Gründer & Software-Architekt

Softwarearchitekt, Ingenieur und Unternehmer. Patrick baut seit über einem Jahrzehnt Produkte und Plattformen — von Enterprise-Backends über Cloud-Native-Infrastruktur bis hin zu KI-gestützten Anwendungen. Als Gründer von encircle360 verbindet er technische Tiefe mit unternehmerischem Denken und treibt Open-Source-Projekte voran, die echten Impact haben.