Перейти к содержимому
Hogin Hogin
Назад

Helm по-взрослому: OCI-реестры, зависимости и чарты без боли

9 мин чтения

Открыть чужой Helm-чарт — почти всегда значит найти values.yaml без единого комментария, шаблон, который падает с невнятной ошибкой на пустом поле, и charts/ с .tgz-файлами, которые непонятно как обновлять. Помогает не «ещё один линтер», а несколько конкретных практик: схема на values, чарты с явными зависимостями и поставка через тот же OCI-реестр, что и образы. Ниже — как это выглядит на практике.

Содержание

Открыть содержание

Почему classic-репозиторий чартов устарел

Изначально Helm распространял чарты так же, как apt или yum распространяют пакеты: helm repo add добавляет URL с файлом index.yaml, который перечисляет все версии всех чартов в репозитории, а helm install скачивает нужный .tgz по ссылке из этого индекса. Модель рабочая, но с двумя болячками. Во-первых, index.yaml нужно регенерировать и заново публиковать при каждом релизе — это отдельный шаг, который либо автоматизируют CI-джобой, либо забывают. Во-вторых, репозиторий чартов живёт отдельно от registry с образами: разная аутентификация, разный RBAC, разный набор инструментов для сканирования на уязвимости.

С версии Helm 3.8 поддержка OCI-реестров стала стабильной, и чарт можно публиковать как обычный OCI-артефакт в тот же registry, где уже лежат образы приложения. Индексный index.yaml в этой модели не нужен вовсе — версия становится тегом артефакта, а helm push/helm pull работают напрямую по oci://-адресу. Но смена транспорта не чинит сам чарт: если внутри values.yaml нет проверки обязательных полей, а зависимости — это скачанные руками .tgz, миграция на OCI просто ускоряет доставку тех же проблем.

Слева типичный чарт с проблемами в values и зависимостях, справа тот же набор файлов с контрактом на values.schema.json и dependencies

Где чарт обычно ломается

Анатомия чарта простая: Chart.yaml с метаданными и списком dependencies, values.yaml с дефолтными значениями, templates/ с манифестами и вспомогательный templates/_helpers.tpl с именованными шаблонами (define/include) для повторяющихся кусков вроде лейблов. Проблема не в структуре — она стандартна и у аккуратного, и у небрежного чарта. Проблема в том, что values.yaml обычно не более чем соглашение: ничто не мешает передать replicas: "три" или вообще забыть обязательное поле image.repository, и шаблон либо тихо сгенерирует битый манифест, либо упадёт с ошибкой Go-шаблонизатора, из которой не понятно, какое поле виновато.

Решение — не «писать аккуратнее», а сделать values.yaml проверяемым контрактом. values.schema.json рядом с values.yaml описывает типы и обязательность полей по JSON Schema, и helm install/helm template отклоняет values, которые не проходят валидацию, ещё до рендера шаблонов:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["image"],
  "properties": {
    "image": {
      "type": "object",
      "required": ["repository", "tag"],
      "properties": {
        "repository": { "type": "string" },
        "tag": { "type": "string" }
      }
    },
    "replicas": { "type": "integer", "minimum": 1 }
  }
}

Внутри самих шаблонов та же идея работает через функции required и fail: required останавливает рендер с понятным сообщением, если поле не задано, а fail — если сочетание полей само по себе невалидно (например, ingress.enabled: true без ingress.host):

image: "{{ required "укажите .Values.image.repository" .Values.image.repository }}:{{ .Values.image.tag }}"
{{- if and .Values.ingress.enabled (not .Values.ingress.host) }}
{{ fail "ingress.enabled=true требует ingress.host" }}
{{- end }}

Разница на практике ощутима: без схемы опечатка в values долетает до kubectl apply и падает где-то в контроллере, со схемой — падает на helm install с указанием конкретного поля.

Зависимости: subcharts, alias и condition вместо копипасты

Второе типичное место боли — зависимости. Классический способ переиспользовать логику между чартами — скопировать шаблон из соседнего чарта. Chart.yaml умеет описывать зависимости декларативно:

dependencies:
  - name: postgresql
    version: "15.x.x"
    repository: "oci://registry-1.docker.io/bitnamicharts"
    condition: postgresql.enabled
  - name: common
    version: "2.x.x"
    repository: "oci://registry.internal/charts"
    alias: common

condition привязывает включение зависимости к булеву полю в values родительского чарта — так один чарт может опционально тянуть встроенную базу для дев-окружения и не тянуть её в проде, где база внешняя. alias позволяет подключить один и тот же чарт дважды под разными именами (например, два экземпляра redis — под кеш и под очередь), у каждого будет свой блок в values под именем алиаса. После правки Chart.yaml helm dependency update скачивает нужные .tgz в charts/ — руками туда ничего класть не нужно.

Отдельный вид зависимости — library-чарт: Chart.yaml с type: library не рендерит ни одного манифеста сам по себе и не устанавливается напрямую, а только объявляет именованные шаблоны в своём _helpers.tpl. Смысл в том, чтобы вынести общие лейблы, аннотации или блок securityContext в одно место и подключать их из нескольких приложенческих чартов через dependencies, а не копировать _helpers.tpl между репозиториями.

Library-чарт common подключается как dependency в checkout-app, финальный чарт публикуется в OCI и устанавливается в кластер

Что нужно, чтобы собрать и опубликовать связку в OCI

Возьмём тот самый случай: library-чарт common с общими helper-ами и чарт приложения checkout-app, который его использует. В common/templates/_helpers.tpl:

{{- define "common.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/managed-by: Helm
{{- end -}}

В checkout-app/Chart.yaml дописываем зависимость и подключаем её шаблон в манифестах:

apiVersion: v2
name: checkout-app
version: 1.4.0
dependencies:
  - name: common
    version: "1.0.0"
    repository: "oci://registry.internal/charts"
# checkout-app/templates/deployment.yaml
metadata:
  labels:
    {{- include "common.labels" . | nindent 4 }}

Дальше — сборка и публикация. Пакуем чарт, при необходимости подписываем provenance-файл GPG-ключом, логинимся в registry и пушим как OCI-артефакт:

helm dependency update checkout-app/
helm package checkout-app/ --sign --key "release@internal" --keyring ~/.gnupg/secring.gpg
helm registry login registry.internal -u "$REGISTRY_USER" -p "$REGISTRY_PASS"
helm push checkout-app-1.4.0.tgz oci://registry.internal/charts

Установка из OCI выглядит так же, как install из classic-репозитория, только вместо helm repo add + имени репозитория — прямой oci://-адрес с явной версией (у OCI-чартов нет helm search repo, версию нужно знать заранее — это и есть та часть GitOps-пайплайна, где Flux или Argo CD просто ссылаются на конкретный тег):

helm pull oci://registry.internal/charts/checkout-app --version 1.4.0
helm install checkout oci://registry.internal/charts/checkout-app --version 1.4.0 -f values-prod.yaml

Линт и тесты перед тем, как пушить в registry

Схема на values ловит опечатки в данных, но не ловит битый Go-шаблон или неверный YAML на выходе. Три уровня проверки, которые стоит гонять в CI до helm push:

Для чартов, которые лежат в одном монорепозитории и меняются пачками, отдельный инструмент chart-testing (ct) добавляет уровень выше: ct lint --config ct.yaml находит через git diff только изменившиеся чарты и линтит их, а ct install разворачивает каждый в эфемерном kind-кластере и проверяет, что helm install и helm test проходят — это то, что обычно гоняют в GitHub Actions перед мержем PR с правкой чарта, а не перед каждым helm push вручную.

Helm или Kustomize: не взаимозаменяемы

Периодически возникает вопрос, не проще ли выкинуть Helm и оставить только Kustomize — он идёт встроенным в kubectl и не требует шаблонизатора. Инструменты решают разные задачи:

HelmKustomize
МодельШаблонизация + пакет с версией и valuesПатчи поверх готовых валидных манифестов
ПараметризацияПолноценные values, схема, required/failНет шаблонного языка — только overlay/patch
Зависимости между пакетамиdependencies, alias, condition, library-чартыНет понятия зависимости — только resources:
РаспространениеOCI-реестр или classic-репозиторий, семверОбычно просто каталог в git-репозитории
Жизненный цикл релизаhelm upgrade/rollback, hooks (pre-install и т.д.)Нет истории релизов — kubectl просто apply
Где сильнееПереиспользуемый пакет для многих потребителейЛокальные различия между окружениями одной команды

Практическое правило: если чарт публикуется вовне — для другой команды, для клиентов, как часть продукта — версионирование и values-контракт Helm окупаются. Если весь смысл в том, чтобы у dev/staging/prod был один и тот же набор YAML с точечными отличиями (другой namespace, другой replica count), Kustomize-overlay короче и не требует поддерживать values.yaml с двумя десятками полей ради трёх реальных различий. Смешивать оба — рендерить helm template и затем накатывать поверх Kustomize-патч — рабочий паттерн для перехода, но как постоянная схема он умножает точки, где может сломаться рендер.

Как проверить, что чарт действительно готов

Перед тем как считать чарт «готовым к GitOps», стоит прогнать всю цепочку руками один раз. helm lint chart/ должен вернуть 0 chart(s) failed. helm template chart/ --values values-prod.yaml --validate (флаг --validate подключает валидацию через Kubernetes API, а не только рендер) не должен падать ни на отсутствующих обязательных полях, ни на schema-ошибках. После helm pushhelm pull oci://registry.internal/charts/checkout-app --version 1.4.0 --prov должен успешно скачать и чарт, и provenance-файл, а helm verify checkout-app-1.4.0.tgz — подтвердить подпись по тому же keyring, что использовался при helm package --sign. И последняя проверка, которую легко забыть: helm install --dry-run --debug с пустым values.yaml (без всех переопределений) должен упасть с понятным сообщением из required, а не с трейсом Go-шаблонизатора — это и есть проверка того, что контракт на values реально работает, а не существует только в документации.

Итог

Хороший Helm-чарт — это не чарт без единого {{ if }}, а чарт, где values.yaml защищён схемой и required/fail, зависимости объявлены декларативно через condition и alias вместо копипасты шаблонов, а поставка идёт через OCI-реестр с семвером и provenance рядом с образами приложения, а не через отдельный index.yaml, который кто-то забыл обновить. Ни один из этих шагов не требует переписывать существующий чарт с нуля — их можно добавлять по одному, начиная со схемы на values, которая ловит больше всего сюрпризов за наименьшую цену.


Поделиться:

Следующая статья
KRO: составные Kubernetes-API без написания собственного оператора