Organisation des Deployments in mehrere k8s-Umgebungen mit helmfile

Helmfile — eine Wrapper für helm, die es ermöglicht, an einem Ort viele Helm-Releases zu beschreiben, deren Charts für verschiedene Umgebungen zu parametrisieren und die Reihenfolge ihres Deployments festzulegen.

Über Helmfile und Beispiele für seine Nutzung kann man im readme und best practices guide.

lesen. Wir werden uns mit weniger offensichtlichen Wegen beschäftigen, Releases in Helmfile zu beschreiben.

Angenommen, wir haben eine Sammlung von Helm-Charts (zum Beispiel PostgreSQL und eine Backend-Anwendung) und mehrere Umgebungen (einige Kubernetes-Cluster, mehrere Namespaces oder beides). Wir nehmen Helmfile, lesen die Dokumentation, und beginnen, unsere Umgebungen und Releases zu beschreiben:

    .
    ├── envs
    │   ├── devel
    │   │   └── values
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   └── production
    │       └── values
    │           ├── backend.yaml
    │           └── postgres.yaml
    └── helmfile.yaml

helmfile.yaml

environments:
  devel:
  production:

releases:
  - name: postgres
    labels:
      app: postgres
    wait: true
    chart: stable/postgresql
    version: 8.4.0
    values:
      - envs/{{ .Environment.Name }}/values/postgres.yaml
  - name: backend
    labels:
      app: backend
    wait: true
    chart: private-helm-repo/backend
    version: 1.0.5
    needs:
      - postgres
    values:
      - envs/{{ .Environment.Name }}/values/backend.yaml

Wir haben 2 Umgebungen erhalten: devel, Produktion — in jeder befinden sich ihre eigenen Werte für die Helm-Chart-Releases. Wir werden sie so deployen:

helmfile -n  -e  apply

Unterschiedliche Versionen von Helm-Charts in unterschiedlichen Umgebungen

Was tun, wenn wir verschiedene Versionen des Backends in unterschiedlichen Umgebungen ausrollen müssen? Wie parametrisieren wir die Version des Releases? Dazu kommen Umgebungswerte ins Spiel, die über {{ .Values }}

helmfile.yaml

environments:
  devel:
+   values:
+   - charts:
+       versions:
+         backend: 1.1.0
  production:
+   values:
+   - charts:
+       versions:
+         backend: 1.0.5
...
  - name: backend
    labels:
      app: backend
    wait: true
    chart: private-helm-repo/backend
-   version: 1.0.5
+   version: {{ .Values.charts.versions.backend }}
...

Verschiedene Anwendungssets in unterschiedlichen Umgebungen

Das ist gut, aber was ist, wenn wir PostgreSQL nicht ausrollen wollen, weil wir wissen, dass wir keine Datenbank in k8s stecken müssen und für die Produktion einen hervorragenden separaten PostgreSQL-Cluster haben? Um dieses Problem zu lösen, haben wir Labels (labels) Produktion helmfile -n -e devel apply helmfile -n -e production -l app=backend apply

helmfile -n <namespace> -e devel apply
helmfile -n <namespace> -e production -l app=backend apply

Das ist großartig, aber persönlich bevorzuge ich es, zu beschreiben, welche Anwendungen in der Umgebung bereitgestellt werden sollen, nicht durch Startargumente, sondern durch die Beschreibung der Umgebungen selbst. Was kann man tun? Man könnte die Versionsbeschreibungen in einen separaten Ordner legen, in der Umgebungsliste die benötigten Versionen aufführen und nur die benötigten Versionen "anschließen", während die anderen ignoriert werden.

    .
    ├── envs
    │   ├── devel
    │   │   └── values
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   └── production
    │       └── values
    │           ├── backend.yaml
    │           └── postgres.yaml
+   ├── releases
+   │   ├── backend.yaml
+   │   └── postgres.yaml
    └── helmfile.yaml

helmfile.yaml


  environments:
    devel:
      values:
      - charts:
          versions:
            backend: 1.1.0
      - apps:
        - postgres
        - backend

    production:
      values:
      - charts:
          versions:
            backend: 1.0.5
      - apps:
        - backend

- releases:
-    - name: postgres
-      labels:
-        app: postgres
-      wait: true
-      chart: stable/postgresql
-      version: 8.4.0
-      values:
-        - envs/{{ .Environment.Name }}/values/postgres.yaml
-    - name: backend
-      labels:
-        app: backend
-      wait: true
-      chart: private-helm-repo/backend
-      version: {{ .Values.charts.versions.backend }}
-      needs:
-       - postgres
-      values:
-       - envs/{{ .Environment.Name }}/values/backend.yaml
+ ---
+ bases:
+ {{- range .Values.apps }}
+   - releases/{{ . }}.yaml
+ {{- end }}

releases/postgres.yaml

releases:
  - name: postgres
    labels:
      app: postgres
    wait: true
    chart: stable/postgresql
    version: 8.4.0
    values:
      - envs/{{ .Environment.Name }}/values/postgres.yaml

releases/backend.yaml

releases:
  - name: backend
    labels:
      app: backend
    wait: true
    chart: private-helm-repo/backend
    version: {{ .Values.charts.versions.backend }}
    needs:
      - postgres
    values:
      - envs/{{ .Environment.Name }}/values/backend.yaml

Notiz

Bei der Nutzung eines bases: es ist zwingend erforderlich, den YAML-Trenner zu verwenden ---, um die Releases (und andere Teile wie helmDefaults) mit Werten aus environments zu templatisieren.

In diesem Fall kommt das Release postgres nicht einmal in die Beschreibung für production. Sehr praktisch!

Globale überschreibbare Werte für Releases

Natürlich ist es großartig, dass man für jede Umgebung Werte für Helm-Charts festlegen kann, aber was ist, wenn wir mehrere Umgebungen beschrieben haben und wir beispielsweise für alle eine identische festlegen möchten Affinity, aber wir nicht möchten, dass sie standardmäßig in den Charts, die in Repos gespeichert sind, konfiguriert ist.

In diesem Fall könnten wir für jedes Release zwei Dateien mit Werten festlegen: eine mit Standardwerten, die die Werte des Charts definieren, und eine zweite mit Werten für die Umgebung, die wiederum die Standardwerte überschreiben wird.

    .
    ├── envs
+   │   ├── default
+   │   │   └── values
+   │   │       ├── backend.yaml
+   │   │       └── postgres.yaml
    │   ├── devel
    │   │   └── values
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   └── production
    │       └── values
    │           ├── backend.yaml
    │           └── postgres.yaml
    ├── releases
    │   ├── backend.yaml
    │   └── postgres.yaml
    └── helmfile.yaml

releases/backend.yaml

releases:
  - name: backend
    labels:
      app: backend
    wait: true
    chart: private-helm-repo/backend
    version: {{ .Values.charts.versions.backend }}
    needs:
      - postgres
    values:
+     - envs/default/values/backend.yaml
      - envs/{{ .Environment.Name }}/values/backend.yaml

envs/default/values/backend.yaml

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 1
      podAffinityTerm:
        labelSelector:
          matchExpressions:
          - key: app.kubernetes.io/name
            operator: In
            values:
            - backend
        topologyKey: "kubernetes.io/hostname"

Definition globaler Werte für Helm-Charts aller Releases auf Umgebungslevel

Angenommen, wir erstellen in mehreren Releases mehrere Ingress, könnten wir für jedes Chart manuell festlegen hosts:, aber in unserem Fall ist die Domain die gleiche, warum sollten wir sie nicht in eine globale Variable auslagern und einfach ihren Wert in die Charts einsetzen? Damit müssen die Werte-Dateien, die wir parametrisieren wollen, die Erweiterung .gotmpl, damit Helmfile weiß, dass sie durch den Template-Processor geleitet werden müssen.

    .
    ├── envs
    │   ├── default
    │   │   └── values
-   │   │       ├── backend.yaml
-   │   │       ├── postgres.yaml
+   │   │       ├── backend.yaml.gotmpl
+   │   │       └── postgres.yaml.gotmpl
    │   ├── devel
    │   │   └── values
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   └── production
    │       └── values
    │           ├── backend.yaml
    │           └── postgres.yaml
    ├── releases
    │   ├── backend.yaml
    │   └── postgres.yaml
    └── helmfile.yaml

helmfile.yaml

  environments:
    devel:
      values:
      - charts:
          versions:
            backend: 1.1.0
      - apps:
        - postgres
        - backend
+     - global:
+         ingressDomain: k8s.devel.domain

    production:
      values:
      - charts:
          versions:
            backend: 1.0.5
      - apps:
        - backend
+     - global:
+         ingressDomain: production.domain
  ---
  bases:
  {{- range .Values.apps }}
    - releases/{{ . }}.yaml
  {{- end }}

envs/default/values/backend.yaml.gotmpl

ingress:
  enabled: true
  paths:
    - /api
  hosts:
    - {{ .Values.global.ingressDomain }}

envs/default/values/postgres.yaml.gotmpl

ingress:
  enabled: true
  paths:
    - /
  hosts:
    - postgres.{{ .Values.global.ingressDomain }}

Notiz

Offensichtlich ist der Ingress im Postgres-Chart äußerst fragwürdig, weshalb er in diesem Artikel nur als theoretisches Beispiel im Vakuum angeführt wird, damit kein neuer Release nur zur Beschreibung des Ingress erstellt wird.

Das Einfügen von Geheimnissen (secrets) aus Umgebungsvariablen

Analog zu dem oben genannten Beispiel können auch verschlüsselte Werte eingefügt werden. helm secrets Anstatt für jede Version eine eigene Sekretdatei zu erstellen, in der die verschlüsselten Werte für das Chart definiert werden, können wir einfach im Release-Standard-default.yaml.gotmpl Werte festlegen, die aus Variablen auf Ebene der Umgebungen entnommen werden. Werte, die wir nicht geheim halten müssen, können dann problemlos in den Release-Werten für die jeweilige Umgebung überschrieben werden.

    .
    ├── envs
    │   ├── default
    │   │   └── values
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   ├── devel
    │   │   ├── values
    │   │   │   ├── backend.yaml
    │   │   │   └── postgres.yaml
+   │   │   └── secrets.yaml
    │   └── production
    │       ├── values
    │       │   ├── backend.yaml
    │       │   └── postgres.yaml
+   │       └── secrets.yaml
    ├── releases
    │   ├── backend.yaml
    │   └── postgres.yaml
    └── helmfile.yaml

helmfile.yaml

  environments:
    devel:
      values:
      - charts:
          versions:
            backend: 1.1.0
      - apps:
        - postgres
        - backend
      - global:
          ingressDomain: k8s.devel.domain
+     secrets:
+       - envs/devel/secrets.yaml

    production:
      values:
      - charts:
          versions:
            backend: 1.0.5
      - apps:
        - backend
      - global:
          ingressDomain: production.domain
+     secrets:
+       - envs/production/secrets.yaml
  ---
  bases:
  {{- range .Values.apps }}
    - releases/{{ . }}.yaml
  {{- end }}

envs/devel/secrets.yaml

secrets:
    elastic:
        password: ENC[AES256_GCM,data:hjCB,iv:Z1P6/6xBJgJoKLJ0UUVfqZ80o4L84jvZfM+uH9gBelc=,tag:dGqQlCZnLdRAGoJSj63rBQ==,type:int]
...

envs/production/secrets.yaml

secrets:
    elastic:
        password: ENC[AES256_GCM,data:ZB/VpTFk8f0=,iv:EA//oT1Cb5wNFigTDOz3nA80qD9UwTjK5cpUwLnEXjs=,tag:hMdIUaqLRA8zuFBd82bz6A==,type:str]
...

envs/default/values/backend.yaml.gotmpl

elasticsearch:
  host: elasticsearch
  port: 9200
  password: {{ .Values | getOrNil "secrets.elastic.password" | default "password" }}

envs/devel/values/backend.yaml

elasticsearch:
  host: elastic-0.devel.domain

envs/production/values/backend.yaml

elasticsearch:
  host: elastic-0.production.domain

Notiz

Übrigens, getOrNil — eine spezielle Funktion für Go-Templates in helmfile, die selbst wenn .Values.secrets nicht existiert, keine Fehler auslöst, sondern es ermöglicht, mithilfe der Funktion default einen Standardwert einzufügen.

Fazit

Die beschriebenen Dinge scheinen recht offensichtlich zu sein, aber die Informationen über eine benutzerfreundliche Beschreibung des Deployments in mehreren Umgebungen mit helmfile sind sehr spärlich, und ich liebe IaC (Infrastructure-as-Code) und möchte eine klare Beschreibung des Deployment-Status haben.

Zusammenfassend möchte ich hinzufügen, dass die Variablen für die Standardumgebung wiederum durch Umgebungsvariablen des Betriebssystems eines bestimmten Runners, von dem das Deployment gestartet wird, parametriert werden können, um so dynamische Umgebungen zu erhalten.

helmfile.yaml

environments:
  default:
    values:
    - global:
        clusterDomain: {{ env "CLUSTER_DOMAIN" | default "cluster.local" }}
        ingressDomain: {{ env "INGRESS_DOMAIN" }}

Quelle: habr.com

60GB SSD 8Gb DDR4