Kubernetes для самых маленьких. Часть 27: Как работает Helm?
Все части серии «Kubernetes для самых маленьких»В части 26 мы разложили манифесты hello на базу и наложения и собирали окружения Kustomize, не меняя сами манифесты. Это хорошо работает для своих приложений, но у Kustomize нет понятия «параметры»: тот, кто ставит приложение, должен знать его манифесты, чтобы их патчить. Helm решает другую задачу — упаковать приложение так, чтобы его можно было поставить одной командой, задав десяток понятных параметров, обновить, откатить и удалить целиком. Драйверы csi-driver-nfs в части 23 и csi-driver-smb в части 24 мы ставили манифестами выпуска, а их Helm-чарты только упомянули одной фразой. Сегодня разберёмся: что такое чарт, как работает шаблонизатор, где Helm хранит состояние, что изменилось в Helm 4, и напишем полный чарт для hello.
Helm 4
Helm — проект CNCF (фонда Cloud Native Computing Foundation, под которым живут Kubernetes, containerd, etcd и другие), утилита командной строки без серверной части. Вторая версия ставила в кластер компонент Tiller с широкими правами; с третьей его нет, и helm работает от имени пользователя с его kubeconfig и его правами RBAC (часть 29) — ровно как kubectl.
В ноябре 2025 года вышел Helm 4, первая мажорная версия за шесть лет. На стенде — актуальная 4.3.1 (выпуск 15 октября 2026 года; ветка Helm 3 ещё получает исправления, последняя — 3.22.1). Чарты прежнего формата (apiVersion: v2) работают без изменений, а различия в основном в поведении команд:
| Что | Helm 3 | Helm 4 |
|---|---|---|
| Как изменения попадают в кластер | client-side: Helm сам вычисляет патч из старого манифеста, нового и живого объекта | server-side apply для новых релизов (флаг --server-side, по умолчанию auto: новые установки — SSA, обновления — тем методом, каким релиз ставился раньше) |
| Откат при неудаче | --atomic |
--rollback-on-failure (старое имя работает с предупреждением) |
| Пересоздание объектов | --force |
--force-replace (старое имя работает с предупреждением) |
| Ожидание готовности | --wait: свои проверки по типам объектов |
--wait принимает стратегию: watcher (по умолчанию при --wait, через kstatus), legacy (как в Helm 3), hookOnly (по умолчанию без флага — ждать только хуки) |
| Пост-обработка | --post-renderer — путь к любой программе |
--post-renderer — имя плагина типа post-renderer |
| Плагины | только внешние программы | плюс плагины на WebAssembly (переносимый байт-код, исполняемый в изолированной среде); установка проверяет подпись |
helm registry login |
принимал URL | принимает только имя хоста: на https://… — invalid registry |
kstatus — библиотека проекта Kubernetes SIGs, которая по полям status и metadata.generation любого объекта (включая пользовательские) определяет, дошёл ли он до желаемого состояния: Deployment — когда все реплики обновлены и доступны, Job — когда завершился, объект CRD — по условию Ready в status.conditions. Старая проверка Helm 3 знала только встроенные типы.
Релизы, поставленные Helm 3, Helm 4 читает и обновляет: формат хранения тот же. Но они остаются на client-side apply, пока при обновлении явно не передать --server-side=true: метод записан в самом релизе (поле apply_method, у новых установок — ssa), и auto берёт его оттуда. Старые имена флагов работают с предупреждением вида Flag --atomic has been deprecated, use --rollback-on-failure instead.
Чарт
Чарт — каталог (или архив .tgz этого каталога) определённой структуры:
hello/
├── Chart.yaml ← имя, версия, зависимости
├── Chart.lock ← точные версии зависимостей (создаёт helm dependency update)
├── values.yaml ← параметры по умолчанию
├── values.schema.json ← JSON Schema для проверки параметров (необязательно)
├── .helmignore ← что не класть в архив
├── charts/ ← скачанные зависимости-подчарты
├── crds/ ← определения CRD (особый случай, ниже)
└── templates/
├── _helpers.tpl ← именованные шаблоны, сами объектов не создают
├── NOTES.txt ← текст, который печатается после установки
├── deployment.yaml
├── service.yaml
├── ...
└── tests/
└── test-connection.yaml
Chart.yaml описывает чарт:
apiVersion: v2 # формат чарта Helm 3 и 4
name: hello
description: Учебное HTTP-приложение
type: application # или library — только именованные шаблоны для других чартов
version: 0.1.0 # версия ЧАРТА, SemVer (MAJOR.MINOR.PATCH); меняется при любой правке чарта
appVersion: "1.0" # версия ПРИЛОЖЕНИЯ внутри, информационно
kubeVersion: ">=1.33.0-0" # на каких кластерах чарт согласен ставиться
dependencies:
- name: redis
version: "~1.4.0"
repository: oci://ghcr.io/example/charts
condition: redis.enabled
Две версии — частый источник путаницы. version — версия упаковки: поменяли шаблон или значение по умолчанию — подняли version, иначе реестр не примет новый архив под тем же номером, а у пользователей окажется два разных чарта 0.1.0. appVersion — версия программы внутри, по соглашению используется как тег образа по умолчанию. kubeVersion проверяется до рендеринга: на кластере старше требуемого Helm откажется работать сразу — Error: chart requires kubeVersion: >=1.33.0-0 which is incompatible with Kubernetes v1.32.0.
Зависимости — другие чарты, которые ставятся вместе с этим как его часть (подчарты). Пример выше — гипотетический чарт Redis (хранилища «ключ-значение» в памяти) из нашего реестра, включаемый параметром redis.enabled. helm dependency update скачивает их в charts/ и записывает точные версии и дайджесты в Chart.lock; helm dependency build восстанавливает charts/ строго по Chart.lock. Параметры подчарта задаются в values.yaml родителя под ключом с его именем (redis:), а ключ global: виден всем подчартам.
Начать новый чарт проще всего с заготовки helm create hello: она создаёт рабочий чарт для nginx со всеми типичными шаблонами, который потом правят под себя. Мы напишем чарт с нуля, чтобы каждая строка была понятна.
Шаблоны
Файлы в templates/ — шаблоны языка Go text/template (стандартный шаблонизатор Go: подстановки в {{ }}, условия, циклы, переменные) с библиотекой функций Sprig (около сотни функций для строк, списков, словарей, дат, хэшей) и несколькими функциями самого Helm. Helm прогоняет каждый файл через шаблонизатор, склеивает результат, разбирает как YAML и отправляет в API.
В шаблоне доступны встроенные объекты:
| Объект | Что содержит |
|---|---|
.Values |
итоговые параметры (после слияния всех источников) |
.Release.Name, .Release.Namespace |
имя релиза и неймспейс установки |
.Release.IsInstall, .Release.IsUpgrade, .Release.Revision |
что сейчас происходит и номер ревизии |
.Chart.Name, .Chart.Version, .Chart.AppVersion |
поля Chart.yaml |
.Capabilities.KubeVersion, .Capabilities.APIVersions |
версия кластера и доступные API (для helm template — заданные флагами) |
.Files |
доступ к прочим файлам чарта (.Files.Get "config/app.conf") |
.Template.Name, .Template.BasePath |
путь текущего шаблона |
Синтаксис, который встречается постоянно:
{{ .Values.replicaCount }} подстановка
{{ .Values.image.tag | default .Chart.AppVersion }} конвейер: значение по умолчанию
{{ .Values.host | quote }} в кавычках
{{- if .Values.ingress.enabled }} ... {{- end }} условие; "-" съедает пробелы и перевод строки
{{- range .Values.extraEnv }} ... {{- end }} цикл
{{- with .Values.nodeSelector }} ... {{- end }} сменить "." на значение, если оно не пустое
{{ include "hello.labels" . | nindent 4 }} именованный шаблон с отступом 4 и новой строкой
{{ toYaml .Values.resources | nindent 12 }} вставить кусок values как YAML
{{ required "host обязателен" .Values.host }} остановить рендеринг с ошибкой, если пусто
Почти все ошибки в чартах — ошибки отступов: шаблонизатор ничего не знает о YAML и вставляет текст как есть. Поэтому вставку блоков всегда пишут через nindent N (перевод строки плюс N пробелов), а результат проверяют helm template.
include вызывает именованный шаблон — блок {{ define "имя" }}…{{ end }}, по соглашению лежащий в _helpers.tpl (файлы, начинающиеся с _, Helm не выводит как манифесты). В отличие от встроенного в Go template, результат include можно передать дальше по конвейеру, например в nindent, поэтому в чартах используют только его.
Свой чарт для hello
Чарт целиком. Сначала параметры:
# hello/values.yaml
replicaCount: 2
image:
repository: ghcr.io/example/hello
tag: "" # пусто — берётся appVersion из Chart.yaml
digest: "" # если задан, важнее тега
pullPolicy: IfNotPresent
imagePullSecrets: []
config:
LOG_LEVEL: info
GREETING: hello
service:
port: 80
ingress:
enabled: true
className: traefik
host: hello.apps.example.com
clusterIssuer: letsencrypt-prod
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
memory: 64Mi
podDisruptionBudget:
enabled: true
minAvailable: 1
nodeSelector: {}
tolerations: []
Именованные шаблоны: полное имя объектов и наборы меток. Метки app.kubernetes.io/* — рекомендованные Kubernetes; селектор строится только из неизменных (name и instance), чтобы обновление чарта никогда не меняло неизменяемый селектор Deployment.
{{/* hello/templates/_helpers.tpl */}}
{{- define "hello.fullname" -}}
{{- if contains .Chart.Name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- define "hello.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{- define "hello.labels" -}}
{{ include "hello.selectorLabels" . }}
app.kubernetes.io/version: {{ .Values.image.tag | default .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
{{- end -}}
{{- define "hello.image" -}}
{{- if .Values.image.digest -}}
{{ .Values.image.repository }}@{{ .Values.image.digest }}
{{- else -}}
{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}
{{- end -}}
{{- end -}}
trunc 63 — потому что имя объекта, попадающее в метки и DNS, ограничено 63 символами. Проверка contains избавляет от имён вида hello-hello, когда релиз назван так же, как чарт.
# hello/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "hello.fullname" . }}
labels:
{{- include "hello.labels" . | nindent 4 }}
data:
{{- range $key, $value := .Values.config }}
{{ $key }}: {{ $value | quote }}
{{- end }}
# hello/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "hello.fullname" . }}
labels:
{{- include "hello.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "hello.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "hello.labels" . | nindent 8 }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: hello
image: {{ include "hello.image" . | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 8080
envFrom:
- configMapRef:
name: {{ include "hello.fullname" . }}
readinessProbe:
httpGet:
path: /healthz
port: http
livenessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 20
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
Аннотация checksum/config — тот же приём, что хэш в имени ConfigMap у Kustomize, только иначе: в шаблон пода кладётся SHA-256 от отрендеренного ConfigMap. Поменялся параметр в config — поменялась аннотация — поменялся шаблон пода — Deployment выкатил новые поды. Поля securityContext подробно разберём в части 30, automountServiceAccountToken: false — в части 29: приложению не нужен доступ к API, и токен в под не монтируется.
# hello/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "hello.fullname" . }}
labels:
{{- include "hello.labels" . | nindent 4 }}
spec:
selector:
{{- include "hello.selectorLabels" . | nindent 4 }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
# hello/templates/ingress.yaml
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "hello.fullname" . }}
labels:
{{- include "hello.labels" . | nindent 4 }}
annotations:
cert-manager.io/cluster-issuer: {{ .Values.ingress.clusterIssuer | quote }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ required "ingress.host обязателен" .Values.ingress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "hello.fullname" . }}
port:
name: http
tls:
- hosts:
- {{ .Values.ingress.host | quote }}
secretName: {{ include "hello.fullname" . }}-tls
{{- end }}
# hello/templates/pdb.yaml
{{- if and .Values.podDisruptionBudget.enabled (gt (int .Values.replicaCount) 1) }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "hello.fullname" . }}
labels:
{{- include "hello.labels" . | nindent 4 }}
spec:
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
selector:
matchLabels:
{{- include "hello.selectorLabels" . | nindent 6 }}
{{- end }}
PodDisruptionBudget создаётся только при двух репликах и больше: с одной репликой и minAvailable: 1 он запретил бы kubectl drain узла навсегда (часть 13).
# hello/templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: {{ include "hello.fullname" . }}-test
labels:
{{- include "hello.labels" . | nindent 4 }}
annotations:
helm.sh/hook: test
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
restartPolicy: Never
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: wget
image: busybox:1.37
command: ["wget", "-qO-", "http://{{ include "hello.fullname" . }}:{{ .Values.service.port }}/healthz"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
{{/* hello/templates/NOTES.txt */}}
hello {{ .Chart.AppVersion }} установлен как релиз {{ .Release.Name }} в {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }}
Адрес: https://{{ .Values.ingress.host }}/
{{- else }}
Проверка: kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "hello.fullname" . }} 8080:{{ .Values.service.port }}
{{- end }}
И схема параметров — она проверяется при install, upgrade, template и lint, и опечатка в -f или --set даёт понятную ошибку вместо молча проигнорированного ключа:
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["image", "ingress"],
"properties": {
"replicaCount": { "type": "integer", "minimum": 1 },
"image": {
"type": "object",
"required": ["repository"],
"properties": {
"repository": { "type": "string" },
"tag": { "type": "string" },
"digest": { "type": "string", "pattern": "^(sha256:[a-f0-9]{64})?$" }
}
},
"ingress": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"host": { "type": "string" }
}
}
}
}
Откуда берутся значения
Итоговые .Values собираются из нескольких источников, каждый следующий перекрывает предыдущий:
values.yaml чарта (и подчартов)
│
▼
-f / --values файл1, файл2 ... ← по порядку в командной строке
│
▼
--set, --set-string, --set-json, --set-file ← точечные правки
│
▼
.Values в шаблонах
Словари сливаются по ключам, списки заменяются целиком. На практике значения окружения держат в файлах — values-dev.yaml, values-prod.yaml в Git рядом с описанием развёртывания, — а --set оставляют для разовых правок вроде --set image.tag=1.1 из CI. Причина простая: --set не оставляет следа, кроме самого релиза, и через месяц никто не вспомнит, почему в prod три реплики.
У --set своя грамматика: точка — вложенность (ingress.host=…), запятая — разделитель пар, индекс в квадратных скобках — элемент списка (tolerations[0].key=…). Значения приводятся к типам: true и false становятся логическими, целые числа — числами, а остальное остаётся строкой. Поэтому --set image.tag=1.10 даст строку "1.10", а --set image.tag=2 — число 2, и наша схема (о ней ниже) остановит установку:
$ helm template hello ./hello -f values-prod.yaml --set image.tag=2
Error: values don't meet the specifications of the schema(s) in the following chart(s):
hello:
- at '/image/tag': got number, want string
Без схемы число молча ушло бы в шаблон. Поэтому теги и всё, что должно остаться строкой, передают через --set-string.
При helm upgrade по умолчанию берутся значения чарта плюс то, что передано в этой команде; значения прошлого релиза не переносятся. Флаг --reuse-values берёт прошлые значения пользователя вместо значений чарта и тем самым игнорирует новые умолчания из обновлённого чарта, а --reset-then-reuse-values берёт значения нового чарта и накладывает поверх прошлые пользовательские. Надёжнее всего не полагаться ни на то, ни на другое и всегда передавать полный набор файлов.
Релиз и где Helm хранит состояние
Релиз — установленный экземпляр чарта с именем, уникальным в неймспейсе. Один чарт можно поставить много раз под разными именами. Каждое install, upgrade и rollback создаёт новую ревизию релиза.
Helm не держит своей базы данных и сервера: состояние лежит в самом кластере, по умолчанию — в Secret в неймспейсе релиза, по одному на ревизию. Имя — sh.helm.release.v1.<релиз>.v<ревизия>, тип — helm.sh/release.v1. Внутри поле release: JSON с описанием релиза, сжатый gzip и закодированный в base64 (а API добавляет ко всем данным Secret свой base64). В JSON лежит всё, что нужно для отката без исходного чарта: шаблоны, values.yaml и схема чарта (chart), пользовательские значения (config), отрендеренный манифест (manifest), хуки (hooks), статус и время (info) и способ применения (apply_method).
helm upgrade hello ./hello -f values-prod.yaml
│
├─ 1. прочитать текущую ревизию из Secret sh.helm.release.v1.hello.v3
├─ 2. собрать values, отрендерить шаблоны
├─ 3. Secret ...v4 со статусом pending-upgrade
├─ 4. хуки pre-upgrade
├─ 5. применить объекты (SSA: PATCH application/apply-patch+yaml,
│ fieldManager=helm; объекты, исчезнувшие из манифеста, — удалить)
├─ 6. --wait: ждать готовности (kstatus)
├─ 7. хуки post-upgrade
└─ 8. v4 → deployed, v3 → superseded
Последствия этого устройства:
- Права на релиз — это права на Secret в неймспейсе. Кто может читать Secret, видит все значения релиза, включая пароли, если их передавали через values.
- Хранится не больше 10 ревизий на релиз (флаг
--history-max), старшие удаляются. - Если
helmпрервали на середине (закрыли ноутбук, упала сеть), релиз остаётся в состоянииpending-installилиpending-upgrade, и следующая операция откажется работать с ошибкойUPGRADE FAILED: another operation (install/upgrade/rollback) is in progress. Лечитсяhelm rollbackна последнюю удачную ревизию. На стендеhelm upgrade --waitубит через 5 секунд — в истории осталась ревизияpending-upgradeс описаниемPreparing upgrade, следующийupgradeполучил эту ошибку, аhelm rollbackпрошёл и записал новую ревизиюdeployed. - Удалив Secret релиза, вы «забудете» релиз, но объекты в кластере останутся. Это иногда используют для передачи объектов другому релизу; для этого с Helm 3.17 есть флаг
--take-ownership, который забирает существующие объекты без удаления.
Каждому объекту Helm ставит метку app.kubernetes.io/managed-by: Helm и аннотации meta.helm.sh/release-name и meta.helm.sh/release-namespace. По ним он отказывается трогать объект с тем же именем, принадлежащий другому релизу или созданный руками.
Установка, обновление, откат, удаление
helm install hello ./hello -n demo -f values-prod.yaml установить
helm upgrade hello ./hello -n demo -f values-prod.yaml обновить
helm upgrade --install hello ./hello -n demo -f values-prod.yaml установить или обновить (для CI)
helm rollback hello 2 -n demo вернуться к ревизии 2 (создаст новую)
helm uninstall hello -n demo удалить объекты и историю
Полезные флаги:
--wait— не возвращать управление, пока объекты не станут готовы, не дольше--timeout(по умолчанию 5 минут). Без него Helm считает установку успешной, как только API принял объекты, даже если поды не стартуют.--rollback-on-failure(бывший--atomic) — при неудаче откатить: наinstall— удалить установленное, наupgrade— вернуть предыдущую ревизию. Включает--waitсам.--cleanup-on-fail— при неудачномupgradeудалить объекты, созданные этим обновлением.--create-namespace— создать неймспейс, если его нет. Удобно, но неймспейс тогда не принадлежит релизу и не получит ваших меток Pod Security (часть 30) — лучше создавать его отдельно.--version— версия чарта из репозитория; без неё берётся последняя, что для рабочего окружения плохая идея.--dry-run=server— всё, кроме записи: шаблоны рендерятся, объекты проходят валидацию и admission на API-сервере.
Откат в Helm — не «отмена»: helm rollback hello 2 берёт манифест ревизии 2, применяет его и записывает как новую ревизию. История линейна и показывает, что произошло. helm uninstall удаляет объекты релиза (кроме помеченных аннотацией helm.sh/resource-policy: keep, ресурсов хуков и CRD) и Secret-ы истории; --keep-history оставит историю, и релиз потом можно будет откатить.
Про server-side apply в Helm 4 важно одно следствие. Helm записывается владельцем (fieldManager) полей, которые есть в чарте. Если кто-то поменял такое поле руками через kubectl edit или kubectl scale, при следующем helm upgrade API-сервер вернёт конфликт владельцев полей, и Helm остановится:
$ kubectl -n demo scale deploy hello --replicas=4
deployment.apps/hello scaled
$ helm upgrade hello ./hello -n demo -f values-prod.yaml --set-string image.tag=1.1
level=WARN msg="upgrade failed" name=hello error="conflict occurred while applying object demo/hello apps/v1, Kind=Deployment: Apply failed with 1 conflict: conflict with \"kubectl\" with subresource \"scale\" using apps/v1: .spec.replicas"
Error: UPGRADE FAILED: conflict occurred while applying object demo/hello apps/v1, Kind=Deployment: Apply failed with 1 conflict: conflict with "kubectl" with subresource "scale" using apps/v1: .spec.replicas
В истории релиза при этом появляется ревизия со статусом failed. --force-conflicts забирает поля себе (повтор с ним вернул replicas: 3 из values); правильнее разобраться, кто их меняет. Поля, которых в чарте нет, — например replicas, если им управляет автомасштабирование, — Helm больше не трогает вовсе, и это удобнее трёхстороннего слияния Helm 3, где такие поля приходилось специально убирать из шаблонов.
Проверка до применения: lint, template, diff
helm lint ./hello проверяет структуру чарта, синтаксис шаблонов и схему значений. helm template рендерит чарт локально и печатает манифесты, не обращаясь к кластеру:
$ helm template hello ./hello -n demo -f values-prod.yaml --kube-version 1.36.0
Из-за этого функция lookup (чтение живого объекта из кластера) в helm template возвращает пустоту, а .Capabilities заполняется умолчаниями или флагами --kube-version и --api-versions. Вывод helm template можно отдать kubectl apply -f - или положить в Git, и это вполне рабочая схема для тех, кто хочет только шаблонизатор без хранения релизов.
Для «что изменится в кластере» нужен плагин helm-diff: он рендерит новую версию и сравнивает её с манифестом текущего релиза (или, с --three-way-merge, с живыми объектами, чтобы увидеть и ручные правки). В Helm 4 плагины ставятся с проверкой подписи, и автор helm-diff публикует подписанные архивы (рядом с каждым — файл .prov) и свой открытый ключ pubkey.asc. Проверка идёт по связке ключей GnuPG (свободной реализации стандарта подписи и шифрования OpenPGP), и без неё установка не пройдёт:
$ helm plugin install https://github.com/databus23/helm-diff/releases/latest/download/helm-diff-linux-amd64.tgz
Verifying plugin signature...
Error: plugin verification failed: open /home/user/.gnupg/pubring.gpg: no such file or directory
Ключ автора скачивается со страницы того же выпуска и переводится из текстового вида в двоичную связку (gpg --dearmor), которую Helm умеет читать; gpg --show-keys перед этим покажет, чей это ключ:
$ curl -sSLO https://github.com/databus23/helm-diff/releases/latest/download/pubkey.asc
$ gpg --show-keys pubkey.asc | head -3
pub rsa4096 2026-06-15 [SC] [expires: 2028-06-14]
C5645EF47482257A1F806D2BEA17A2A206AFF8CD
uid Fabian Ruff (helm-diff release key) <fabian@progra.de>
$ gpg --dearmor < pubkey.asc > helm-diff.gpg
$ helm plugin install https://github.com/databus23/helm-diff/releases/latest/download/helm-diff-linux-amd64.tgz --keyring ./helm-diff.gpg
Verifying plugin signature...
Using Key With Fingerprint: C5645EF47482257A1F806D2BEA17A2A206AFF8CD
Plugin Hash Verified: sha256:504e99410611daa70702362e1b10dc74929df8a912cb3f6b5d21d1ee9d0cb7d9
Installed plugin: diff
$ helm plugin list
NAME VERSION TYPE APIVERSION PROVENANCE SOURCE
diff 3.15.15 cli/v1 legacy unknown unknown
--verify=false пропускает проверку, но тогда в Helm попадает непроверенный исполняемый файл. Сравниваем релиз на 1.0 с обновлением до 1.1 (вывод сокращён до Deployment):
$ helm diff upgrade hello ./hello -n demo -f values-prod.yaml --set-string image.tag=1.1
…
demo, hello, Deployment (apps) has changed:
# Source: hello/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
labels:
app.kubernetes.io/name: hello
app.kubernetes.io/instance: hello
- app.kubernetes.io/version: "1.0"
+ app.kubernetes.io/version: "1.1"
app.kubernetes.io/managed-by: Helm
helm.sh/chart: hello-0.1.0
…
annotations:
- checksum/config: ab2f9bbf23579c4ac7887de51dac11673fb2c6513f1355444e4e379dd4c8dc7d
+ checksum/config: 3d144dda669f1e95d4553b25f616f923469148ecc258d833e75c808fe995f7d6
…
- image: "ghcr.io/example/hello:1.0"
+ image: "ghcr.io/example/hello:1.1"
…
Метка app.kubernetes.io/version меняется у всех пяти объектов чарта, и вместе с ней — checksum/config: хэш считается от всего отрендеренного ConfigMap, включая метки, а не только от data. Здесь это не страшно, поды и так перезапускаются из-за нового образа, но если в метки ConfigMap попадает что-то часто меняющееся, поды будут перезапускаться без изменения конфигурации. Перед любым helm upgrade рабочего релиза helm diff upgrade с теми же аргументами — хорошая привычка.
Хуки и тесты
Хук — объект из templates/ с аннотацией helm.sh/hook. Helm не применяет его вместе с остальными, а создаёт в определённый момент жизненного цикла релиза и ждёт его завершения (для Job — успешного окончания, для пода — фазы Succeeded):
| Хук | Когда |
|---|---|
pre-install, post-install |
до и после создания объектов при установке |
pre-upgrade, post-upgrade |
до и после обновления |
pre-rollback, post-rollback |
до и после отката |
pre-delete, post-delete |
до и после удаления |
test |
только по команде helm test |
Типичный пример — миграция схемы базы данных Job-ом в pre-upgrade: если она упала, обновление не начнётся. Порядок нескольких хуков задаёт helm.sh/hook-weight (строка с числом, меньшие первыми), а судьбу объекта после выполнения — helm.sh/hook-delete-policy: before-hook-creation (удалить прошлый экземпляр перед созданием нового, это поведение по умолчанию), hook-succeeded, hook-failed.
Две особенности хуков, о которых узнают на практике. Они не входят в релиз: helm uninstall их не удаляет, а helm diff и helm get manifest их не показывают. И Helm ждёт их по --timeout: хук-Job, застрявший в Pending из-за нехватки ресурсов, держит весь upgrade.
Тесты — хуки test: поды, которые проверяют, что установленное работает. Наш test-connection.yaml делает запрос к /healthz через Service. Запускаются отдельно, после установки:
$ helm test hello -n demo
NAME: hello
LAST DEPLOYED: Sat Oct 31 04:49:07 2026
NAMESPACE: demo
STATUS: deployed
REVISION: 4
DESCRIPTION: Rollback to 2
TEST SUITE: hello-test
Last Started: Sat Oct 31 04:49:15 2026
Last Completed: Sat Oct 31 04:49:23 2026
Phase: Succeeded
Под теста после успеха удалён (hook-succeeded), поэтому kubectl get pod hello-test его уже не найдёт; при неудаче он останется, и его журнал можно прочитать.
Чарты в OCI-реестре
Исторически чарты раздавали репозиториями Helm — HTTP-сервером с файлом index.yaml (список всех чартов и версий) и архивами рядом. Его добавляют командой helm repo add, и так распространяется большинство публичных чартов. Современный способ — хранить чарт в обычном OCI-реестре, том же, что для образов (часть 5): чарт становится OCI-артефактом с манифестом, конфигурацией типа application/vnd.cncf.helm.config.v1+json и одним слоем — архивом чарта. Никакого index.yaml, права и аутентификация — те же, что у образов, версии — теги.
$ helm package ./hello
Successfully packaged chart and saved it to: /home/user/hello-0.1.0.tgz
$ helm registry login ghcr.io -u example
Password:
Login Succeeded
$ helm push hello-0.1.0.tgz oci://ghcr.io/example/charts
Pushed: ghcr.io/example/charts/hello:0.1.0
Digest: sha256:afdb173bddd50518e7fccf77906afcd549376dc53ab384c446f7bd6844babb5e
$ helm install hello oci://ghcr.io/example/charts/hello --version 0.1.0 -n demo -f values-prod.yaml
Pulled: ghcr.io/example/charts/hello:0.1.0
Digest: sha256:afdb173bddd50518e7fccf77906afcd549376dc53ab384c446f7bd6844babb5e
NAME: hello
…
На стенде чарт загружался не в ghcr.io, а во временный реестр CNCF Distribution (реестр образов с открытым кодом, бывший Docker Registry; образ registry:3), запущенный подом в кластере и доступный через kubectl port-forward; к командам там добавлялся флаг --plain-http, потому что этот реестр работал без TLS. Манифест загруженного артефакта, прочитанный из реестра напрямую:
$ curl -s -H 'Accept: application/vnd.oci.image.manifest.v1+json' \
localhost:5000/v2/charts/hello/manifests/0.1.0 | jq '{config: .config.mediaType, layers: [.layers[].mediaType], annotations}'
{
"config": "application/vnd.cncf.helm.config.v1+json",
"layers": [
"application/vnd.cncf.helm.chart.content.v1.tar+gzip"
],
"annotations": {
"org.opencontainers.image.created": "2026-10-31T04:52:42Z",
"org.opencontainers.image.description": "Учебное HTTP-приложение",
"org.opencontainers.image.title": "hello",
"org.opencontainers.image.version": "0.1.0"
}
}
helm push берёт имя и версию из архива, путь в адресе — «каталог» в реестре. Вложенные пути вида ghcr.io/<владелец>/charts/<чарт> GitHub принимает: так, например, публикует свои чарты prometheus-community (helm show chart oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack скачивает его без входа). helm registry login в Helm 4 принимает только имя хоста: на https://ghcr.io или путь в реестре он ответит invalid registry. Учётные данные helm registry login сохраняет в ~/.config/helm/registry/config.json. Пароль здесь — токен GitHub с правом write:packages, тот же, что для загрузки образов в части 5; для CI заводят отдельный токен и хранят его в секретах конвейера, а не используют личный. Целостность и происхождение чарта можно дополнительно подтверждать подписью: helm package --sign создаёт рядом файл .prov, а helm install --verify его проверяет; для OCI-артефактов распространена и подпись Cosign — утилитой проекта Sigstore, которая хранит подпись в том же реестре рядом с артефактом.
Post-renderer: Kustomize поверх Helm
Бывает, что чужой чарт почти подходит, но в нём нет параметра для нужного поля: скажем, метки или toleration для одного DaemonSet или securityContext контейнера. Форкать чарт — значит потом сопровождать форк. Post-renderer — программа, которой Helm отдаёт отрендеренные манифесты на стандартный ввод и забирает результат со стандартного вывода, прежде чем применить; результат же и записывается в релиз. Естественный кандидат на эту роль — Kustomize.
В Helm 3 --post-renderer принимал путь к любой исполняемой программе. В Helm 4 это плагин типа postrenderer/v1. Плагин — каталог с plugin.yaml и скриптом:
# kustomize-pr/plugin.yaml
apiVersion: v1
type: postrenderer/v1
name: kustomize-pr
version: 0.1.0
runtime: subprocess
runtimeConfig:
platformCommand:
- command: ${HELM_PLUGIN_DIR}/render.sh
#!/bin/sh
# kustomize-pr/render.sh: $1 — каталог с kustomization.yaml и патчами
set -eu
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
cp -r "$1"/. "$work"/
cat > "$work/helm-output.yaml"
kubectl kustomize "$work"
Каталог с патчами живёт рядом с values окружения, а его kustomization.yaml ссылается на файл, который скрипт создаёт из вывода Helm:
# deploy/post-render/kustomization.yaml
resources:
- helm-output.yaml
patches:
- target:
kind: DaemonSet
name: csi-nfs-node
patch: |-
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: csi-nfs-node
spec:
template:
metadata:
labels:
team: storage
Здесь патч добавляет подам DaemonSet метку, для которой в чарте нет параметра; так же добавляют toleration, аннотацию, переменную окружения или ограничение ресурсов.
$ helm plugin install ./kustomize-pr
Installing plugin from local directory (development mode)
Installed plugin: kustomize-pr
$ helm plugin list
NAME VERSION TYPE APIVERSION PROVENANCE SOURCE
diff 3.15.15 cli/v1 legacy unknown unknown
kustomize-pr 0.1.0 postrenderer/v1 v1 local dev unknown
$ helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
"csi-driver-nfs" has been added to your repositories
$ helm upgrade csi-driver-nfs csi-driver-nfs/csi-driver-nfs -n kube-system --version 4.13.6 \
--post-renderer kustomize-pr --post-renderer-args "$PWD/deploy/post-render"
Тот же post-renderer работает и с helm template, так что результат можно посмотреть, ничего не устанавливая:
$ helm template csi-driver-nfs csi-driver-nfs/csi-driver-nfs -n kube-system --version 4.13.6 \
--post-renderer kustomize-pr --post-renderer-args "$PWD/deploy/post-render" \
| sed -n '/^kind: DaemonSet/,/containers:/p' | sed -n '/template:/,$p'
template:
metadata:
labels:
app: csi-nfs-node
app.kubernetes.io/instance: csi-driver-nfs
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: csi-driver-nfs
app.kubernetes.io/version: 4.13.6
helm.sh/chart: csi-driver-nfs-4.13.6
team: storage
spec:
containers:
Плагин из локального каталога ставится в режиме разработки без проверки подписи (PROVENANCE local dev); для команды его лучше упаковать и подписать, как любой другой. Два правила при работе с post-renderer. Все, кто обновляет этот релиз, должны вызывать его одинаково: результат post-renderer записывается в манифест релиза, и следующий upgrade без него молча уберёт патч — на стенде метка, добавленная так подам hello, исчезла с них после обычного helm upgrade. И post-renderer видит все манифесты, включая Secret с паролями, поэтому в этой роли используют только то, чему доверяют.
Подводные камни
CRD. Определения пользовательских ресурсов (часть 28) в каталоге crds/ Helm применяет только при первой установке, до шаблонов, без шаблонизации, и никогда не обновляет и не удаляет. Это сознательное решение: удаление CRD удаляет все объекты этого типа во всём кластере, и случайный helm uninstall не должен уничтожать, скажем, все сертификаты cert-manager. Следствие — при обновлении чарта оператора новые поля CRD сами не появятся. Поэтому крупные проекты либо кладут CRD в templates/ с отдельным параметром и аннотацией helm.sh/resource-policy: keep, либо просят применить CRD отдельно kubectl apply --server-side -f crds.yaml перед helm upgrade. Перед обновлением любого оператора стоит прочитать, какой путь выбран в его чарте.
Неизменяемые поля. Если новая версия чарта меняет spec.selector Deployment, volumeClaimTemplates StatefulSet, spec.clusterIP Service или поля Job, helm upgrade упадёт с field is immutable. Выхода три: не допускать таких изменений в своих чартах (селектор — только из неизменных меток, как в _helpers.tpl выше); для чужих — удалить объект вручную перед upgrade (для StatefulSet — kubectl delete sts --cascade=orphan, чтобы поды и тома остались); или --force-replace, который удаляет и создаёт объекты заново — с простоем.
Секреты в values. Пароль в values-prod.yaml в Git — тот же открытый секрет, что и в манифесте; к тому же он оседает в Secret истории релиза. Варианты: плагин helm-secrets расшифровывает файл значений, зашифрованный SOPS (утилитой, которая шифрует значения в YAML ключом age, PGP или облачного KMS, оставляя ключи читаемыми), прямо перед вызовом Helm (helm secrets upgrade … -f secrets.yaml); или чарт вообще не принимает пароль, а ссылается на Secret, который создаёт External Secrets Operator — контроллер, копирующий значения из внешнего хранилища секретов в обычные Secret. Второй вариант лучше: пароль не проходит через Helm и не попадает в историю релиза.
latest и плавающие версии. helm install без --version и зависимости с диапазоном ~1.4.0 без Chart.lock дают разный результат в разные дни. В рабочих окружениях версия чарта фиксируется, Chart.lock коммитится.
Чужой чарт — чужой код. Шаблоны могут создать что угодно, включая ClusterRoleBinding на cluster-admin. Перед установкой незнакомого чарта стоит прочитать его вывод helm template хотя бы на предмет RBAC и привилегированных подов.
Смотрим руками
Значения рабочего окружения — в отдельном файле рядом с описанием развёртывания:
# values-prod.yaml
replicaCount: 3
ingress:
host: hello.apps.example.com
$ helm version
version.BuildInfo{Version:"v4.3.1", GitCommit:"c0d08d0d6d9424d4a14febb5e8a3e51b0dd00dff", GitTreeState:"clean", GoVersion:"go1.27.2", KubeClientVersion:"v1.37"}
$ helm lint ./hello -f values-prod.yaml
==> Linting ./hello
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed
Схема ловит ошибку в значениях ещё до обращения к кластеру:
$ helm template hello ./hello --set replicaCount=0
Error: values don't meet the specifications of the schema(s) in the following chart(s):
hello:
- at '/replicaCount': minimum: got 0, want 1
Установка с ожиданием и откатом при неудаче. Образ hello частный, поэтому в неймспейсе demo, как в части 26, заранее лежит Secret ghcr-pull, привязанный к ServiceAccount default (часть 5):
$ helm install hello ./hello -n demo -f values-prod.yaml --wait --rollback-on-failure
NAME: hello
LAST DEPLOYED: Sat Oct 31 04:46:22 2026
NAMESPACE: demo
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
NOTES:
hello 1.0 установлен как релиз hello в demo.
Адрес: https://hello.apps.example.com/
$ helm list -n demo
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
hello demo 1 2026-10-31 04:46:22.689654138 +0000 UTC deployed hello-0.1.0 1.0
Что лежит в кластере:
$ kubectl -n demo get secret -l owner=helm
NAME TYPE DATA AGE
sh.helm.release.v1.hello.v1 helm.sh/release.v1 1 17s
$ kubectl -n demo get secret sh.helm.release.v1.hello.v1 --show-labels
NAME TYPE DATA AGE LABELS
sh.helm.release.v1.hello.v1 helm.sh/release.v1 1 17s modifiedAt=1793421992,name=hello,owner=helm,status=deployed,version=1
$ kubectl -n demo get secret sh.helm.release.v1.hello.v1 -o jsonpath='{.data.release}' \
| base64 -d | base64 -d | gunzip | jq '{name, version, namespace, info: .info.status, chart: .chart.metadata.version, config}'
{
"name": "hello",
"version": 1,
"namespace": "demo",
"info": "deployed",
"chart": "0.1.0",
"config": {
"ingress": {
"host": "hello.apps.example.com"
},
"replicaCount": 3
}
}
Два base64 -d — один снимает кодирование API, второй — кодирование самого Helm. config — ровно то, что пришло от пользователя (-f и --set), без умолчаний чарта. То же самое без ручного разбора показывают helm get values hello -n demo, helm get manifest hello -n demo и helm get all hello -n demo.
Кому принадлежат поля после установки Helm 4:
$ kubectl -n demo get deploy hello -o jsonpath='{range .metadata.managedFields[*]}{.manager}{"\t"}{.operation}{"\n"}{end}'
helm Apply
kube-controller-manager Update
Apply — значит server-side apply; у релизов Helm 3 здесь было бы helm Update. Обновление с новым тегом и затем откат:
$ helm upgrade hello ./hello -n demo -f values-prod.yaml --set-string image.tag=1.1 --wait
Release "hello" has been upgraded. Happy Helming!
NAME: hello
LAST DEPLOYED: Sat Oct 31 04:46:45 2026
NAMESPACE: demo
STATUS: deployed
REVISION: 2
DESCRIPTION: Upgrade complete
…
$ helm upgrade hello ./hello -n demo -f values-prod.yaml --set-string image.tag=1.2 --wait --timeout 2m --rollback-on-failure
level=WARN msg="upgrade failed" name=hello error="resource Deployment/demo/hello not ready. status: InProgress, message: Updated: 1/3\ncontext deadline exceeded"
Error: UPGRADE FAILED: release hello failed, and has been rolled back due to rollback-on-failure being set: resource Deployment/demo/hello not ready. status: InProgress, message: Updated: 1/3
context deadline exceeded
$ helm history hello -n demo
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Sat Oct 31 04:46:22 2026 superseded hello-0.1.0 1.0 Install complete
2 Sat Oct 31 04:46:45 2026 superseded hello-0.1.0 1.0 Upgrade complete
3 Sat Oct 31 04:47:06 2026 failed hello-0.1.0 1.0 Upgrade "hello" failed: resource Deployment/demo/hello not ready. status: InProgress, message: Updated: ...
4 Sat Oct 31 04:49:07 2026 deployed hello-0.1.0 1.0 Rollback to 2
Тега 1.2 в реестре нет, новый под ушёл в ImagePullBackOff, за две минуты Deployment не стал готов — kstatus так и сообщает: status: InProgress, message: Updated: 1/3, — и Helm сам вернул ревизию 2 новой ревизией 4. APP VERSION во всех строках — 1.0: это appVersion чарта, а не тег, переданный через --set-string. Старые поды при этом не пострадали: rolling update не убирает старые реплики, пока новые не готовы (часть 7), так что пользователи сбоя не заметили. Без --rollback-on-failure релиз остался бы в failed с подвисшим новым ReplicaSet, и откатывать пришлось бы руками: helm rollback hello 2 -n demo.
Удаление:
$ helm uninstall hello -n demo
release "hello" uninstalled
$ kubectl -n demo get secret -l owner=helm,name=hello
No resources found in demo namespace.
Helm или Kustomize
| Kustomize | Helm | |
|---|---|---|
| Входные данные | валидные манифесты | шаблоны, которые без рендеринга не YAML |
| Параметры | нет, только патчи | values.yaml со схемой |
| Логика (условия, циклы) | нет | есть |
| Состояние в кластере | нет | релиз и история ревизий в Secret |
| Откат | через Git и повторное применение | helm rollback |
| Удаление лишних объектов | нет (apply не удаляет) |
да, по манифесту прошлой ревизии |
| Распространение | каталог в Git | архив в репозитории или OCI-реестре |
| Встроен в kubectl | да | нет |
Правило, которое работает в большинстве команд: свои приложения — Kustomize (или простой чарт, если параметров действительно много); чужие компоненты — их официальный Helm-чарт с values-файлом в Git, плюс post-renderer, если чарт чего-то не умеет. Инструменты не конкурируют: Argo CD — GitOps-контроллер, который сам применяет в кластер содержимое Git-репозитория (часть 41), — умеет и то и другое.
Источники
- Helm 4 Overview и выпуск v4.3.1 — что изменилось в Helm 4: server-side apply, kstatus, новые имена флагов, плагины.
- Helm: Charts — структура чарта,
Chart.yaml,versionиappVersion,kubeVersion, зависимости,values.schema.json, каталогcrds/. - Helm: Chart Template Guide, Built-in Objects и Values Files — шаблоны, встроенные объекты, порядок слияния значений; Go text/template и Sprig — язык шаблонов и библиотека функций.
- Helm: Using Helm, helm install и helm upgrade — релизы,
--setи его типы, флаги--wait,--rollback-on-failure,--server-side,--take-ownership. - Helm: Chart Hooks и Chart Tests — хуки, веса и политики удаления,
helm test. - Helm: Use OCI-based registries —
helm registry login,helm push, типы содержимого OCI-артефакта чарта; Helm: Provenance and Integrity — подпись чартов и файлы.prov. - Helm: Advanced Techniques и Helm Plugins — post-renderer и плагины Helm 4, в том числе на WebAssembly.
- Helm: Custom Resource Definitions — почему CRD из
crds/не обновляются и не удаляются. - Kubernetes: Server-Side Apply — владельцы полей и конфликты; kstatus — как вычисляется готовность объекта; Recommended Labels — метки
app.kubernetes.io/*. - helm-diff и helm-secrets — плагины для сравнения и для зашифрованных значений; CNCF Distribution — реестр, на котором проверялась загрузка чарта.
Дальше — CRD и операторы: как добавить в API собственный тип объекта, как устроен контроллер, который превращает его в поды и сервисы, и что делают за администратора операторы PostgreSQL и RabbitMQ.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.