Nelmwave – декларативный оркестратор релизов поверх nelm

от автора

Для тех кто впервые

Если в кластере живёт больше 5 Helm-релизов, рано или поздно появляется скрипт.

Сначала это десять строк с helm upgrade --install, потом в нём заводятся sleep 30 между базой и приложением, потом --set image.tag=$CI_COMMIT_SHA, потом ветка if [ "$ENV" = prod ], потом sops -d values.yml | helm ... -f -.

В какой-то момент скрипт становится главным артефактом инфраструктуры, и никто не может сказать, что именно он раскатит на проде до того, как он это сделает.

https://github.com/helmwave/helmwave — попытка заменить этот скрипт одним декларативным манифестом. Он описывает много релизов сразу, строит между ними граф зависимостей, резолвит values из произвольных источников, и применяет всё.

Nelmwave vs helmwave?

Nelmwave это брат близнец helmwave, но с новой схемой и другим движком под капотом.

Helmwave использует Helm. А Nelmwave – Nelm. Подробнее про Nelm https://habr.com/ru/companies/flant/articles/970498/

Идея Nelmwave — проверить новый движок. Несмотря на схожую функциональность, публичный API Nelm отличается от API Helm, поэтому внедрение обоих движков или замена одного на другой может оказаться довольно затратной.

В Nelmwave реализованы концепции и идеи, которые изначально разрабатывались для Helmwave. Однако из-за накопившейся кодовой базы проверить эти идеи непосредственно в Helmwave сейчас довольно сложно.

Нужно ли с helmwave переезжать на nelmwave?

Nelmwave — экспериментальный проект. Его стоит рассматривать как площадку для проверки новых идей и возможный Release Candidate для Helmwave, но на отдельном движке.

Мы хотим собрать ваш опыт использования Nelmwave и понять, какие возможности действительно нужны индустрии. В дальнейшем это поможет нам выпустить Helmwave v1.0.0.

Большая часть экспериментальных возможностей в итоге появится в Helmwave. Поэтому Nelmwave и Helmwave v1.0.0 будут во многом похожи. Основные отличия могут быть связаны со специфичными для Nelm и Helm флагами. Stores, Values и остальная обвязка будут идентичными.

Иными словами, мы стремимся к тому, чтобы конфигурация, запускаемая командой

helmwave -f nelmwave.yml.tpl

переносилась между проектами примерно на 90%.

Переходить на Nelmwave прямо сейчас не обязательно. Если вам нужны стабильность и предсказуемость, продолжайте использовать Helmwave. Nelmwave стоит попробовать тем, кто готов тестировать новый движок и делиться обратной связью.

Что нового в Nelmwave?

Самое вкусное! Некоторые могут слегка показаться вам знакомыми с helmwave.

build отделён от apply

У nelmwave четыре команды: buildupdowndiffРендерит шаблоны и ходит в датасорсы только build. Он не требует кластера и не делает ни одного запроса к API Kubernetes. Всё, что он делает — превращает манифест в самодостаточную директорию:

.nelmwave/  planfile.yml              разрешённый план: релизы, рёбра графа, артефакты  values/<uniqname>/...     values-файлы, в порядке слияния  stores/<uniqname>/...     сопутствующие файлы из stores:  charts/<chart>/...        сами чарты — с --download-charts

updown и diff читают этот план и никогда не рендерят заново. Следствие простое: то, что вы посмотрели глазами в review-джобе, — ровно то, что уедет в кластер. Между diff и up манифест не может «передумать», потому что на этом этапе манифеста уже нет, есть только план.

Планфайл детерминирован (ключи map отсортированы), так что он нормально диффится между сборками, и его не стыдно читать на ревью.

~/demo $ ENV=prod API_TAG=1.5.0 nelmwave build --download-charts --output .nelmwave-prod~/demo $ diff .nelmwave/planfile.yml .nelmwave-prod/planfile.yml6c6<             env: stg--->             env: prod16c16<             image.tag: 1.4.2--->             image.tag: 1.5.028c28<             env: stg--->             env: prod44c44<             env: stg--->             env: prod

Идентичность релиза

Релизы — это map, ключ которой называется uniqname и имеет вид name[@namespace[@kubecontext]]:

releases:  api:                      # текущий контекст, его дефолтный namespace  api@app:                  # namespace app  api@app@staging:          # namespace app, kube-context staging

Внутри тела релиза нет полей name и namespace. Это сознательный выбор: имя релиза — не атрибут, а идентичность, и дублировать её в двух местах — способ однажды получить два разных релиза с одинаковым именем. Эквивалентные написания ключей нормализуются и схлопываются, коллизии — ошибка сборки. Заодно нормализуются и записи в needs, чтобы ссылка на зависимость не разъезжалась с её объявлением.

Namespace и kube-context необязательны: если их нет, берётся текущий контекст и его дефолтный namespace, причём резолвится это в момент применения, а не сборки.

Зависимости между релизами

Для этого используется директива needs. Она позволяет указать завимость как от имени релиза. Так и от лейблов. При этом можно задать k8s-like expressions

releases:  api@app: # Пример со всеми  Needs    needs:      releases:        postgres@data: {} # required        redis@data:          optional: true      matchLabels:        tier: db      matchLabelsExpressions:        - { key: tier, operator: In, values: [backend, secrets] }        - { key: app, operator: NotIn, values: [worker] }

Явно названная зависимость по умолчанию обязательна: выбрать зависимый релиз без неё — ошибка, ничего не применяется. optional: true понижает это до предупреждения и просто выкидывает ребро.

Labels для работы с релизами

В Helmwave v0.44.4 релизы выбираются с помощью тегов:

helmwave build -t prod

По мере роста конфигурации имена тегов становятся всё длиннее. Из-за самой концепции тегов команда может превратиться, например, в такую:

helmwave build -t prod-bla-bla

Поэтому в Nelmwave используются лейблы.

Лейблы позволяют описывать релизы по нескольким независимым признакам: окружению, проекту, команде, региону и другим параметрам. Благодаря этому не нужно создавать длинные составные теги для каждой комбинации.

nelmwave up   -l 'tier=backend' nelmwave diff -l 'env=prod,!track'nelmwave up -l 'env!=prod'nelmwave up -l 'app=api,env in (prod,stg),tier!=db'

Независимые релизы применяются параллельно, --concurrency ограничивает одновременность. Падение останавливает свою ветку графа: зависимые пропускаются, несвязанные ветки продолжают работать. 

Флаг --include-needs расширяет выборку по графу — в направлении, в котором движется команда, и оно разное:

Команда

Подтягивает

Пример

up

то, от чего зависит выборка

up -l 'app=api' --include-needs поставит и postgres

diff

то же, что up — чтобы превью совпало

diff -l 'app=api' --include-needs спланирует и postgres

down

то, что зависит от выборки

down -l 'app=postgres' --include-needs снесёт и api

Инверсия для down – намеренная. Снос, который тянул бы за собой зависимости, удалил бы больше, чем вы выбрали, и оставил бы выживших сломанными. А подтягивание зависимых убирает ровно то, что иначе осталось бы смотреть в пустоту.

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

И отдельно кричит про всё, чего селектор не называл:

INFO   uninstall selection          {"count": 3, "releases": ["api@app", "cache@app", "postgres@data"]}WARN   pulled in by --include-needs {"count": 2, "releases": ["api@app", "cache@app"]}

Tracking Resources / Workloads

Тут все просто – за это отвечает Nelm. Тут смотрите документацию Nelm.

Sets

Когда заводить целый values – избыточно.

Ключи — точечные пути, как у helm --set, но значения сохраняют YAML-тип: до nelm они доезжают типизированным JSON, так что 3 остаётся числом, а false — булевым. Все, кто ловил --set replicas=3, превратившееся в строку внутри чарта, оценят.

releases:  api@app:    values:      - values/api.yml.tpl    sets:      replicaCount: 3      image.tag: [[ getenv "API_TAG" "1.4.2" ]]

Динамический выбор рендера

В src можно писать любую схему датасорса gomplate: env:http(s)://s3://git://vault://. А вот что с источником сделают, решает расширение, а не схема:

Расширение

Поведение

.yml / .yaml

копируется как есть

.yml.tpl

рендерится через gomplate ([[ ]])

.yml.sops

расшифровывается

.yml.tpl.sops

расшифровывается, потом рендерится

.sops-источники расшифровываются в процессе, бинарь sops не нужен. Ключи берутся из окружения ровно так же, как их берёт CLI: SOPS_AGE_KEY_FILE/SOPS_AGE_KEY, GnuPG, облачные KMS. Никакого своего хранилища ключей nelmwave не заводит.

Расшифрованное попадает в .nelmwave/ в открытом виде — это build-артефакт. Это опасно. О чем build говорит об этом в WARN, если за прогон что-то расшифровал:

WARN  decrypted secrets written in cleartext  {"sources": 1, "dir": ".nelmwave",      "hint": "treat this directory as sensitive: do not publish it as a build artifact"}

Gomplate V5

Полностью отказались от Sprig. Чтобы не путать людей.

Для рендера Nelmwave’ом используются только [[ ]] .  А Helm-овские {{ }} доезжают до артефакта не тронутыми по умолчанию.

Stores Files

Store – позволяет вам указывать любой Datasource (http, vault, aws…) и можете использовать его при рендеринге helm-values.

Также Store умеет ссылаться на другой Store.

Отличия с helmwave v0.43.0?

Store можно было задать только на уровне релиза. Нет Датасорсов. Нельзя ссылаться на другой Store.

В helmwave из-за этого приходится заниматься менеджментом Store самому пользователю, покружаясь в сложности сравнения с сливания объектов с помощью gomplate.

Values Files

Values теперь можно достать также как из любого Datasource.

values:  - { src: values/base.yml,     name: base.yml }  - { src: values/app.yml.tpl,  name: app.yml }   # видит base.yml
[[ (ds "values/base.yml").image.registry ]][[ include "stores/netpol.yml" ]]

ds даёт распарсенный объект, include — сырое содержимое. Резолвинг строго «назад»: элемент видит только то, что разрешилось до него. Это позволяет держать один источник правды по сайзингу или тегу образа в base.yml и не размазывать его по шаблонам.

Имена артефактам стоит задавать явно: тогда ключ датасорса не поедет при переупорядочивании списка.

Разница с helmwave v0.43.0

Values могли быть только локальными или http. С помощью Датасорсов есть весь спектр. Также Валуес могут ссылаться нативно через датасорс на другой values.

Для обхода этого ограчения у helmwave появились дополнительные функции для рендера – getValues().

HELM DRIVER

Где живёт состояние релиза

releases:  api@app:    labels: { app: api }    chart: { name: ../charts/stub }    driverURL: kubernetes://secrets      # the default, spelled out

Один URL вместо флага и набора параметров к нему:

URL

Состояние в

kubernetes://secrets

Secret на ревизию, в неймспейсе релиза. По умолчанию.

kubernetes://configmaps

ConfigMap на ревизию.

psql://user@host:5432/db

PostgreSQL.

PostgreSQL имеет смысл, когда релизы перерастают ~1 МБ на объект (большие CRD это умеют), когда история должна пережить удаление неймспейс.

пароль в URL писать НЕЖЕЛАТЕЛЬНО, потому что build копирует манифест в planfile.yml, и он окажется открытым текстом на диске и в артефактах CI. Задавайте пароль через PGPASSWORDbuild предупредит, если найдёт встроенный пароль в URL.

Managed Namespace

namespace: не «какой», а «как»

Блок namespace — не про то, в каком неймспейсе живёт релиз (это часть ключа), а про то, создавать ли его и какие метаданные на нём держать:

releases:  api@production:    chart: { name: repo/api }    namespace:      create: true      delete: true      labels:        pod-security.kubernetes.io/enforce: restricted        istio-injection: enabled      annotations:        owner: platform-team

Метаданные применяются до релиза, а не после. Лейбл вроде istio-injection или pod-security.kubernetes.io/enforce влияет только на поды, созданные уже после того, как он встал. Собственный API nelm создаёт неймспейс с одним лишь именем, поэтому метаданные пишет сам nelmwave — и делает это до того, как передать управление движку. Лейблы и аннотации при этом мержатся: то, чем nelmwave не управляет, он не трогает, так что он мирно сосуществует с чем угодно ещё, что управляет этим неймспейсом.

delete — не зеркало create. create по умолчанию truedelete — false. Неймспейс не принадлежит релизу: удалив его, вы снесёте всё, что там живёт, включая чужие релизы, секреты и PVC, а не только то, что положил туда nelmwave. Поэтому симметрии здесь нет намеренно, а down пишет предупреждение по каждому релизу, у которого выставлен delete.

namespace: production строкой — это ошибка, а не тихий no-op. Имя живёт в ключе релиза, и молча проглотить попытку задать его в другом месте было бы худшим из вариантов.

Defaults

Сделали отдельную библиотеку для жадного конфигуратора – https://github.com/helmwave/confijer (читается как конфижьор)

Release:  labels: { team: platform, env: prod }releases:  api@app:    labels: { team: data }     # -> {team: data, env: prod}

Верхнеуровневый блок Release: — это type default конфиг-загрузчика: он применяется к каждому значению Go-типа Release, то есть к каждой записи под releases:

Так работает со всеми go-типами. Release – самый наглядный.

Map’ы сливаются вглубь, собственный ключ релиза выигрывает. Списки заменяются, а не мержатся — это семантика YAML, и она сохранена сознательно. Так что релиз со своим values: не унаследует общий; общий базовый файл перечисляется явно.

Политики обращения с ресурсами

Поле

По умолчанию

Что делает

forceAdoption

false

Забрать ресурс, который через meta.helm.sh/release-name числится за другим релизом.

removeManualChanges

true

Отобрать поля, добавленные руками через kubectl edit, которых нет в манифесте.

installCRDs

true

Ставить CRD из директории crds/ чарта.

deletePropagation

Foreground

Стратегия удаления: ForegroundBackgroundOrphan. Валидируется на build.

historyLimit

10

Сколько ревизий хранить.

forceAdoption — инструмент для миграций и переименований: переименование делает релиз новым владельцем существующих ресурсов. Оставлять его включённым навсегда означает, что следующая коллизия имён молча украдёт чужие ресурсы вместо того, чтобы упасть.

removeManualChanges действует и на diff тоже — чтобы превью совпадало с тем, что реально сделает применение.

QuickStart

https://asciinema.org/a/WjQFSLsuylNubIFM

Как попробовать?

# macOS и Linuxbrew install helmwave/tap/nelmwave# контейнерdocker run --rm -v "$PWD:/workspace" ghcr.io/helmwave/nelmwave:latest build# из исходниковgo install github.com/helmwave/nelmwave/cmd/nelmwave@latest

Начать проще всего с примеров: в репозитории лежит по проекту на каждую фичу — зависимости, источники чартов, неймспейсы, политики ресурсов, хранилище состояния, sops, перекрёстные датасорсы, работа в CI. Все они собираются без кластера, реестра и сети, потому что чарты указывают на локальную заглушку, а make examples собирает их разом — заодно это способ держать примеры честными при изменении схемы.

Не забудьте автодополнение: оно подсказывает не только имена, но и значения — ключи лейблов и их значения после -l берутся из собранного плана, контексты — из kubeconfig.

Код, примеры и подробный референс схемы: github.com/helmwave/nelmwave. Issues и обсуждения приветствуются — особенно про то, какой части схемы не хватает под ваш пайплайн.

ссылка на оригинал статьи https://habr.com/ru/articles/1068644/