
Выпускник курса 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 часов на каждую «курсовую» работу, потом приходил грамотный и подробный ревью, и я еще часик-два тратил на исправление замечаний. Первую половину курса было очень сложно, но, пройдя через это, я получил колоссальный опыт».
