Helmfile für Multi-Cluster-Deployments: Environments und Selektoren
Von einem Cluster zu dreien
Als wir Helmfile vor knapp drei Jahren in unseren Stack aufgenommen haben, war die Ausgangslage überschaubar: ein Cluster, eine Handvoll Releases, eine helmfile.yaml. Das funktionierte hervorragend -- deklaratives Helm-Management, umgebungsspezifische Values, ein einzelner Befehl für das gesamte Deployment. Wir haben damals beschrieben, wie Helmfile den imperativen Ansatz mit einzelnen helm upgrade-Befehlen durch eine deklarative YAML-Datei ersetzt.
Mittlerweile betreiben wir bei encircle360 drei K3s-Cluster: Development, Staging und Production. Jeder Cluster läuft auf eigenen Nodes, wie wir das in unserem Erfahrungsbericht zu K3s im Produktiveinsatz beschrieben haben. Die Cluster teilen sich den Großteil der Releases -- dieselben Spring-Boot-Services, denselben Ingress-Controller, dasselbe Monitoring -- aber die Konfigurationen unterscheiden sich. Unterschiedliche Domains, unterschiedliche Ressourcenlimits, unterschiedliche Replica-Counts, unterschiedliche Secrets.
Die Frage war: Wie verwalten wir drei Cluster mit einer einzigen Helmfile-Konfiguration, ohne in Kopier-und-Einfüge-Chaos zu enden?
Environments als Grundlage
Helmfile bringt das Konzept der Environments von Haus aus mit. In unserem ersten Helmfile-Artikel haben wir sie kurz erwähnt. Für Multi-Cluster-Setups sind sie das zentrale Werkzeug.
Ein Environment in Helmfile entspricht einem Deployment-Ziel -- in unserem Fall einem Cluster. Jedes Environment bringt eigene globale Variablen mit, die in allen Release-Definitionen verfügbar sind.
# helmfile.yaml
environments:
dev:
values:
- environments/dev.yaml
kubeContext: k3s-dev
staging:
values:
- environments/staging.yaml
kubeContext: k3s-staging
production:
values:
- environments/production.yaml
kubeContext: k3s-production
Die kubeContext-Direktive ist entscheidend. Sie sorgt dafür, dass Helmfile automatisch den richtigen kubectl-Kontext verwendet, wenn man ein Environment wählt. Kein manuelles kubectl config use-context vor jedem Deployment. Ein helmfile -e production apply spricht immer das Production-Cluster an, unabhängig davon, welcher Kontext gerade aktiv ist.
Die Environment-Values-Dateien definieren globale Variablen, die sich zwischen den Clustern unterscheiden:
# environments/production.yaml
clusterName: production
domain: encircle360.com
registry: registry.encircle360.com
ingressClass: traefik
certManager:
issuer: letsencrypt-prod
resources:
defaultCpuRequest: 250m
defaultMemoryRequest: 256Mi
defaultCpuLimit: 500m
defaultMemoryLimit: 512Mi
replicas:
default: 2
# environments/dev.yaml
clusterName: dev
domain: dev.encircle360.com
registry: registry.encircle360.com
ingressClass: traefik
certManager:
issuer: letsencrypt-staging
resources:
defaultCpuRequest: 100m
defaultMemoryRequest: 128Mi
defaultCpuLimit: 250m
defaultMemoryLimit: 256Mi
replicas:
default: 1
Diese Werte stehen dann in den Release-Definitionen als Go-Template-Variablen zur Verfügung. Statt pro Cluster eigene Values-Dateien zu pflegen, referenzieren die Templates die globalen Variablen:
releases:
- name: api-service
namespace: application
chart: ./charts/spring-boot-app
version: 2.3.0
values:
- values/api-service.yaml
- values/{{ .Environment.Name }}/api-service.yaml
Die erste Values-Datei enthält die clusterübergreifenden Defaults. Die zweite -- optional -- enthält umgebungsspezifische Überschreibungen. So bleibt die Konfiguration DRY, und spezifische Anpassungen sind klar separiert.
Selektoren für gezielte Deployments
Nicht bei jedem Deployment will man alle Releases aktualisieren. Wenn der Monitoring-Stack stabil läuft und nur ein einzelner Service ein Update braucht, wäre ein vollständiges helmfile apply überdimensioniert. Helmfile löst das mit Labels und Selektoren.
Jedes Release kann beliebige Labels tragen:
releases:
- name: api-service
namespace: application
chart: ./charts/spring-boot-app
labels:
tier: application
team: backend
values:
- values/api-service.yaml
- name: web-frontend
namespace: application
chart: ./charts/angular-app
labels:
tier: application
team: frontend
values:
- values/web-frontend.yaml
- name: prometheus
namespace: monitoring
chart: prometheus-community/kube-prometheus-stack
labels:
tier: infrastructure
team: platform
values:
- values/monitoring.yaml
- name: ingress-nginx
namespace: ingress
chart: ingress-nginx/ingress-nginx
labels:
tier: infrastructure
team: platform
values:
- values/ingress.yaml
Über den --selector-Parameter (oder kurz -l) filtert man bei der Ausführung:
# Nur Application-Tier deployen
helmfile -e production -l tier=application apply
# Nur Releases des Backend-Teams
helmfile -e staging -l team=backend apply
# Nur ein einzelnes Release
helmfile -e dev -l name=api-service apply
# Kombination: Infrastructure-Tier im Staging
helmfile -e staging -l tier=infrastructure diff
In unserer täglichen Arbeit verwenden wir Selektoren ständig. Ein typischer Workflow sieht so aus: Das Backend-Team aktualisiert seinen Service, schiebt die Chart-Version und Values-Änderung ins Repository, und die Pipeline führt helmfile -e staging -l team=backend apply aus. Der Rest des Clusters bleibt unangetastet.
helmfile diff: Änderungen vor dem Deployment prüfen
Bevor wir auf Production deployen, nutzen wir helmfile diff als Pflichtschritt. Der Befehl vergleicht den gewünschten Zustand aus der Helmfile-Konfiguration mit dem Ist-Zustand im Cluster und zeigt die Unterschiede als farbcodierten Diff an.
# Vollständiger Diff gegen Production
helmfile -e production diff
# Diff nur für Application-Releases
helmfile -e production -l tier=application diff
Die Ausgabe ähnelt einem git diff auf Kubernetes-Manifest-Ebene. Man sieht genau, welche Felder sich ändern -- neue Umgebungsvariablen, geänderte Ressourcenlimits, aktualisierte Image-Tags. In unseren CI/CD-Pipelines läuft helmfile diff als eigener Job, dessen Ausgabe im Pipeline-Log sichtbar ist. Das gibt dem Team die Möglichkeit, die Änderungen zu prüfen, bevor helmfile apply sie tatsächlich anwendet.
Für den Production-Cluster ist das nicht optional. Wir haben zu oft erlebt, dass eine vermeintlich harmlose Values-Änderung unerwartete Nebenwirkungen hatte -- ein fehlender Wert, der zu einem leeren String wurde, ein geänderter Port, der den Health-Check brach. helmfile diff fängt das ab, bevor es den Cluster erreicht.
Secrets-Management mit vals
Secrets gehören nicht im Klartext ins Git-Repository. Für Helmfile gibt es mehrere Ansätze, und wir haben uns für vals entschieden -- ein Tool, das Secrets zur Laufzeit aus externen Quellen injiziert.
vals unterstützt verschiedene Backends: SOPS-verschlüsselte Dateien, HashiCorp Vault, AWS Secrets Manager und andere. In unserem Setup verwenden wir SOPS mit age als Verschlüsselungstool. Die verschlüsselten Secrets liegen im Repository, und vals entschlüsselt sie während des helmfile apply.
# values/production/secrets.yaml (verschluesselt mit SOPS)
databasePassword: ENC[AES256_GCM,data:abc123...,type:str]
apiKey: ENC[AES256_GCM,data:def456...,type:str]
In der helmfile.yaml aktiviert man vals als Secrets-Backend:
helmDefaults:
wait: true
timeout: 300
environments:
production:
secrets:
- environments/production-secrets.yaml
Der Vorteil gegenüber dem älteren helm-secrets-Plugin: vals ist unabhängig von Helm und lässt sich auch außerhalb von Helmfile nutzen. Die Secrets liegen verschlüsselt im Repository, durchlaufen den normalen Code-Review-Prozess und sind trotzdem nie im Klartext sichtbar -- außer während des Deployments, wo vals sie entschlüsselt und an Helm weitergibt.
Verzeichnisstruktur für Multi-Cluster
Mit drei Clustern, Dutzenden Releases und umgebungsspezifischen Values wächst die Verzeichnisstruktur. Wir haben mehrere Ansätze ausprobiert und sind bei folgender Struktur gelandet:
helmfile/
helmfile.yaml
environments/
dev.yaml
dev-secrets.yaml
staging.yaml
staging-secrets.yaml
production.yaml
production-secrets.yaml
values/
api-service.yaml # Cluster-uebergreifende Defaults
web-frontend.yaml
monitoring.yaml
ingress.yaml
dev/
api-service.yaml # Dev-spezifische Overrides
staging/
api-service.yaml
production/
api-service.yaml
web-frontend.yaml
charts/
spring-boot-app/
angular-app/
.sops.yaml # SOPS-Konfiguration
Die Grundidee: Defaults stehen in values/<release>.yaml, umgebungsspezifische Abweichungen in values/<env>/<release>.yaml. Nicht jedes Release braucht umgebungsspezifische Values -- wenn die Defaults ausreichen, existiert die Datei schlicht nicht. Helmfile kommt damit zurecht, wenn man missingFileHandler: Warn setzt.
helmDefaults:
missingFileHandler: Warn
Für größere Setups unterstützt Helmfile auch das Aufteilen in mehrere Dateien mit helmfiles:
helmfiles:
- path: helmfile.d/infrastructure.yaml
- path: helmfile.d/applications.yaml
- path: helmfile.d/monitoring.yaml
Jede Teildatei enthält ihre eigenen Releases und kann unabhängig deployed werden. Wir nutzen das noch nicht, da unsere Gesamtkonfiguration mit rund dreißig Releases noch in einer Datei überschaubar bleibt. Aber die Option steht bereit, wenn die Komplexität weiter wächst.
CI/CD-Integration: Drei Cluster, eine Pipeline
Die Pipeline-Integration folgt einem klaren Muster. Jeder Cluster hat seine eigene Deployment-Stage, aber alle nutzen dieselbe Helmfile-Konfiguration:
# Development: Automatisch bei jedem Push auf develop
helmfile -e dev apply
# Staging: Automatisch bei Merge auf main
helmfile -e staging apply
# Production: Manuell ausgeloest nach Staging-Validierung
helmfile -e production diff # Review-Schritt
helmfile -e production apply # Nach Freigabe
Der kubectl-Kontext wird in der Pipeline über die kubeconfig gesetzt. Jeder Cluster hat seinen eigenen Service Account mit eingeschränkten Rechten. Die Pipeline braucht keine Logik, um zwischen Clustern zu unterscheiden -- das übernimmt Helmfile über die Environments.
Fazit
Helmfile skaliert von einem einzelnen Cluster zu einer Multi-Cluster-Landschaft, ohne dass man das Grundkonzept ändern muss. Environments bilden die Cluster ab, Selektoren ermöglichen gezielte Deployments, und vals hält die Secrets sicher. Die Verzeichnisstruktur mit clusterübergreifenden Defaults und umgebungsspezifischen Overrides vermeidet Duplikation, ohne die Übersicht zu verlieren.
Wer bereits Helmfile für ein einzelnes Cluster nutzt -- wie wir es in unserem Einführungsartikel beschrieben haben -- kann den Schritt zu mehreren Clustern schrittweise gehen. Zuerst Environments definieren, dann Values aufteilen, dann Selektoren einführen. Jeder Schritt bringt sofort Nutzen und erfordert keinen Umbau der bestehenden Konfiguration.
In unseren Projekten hat sich dieses Setup über die letzten Monate bewährt. Drei K3s-Cluster, rund dreißig Releases pro Cluster, ein Git-Repository als Single Source of Truth. Änderungen durchlaufen Pull Requests, helmfile diff zeigt den Impact, und helmfile apply bringt den gewünschten Zustand auf den Cluster. Deklarativ, nachvollziehbar und -- mit etwas Struktur -- auch bei mehreren Clustern übersichtlich.
Geschrieben von
Patrick HütterGrü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.
Das könnte dich auch interessieren
CI-Runner in der microVM: Docker-Builds mit Kata Containers auf Kubernetes
31.08.2026 · 10 Min. Lesezeit
GitOps mit Helmfile und Kyverno: Unser Deployment-Workflow
10.03.2025 · 7 Min. Lesezeit
K3s und KubeVirt: Konvergente Infrastruktur auf Bare Metal
20.01.2025 · 6 Min. Lesezeit