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