Представь, что у тебя 4 окружения - dev, stage, prod-eu, prod-ru - и для каждого почти одинаковый набор манифестов: Deployment, Service, Ingress (или Gateway), ConfigMap, HPA, ServiceAccount. Отличаются они на копейки: число реплик, имя образа, домен, лимиты, флаги. Если копировать YAML руками, ты получишь четыре расходящиеся версии правды, и через месяц никто не вспомнит, почему на prod-ru лимит памяти 512Mi, а на stage 256Mi. Это и есть боль, которую решает helm: один параметризованный пакет (chart) плюс набор значений на каждое окружение.
Helm - это менеджер пакетов для Kubernetes. По смыслу как apt или npm, только пакет здесь - это helm chart, архив с шаблонами манифестов. Установленный экземпляр чарта называется release. Команда helm install рендерит шаблоны в готовые манифесты и отправляет их в API-сервер; helm upgrade обновляет уже существующий релиз; helm rollback откатывает на прошлую ревизию. В этом уроке мы не повторяем "helm install nginx" из быстрого старта, а лезем под капот: как устроены Go-шаблоны, как работают хуки жизненного цикла, как подключать зависимости и почему чарты в 2026 живут в OCI-реестрах рядом с образами.
Важно сразу понять модель состояния. В Helm 3 нет Tiller (серверного компонента из Helm 2). Состояние релиза хранится в обычных Secret-ах в namespace релиза - по одному Secret на ревизию, с типом helm.sh/release.v1. Внутри лежит gzip+base64 снимок того, что было применено. Поэтому helm install kubernetes-кластеру не требует ничего, кроме твоих прав в namespace: вся логика на клиенте.

Анатомия чарта: из чего собран helm chart
Минимальный чарт - это директория с предсказуемой структурой. Сгенерировать скелет:
Код: Выделить всё
helm create webappКод: Выделить всё
webapp/
Chart.yaml # метаданные: имя, версия, apiVersion, зависимости
values.yaml # значения по умолчанию
values.schema.json # (опционально) JSON Schema для валидации values
charts/ # вендоренные субчарты (зависимости)
crds/ # CRD, ставятся ДО рендера шаблонов
templates/
_helpers.tpl # именованные шаблоны (helpers), не рендерятся сами по себе
deployment.yaml
service.yaml
NOTES.txt # текст, который печатается после install
.helmignore
Код: Выделить всё
apiVersion: v2
name: webapp
description: Демо веб-приложение
type: application
version: 1.4.2
appVersion: "2.7.0"
Go-шаблоны: .Values, .Release, range, if, with
Helm рендерит шаблоны движком text/template из Go, расширенным функциями из библиотеки Sprig. В шаблон передаётся объект верхнего уровня, к полям которого ты обращаешься через точку. Главные ветки:
- .Values - дерево из values.yaml, плюс то, что переопределили через --set и -f.
- .Release - .Release.Name, .Release.Namespace, .Release.Revision, .Release.IsInstall, .Release.IsUpgrade.
- .Chart - содержимое Chart.yaml (.Chart.Name, .Chart.Version, .Chart.AppVersion).
- .Capabilities - что умеет кластер (.Capabilities.KubeVersion, доступные API).
- .Files - доступ к произвольным файлам чарта (например, залить конфиг через .Files.Get).
Код: Выделить всё
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-webapp
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
spec:
replicas: {{ .Values.replicaCount }}
template:
spec:
containers:
- name: web
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
ports:
- containerPort: {{ .Values.service.port }}
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.env }}
env:
{{- range $k, $v := .Values.env }}
- name: {{ $k }}
value: {{ $v | quote }}
{{- end }}
{{- end }}
Функции делают шаблоны живыми. default подставляет запасное значение, если поле пустое. quote оборачивает в кавычки (спасает от того, что "true" или "12345" YAML примет за bool/число). toYaml сериализует произвольное дерево обратно в YAML - незаменимо для блоков resources, nodeSelector, affinity. required валит рендер с понятной ошибкой, если значение не задано:
Код: Выделить всё
image: {{ required "Укажи image.repository в values" .Values.image.repository }}
Named templates, include и tpl - переиспользование без копипасты
Чтобы не дублировать набор лейблов в каждом файле, их выносят в _helpers.tpl как именованный шаблон:
Код: Выделить всё
{{- define "webapp.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
{{- end -}}
Отдельная функция tpl рендерит строку как шаблон в рантайме. Это нужно, когда пользователь кладёт в values уже шаблонизированное значение, например ingress.host: "{{ .Release.Name }}.example.com". Без tpl это уедет в манифест дословно; с tpl - дорендерится:
Код: Выделить всё
host: {{ tpl .Values.ingress.host . | quote }}
Иногда нужно выполнить действие не "вместе" с релизом, а строго до или после. Классика - миграции БД перед раскаткой новой версии или прогрев кеша после. Для этого есть хуки: обычный манифест (чаще Job) с аннотацией helm.sh/hook. Helm вынимает его из основного потока и запускает в нужной фазе.
Код: Выделить всё
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-db-migrate
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./migrate", "up"]
Ключевой нюанс под капотом: Helm ждёт, пока хук-под дойдёт до Completed, и только потом продолжает. Если миграция упала - весь upgrade считается провалившимся, и основные манифесты не применяются. Это и хорошо (не выкатим код на несовместимую схему), и опасно (хук без таймаута может подвесить пайплайн - ставь activeDeadlineSeconds на Job).
Зависимости: dependencies, condition и tags
Реальное приложение редко одиноко: ему нужен PostgreSQL, Redis, очередь. Вместо того чтобы тащить их манифесты в свой чарт, объявляешь зависимости в Chart.yaml:
Код: Выделить всё
dependencies:
- name: postgresql
version: "15.5.x"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
- name: redis
version: "19.x.x"
repository: "oci://registry-1.docker.io/bitnamicharts"
condition: redis.enabled
tags:
- cache
OCI-репозитории: чарты живут рядом с образами
Старая модель Helm - это HTTP-репозиторий с index.yaml. С Helm 3.8 общедоступна (GA) поддержка OCI: чарт пакуется как OCI-артефакт и кладётся в тот же container registry, что и образы. Это убирает отдельную инфраструктуру для index.yaml и даёт единый ACL/скан на реестр. Поддерживают Docker Hub, GHCR, Yandex Container Registry, VK Cloud, Harbor.
Код: Выделить всё
helm package webapp # webapp-1.4.2.tgz
helm registry login cr.yandex -u <user>
helm push webapp-1.4.2.tgz oci://cr.yandex/<registry-id>
# установка прямо из реестра
helm install web oci://cr.yandex/<registry-id>/webapp --version 1.4.2
Проверяем до кластера: template, lint, diff
Никогда не применяй чарт вслепую. helm lint ловит структурные ошибки и плохие практики:
Код: Выделить всё
$ helm lint ./webapp
==> Linting ./webapp
[INFO] Chart.yaml: icon is recommended
[ERROR] templates/: parse error at (webapp/templates/deployment.yaml:14): unexpected "}" in operand
Error: 1 chart(s) linted, 1 chart(s) failed
Код: Выделить всё
$ helm template web ./webapp --set replicaCount=3 | head -n 8
---
# Source: webapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-webapp
spec:
replicas: 3
Много релизов: helmfile, Argo CD, Flux
Когда релизов десятки, императивный helm install по одному превращается в боль. Тут два пути. Первый - helmfile: декларативный helmfile.yaml описывает список релизов, их values и порядок, а helmfile apply приводит кластер к этому состоянию. Второй, более продакшен-зрелый, - GitOps: Argo CD или Flux держат желаемое состояние в git и сами синхронизируют его в кластер. В Argo CD ты заводишь Application, указываешь репозиторий с чартом и values - и контроллер сам делает рендер и применяет, давая drift-детекцию и автооткат. Helm здесь часто работает только как шаблонизатор (Argo рендерит через helm template и применяет сам), поэтому хуки helm в этом режиме могут вести себя иначе - проверяй документацию своего инструмента.
Типичные грабли и антипаттерны
- Отступы в YAML. Самая частая ошибка. Путаница indent/nindent ломает структуру, и kubectl ругается на "did not find expected key". Всегда прогоняй helm template и смотри глазами; toYaml без nindent почти всегда баг.
- --reuse-values на апгрейде. Флаг тащит значения прошлого релиза и накладывает поверх только новые --set. Если ты при этом ещё и поднял версию чарта с новыми ключами в values - получишь дыры: новые дефолты не подхватятся, потому что Helm возьмёт старый набор. Для предсказуемости держи values в git и ставь с -f values.yaml без --reuse-values, либо используй --reset-then-reuse-values осознанно.
- Числа и булевы без quote. Тег образа "12345" или строка "true" без кавычек уедут как число/bool - и манифест либо упадёт, либо тихо изменит смысл. quote по умолчанию для строковых значений из values.
- Незакрытый хук-Job. Хук без hook-delete-policy: before-hook-creation падает на втором апгрейде. Без activeDeadlineSeconds зависший хук вешает весь релиз.
- CRD из templates/. CRD кладут в crds/ (ставятся один раз до шаблонов и не удаляются при helm uninstall) либо ставят отдельно; рендер CRD как обычного шаблона создаёт гонки.
- Helm vs Kustomize. Это не "что лучше", а разные инструменты. Helm - шаблоны плюс пакетирование и версионирование с хуками и зависимостями; Kustomize - наложение патчей (overlays) на готовые манифесты без логики и без условий. Helm силён, когда чарт переиспользуют наружу и нужна параметризация; Kustomize - когда у тебя свои манифесты и хочется чистых diff-патчей без шаблонного синтаксиса. На практике их комбинируют: рендерят helm template и патчат Kustomize сверху.
- helm create lab и удали лишнее из templates/, оставив deployment.yaml и service.yaml.
- Вынеси набор лейблов в _helpers.tpl через define, подключи в обоих файлах через include ... . | nindent 4.
- Добавь в values.yaml блок env как словарь и отрендери его через range; проверь helm template lab ./lab.
- Сделай хук-Job pre-install с hook-weight "-1" и hook-delete-policy: before-hook-creation,hook-succeeded, который просто печатает echo.
- Объяви зависимость redis с condition: redis.enabled, выполни helm dependency update, посмотри Chart.lock.
- Запакуй helm package lab, залогинься в любой OCI-реестр и сделай helm push, затем helm install из oci://.
- Поломай отступ в шаблоне намеренно и поймай ошибку через helm lint и helm template - так ты запомнишь, как читать parse error.
- Чем include отличается от template и почему для пайпов через nindent годится только первый?
- Что произойдёт при helm upgrade, если pre-upgrade хук-Job завершится с ошибкой, и в каком состоянии останется релиз?
- Зачем нужны condition и tags в dependencies и как через них отключить субчарт PostgreSQL в пользу внешней Managed-БД?
- Чем установка чарта из oci:// отличается по синтаксису от установки из классического HTTP-репозитория?
Helm - это не "ещё один способ применить YAML", а полноценный пакетный менеджер с шаблонами, версионированием, хуками жизненного цикла и зависимостями. Понимая, как Go-шаблоны видят .Values и .Release, как include и tpl убирают копипасту, как хуки и веса управляют порядком, а condition/tags - составом релиза, ты перестаёшь бояться чужих чартов и начинаешь писать свои. Добавь сюда OCI-реестры вместо отдельной инфраструктуры и связку с GitOps - и получишь воспроизводимую, проверяемую (helm template/lint/diff) доставку в Kubernetes от dev до prod.