Організація деплою в безлічі k8s оточень за допомогою helmfile

Helmfile - обгортка для рульове колесо, яка дозволяє в одному місці описувати безліч helm релізів, параметризувати їх чарти для кількох оточень, а також задавати порядок їх деплою.

Про сам helmfile і приклади його використання можна почитати в ридми и посібник з найкращих практик.

Ми ж познайомимося з неочевидними способами описати релізи в helmfile

Припустимо, у нас є пачка helm-чартів (для прикладу нехай буде postgres і якийсь backend додаток) і кілька оточень (кілька kubernetes кластерів, кілька namespace'ів або кілька і того, і іншого). Беремо helmfile, читаємо документацію і починаємо описувати наші оточення та релізи:

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

У нас вийшло 2 оточення: розробити, виробництво - У кожному знаходяться свої значення для helm чартів релізів. Ми будемо деплоїти в них так:

helmfile -n <namespace> -e <env> apply

Різні версії helm чартів у різних оточеннях

Що робити, якщо нам треба викочувати різні версії бекенда в різні оточення? Як налаштувати версію релізу? На допомогу приходять значення оточення, доступні через {{ .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 }}
...

Різний набір додатків у різних оточеннях

Відмінно, але якщо нам не треба в production викочувати postgres, тому що ми знаємо, що не треба базу даних пхати в k8s і для прода у нас є чудовий окремий кластер postgres? Для вирішення цієї проблеми ми маємо лейбли (labels)

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

Це здорово, але особисто я вважаю за краще описувати, які програми розгортати в оточенні не за допомогою аргументів запуску, а в описі самих оточень. Що робити? Можна помістити опис релізів в окрему папку, в описі оточення завести список потрібних релізів і "підчіпляти" тільки потрібні релізи, ігноруючи інші

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

замітка

Під час використання bases: необхідно обов'язково використовувати yaml роздільник ---, щоб можна було шаблонізувати releases (та інші частини, типу helmDefaults) значеннями з environments

У такому разі реліз postgres навіть не потрапить до опису для production. Дуже зручно!

Перевизначені глобальні значення для релізів

Звичайно, здорово, що можна для кожного оточення задавати значення для helm чартів, але якщо у нас описано кілька оточень, і ми хочемо, припустимо, задати однаковий для всіх affinity, але не хочемо налаштовувати його за умовчанням у самих чартах, які зберігаються в ріпах.

У такому разі ми могли б для кожного релізу задати 2 файли з values: перший з дефолтними значеннями, які будуть визначати значення чарту, а другий зі значеннями для оточення, який у свою чергу вже буде визначати дефолтні.

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

Визначення глобальних значень для helm чартів усіх релізів на рівні оточення

Допустимо, у нас у кількох релізах створюються кілька ingress — ми могли б вручну для кожного чарту визначити hosts:, але в нашому випадку домен один і той же, то чому ж його не винести в якусь глобальну змінну і просто підставляти її значення в чарти? Для цього файли з values, які ми хочемо параметризувати, повинні будуть мати розширення .gotmplЩоб helmfile знав, що його треба прогнати через шаблонизатор.

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

замітка

Очевидно, що ingress у чарті postgres — це щось вкрай сумнівне, тому в статті це наведено просто як сферичний приклад у вакуумі і для того, щоб не вводити в статтю якийсь новий реліз лише заради опису ingress

Підстановка секретів (secrets) із значень оточення

За аналогією з наведеним вище прикладом можна підставляти і зашифровані за допомогою helm secrets значення. Замість того, щоб для кожного релізу створювати свій файл secrets, в якому визначати для чарта зашифровані значення, ми можемо просто визначити в релізному значення default.yaml.gotmpl, які будуть братися зі змінних, заданих на рівні оточень. А значення, які нам не треба ні від кого приховувати, можна спокійно перевизначити в значеннях релізу в конкретному оточенні.

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

замітка

До речі, getOrNil — спеціальна функція для go шаблонів у helmfile, яка, навіть якщо .Values.secrets не існуватиме, не викине помилку, а дозволить в результаті за допомогою функції default підставити значення за замовчуванням

Висновок

Описані речі здаються досить очевидними, але інформація щодо зручного опису деплою в кілька оточень за допомогою helmfile дуже мізерна, а я люблю IaC(Infrastructure-as-Code) і хочу мати чіткий опис стейту деплою.

Насамкінець хочу додати, що змінні для оточення default можна в свою чергу параметризувати змінними оточення ОС якогось раннера, з якого буде запускатися деплою, і таким чином отримати динамічні оточення

helmfile.yaml

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

Джерело: habr.com

Додати коментар або відгук