Выпускник курса Python для инженеров Максим Дубакин рассказал о рабочем проекте собственного производства, который заавтоматизировал повторяющиеся задачи по переводу с деплоя bash-скриптами на helmfile при помощи Python и уменьшил затраты времени на ~ 2 часа.

Проблема

Команда использует helm для деплоя приложений в K8s. Ранее helm-чарты деплоились с помощью bash-скриптов, которые в свою очередь запускали еще bash-скрипты. Это крайне неудобно с точки зрения поддержки и дебага: скрипты хоть и имели общую форму, но могли быть произвольно дописаны разработчиками. В итоге это превращалось в большую кашу.

Нашли решение, им оказался [helmfile] (https://github.com/helmfile/helmfile). Очень удобная программа, которая позволяет запускать несколько helm-чартов из одного места, указав при этом их последовательность. Это полезно, если у вас есть несколько окружений (у Максима — production / stage / QA), и каждые окружения имеют свои values файлы.

В компании более 250 сервисов, а переводить на helmfile приходилось вручную — это занимало порядка 6–12 часов на подготовку файлов, тестирование и перекатку каждой среды. В QA-средах используются helm-чарты для инфраструктуры типа баз данных, кешей и очередей, поэтому количество values файлов и длина helmfile могли быть существенными.

Пример типичного helmfile для QA:

```yaml
environments:
 default:
   values:
     - VERSION: {{ requiredEnv "version" | quote }}
     - NAMESPACE: {{ requiredEnv "namespace" | quote }}
     - CLUSTER: {{ requiredEnv "cluster" | quote }}
     - RELEASE: <name_of_service>
---
templates:
 default: &default
   namespace: {{ .Values.NAMESPACE }}
   missingFileHandler: Warn
 
releases:
 - name: "{{ .Values.RELEASE }}"
   chart: artifactory/unified
   <<: *default
   version: "2.5.3"
   values:
     - values/common.yaml.gotmpl
     - values/app.yaml.gotmpl
     - values/override/{{.Values.CLUSTER}}/{{.Values.NAMESPACE}}/common.yaml
     - values/override/{{.Values.CLUSTER}}/{{.Values.NAMESPACE}}/app.yaml
   needs:
     - "{{ .Values.RELEASE }}-migration"
 
 - name: "{{ .Values.RELEASE }}-job"
   ...
   needs:
     - {{ .Values.RELEASE }}
 
 # migration
 - name: "{{ .Values.RELEASE }}-migration"
   ...
   needs:
     - "{{ .Values.RELEASE }}-drop-restore-db"
  - name: "{{ .Values.RELEASE }}-drop-restore-db"
   ...
   needs:
     - "{{ .Values.RELEASE }}-sku-generator"
     - "pgsql-{{ .Values.RELEASE }}"
     - "{{ .Values.RELEASE }}-rabbitmq"
     - "{{ .Values.RELEASE }}-webdav"
     - "{{ .Values.RELEASE }}-wiremock"
     - "{{ .Values.RELEASE }}-mailhog"
 
 # sku-generator
 - name: "{{ .Values.RELEASE }}-sku-generator"
   ...
  # postgres
 - name: "pgsql-{{ .Values.RELEASE }}"
   ...
 
 # rabbitmq
 - name: "{{ .Values.RELEASE }}-rabbitmq"
   ...
  # webdav
 - name: "{{ .Values.RELEASE }}-webdav"
   ...
  # wiremock
 - name: "{{ .Values.RELEASE }}-wiremock"
   ...
 
 # mailhog
 - name: "{{ .Values.RELEASE }}-mailhog"
   ...
bases:
 - "../_helmfile.d/helm.yaml"
 - ../_helmfile.d/repositories.yaml
```

Первым релизом идет основной helm-чарт приложения, затем — все его инфраструктурные зависимости. Под ... скрыты похожие части с указанием чарта, его версии и values файлов.

Здесь явно видна проблема ручного повторяющегося труда.

Решение

Во время перевода сервисов Максим как раз учился на курсе и решил в свободное время написать инструмент, который бы просто создавал файловую структуру проекта на helmfile, а также пустые файлы values и helmfile.yaml. Но со временем его проект начал разрастаться. Вот, что из этого вышло.

Как работает программа:

1. Принимает на вход: название проекта, нужно ли использовать миграции, в какой папке создавать проект, в какой namespace будет деплоиться приложение.

2. Создает структуру проекта в одном из репозиториев с helmfile (для production и QA мы используем разные репозитории).

3. Создает шаблоны файлов helmfile.yaml и пустые values для возможности override с помощью merge шаблонов jinja2 на стороне приложения (values и helmfile.yaml) и общих шаблонов gotmpl (шаблоны отдельных релизов и их values по умолчанию), которые хранятся в репозитории с helmfile.

4. Внутри helmfile будут сразу все возможные инфразависимости, но они будут закоменчены, так что остается только раскоментить нужные и удалить ненужные.

Результат

Таким образом, команда OPS перестала самостоятельно создавать все папки и файлы, указывать вручную кучу values и добавлять в helmfile.yaml те чарты, которые нужны.

Теперь про фичи:

1. Программа helmfile_template использует gotmpl шаблоны внутри репозитория в helmfile. Вот пример шаблона релиза:

```yaml
pgsql: &pgsql
 <<: *default
 name: pgsql-{{ .Values.releaseName }}
 chart: artifactory/postgres-instance
 version: 0.4.2
 values:
   - ../_helmfile.d/templates/values/pgsql.yaml.gotmpl
   - values/pgsql.yaml.gotmpl
   - values/override/{{ .Values.CLUSTER }}/{{ .Values.NAMESPACE }}/pgsql.yaml
```

А вот шаблон values для pgsql:

```yaml
pgsql:
 version: 12
 instances: 1
 limits:
   cpu: 200m
   memory: 512Mi
 requests:
   cpu: 200m
   memory: 512Mi
 storageClass: ceph-block
 
database:
 - name: {{ .Values.releaseName | replace "-" "_" }}
   size: 10
 
service:
 annotations:
   external-dns.alpha.kubernetes.io/hostname: {{ .Release.Name }}{{ .Values.IngressPostfix }}
```

Как вы видите, первым values-файлом используется - ../_helmfile.d/templates/values/pgsql.yaml.gotmpl. Это как раз шаблон с values по умолчанию, о котором писалось выше. Если требуется, они могут быть перезаписаны с помощью файлов в папке с проектом.

2. Используются шаблоны [jinja2] (https://jinja.palletsprojects.com/en/3.1.x/).

3. Программа распространяется как один исполняемый файл. Для этого используется модуль [pyinstaller] (https://pyinstaller.org/en/stable/).

4. Программа может работать как в интерактивном режиме, так и запускаться с аргументами. Для этого используется модуль [argparse] (https://docs.python.org/3/library/argparse.html), который позволяет выводить классный help с помощью ключа -h.

Получение справки:

```bash
helmfile_template -h
usage: helmfile_template [-h] [-v] [-g] [-s PROJECT_NAME] [-p] [--no-production] [-n NAMESPACE] [-u] [--no-unified]
                        [-m] [--no-migration]
 
Get helmfile templates.
 
options:
 -h, --help            show this help message and exit
 -v, --version         show program's version number and exit
 -g, --get-help        get templates
 -s PROJECT_NAME, --name PROJECT_NAME
 -p, --production      use deploy repository
 --no-production       use deploy repository
 -n NAMESPACE, --namespace NAMESPACE
 -u, --unified         use unified helmchart
 --no-unified          don't use unified helmchart
 -m, --migration       use migration
 --no-migration        don't use migration
```

Пример работы в интерактивном режиме:

```bash
$ helmfile_template
******************************
Введите название проекта: slurm
******************************
******************************
1. deploy
2. qa-deploy
Выберите репозиторий (default: 2): 2
******************************
Использовать unified? (y/n) (default: n): y
******************************
Нужны миграции? (y/n) (default: n): n
Проект slurm был создан в папке /Users/<my_user>/<some_path>/qa-deploy.
```

Тот же проект, но созданный с помощью аргументов:

```bash
$ helmfile_template -s slurm --no-production -u --no-migration
Проект slurm был создан в папке /Users/<my_user>/<some_path>/qa-deploy.
```

Результат работы программы

Файловая структура:

```
slurm
├── helmfile.yaml
└── values
   ├── app.yaml.gotmpl
   ├── common.yaml.gotmpl
   ├── job.yaml.gotmpl
   └── override
```

Содержимое helmfile.yaml:

```yaml
{{ tpl (readFile "../_helmfile.d/environments.tmpl") . }}
---
environments:
 default:
   values:
     - releaseName: "slurm"
---
{{ tpl (readFile "../_helmfile.d/templates.tmpl") . }}
{{ tpl (readFile "../_helmfile.d/bases.yaml") . }}
releases:
- <<: *unified
 labels:
   type: app
# - <<: *unified
#   name: {{ .Values.releaseName }}-{{`{{ .Release.Labels.type }}`}}
#   labels:
#     type: job
#   needs:
#     - "pgsql-{{ .Values.releaseName }}"
#     - "{{ .Values.releaseName }}-mysql"
#     - "{{ .Values.releaseName }}-es"
#     - "{{ .Values.releaseName }}-kafka"
#     - "{{ .Values.releaseName }}-rabbitmq"
#     - "{{ .Values.releaseName }}-redis"
#     - "{{ .Values.releaseName }}-webdav"
#     - "{{ .Values.releaseName }}-saml"
# - <<: *pgsql
# - <<: *mysql
# - <<: *elastic
# - <<: *kafka
# - <<: *rabbitmq
# - <<: *redis-standalone
# - <<: *redis-sentinel
# - <<: *webdav
# - <<: *saml
```

Предположим, что сервису slurm нужны pgsql, kafka и saml. В таком случае можно немного отредактировать файл, и сервис готов к деплою:

```yaml
{{ tpl (readFile "../_helmfile.d/environments.tmpl") . }}
---
environments:
 default:
   values:
     - releaseName: "slurm"
---
{{ tpl (readFile "../_helmfile.d/templates.tmpl") . }}
{{ tpl (readFile "../_helmfile.d/bases.yaml") . }}
releases:
- <<: *unified
 labels:
   type: app
 needs:
   - "pgsql-{{ .Values.releaseName }}"
   - "{{ .Values.releaseName }}-kafka"
   - "{{ .Values.releaseName }}-saml"
 
- <<: *pgsql
 
- <<: *kafka
 
- <<: *saml
```

Проект сейчас находится в сыром состоянии, и в планах есть большой Roadmap по его улучшению.

Вместо заключения

Максим поделился о себе и о курсе, который изучал:

«Программированием я увлекаюсь давно, начинал с бесплатных курсов на разных платформах. Но все равно не чувствовал уверенности в том, что могу написать нечто прикладное и реально полезное моей команде. Поэтому я решил пойти на курс Python для инженеров, и он однозначно придал мне уверенности в своих силах.

Я научился работать с модулями / библиотеками для работы с файлами, запросами. Получил неплохой опыт в написании программ больше 100 строк, научился разбивать программу на модули и использовать ООП в работе. Также написал свой Ansible модуль и Prometheus Exporter. Чего давно хотел, но не хватало знаний.

Больше всего мне понравилась сложность курса. Я тратил 8–12 часов на каждую «курсовую» работу, потом приходил грамотный и подробный ревью, и я еще часик-два тратил на исправление замечаний. Первую половину курса было очень сложно, но, пройдя через это, я получил колоссальный опыт».

Комментарии (2)


  1. chemtech
    15.12.2022 10:23

    Коллеги, кто читает этот пост, кто как использует helm чарты бд/очереди для работы основного приложения: как сабчарт или как отдельный чарт?


    1. w84mepls
      16.12.2022 02:14
      +2

      Подключаем их в качестве отдельных чартов с помощью helmfile. ИМХО не стоит включать их как сабчарты.