Zum Inhalt

Helm Charts

Ziel

In diesem Projekt geht es darum, aus normalen Kubernetes Manifesten Schritt für Schritt ein eigenes Helm Chart zu bauen. Sie werden:

  • ein Chart mit helm create vorbereiten
  • plain Kubernetes Manifeste in ein Chart übernehmen
  • Werte aus values.yaml verwenden
  • Namen, Labels und Umgebungsvariablen templaten
  • optionale Ressourcen mit if ein- und ausschalten
  • das Chart mit helm template, helm lint, helm install und helm upgrade testen

Hilfsmittel

  • Versuchen Sie, die unten stehenden Aufgaben mit Hilfe der Folien und des Helm Cheatsheets eigenständig zu lösen.
  • Sollten Sie dabei Probleme haben, finden Sie bei jeder Aufgabe einen ausklappbaren Block, in dem der Lösungsweg beschrieben wird.
  • Prüfen Sie Ihr Chart nach jeder größeren Änderung mit helm template. So sehen Sie direkt, welche Kubernetes Manifeste Helm erzeugt.

Vorbereitung

In diesem Hands-on bauen Sie ein Helm Chart für die Demoanwendung corewire/docker-demoapp. Die Anwendung läuft im Container auf Port 5000.

Stellen Sie sicher, dass Sie sich im Workspace-Ordner befinden:

cd /home/coder/workspace

Erstellen Sie einen neuen Projektordner und wechseln Sie hinein:

mkdir helm-demoapp
cd helm-demoapp

Aufgabe 1 - Erstes Helm Chart erstellen

In dieser Aufgabe werden die bestehenden Manifeste in ein Helm Chart überführt. Im ersten Schritt soll das Chart noch möglichst wenig Helm-Logik enthalten.

1.1: Chart-Grundstruktur erzeugen

  • Erzeugen Sie mit Helm ein neues Chart mit dem Namen docker-demoapp mit helm create.
  • Schauen Sie sich die erzeugte Ordnerstruktur an.
Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
helm create docker-demoapp

Schauen Sie sich anschließend die erzeugte Struktur an:

tree docker-demoapp

Helm legt unter anderem folgende Dateien und Ordner an:

docker-demoapp
├── Chart.yaml
├── templates
└── values.yaml

1.2: Beispieltemplates entfernen

Das automatisch erzeugte Chart enthält Beispieltemplates. Diese werden für dieses Hands-on nicht benötigt.

  • Löschen Sie im Ordner docker-demoapp/templates alle automatisch angelegten Dateien.
Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
rm -rf docker-demoapp/templates/*

1.3: Chart-Metadaten anpassen

  • Öffnen Sie die Datei docker-demoapp/Chart.yaml und passen Sie die Metadaten an.
apiVersion: v2
name: docker-demoapp
description: A simple Helm chart for the docker-demoapp
type: application
version: 0.1.0
appVersion: "1.1.1"
  • version beschreibt die Version des Charts.
  • appVersion beschreibt die Version der Anwendung.

1.4: Values Datei löschen

  • Löschen Sie den Inhalt der Datei docker-demoapp/values.yaml, da die Standardwerte später selbst definiert werden.
Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
> docker-demoapp/values.yaml

Aufgabe 2 - Plain Manifeste in Templates übernehmen

In diesem Schritt werden Kubernetes Manifeste in den templates-Ordner kopiert. Die Dateien sind dadurch bereits Teil des Charts, verwenden aber noch keine Helm Variablen.

2.1: Deployment Template erstellen

Erstellen Sie die Datei docker-demoapp/templates/deployment.yaml mit folgendem Inhalt:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: docker-demoapp
  labels:
    app: docker-demoapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: docker-demoapp
  template:
    metadata:
      labels:
        app: docker-demoapp
    spec:
      containers:
        - name: docker-demoapp
          image: docker.io/corewire/docker-demoapp:1.1.1
          ports:
            - containerPort: 5000
          env:
            - name: DOCKERDEMO_NOTES_DIR
              value: "/data/notes"
            - name: DATABASE_HOST
              value: "database"
            - name: DATABASE_PORT
              value: "3306"
            - name: DATABASE_USER
              value: "example-user"
            - name: DATABASE_USER_PASSWORD
              value: "password"

Namespace im Chart

Im Chart wird kein fester Namespace in die Ressourcen geschrieben. Der Namespace wird später beim Installieren mit --namespace gesetzt. Dadurch kann dasselbe Chart in verschiedenen Namespaces installiert werden.

2.2: Service Template erstellen

  • Erstellen Sie die Datei docker-demoapp/templates/service.yaml mit folgendem Inhalt:
apiVersion: v1
kind: Service
metadata:
  name: docker-demoapp
  labels:
    app: docker-demoapp
spec:
  type: ClusterIP
  selector:
    app: docker-demoapp
  ports:
    - port: 5000
      targetPort: 5000

2.3: Templates rendern

  • Lassen Sie sich anzeigen, welche Manifeste Helm aus dem Chart erzeugt.
Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
helm template demoapp ./docker-demoapp

Sie sollten ein Deployment und einen Service sehen. Inhaltlich entsprechen diese noch den plain Manifesten.

2.4: Chart installieren

  • Installieren Sie das Chart in den Namespace demoapp.
  • Prüfen Sie, ob die Ressourcen angelegt wurden.
Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
helm install demoapp ./docker-demoapp --namespace demoapp --create-namespace

Prüfen Sie anschließend die erzeugten Ressourcen:

helm list --namespace demoapp
kubectl -n demoapp get all

Aufgabe 3 - Namen mit Helm dynamisch machen

Bisher heißen Deployment und Service immer docker-demoapp. Das ist unpraktisch, wenn dasselbe Chart mehrfach installiert werden soll. Deshalb wird nun der Release-Name genutzt.

3.1: Namen mit .Release.Name ersetzen

Passen Sie in templates/deployment.yaml und templates/service.yaml den Namen von docker-demoapp an.

Verwenden Sie für metadata.name:

name: {{ .Release.Name }}

Passen Sie außerdem alle app Labels und Selector auf denselben Wert an.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)

templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}
  labels:
    app: {{ .Release.Name }}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}
    spec:
      containers:
        - name: docker-demoapp
          image: docker.io/corewire/docker-demoapp:1.1.1
          ports:
            - containerPort: 5000
          env:
            - name: DOCKERDEMO_NOTES_DIR
              value: "/data/notes"
            - name: DATABASE_HOST
              value: "database"
            - name: DATABASE_PORT
              value: "3306"
            - name: DATABASE_USER
              value: "example-user"
            - name: DATABASE_USER_PASSWORD
              value: "password"

templates/service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}
  labels:
    app: {{ .Release.Name }}
spec:
  type: ClusterIP
  selector:
    app: {{ .Release.Name }}
  ports:
    - port: 5000
      targetPort: 5000

3.2: Unterschiedliche Release-Namen testen

  • Rendern Sie das Chart mit unterschiedlichen Release-Namen mit helm template.
Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)

helm template demoapp ./docker-demoapp
helm template testapp ./docker-demoapp

Achten Sie darauf, wie sich metadata.name, Labels und Selector ändern.

3.3: Bestehenden Release aktualisieren

  • Wenden Sie die Änderung auf den bestehenden Release an.
Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
helm upgrade demoapp ./docker-demoapp --namespace demoapp

Aufgabe 4 - values.yaml verwenden

Feste Werte im Template sind unflexibel. Deshalb werden nun Replikate, Image und Service-Port in die Datei values.yaml ausgelagert.

4.1: Werte definieren

Ersetzen Sie den Inhalt von docker-demoapp/values.yaml durch:

replicaCount: 1

image:
  repository: docker.io/corewire/docker-demoapp
  tag: "1.1.1"

service:
  type: ClusterIP
  port: 5000

4.2: Werte im Deployment verwenden

Passen Sie templates/deployment.yaml so an, dass replicaCount und image aus values.yaml gelesen werden.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}
  labels:
    app: {{ .Release.Name }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}
    spec:
      containers:
        - name: docker-demoapp
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: 5000
          env:
            - name: DOCKERDEMO_NOTES_DIR
              value: "/data/notes"
            - name: DATABASE_HOST
              value: "database"
            - name: DATABASE_PORT
              value: "3306"
            - name: DATABASE_USER
              value: "example-user"
            - name: DATABASE_USER_PASSWORD
              value: "password"

4.3: Werte im Service verwenden

Passen Sie templates/service.yaml so an, dass Type und Port aus values.yaml gelesen werden.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}
  labels:
    app: {{ .Release.Name }}
spec:
  type: {{ .Values.service.type }}
  selector:
    app: {{ .Release.Name }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: 5000

4.4: Release aktualisieren

Aktualisieren Sie das installierte Chart, damit die Änderungen wirksam werden.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
helm upgrade demoapp ./docker-demoapp \
  --namespace demoapp

Aufgabe 5 - Umgebungsvariablen als Map templaten

Die Anwendung nutzt mehrere Environment Variablen. Diese werden nun ebenfalls in die values.yaml verschoben und im Template mit einer Schleife erzeugt.

5.1: Environment Werte definieren

Ergänzen Sie values.yaml um den Abschnitt env:

env:
  DOCKERDEMO_NOTES_DIR: "/data/notes"
  DATABASE_HOST: "database"
  DATABASE_PORT: "3306"
  DATABASE_USER: "example-user"
  DATABASE_USER_PASSWORD: "password"

5.2: Environment Variablen mit range erzeugen

Ersetzen Sie im Deployment den bisherigen env-Block durch eine range-Schleife.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)

Der Container-Abschnitt sieht danach so aus:

      containers:
        - name: docker-demoapp
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: 5000
          env:
            {{- range $key, $value := .Values.env }}
            - name: {{ $key }}
              value: {{ $value | quote }}
            {{- end }}

quote sorgt dafür, dass Werte wie 3306 als String ausgegeben werden.

5.3: Template prüfen

Prüfen Sie, ob Helm gültiges YAML erzeugt.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
helm template demoapp ./docker-demoapp

Wenn die Einrückung nicht stimmt, ist die Ausgabe häufig kein gültiges YAML. Achten Sie besonders auf die Leerzeichen vor {{- range ... }} und - name.

Aufgabe 6 - Helper Templates und Labels einführen

In größeren Charts sollen Namen und Labels nicht mehrfach von Hand gepflegt werden. Dafür werden Helper Templates verwendet.

6.1: _helpers.tpl erstellen

Erstellen Sie die Datei docker-demoapp/templates/_helpers.tpl mit folgendem Inhalt:

{{/*
Generate a fullname.
*/}}
{{- define "docker-demoapp.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
Common labels.
*/}}
{{- define "docker-demoapp.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}

Warum trunc 63?

Viele Kubernetes Namen dürfen maximal 63 Zeichen lang sein. Der Helper kürzt den erzeugten Namen deshalb auf 63 Zeichen und entfernt ein mögliches - am Ende.

6.2: Fullname im Deployment verwenden

Passen Sie das Deployment so an, dass metadata.name, Labels und Selector den Helper verwenden.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "docker-demoapp.fullname" . }}
  labels:
    {{- include "docker-demoapp.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ include "docker-demoapp.fullname" . }}
  template:
    metadata:
      labels:
        app: {{ include "docker-demoapp.fullname" . }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: 5000
          env:
            {{- range $key, $value := .Values.env }}
            - name: {{ $key }}
              value: {{ $value | quote }}
            {{- end }}

6.3: Fullname im Service verwenden

Passen Sie den Service ebenfalls an.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
apiVersion: v1
kind: Service
metadata:
  name: {{ include "docker-demoapp.fullname" . }}
  labels:
    {{- include "docker-demoapp.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  selector:
    app: {{ include "docker-demoapp.fullname" . }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: 5000

6.4: Ausgabe prüfen

Rendern Sie das Chart und prüfen Sie die erzeugten Namen und Labels.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
helm template demoapp ./docker-demoapp

Der Name der Ressourcen sollte nun ungefähr so aussehen:

demoapp-docker-demoapp

Aufgabe 7 - Optionalen Ingress einbauen

Ein Ingress soll nur erzeugt werden, wenn er in values.yaml aktiviert wird. Dafür wird ein if-Block verwendet.

7.1: Ingress Werte definieren

Ergänzen Sie values.yaml:

ingress:
  enabled: false
  host: demoapp.example.local
  annotations: {}

7.2: Ingress Template erstellen

Erstellen Sie die Datei docker-demoapp/templates/ingress.yaml:

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "docker-demoapp.fullname" . }}
  labels:
    {{- include "docker-demoapp.labels" . | nindent 4 }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  rules:
    - host: {{ .Values.ingress.host }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ include "docker-demoapp.fullname" . }}
                port:
                  number: {{ .Values.service.port }}
{{- end }}

7.3: Ohne und mit Ingress rendern

Prüfen Sie die Ausgabe einmal ohne und einmal mit aktiviertem Ingress.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)

Ohne Ingress:

helm template demoapp ./docker-demoapp

Mit Ingress:

helm template demoapp ./docker-demoapp \
  --set ingress.enabled=true

Beim ersten Befehl sollte kein Ingress erzeugt werden. Beim zweiten Befehl sollte ein zusätzliches kind: Ingress erscheinen.

Aufgabe 8 - Qualität prüfen und Chart paketieren

Zum Abschluss prüfen Sie das Chart und erstellen ein Chart-Paket.

8.1: Helm Lint ausführen

Führen Sie helm lint aus.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
helm lint ./docker-demoapp

Die Ausgabe sollte ungefähr so aussehen:

==> Linting ./docker-demoapp
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed

Die Info zum Icon ist unkritisch.

8.2: Chart paketieren

Erstellen Sie ein .tgz-Paket aus dem Chart.

Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
helm package ./docker-demoapp

Danach sollte im aktuellen Ordner eine Datei ähnlich zu dieser liegen:

docker-demoapp-0.1.0.tgz

8.3: Paket installieren

  • Installieren Sie das Chart testweise aus dem Paket in einem anderen Namespace.
  • Prüfen Sie, ob die Ressourcen angelegt wurden.
Lösung (Klicken Sie auf den Pfeil, falls Sie nicht weiterkommen)
helm install packaged-demoapp ./docker-demoapp-0.1.0.tgz \
  --namespace packaged-demoapp \
  --create-namespace

Prüfen Sie danach:

helm list --namespace packaged-demoapp
kubectl -n packaged-demoapp get all

Ergebnis

Sie haben aus einfachen Kubernetes Manifesten Schritt für Schritt ein Helm Chart gebaut. Das Chart enthält nun:

  • ein Deployment für die Demoanwendung
  • einen Service für die Demoanwendung
  • konfigurierbare Werte in values.yaml
  • Helper Templates für Namen und Labels
  • eine range-Schleife für Environment Variablen
  • einen optionalen Ingress

Damit haben Sie die wichtigsten Bausteine kennengelernt, um eigene Helm Charts zu erstellen und bestehende Kubernetes Manifeste in Helm zu überführen.