Открыть чужой Helm-чарт — почти всегда значит найти values.yaml без единого комментария, шаблон, который падает с невнятной ошибкой на пустом поле, и charts/ с .tgz-файлами, которые непонятно как обновлять. Помогает не «ещё один линтер», а несколько конкретных практик: схема на values, чарты с явными зависимостями и поставка через тот же OCI-реестр, что и образы. Ниже — как это выглядит на практике.
Содержание
Открыть содержание
- Почему classic-репозиторий чартов устарел
- Где чарт обычно ломается
- Зависимости: subcharts, alias и condition вместо копипасты
- Что нужно, чтобы собрать и опубликовать связку в OCI
- Линт и тесты перед тем, как пушить в registry
- Helm или Kustomize: не взаимозаменяемы
- Как проверить, что чарт действительно готов
- Итог
Почему 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 просто ускоряет доставку тех же проблем.
Где чарт обычно ломается
Анатомия чарта простая: 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 между репозиториями.
Что нужно, чтобы собрать и опубликовать связку в 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:
helm lint chart/— статическая проверка структуры чарта и очевидных ошибок в шаблонах;helm template chart/ -f values-prod.yaml | kubectl apply --dry-run=server -f -— рендерит чарт с реальными values и проверяет получившиеся манифесты через API-сервер, а не только синтаксически;helm test <release>— запускает поды с аннотациейhelm.sh/hook: testуже после установки релиза, то есть проверяет не шаблон, а живой сервис (типичный кейс — под, который делаетcurlдо/healthzприложения).
Для чартов, которые лежат в одном монорепозитории и меняются пачками, отдельный инструмент 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 и не требует шаблонизатора. Инструменты решают разные задачи:
| Helm | Kustomize | |
|---|---|---|
| Модель | Шаблонизация + пакет с версией и 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 push — helm 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, которая ловит больше всего сюрпризов за наименьшую цену.