Thank you for reading this post, don't forget to subscribe!
Helmfile — обёртка для helm, которая позволяет в одном месте описывать множество helm релизов, параметризовать их чарты для нескольких окружений, а также задавать порядок их деплоя.
О самом helmfile и примерах его использования можно почитать в readme и best practices guide.
Допустим, у нас есть пачка helm-чартов (для примера пусть будет postgres и некое backend приложение) и несколько окружений (несколько kubernetes кластеров, несколько namespace'ов или несколько и того, и другого). Берём helmfile, читаем документацию и начинаем описывать наши окружения и релизы:
1 2 3 4 5 6 7 8 9 10 11 |
. ├── envs │ ├── devel │ │ └── values │ │ ├── backend.yaml │ │ └── postgres.yaml │ └── production │ └── values │ ├── backend.yaml │ └── postgres.yaml └── helmfile.yaml |
helmfile.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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
Это здорово, но лично я предпочту описывать, какие приложения разворачивать в окружении не с помощью аргументов запуска, а в описании самих окружений. Что делать? Можно поместить описание релизов в отдельную папку, в описании окружения завести список нужных релизов и "подцеплять" только нужные релизы, игнорируя остальные
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
. ├── envs │ ├── devel │ │ └── values │ │ ├── backend.yaml │ │ └── postgres.yaml │ └── production │ └── values │ ├── backend.yaml │ └── postgres.yaml + ├── releases + │ ├── backend.yaml + │ └── postgres.yaml └── helmfile.yaml |
helmfile.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
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
1 2 3 4 5 6 7 8 9 |
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
1 2 3 4 5 6 7 8 9 10 11 |
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: первый с дефолтными значениями, которые будут определять значения самого чарта, а второй со значениями для окружения, который в свою очередь уже будет переопределять дефолтные.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
. ├── 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
1 2 3 4 5 6 7 8 9 10 11 12 |
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
1 2 3 4 5 6 7 8 9 10 11 12 |
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 знал, что его надо прогнать через шаблонизатор.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
. ├── 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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
1 2 3 4 5 6 |
ingress: enabled: true paths: - /api hosts: - {{ .Values.global.ingressDomain }} |
envs/default/values/postgres.yaml.gotmpl
1 2 3 4 5 6 |
ingress: enabled: true paths: - / hosts: - postgres.{{ .Values.global.ingressDomain }} |
Подстановка секретов (secrets) из значений окружения
По аналогии с вышеприведённым примером можно подставлять и зашифрованные с помощью helm secrets значения. Вместо того, чтобы для каждого релиза создавать свой файл secrets, в котором определять для чарта зашифрованные значения, мы можем просто определить в релизном default.yaml.gotmpl значения, которые будут браться из переменных, заданных на уровне окружений. А значения, которые нам не надо ни от кого скрывать, можно уже спокойно переопределить в значениях релиза в конкретном окружении.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
. ├── 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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
1 2 3 4 |
secrets: elastic: password: ENC[AES256_GCM,data:hjCB,iv:Z1P6/6xBJgJoKLJ0UUVfqZ80o4L84jvZfM+uH9gBelc=,tag:dGqQlCZnLdRAGoJSj63rBQ==,type:int] ... |
envs/production/secrets.yaml
1 2 3 4 5 |
secrets: elastic: password: ENC[AES256_GCM,data:ZB/VpTFk8f0=,iv:EA//oT1Cb5wNFigTDOz3nA80qD9UwTjK5cpUwLnEXjs=,tag:hMdIUaqLRA8zuFBd82bz6A==,type:str] ... |
envs/default/values/backend.yaml.gotmpl
1 2 3 4 |
elasticsearch: host: elasticsearch port: 9200 password: {{ .Values | getOrNil "secrets.elastic.password" | default "password" }} |
envs/devel/values/backend.yaml
1 2 |
elasticsearch: host: elastic-0.devel.domain |
envs/production/values/backend.yaml
1 2 |
elasticsearch: host: elastic-0.production.domain |
getOrNil
— специальная функция для go шаблонов в helmfile, которая, даже если .Values.secrets
не будет существовать, не выкинет ошибку, а позволит в результате с помощью функции default
подставить значение по-умолчанию
переменные для окружения default можно в свою очередь параметризовать переменными окружения ОС некоего раннера, с которого будет запускаться деплой, и таким образом получить динамические окружения
helmfile.yaml
1 2 3 4 5 6 |
environments: default: values: - global: clusterDomain: {{ env "CLUSTER_DOMAIN" | default "cluster.local" }} ingressDomain: {{ env "INGRESS_DOMAIN" }} |