Управление десятками и сотнями Helm-релизов быстро перестает быть тривиальной задачей, особенно когда появляются зависимости между релизами, CRD, несколько окружений и своя логика деплоя. 

Меня зовут Владимир Фидунин, я работаю в команде мессенджера VK WorkSpace. В статье расскажу, как мы прошли путь от Puppet + Helm до Helmwave и почему в итоге он стал для нас универсальным инструментом управления Helm-релизами. 

О проекте и начале этой истории

VK Workspace — платформа для совместной работы команд: корпоративная почта, календарь, мессенджер, видеозвонки, диск, доска и документы в облаке или на серверах компании. Всё это представляет собой сложный продукт с длинной историей. Сервисы писались в разное время, на разных технологических решениях, и требуют индивидуального подхода. 

Для сборки и настройки сервисов у нас исторически использовался Puppet. Несколько лет назад появилась задача миграции сервисов в Kubernetes. Было решено использовать Helm как стандарт де-факто на тот момент в индустрии. Все продуктовые сервисы решили мигрировать в Helm, а линию разделения между инфраструктурой и продуктовыми сервисами провести по KubeAPI.  В результате мы взяли Puppet-модуль Helm и обработали его напильником.

Стартовая точка

В какой-то момент стало ясно, что в текущей реализации не хватает зависимостей Helm релизов между собой. Мы стали ловить ошибку: обращение к CRD идет раньше, чем он определен. Было два варианта дописать функционал зависимостей в Puppet-модуле или взять готовое решение.

После обсуждения решили взять готовое решение, чтобы не изобретать и строить свой велосипед.

На тот момент мы рассматривали две известные утилиты для Helm: 

  • Helmfile. На тот момент утилита поддерживала Helm v2/v3 и работала через вызов бинаря Helm с нужными флагами. 

  • Helmwave. Поддерживала только Helm v3 и использовал встроенную библиотеку Helm.

Так как мы на тот момент только начинали свой путь, то поддержка Helm v2 нам была не нужна. Кроме того, у Helmwave был более развитый функционал для управления зависимостями чартов и шаблонизации релизов, а интеграция с Kubedog давала более удобный трекинг и визуализацию установки чартов. 

Появление helmwave_projects

Когда начали внедрять Helmwave, стало понятно, что мы идём к постепенному замещению Puppet. Переезд планировался постепенный, поэтому Helmwave решили оформить как Puppet-модуль, который со временем станет самостоятельной частью системы. 

Чтобы упростить миграцию, сделали модуль максимально автономным.

Puppet коду были отданы следующие задачи:

  • установка Helmwave;

  • создание необходимой структуры каталога;

  • копирование файлов Helmwave проектов;

  • запуск Helmwave;

  • возможность проброса переменных и секретов из Puppet Hiera.

Интеграция с Puppet Hiera

Puppet очень мощный SCM с Ruby под капотом. Многие используют только публичные модули сообщества, которых хватает почти на всё. Но у нас потребности были шире стандартных, поэтому для интеграции мы сделали свой модуль со следующими параметрами класса: 

  • helmwave_projects::store: создавал store файл.

  • helmwave_projects::registries: задавал настройки Helmwave registry.

  • helmwave_projects::repositories: задавал настройки Helmwave repository.

  • helmwave_projects::secrets: давал возможность прокидывать секретную информацию в конкретный Helm релиз.

Этого вполне достаточно для использования helmwave. Давайте рассмотрим ключевые моменты реализации

helmwave_projects::store:

В Helmwave есть функционал Store, через котрый можно делиться значениями между релизами.

Мы решили сделать его единым для всех релизов. Store позволяет создать единую точку хранения параметров проекта (single source of truth). Но проект находится в процессе миграции в Kubernetes, а все настройки изначально лежат в Puppet, поэтому появилась задача собирать Store из Puppet. 

В Puppet мы используем похожий механизм — Hiera В Hiera определяется, например, такой хеш:

helmwave_projects::store:
  fileName:
    key: value

Puppet превращает его в fileName.yml с содержимым key: value

Код очень простой, он создает yaml-файл из Hiera в нужной директории.

 $helmwave_projects::store.each| String[2] $file_name, Hash $content | {
    file { "${helmwave_projects::store_directory}/${file_name}.yml":
      ensure  => file,
      content => to_yaml($content),
      require => Imlib::Mkdir_p[$helmwave_projects::store_directory],
    }
  }

Потом к этому ключу можно обратиться из Helmwave в values как [[ .Release.fileName.Store.key ]].

helmwave_projects::secrets:

Следующий вопрос при интеграции с Puppet — как хранить и передавать секреты. В Puppet у нас они хранятся в зашифрованном хранилище Hiera-eyaml. И хотелось и дальше использовать существующие секреты и создавать новые через Hiera-eyaml. Для этого мы сделали механизм, который брал эти значения и превращал их в файлы со значениями для Helm. 

Для того чтобы внедрить секретное значение, необходимо сделать в .eyaml такую запись

helmwave_projects::secrets:
  files: # Имя проекта
    files-mysql: # Имя релиза
      rootPassword: СекретныйСекрет # Содержимое файла которое будет передано в релиз

Все данные, которые находятся в ключе с именем релиза (в примере files-mysql), будут сохранены в файл и использоваться как файл значений этого релиза.

Структура каталогов

Так как модуль должен был поглотить весь текущий Puppet, было решено описывать все релизы и проекты файлами в Puppet модуле. Дальше их собирал шаблонизатор Helmwave. 

имя_проекта
├── helmwave.yml
└── values
    └── имя_проекта.yml

Файл helmwave.yml представлял собой описание Helm релиза с передачей большинства параметров

---
- name: имя-релиза
  chart:
    name: oci://.../helm-charts/some-chart
    version: 0.1.2
  wait: false

Что дал сам Helmwave

Я написали простой шаблон helmwave.yml.tpl, который проходил по всем каталогам и собирал результирующий helmwave.yml. За счёт шаблонизатора Gomplate Helmwave позволил нам не только поддержать нужную структуру каталогов, но и добавить функциональность, которой не было в самом Helm/Helmwave. 

Развитие helmwave_projects

Приведу несколько примеров функциональности, которую удалось реализовать на уровне шаблонизатора без изменений в Helm/Helmwave. 

Развертывание в закрытом контуре

Мы должны уметь разворачиваться в закрытом контуре у заказчика где нет доступа к нашим Helm registry. Эту проблему удалось просто решить используя Helm cache — библиотека Helm кеширует все чарты в каталоге ~/.cache/helm/repository/. Мы решили использовать это поведение и добавили нужную логику в шаблонизатор.

..
{{- $UseLocalRepoCache := conv.ToBool (getenv "HELMWAVE_USE_LOCAL_REPO_CACHE") -}}
{{- $HelmCacheDir := filepath.Join (getenv "HOME") ".cache/helm/repository" -}}
...
{{- $releaseName := .name }}
- name: {{ $releaseName }}
  <<: *options
  chart:
  {{- if $UseLocalRepoCache }}
    name: {{ filepath.Join $HelmCacheDir (printf "%s-%s.tgz" (path.Base .chart.name) .chart.version) }}
    skip_dependency_update: true
    skip_refresh: true
  {{- else }}
    name: {{ .chart.name }}
    version: {{ .chart.version }}
  {{- end }}
...

Глобальное отключение зависимостей

До версии v0.38.0 у Helmwave не было функционала отключения зависимостей, но была необходимость для целей разработки и тестирования применять изменения только для одного Helm релиза. Поэтому мы решили это в шаблонизаторе и по переменной окружения отключали обработку зависимостей из проектных файлов helmwave.yml. 

...
{{- $dependsDisabled := conv.ToBool (getenv "HELMWAVE_DEPENDS_DISABLED") -}}
...
  {{- if not $dependsDisabled }}
  {{- with .depends_on }}
  depends_on:
  {{- range . }}
    - {{ . }}
  {{- end }}
  {{- end }}
...

Система слоев

Слои формируются динамически на основе предоставленных файлов или секретов. Чтобы передать в релиз values-файл, его нужно назвать именем релиза. 

Например, файл helmwave.yml выглядит таким образом

---
- name: minio
  chart:
    name: oci://.../helm-charts/minio
    version: 4.0.32

Тогда файл со значениями для этого релиза нужно назвать так же, как релиз в name:, и добавить расширение .yml.  Файл значений для примера будет называться minio.yml. Допустим мы создали один файл значений и один секрет. То файлы значений будут переданы в релиз в таком виде:

values:
  - projects/minio/values/minio.yml
  - projects/minio/secrets/minio.yml

Если значений много, можно создать директорию с именем релиза и разнести их по нескольким файлам: 

values:
  - projects/minio/values/minio/buckets.yml
  - projects/minio/values/minio/users.yml
  - projects/minio/values/minio/minio.yml
  - projects/minio/secrets/minio.yml

Если существуют и директория с названием релиза, и файл с именем релиза и расширением .yml, будут добавлены только файлы из директории. Генератор проверяет существует ли файл secrets/ИМЯ-РЕЛИЗА, и  в случае его отсутствие не добавляет его в план.

Дополнительные файлы значений можно указать в helmwave.yml передав values: — они будут добавлены к текущим файлам значений и идти первыми в списке.

- name: minio
  chart:
    name: oci://.../helm-charts/minio
    version: 4.0.32
  values:
    - src: projects/minio/extra_values.yml

Можно переопределять значения для конкретного кластера и namespace. Для этого необходимо определить переменную окружения HELMWAVE_ENV_NAME. После этого Helmwave будет генерировать следующий список файлов значений:

values:
  # базовый слой значений
  - projects/minio/values/minio.yml
  - projects/minio/secrets/minio.yml
  # слой переопределения значений
  - projects/minio/values/override/$ENV_NAME/$NAMESPACE/minio.yml

Здесь $ENV_NAME — это значение переменной окружения, а $NAMESPACE — это namespace куда будет происходить развертывание чарта.

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

Сейчас у нас 109 релизов под управлением Helmwave. Helmwave стал для нас не просто инструментом деплоя, а слоем абстракции над Helm. С его помощью мы получили:

  • единую структуру проектов;

  • динамическую система values и secrets;

  • переопределение значений под окружения и namespace;

  • управление зависимостями между релизами;

  • теги и группировку релизов;

  • управление включением и отключением проектов;

  • lifecycle-хуки;

  • пользовательский слой store;

  • работу в закрытых контурах;

  • управление через переменные окружения.

Для наглядности выкладываю наш helmwave.yml.tpl целиком, как пример того, как можно организовать работу с Helm и адаптировать её под свои задачи с помощью Helmwave. Делитесь в комментариях опытом таких проектов и автоматизаций, обсудим детали и подводные камни.

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