Organizacja wdrożenia w wielu środowiskach k8s za pomocą helmfile

Helmfile — nakładka dla helm, która umożliwia opisanie wielu wdrożeń helm w jednym miejscu, parametryzację ich chartów dla kilku środowisk, a także określenie kolejności ich wdrożenia.

O samym helmfile i przykładach jego zastosowania można poczytać w readme i przewodniku najlepszych praktyk.

Zapoznamy się z nieoczywistymi sposobami opisywania wydań w helmfile.

Załóżmy, że mamy zbiór chartów helm (na przykład postgres i jakieś aplikacje backendowe) oraz kilka środowisk (kilka klastrów kubernetes, kilka namespace'ów lub jedno i drugie). Bierzemy helmfile, czytamy dokumentację i zaczynamy opisywać nasze środowiska i wydania:

    .
    ├── 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

Udało nam się stworzyć 2 środowiska: devel, production — w każdym z nich znajdują się własne wartości dla chartów helm wydań. Będziemy wdrażać je w następujący sposób:

helmfile -n  -e  apply

Różne wersje chartów helm w różnych środowiskach

Co zrobić, jeśli musimy wdrażać różne wersje backendu w różnych środowiskach? Jak parametryzować wersję wydania? Z pomocą przychodzą wartości środowiska, dostępne za pośrednictwem {{ .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 }}
...

Różny zestaw aplikacji w różnych środowiskach

Świetnie, ale co jeśli nie musimy wdrażać production postgres, ponieważ wiemy, że nie powinniśmy tej bazy danych mieć w k8s, a dla produkcji mamy wspaniały osobny klaster postgres? Aby rozwiązać ten problem, mamy etykiety (labels)

helmfile -n  -e devel apply
helmfile -n  -e production -l app=backend apply

To świetnie, ale osobiście wolę opowiedzieć, jakie aplikacje wdrażać w środowisku nie za pomocą argumentów uruchomieniowych, ale w opisie samych środowisk. Co robić? Można umieścić opisy wydań w osobnym folderze, w opisie środowiska stworzyć listę potrzebnych wydań i "podpiąć" tylko potrzebne wydania, ignorując pozostałe.

    .
    ├── 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

Notatka

Podczas korzystania z bases: należy obowiązkowo używać separatora yaml ---, aby można było szablonować wydania (i inne części, takie jak helmDefaults) wartościami z środowisk

W takim przypadku wydanie postgres nawet nie trafi do opisu dla produkcji. Bardzo wygodne!

Nadpisywalne globalne wartości dla wydań

Oczywiście, świetnie, że można dla każdego środowiska ustawiać wartości dla chartów helm, ale co jeśli mamy opisane kilka środowisk i chcemy, powiedzmy, ustawić to samo dla wszystkich affinity, ale nie chcemy ustawiać go domyślnie w samych chartach, które są przechowywane w repozytoriach.

W takim przypadku moglibyśmy dla każdego wydania ustawić 2 pliki z wartościami: pierwszy z domyślnymi wartościami, które będą określać wartości samego chartu, a drugi z wartościami dla środowiska, który z kolei już będzie nadpisywał domyślne.

    .
    ├── 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"

Definiowanie globalnych wartości dla chartów helm wszystkich wydań na poziomie środowiska

Załóżmy, że w kilku wydaniach tworzymy kilka ingress – moglibyśmy ręcznie określić dla każdego chartu hosts:, ale w naszym przypadku domena jest ta sama, więc dlaczego nie zdefiniować jej jako globalnej zmiennej i po prostu podstawiać jej wartość do chartów? W tym celu pliki z wartościami, które chcemy parametryzować, powinny mieć rozszerzenie .gotmpl, aby helmfile wiedział, że należy je przeprowadzić przez szablon.

    .
    ├── 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 }}

Notatka

Jasne jest, że ingress w wykresie postgres jest czymś bardzo wątpliwym, dlatego w artykule przedstawiono go jedynie jako sferyczny przykład w próżni i po to, aby nie wprowadzać w artykuł jakiegoś nowego wydania tylko w celu opisu ingress.

Podstawianie sekretów (secrets) z wartości środowiskowych.

Podobnie jak w powyższym przykładzie, możemy podstawiać również zaszyfrowane przy pomocy helm secrets wartości. Zamiast tworzyć oddzielny plik secrets dla każdej wersji, w którym definiujemy zaszyfrowane wartości dla wykresu, możemy po prostu określić w domyślnym pliku default.yaml.gotmpl wartości, które będą pobierane z zmiennych zdefiniowanych na poziomie środowisk. A wartości, których nie musimy nikogo ukrywać, możemy spokojnie nadpisać w wartości wydania w konkretnym środowisku.

    .
    ├── 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

Notatka

Przy okazji, getOrNil — specjalna funkcja dla szablonów go w helmfile, która, nawet jeśli .Values.secrets nie będzie istniała, nie wyrzuci błędu, a pozwoli na użycie funkcji default do podstawienia wartości domyślnej

Podsumowanie

Opisane rzeczy zdają się być dość oczywiste, ale informacje na temat wygodnego opisu wdrożenia w kilku środowiskach za pomocą helmfile są bardzo skąpe, a ja lubię IaC (Infrastructure-as-Code) i chcę mieć klarowny opis stanu wdrożenia.

Na zakończenie chciałbym dodać, że zmienne dla środowiska domyślnego można z kolei parametryzować zmiennymi środowiska systemu operacyjnego pewnego wykonawcy, z którego będzie uruchamiane wdrożenie, i w ten sposób uzyskać dynamiczne środowiska.

helmfile.yaml

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

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster