Helm-Deploy erfolgreich. Config nie angewendet.

kubernetes helm helmfile devops platform-engineering

Wir haben eine neue API-Route zum Gateway-Service hinzugefügt. Die helmfile-Pipeline lief durch, jede Stage war grün, und das Deploy schloss ohne Fehler ab.

Die Route gab 404 zurück.

Ein port-forward und ein curl bestätigten es: Die neue Route-Definition war nicht auf dem Live-Gateway aktiv. Der Pod lief noch mit der Config aus dem letzten Neustart, nicht mit der, die wir soeben deployt hatten. Wir hatten eine ConfigMap aktualisiert. Der Pod wusste davon nichts.

Was die Ursache schwer erkennbar machte: Es hatte zuvor funktioniert. Zwei Monate früher war eine ähnliche Config-Änderung ohne Probleme live gegangen. Was wir damals nicht wussten: Die Änderung hatte zufällig auch einen Volume-mountPath berührt, was Kubernetes veranlasste, den Pod als Nebeneffekt neu zu starten. Das Config-Update wirkte kausal. Es war Zufall.

Warum ConfigMaps keinen Neustart auslösen

Kubernetes startet einen Pod neu, wenn sich seine Pod-Template-Spec ändert. Eine ConfigMap ist ein eigenständiges Objekt in der Kubernetes-API. Eine Aktualisierung mutiert das Pod-Template nicht. Der Pod läuft weiter mit dem, was beim letzten Start eingebunden wurde.

Das ist so gewollt. Kubernetes trennt bewusst “die Daten haben sich geändert” von “der Consumer muss neu starten.” Für Apps mit prozessinternen File-Watchern ist das das richtige Verhalten: Sie können Config nachladen, ohne dass der Pod neu startet. Für Apps, die Config-Dateien beim Start lesen und im Speicher halten, ist es eine Falle.

Der Gateway-Service fällt in die zweite Kategorie. Er liest API-Definitionen aus eingebundenen Dateien beim Start. Es gibt keinen File-Watcher. Eine ConfigMap-Aktualisierung landet auf der Festplatte und bleibt dort, für den laufenden Prozess unsichtbar, bis etwas den Pod neu startet.

Die Standardlösung: Checksum-Annotation

Die kanonische Lösung besteht darin, eine checksum/config-Annotation zum Pod-Template des Deployments hinzuzufügen, die aus dem ConfigMap-Inhalt abgeleitet wird:

# templates/deployment.yaml — standard single-chart pattern
spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include "gateway.configmap" . | sha256sum | quote }}

Wenn sich der ConfigMap-Inhalt ändert, ändert sich der Annotation-Wert. Die Annotation liegt innerhalb von spec.template.metadata, das Teil der Pod-Spec ist. Kubernetes erkennt, dass das Pod-Template sich geändert hat, und löst einen Rolling Restart aus. Der Annotation-Wert ist als Metadatum bedeutungslos; seine einzige Aufgabe besteht darin, das Änderungssignal in die Pod-Spec zu tragen.

Dieses Pattern erscheint in der Helm-Dokumentation und den meisten Artikeln zum erzwungenen Neustart von Pods bei ConfigMap-Änderungen. Es funktioniert gut in einem einzelnen Helm-Chart, wo include "gateway.configmap" das benannte Template erreichen kann.

Warum das bei mehreren Releases nicht funktioniert

Unser Setup hat zwei separate Helm-Releases: eines, das das Gateway-Deployment verwaltet, und eines, das die API-Definitions-ConfigMaps aus einzelnen Definitionsdateien generiert.

include ist auf den aktuellen Chart begrenzt. Man kann include "other-release.configmap" nicht aus einem Deployment-Template aufrufen; das benannte Template existiert im Rendering-Kontext dieses Charts nicht. Der ConfigMap-Inhalt ist dem Deployment-Chart zur Render-Zeit nicht zugänglich.

Das ist der Teil, den die meisten Checksum-Annotation-Artikel überspringen. Sie setzen einen monolithischen Chart voraus, in dem Deployment und ConfigMap einen Template-Namespace teilen. Sobald man sie in separate Releases aufteilt, was bei dynamisch generierten Configs üblich ist, hat das Standardpattern keine Möglichkeit, darüber hinaus zu greifen.

Die helmfile-Lösung

helmfile-Values-Dateien unterstützen Go-Templating mit einer Reihe von Funktionen, die über Helm-Standard hinausgehen. Zwei davon lösen dieses Problem direkt.

readFile liest den Rohinhalt einer Datei und gibt ihn als String zurück. readDirEntries gibt die Einträge eines Verzeichnisses zurück. Beide lösen Pfade relativ zum eigenen Verzeichnis der Values-Datei auf, nicht zum Repo-Root. Dieses letzte Detail ist wichtig.

In der Values-Datei des Gateway-Releases lesen wir jede Quelldatei, die die ConfigMaps speist, verketten den Inhalt, hashen ihn und exponieren das Ergebnis als Values-Key:

# helmfile-values/gateway/values.yaml.gotmpl
{{- $apiDir := "./apis" -}}
{{- $content := "" -}}
{{- range readDirEntries $apiDir -}}
{{-   $content = cat $content (readFile (printf "%s/%s" $apiDir .Name)) -}}
{{- end -}}
configChecksum: {{ $content | sha256sum | quote }}

Der ./apis-Pfad löst sich relativ zum Speicherort der Values-Datei auf. Liegt values.yaml.gotmpl unter helmfile-values/gateway/values.yaml.gotmpl, muss ./apis als helmfile-values/gateway/apis/ auf der Festplatte vorhanden sein. Stimmt das nicht, gibt readDirEntries eine leere Liste zurück, $content bleibt leer, und der Hash ist konstant. Die Annotation wird gerendert, ändert sich aber nie, unabhängig davon, was in den Quelldateien steht.

Das Deployment-Template liest dann den vorberechneten Hash aus den Values:

# gateway chart — templates/deployment.yaml
spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ .Values.configChecksum }}

Die zentrale Erkenntnis: Die Hash-Berechnung findet in der helmfile-Values-Schicht statt, bevor ein chart-bezogenes Templating läuft. Der Deployment-Chart muss nichts über den ConfigMap-Chart wissen. Die beiden Releases sind entkoppelt; die Checksum verbindet sie über das Values-Interface.

Verifikation

helmfile template rendert das Deployment-YAML mit der befüllten Annotation. Eine Datei im APIs-Verzeichnis bearbeiten, helmfile template erneut ausführen und bestätigen, dass sich der Hash in der Annotation ändert. Ändert er sich nicht, ist der Pfad falsch. {{ $content | len }} zum Values-Template hinzufügen, um zu bestätigen, dass überhaupt Inhalt gelesen wird.

helmfile diff zeigt die Annotation-Aktualisierung als saubere Pod-Template-Änderung: nur der Annotation-Wert, sonst nichts. Nach dem Apply bestätigen ein port-forward und ein curl auf die neue Route, dass die Config live ist.

Was bleibt

Multi-Release-Architekturen brechen Single-Release-Annahmen. Die Checksum-Annotation ist für einzelne Charts gut dokumentiert; die Cross-Release-Variante erscheint kaum in offiziellen Docs oder Blog-Artikeln.

helmfiles readFile und readDirEntries sind der richtige Ausweg, weil sie auf der Values-Schicht operieren, bevor ein chart-bezogener Template-Kontext läuft. Das macht sie für jedes Release verfügbar, das Inhalte aus den Quelldateien eines anderen Releases hashen muss.

Das Verhalten mit relativen Pfaden ist die einzige Stelle, an der die Abstraktion undicht wird. Den Pfad-Mapping einmal mit einem Kommentar in der Values-Datei des Repos dokumentieren, und es bleibt gelöst.

Das gleiche Pattern gilt für jeden Service, der Config aus eingebundenen Dateien beim Start liest: Message Broker, Proxies, Zertifikat-Bundles, Policy Engines. Wenn der Service zur Laufzeit keine Dateiänderungen beobachtet, ist ein ConfigMap-Update still, bis etwas den Pod neu startet. Die Checksum-Annotation macht diesen Neustart automatisch und deterministisch.