{"id":6,"date":"2026-11-03T09:02:17","date_gmt":"2026-11-03T08:02:17","guid":{"rendered":"https:\/\/marcohanisch.de\/?p=6"},"modified":"2026-11-03T09:02:17","modified_gmt":"2026-11-03T08:02:17","slug":"helm-charts-erstellen-struktur-templates-und-der-weg-zum-release","status":"publish","type":"post","link":"https:\/\/marcohanisch.de\/?p=6","title":{"rendered":"Helm Charts erstellen: Struktur, Templates und der Weg zum Release"},"content":{"rendered":"<p>Helm ist der Paketmanager f\u00fcr Kubernetes. Ein Chart b\u00fcndelt Manifeste, Standardwerte und Metadaten zu einer installierbaren Einheit. Wer eigene Anwendungen ausrollt, kommt an eigenen Charts kaum vorbei: Sie machen Deployments wiederholbar, umgebungsabh\u00e4ngig konfigurierbar und versionierbar.<\/p>\n<h2>Was ein Chart eigentlich ist<\/h2>\n<p>Ein Helm Chart ist ein Verzeichnis mit einer festen Struktur. Helm rendert daraus Kubernetes-Manifeste und wendet sie auf ein Cluster an. Die Trennung ist bewusst: Templates beschreiben die Form der Ressourcen, Values die ver\u00e4nderlichen Werte. Dieselbe Chart-Version l\u00e4sst sich so in Entwicklung, Staging und Produktion installieren, ohne die Manifeste zu kopieren.<\/p>\n<h2>Voraussetzungen<\/h2>\n<ul>\n<li>Helm 3, lokal installiert (<code>helm version<\/code>)<\/li>\n<li>Ein Kubernetes-Cluster zum Testen, zum Beispiel kind oder minikube<\/li>\n<li><code>kubectl<\/code>, konfiguriert auf dieses Cluster<\/li>\n<\/ul>\n<p>Zum reinen Rendern der Manifeste reicht Helm allein. Ein Cluster brauchst du erst, wenn du installieren oder upgraden willst.<\/p>\n<h2>Ein Chart anlegen<\/h2>\n<p>Der schnellste Einstieg ist das Ger\u00fcst von Helm selbst:<\/p>\n<pre><code>helm create meine-app\n<\/code><\/pre>\n<p>Daraus entsteht diese Struktur:<\/p>\n<pre><code>meine-app\/\n  Chart.yaml          # Name, Version, Beschreibung\n  values.yaml         # Standardwerte\n  charts\/             # Abh\u00e4ngigkeiten\n  templates\/          # Kubernetes-Manifeste als Templates\n    deployment.yaml\n    service.yaml\n    ingress.yaml\n    _helpers.tpl       # wiederverwendbare Template-Funktionen\n    NOTES.txt          # Hinweis nach der Installation\n<\/code><\/pre>\n<p><code>helm create<\/code> legt ein vollst\u00e4ndiges Beispiel f\u00fcr einen Webdienst an, inklusive Ingress, ServiceAccount und Tests. F\u00fcr ein erstes eigenes Chart ist das hilfreich. Was du nicht brauchst, l\u00f6schst du, statt um leere Dateien herumzubauen.<\/p>\n<h2>Chart.yaml<\/h2>\n<p>Die Metadaten stehen in <code>Chart.yaml<\/code>. F\u00fcr Helm 3 ist <code>apiVersion: v2<\/code> Pflicht. <code>version<\/code> ist die Version des Charts nach SemVer, <code>appVersion<\/code> die Version der Anwendung, die das Chart ausrollt. Beides ist unabh\u00e4ngig: Ein Chart kann Version 1.4.0 haben und trotzdem Anwendung 2.3.1 deployen.<\/p>\n<pre><code>apiVersion: v2\nname: meine-app\ndescription: Webdienst mit Deployment, Service und Ingress\ntype: application\nversion: 0.1.0\nappVersion: \"1.0.0\"\n<\/code><\/pre>\n<p><code>type: application<\/code> markiert ein installierbares Chart. <code>type: library<\/code> ist f\u00fcr Charts gedacht, die nur Hilfs-Templates bereitstellen und nicht selbst installiert werden.<\/p>\n<h2>Werte und Templates<\/h2>\n<p>Alles, was sich zwischen Umgebungen unterscheidet, geh\u00f6rt nach <code>values.yaml<\/code>: Image, Replikas, Ressourcen, Ingress-Host, Feature-Flags. Templates lesen diese Werte \u00fcber <code>.Values<\/code>.<\/p>\n<pre><code># values.yaml\nreplicaCount: 2\n\nimage:\n  repository: ghcr.io\/example\/meine-app\n  tag: \"1.0.0\"\n  pullPolicy: IfNotPresent\n\nservice:\n  type: ClusterIP\n  port: 80\n\nresources:\n  requests:\n    cpu: 100m\n    memory: 128Mi\n  limits:\n    cpu: 500m\n    memory: 256Mi\n<\/code><\/pre>\n<p>Im Deployment ziehst du die Werte so hinein:<\/p>\n<pre><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"meine-app.fullname\" . }}\n  labels:\n    {{- include \"meine-app.labels\" . | nindent 4 }}\nspec:\n  replicas: {{ .Values.replicaCount }}\n  selector:\n    matchLabels:\n      {{- include \"meine-app.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      labels:\n        {{- include \"meine-app.selectorLabels\" . | nindent 8 }}\n    spec:\n      containers:\n        - name: {{ .Chart.Name }}\n          image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n          imagePullPolicy: {{ .Values.image.pullPolicy }}\n          ports:\n            - name: http\n              containerPort: 8080\n          resources:\n            {{- toYaml .Values.resources | nindent 12 }}\n<\/code><\/pre>\n<p>Drei Stellen lohnen einen zweiten Blick:<\/p>\n<ul>\n<li><code>include<\/code> ruft benannte Templates aus <code>_helpers.tpl<\/code> auf. Namen, Labels und Selektoren sollten dort zentral stehen, damit Deployment und Service dieselben Selektoren verwenden.<\/li>\n<li><code>nindent<\/code> r\u00fcckt mehrzeilige Bl\u00f6cke korrekt ein. Ohne Einr\u00fcckung wird das gerenderte YAML ung\u00fcltig.<\/li>\n<li><code>toYaml<\/code> \u00fcbernimmt verschachtelte Werte wie Resources oder Annotations, ohne sie Feld f\u00fcr Feld nachzubauen.<\/li>\n<\/ul>\n<h2>Bl\u00f6cke optional machen<\/h2>\n<p>Nicht jede Installation braucht einen Ingress oder ein PersistentVolume. Optionale Ressourcen steuerst du mit <code>if<\/code>:<\/p>\n<pre><code>{{- if .Values.ingress.enabled }}\napiVersion: networking.k8s.io\/v1\nkind: Ingress\nmetadata:\n  name: {{ include \"meine-app.fullname\" . }}\nspec:\n  rules:\n    - host: {{ .Values.ingress.host | quote }}\n      http:\n        paths:\n          - path: \/\n            pathType: Prefix\n            backend:\n              service:\n                name: {{ include \"meine-app.fullname\" . }}\n                port:\n                  number: {{ .Values.service.port }}\n{{- end }}\n<\/code><\/pre>\n<p>Das f\u00fchrende Minus in <code>{{-<\/code> entfernt die Leerzeile davor. Sonst sammeln sich im gerenderten Manifest leere Zeilen, die sp\u00e4ter schwer zu lesen sind.<\/p>\n<h2>Pr\u00fcfen, bevor etwas im Cluster landet<\/h2>\n<p>Drei Befehle decken die meisten Fehler ab, bevor ein Release entsteht:<\/p>\n<pre><code>helm lint meine-app\nhelm template meine-app .\/meine-app\nhelm install meine-app .\/meine-app --dry-run --debug\n<\/code><\/pre>\n<ul>\n<li><code>helm lint<\/code> pr\u00fcft Struktur, Pflichtfelder und \u00fcbliche Chart-Fehler.<\/li>\n<li><code>helm template<\/code> rendert die Manifeste lokal, ganz ohne Cluster. Die Ausgabe geh\u00f6rt in ein Review oder in die CI.<\/li>\n<li><code>helm install --dry-run<\/code> geht denselben Weg wie eine echte Installation, wendet aber nichts an. Fehlende Werte und ung\u00fcltiges YAML fallen hier auf.<\/li>\n<\/ul>\n<p>Werte f\u00fcr eine Umgebung \u00fcbergibst du per Datei oder einzeln:<\/p>\n<pre><code>helm template meine-app .\/meine-app -f values-prod.yaml --set replicaCount=3\n<\/code><\/pre>\n<p><code>--set<\/code> \u00fcberschreibt Werte aus Dateien. F\u00fcr alles, was l\u00e4nger als ein einzelner Wert ist, ist eine eigene Values-Datei lesbarer.<\/p>\n<h2>Installieren und aktualisieren<\/h2>\n<pre><code>helm install meine-app .\/meine-app --namespace meine-app --create-namespace\nhelm upgrade meine-app .\/meine-app --namespace meine-app\nhelm rollback meine-app 1 --namespace meine-app\n<\/code><\/pre>\n<p><code>upgrade<\/code> ist auch der richtige Befehl f\u00fcr die erste Installation, wenn das Release vielleicht schon existiert:<\/p>\n<pre><code>helm upgrade --install meine-app .\/meine-app --namespace meine-app --create-namespace\n<\/code><\/pre>\n<p>Helm speichert jedes Release als Secret im Ziel-Namespace. Deshalb funktionieren Rollback und History, ohne dass das Chart selbst Zustand h\u00e4lt.<\/p>\n<h2>Abh\u00e4ngigkeiten<\/h2>\n<p>Charts k\u00f6nnen andere Charts einbinden, etwa eine Datenbank oder einen Ingress-Controller. Die Deklaration steht in <code>Chart.yaml<\/code>:<\/p>\n<pre><code>dependencies:\n  - name: postgresql\n    version: \"16.x.x\"\n    repository: https:\/\/charts.bitnami.com\/bitnami\n    condition: postgresql.enabled\n<\/code><\/pre>\n<pre><code>helm dependency update .\/meine-app\n<\/code><\/pre>\n<p>Helm l\u00e4dt die Abh\u00e4ngigkeit als Archiv nach <code>charts\/<\/code>. Werte des Untercharts adressierst du unter dem Chart-Namen, also <code>postgresql.auth.password<\/code> in der eigenen <code>values.yaml<\/code>. <code>condition<\/code> schaltet die Abh\u00e4ngigkeit ab, wenn du in Produktion eine externe Datenbank nutzt.<\/p>\n<h2>Packen und ver\u00f6ffentlichen<\/h2>\n<p>Ein Chart wird als <code>.tgz<\/code> verteilt:<\/p>\n<pre><code>helm package .\/meine-app\n<\/code><\/pre>\n<p>Es entsteht <code>meine-app-0.1.0.tgz<\/code>. Der Dateiname setzt sich aus <code>name<\/code> und <code>version<\/code> zusammen. F\u00fcr ein eigenes Repository gen\u00fcgt ein Verzeichnis mit den Paketen und einem Index:<\/p>\n<pre><code>mkdir -p repo\nmv meine-app-0.1.0.tgz repo\/\nhelm repo index repo --url https:\/\/example.com\/charts\n<\/code><\/pre>\n<p><code>index.yaml<\/code> ist der Katalog, den <code>helm repo add<\/code> sp\u00e4ter liest. In der Praxis landet dieses Verzeichnis auf einem Webserver, in einer OCI-Registry oder in einem Chart-Museum. OCI ist der Weg, den aktuelle Helm-Versionen bevorzugen:<\/p>\n<pre><code>helm push meine-app-0.1.0.tgz oci:\/\/ghcr.io\/example\/charts\n<\/code><\/pre>\n<h2>Worauf es in der Praxis ankommt<\/h2>\n<ul>\n<li><strong>Keine Umgebungsdaten ins Chart.<\/strong> Hostnamen, Replikas und Ressourcengrenzen bleiben in Values-Dateien au\u00dferhalb des Charts oder in einem \u00dcberschreibungs-Mechanismus wie Helmfile oder Argo CD.<\/li>\n<li><strong>Ressourcen immer setzen.<\/strong> Requests und Limits geh\u00f6ren in die Standardwerte, sonst plant der Scheduler blind.<\/li>\n<li><strong>Labels zentral halten.<\/strong> Selektoren von Deployment und Service m\u00fcssen \u00fcbereinstimmen. Ein Helper-Template verhindert, dass sie auseinanderlaufen.<\/li>\n<li><strong>Version bei jeder \u00c4nderung erh\u00f6hen.<\/strong> Helm behandelt ein Chart mit gleicher Version als dasselbe Paket. Ohne Versionsbump \u00fcberschreibst du ein bereits ver\u00f6ffentlichtes Archiv.<\/li>\n<li><strong>Secrets nicht in values.yaml committen.<\/strong> Passw\u00f6rter kommen \u00fcber <code>--set<\/code>, eine externe Secrets-Datei au\u00dferhalb von Git oder \u00fcber einen Operator wie External Secrets.<\/li>\n<li><strong>Lint und Template in der CI.<\/strong> <code>helm lint<\/code> und <code>helm template<\/code> fangen YAML- und Strukturfehler, bevor ein Release gebaut wird.<\/li>\n<\/ul>\n<h2>Kurzfassung<\/h2>\n<p>Ein Helm Chart ist ein versioniertes Verzeichnis: <code>Chart.yaml<\/code> beschreibt es, <code>values.yaml<\/code> konfiguriert es, <code>templates\/<\/code> erzeugt die Manifeste. <code>helm create<\/code> liefert das Ger\u00fcst, <code>helm lint<\/code> und <code>helm template<\/code> pr\u00fcfen es, <code>helm upgrade --install<\/code> rollt es aus. Wer Werte, Labels und Versionen sauber trennt, kann dasselbe Chart \u00fcber alle Umgebungen hinweg benutzen.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Helm ist der Paketmanager f\u00fcr Kubernetes. Ein Chart b\u00fcndelt Manifeste, Standardwerte und Metadaten zu einer installierbaren Einheit. Wer eigene Anwendungen ausrollt, kommt an eigenen Charts kaum vorbei: Sie machen Deployments wiederholbar, umgebungsabh\u00e4ngig konfigurierbar und versionierbar. Was ein Chart eigentlich ist Ein Helm Chart ist ein Verzeichnis mit einer festen Struktur. Helm rendert daraus Kubernetes-Manifeste und [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-6","post","type-post","status-publish","format-standard","hentry","category-allgemein"],"_links":{"self":[{"href":"https:\/\/marcohanisch.de\/index.php?rest_route=\/wp\/v2\/posts\/6","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/marcohanisch.de\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/marcohanisch.de\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/marcohanisch.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=6"}],"version-history":[{"count":2,"href":"https:\/\/marcohanisch.de\/index.php?rest_route=\/wp\/v2\/posts\/6\/revisions"}],"predecessor-version":[{"id":8,"href":"https:\/\/marcohanisch.de\/index.php?rest_route=\/wp\/v2\/posts\/6\/revisions\/8"}],"wp:attachment":[{"href":"https:\/\/marcohanisch.de\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=6"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/marcohanisch.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=6"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/marcohanisch.de\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=6"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}