Es führen viele Wege zu einem automatisierten Deployment. Teilweise können religiöse Diskussionen über das “Wie” und das “Womit” geführt werden. Entscheidend ist jedoch, dass man mit möglichst einfachen, strukturgebenden Mitteln einen hohen Automatisierungsgrad beim Rollout der Infrastruktur- und Anwendungskomponenten erreicht. Eine CI/CD-Pipeline, die unter Versionskontrolle stehende Provisionierungsskripte ausführt, ist ein guter Start. Mit der Zeit wachsen die Skripte aber an und es wird zunehmend schwer, sie wartbar und wiederholbar zu halten. Deshalb empfiehlt es sich, stattdessen Werkzeuge einzusetzen, die statt der sequenziellen Abarbeitung von Schritten ein Modell des Wunschzustands (desired state) verarbeiten und versuchen, diesen im Zielsystem herzustellen. Helmfile ist ein solches Tool und gleicht so manche Unzulänglichkeit von Helm aus.
Was ist Helm?
Helm ist ein Paketmanager für Kubernetes. Obwohl nicht unumstritten, hat es sich doch als De-facto-Standard zur Paketierung von Anwendungen für Kubernetes durchgesetzt. Es bildet eine logische Klammer um die zusammengehörenden Kubernetes-Manifeste, die eine Anwendung beschreiben. Diese Manifeste werden durch (Go-)Templates generiert. Die Werte, mit denen die Template-Platzhalter ersetzt werden, werden Helm von außen entweder als Values-Datei oder über Parameter mitgegeben. Templates und eine Default-Belegung der Werte werden in einem sogenannten Helm Chart zusammengefasst. Dadurch können die Kubernetes-Interna der Anwendung vor dem Benutzer versteckt werden. Die Werte sind die Schnittstelle zur Konfiguration des Rollouts, aber auch der Anwendung. Das gibt Autoren die Möglichkeit, ihre Charts anderen zur Verfügung zu stellen, ohne dass diese tiefere Kenntnis über die Ausführungsmechanik besitzen müssen. Helm Charts werden nach semver versioniert und in Helm-Repositories (z.B. in S3 oder via OCI) publiziert. Mit dem Artifact Hub existiert ein Aggregator, der die Repositories anderer durchsuchbar macht. Dadurch ist ein sehr vitales Ökosystem mit unzähligen Komponenten entstanden.
Helm weist allerdings auch Unzulänglichkeiten auf, gerade wenn man es in Deployment-Pipelines einsetzen möchte. Eine Herausforderung ist das Secrets Management. Secrets lassen sich nicht ohne Umwege aus Umgebungsvariablen oder aus anderen Quellen in die Templates einbringen. Wenn man mit Pull Requests arbeiten möchte, wäre es hilfreich, die auszurollenden Änderungen vorher begutachten zu können. Wenn man mehrere Helm Charts zu einem sogenannten Umbrella-Chart kombiniert, müssen Werte wie Hostnames in der Regel mehrfach angegeben werden, denn eine Interpolation der Werte ist nicht ohne Weiteres möglich.
Der typische Ausweg ist nun meistens Shell-Scripting, z.B. um Secrets oder Umgebungsvariablen mittels gettext envsubst, jq, yq in die Values-Dateien zu schreiben und dann an Helm weiterzugeben. Ein fehleranfälliger Prozess.
Helmfile zur Rettung
Helmfile bietet sich als Alternative an. Es ist ein Wrapper für Helm und erlaubt eine deklarative Beschreibung der Releases, also der Information, welches Chart mit welchen Werten ausgerollt werden soll. Diese Beschreibung kann in einer Datei vorgenommen werden, aber auch auf mehrere Dateien modular verteilt werden.
Helmfile kann mehrere Umgebungen (Environments) verwalten. Dieses Feature ist sehr nützlich, um gleiche oder ähnliche Rollouts mit abweichenden Werten zu deployen. Naheliegend ist die Verwendung, um dev-, staging- und prod-Umgebungen aufzubauen, man kann es aber auch benutzen, um z.B. Umgebungen pro Mandant aufzubauen. Die Zielumgebung wird beim Aufruf als Parameter übergeben. Helmfile stellt dann die Werte, die in der Umgebung definiert wurden, den Templates als Variablen zur Verfügung.
Der Aufruf
setzt die Umgebung auf development und führt apply aus. Das bedeutet, dass zunächst ein Diff zwischen den auf Basis der Werte generierten Manifestdateien und den im Kubernetes-Cluster vorhandenen Manifesten berechnet wird. Die Unterschiede werden ausgegeben und dann in den Cluster eingebracht.
Helmfile führt ein mehrstufiges Templating durch. Zunächst werden Template-Ausdrücke in den Helmfiles selbst evaluiert. Dadurch ist es möglich, ein Release innerhalb einer For-Schleife zu definieren und damit beispielsweise für jeden Mandanten eine Instanz einer Komponente auszurollen. So kann man auch komplexe Rollouts elegant ausdrücken.
Außerdem kann Helmfile die an Helm zu übergebenden Values-Dateien (so sie die Dateiendung gotmpl verwenden) mittels Templating generieren. Dadurch ergeben sich vielfältige Möglichkeiten, auch weil sich die Werte mittels variantdev/vals aus externen Quellen wie S3, Vault oder Secret Managern in der Cloud beziehen lassen.
Listing 1 zeigt ein Minimalbeispiel zum Ausrollen einer Anwendung. Zunächst wird das Repository bekannt gemacht, aus dem das Chart bezogen werden soll. Dann werden die zwei Umgebungen development und production definiert. Für development wollen wir die Version 3.9 oder größer installieren, weil wir neue Versionen sofort integrieren möchten, um schnell zu merken, wenn etwas bricht. Das Caret drückt nach semver aus, dass neuere Minor- und Patch-Versions herangezogen werden dürfen, nur eine neue Major-Version wollen wir nicht. Auf der Produktionsumgebung sind wir konservativ und geben eine genaue Versionsnummer an.
Danach wird das Release definiert. Die Anwendung wird in einen Namespace installiert, der sich dank Templating aus dem Namen der Umgebung und dem Suffix my-app zusammensetzt, also z.B. production-my-app. So können beide Umgebungen in einem Cluster existieren. Die auszurollende Version wird durch die gewählte Umgebung bestimmt. Wird die production-Umgebung ausgerollt, wird 3.8.0 installiert. Dem Release werden die Werte aus zwei Values-Dateien übergeben. Eine, die für alle Umgebungen gilt, und eine, die umgebungsspezifisch ist. Das wird durch den Template-Ausdruck im Dateinamen erreicht. Die Anzahl der Replicas würde man z.B. in der globalen Values-Datei definieren, könnte sie aber explizit pro Umgebung überschreiben. Der Datenbank-Host wäre umgebungsspezifisch und würde in der zweiten Datei definiert. Helm mergt die beiden YAML-Dateien, sodass die zweite die Werte der ersten überschreibt.
Bleibt noch das Thema Secrets Management. Die Zugangsdaten für die Datenbank kann man sich entweder mittels der Templatefunktion requireEnv aus einer Umgebungsvariable besorgen, schöner geht das aber mit den eingebauten Mechanismen. Helmfile integriert das Helm-Plug-in helm-secrets, das wiederum Mozilla SOPS verwendet. Damit ist es möglich, Secrets verschlüsselt im Git-Repository abzulegen. Die Secrets können per GPG, aber auch mithilfe von in der Cloud gespeicherten Schlüsseln (z.B. Azure KeyVault, GCP KMS) ver- und entschlüsselt werden. So kann recht feingranular geregelt werden, wer die Secrets lesen oder schreiben darf. Unter Versionskontrolle liegen dann nur die verschlüsselten YAML-Dateien. Helmfile kümmert sich transparent um die Entschlüsselung, wenn die Dateien der Release-Definition als Secret übergeben werden. Hier ist also ein guter Ort für umgebungsspezifische Zugangsdaten.