Организация деплоя в множество 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 -e 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 -e devel apply
helmfile -n -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" }}