Организация деплоя в множество k8s окружений с помощью helmfile

от автора

Helmfile — обёртка для helm, которая позволяет в одном месте описывать множество helm релизов, параметризовать их чарты для нескольких окружений, а также задавать порядок их деплоя.

О самом helmfile и примерах его использования можно почитать в readme и best practices guide.

Мы же познакомимся с неочевидными способами описать релизы в 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 окружения: devel, production — в каждом находятся свои значения для 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" }}

ссылка на оригинал статьи https://habr.com/ru/post/491108/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *