Helm templating: values, helpers, hooks
helm install под капотом — это два этапа: рендеринг templates в plain YAML и применение результата к кластеру. Этот урок — про первый этап. Здесь мы разбираем, как Go templates превращаются в манифесты, какие тонкости есть с indentation и type coercion, как делать переиспользуемый код через _helpers.tpl, и как использовать hooks для координации lifecycle событий (DB migrations перед app upgrade).
awk: модель pattern-action и переменные
Built-in objects: четыре источника данных
Внутри template доступны pre-injected объекты:
.Values— данные из values.yaml + любые-foverrides +--set. Это user-controlled источник..Release— метаданные текущего release:.Release.Name— имя релиза (то, что вhelm install <name>)..Release.Namespace— namespace..Release.Revision— номер ревизии (1 при install, увеличивается при upgrade)..Release.IsInstall/.Release.IsUpgrade— boolean, для conditional logic..Release.Service— всегда “Helm”.
.Chart— содержимое Chart.yaml:.Chart.Name,.Chart.Version(SemVer chart-а),.Chart.AppVersion(версия app).
.Capabilities— feature detection:.Capabilities.KubeVersion.Major,.Capabilities.KubeVersion.Minor..Capabilities.APIVersions.Has "networking.k8s.io/v1/Ingress"— проверка наличия API.
.Files— доступ к файлам внутри chart:.Files.Get "config/nginx.conf"— прочитать как string..Files.Glob "config/**.conf"— glob по нескольким файлам.
.Template— метаданные текущего template:.Template.Name,.Template.BasePath.
Пример использования всех:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
spec:
replicas: {{ .Values.replicaCount }}
Pipe и функции
| — это pipeline (как в shell). Значение слева передаётся как последний аргумент функции справа. Это самая частая конструкция в чартах.
# Default value, если values.yaml не указан
image: {{ .Values.image.tag | default "latest" }}
# Quote string (важно для tags типа "1.25" чтобы не стать float)
image: nginx:{{ .Values.image.tag | quote }}
# Upper/lower case
env:
- name: APP_ENV
value: {{ .Values.environment | upper }}
# Base64 encoding (для Secret data)
data:
password: {{ .Values.password | b64enc }}
# Множественные функции в pipeline
name: {{ .Release.Name | trunc 63 | trimSuffix "-" }}
default — отдельно отметить: если values.yaml не задал ключ, или задал nil/empty string — берётся default. Удобно для optional полей.
Conditional logic: if / else / with
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}
spec:
rules:
- host: {{ .Values.ingress.host }}
{{- if .Values.ingress.tls }}
tls:
- secretName: {{ .Values.ingress.tls.secretName }}
{{- end }}
{{- end }}
{{- if ... }}—-слева убирает whitespace (включая newline) перед действием. Это критично для читаемого output без лишних пустых строк.{{- end }}— закрывает блок. Безendtemplate не валиден.with— задаёт scope:
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
Внутри with . — это .Values.resources (если непустое). Если пусто — блок целиком пропускается. Удобно для optional блоков.
{{- if .Values.foo }} истинно если .Values.foo — non-empty (не nil, не "", не 0, не false, не empty slice). Если .Values.foo: 0 — if будет false (0 интерпретируется как falsy). Для явной проверки на existence используй hasKey .Values "foo" или (.Values.foo | default 0) | int.
range: итерация
# values.yaml
envVars:
- name: DB_HOST
value: postgres
- name: DB_PORT
value: "5432"
- name: LOG_LEVEL
value: info
# template
env:
{{- range .Values.envVars }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
Внутри range . — текущий элемент. Если нужен индекс:
{{- range $index, $env := .Values.envVars }}
- name: {{ $env.name }}
value: {{ $env.value | quote }}
{{- end }}
Для map:
{{- range $key, $value := .Values.labels }}
{{ $key }}: {{ $value | quote }}
{{- end }}
toYaml + nindent: сериализация map в YAML
Самый частый паттерн — взять секцию из values.yaml и вставить её as-is в template:
# values.yaml
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
# template
containers:
- name: app
resources:
{{- toYaml .Values.resources | nindent 6 }}
Что делает:
toYamlберёт Go map и сериализует в YAML string.nindent 6добавляет newline + indent 6 пробелов в начало каждой линии.
Результат:
containers:
- name: app
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
Без nindent indentation поломалась бы. Это главный source багов в helm charts: неправильный nindent ломает YAML структуру, и helm template рендерит invalid YAML.
Helm не валидирует YAML структуру до отправки на apiserver. helm template покажет тебе результат — всегда смотри, как реально отрендерилось. Тип ошибок: mapping values are not allowed in this context — это значит индентация поехала.
_helpers.tpl: переиспользуемые шаблоны
Файл templates/_helpers.tpl — это define-блоки, которые можно include в других templates. Имя начинается с _ — Helm не пытается рендерить его как K8s манифест.
# templates/_helpers.tpl
{{/*
Полное имя release-а (truncated to 63 chars for k8s label limit).
*/}}
{{- define "mychart.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Standard labels.
*/}}
{{- define "mychart.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end -}}
{{/*
Selector labels (subset of labels, used in matchLabels).
*/}}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
Использование:
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
Ключевые моменты:
include(НЕtemplate) — потому чтоincludeвозвращает строку, которую можно pipe-нуть вnindent.templateдействие выводит сразу в output без возможности pipe.{{- define "name" -}} ... {{- end -}}—-режут whitespace по краям, иначе define содержит ведущий/висячий newline.- Передаём
.— это весь scope (с .Values, .Release, .Chart). Без этого helper не имеет доступа к built-in objects.
Sprig: stdlib функции
Helm включает Sprig — большую stdlib функций для templates. Частые:
- String:
upper,lower,title,trim,quote,squote,replace,substr,trunc,contains,hasPrefix. - Default & nil:
default,empty,coalesce(первое non-empty). - Encoding:
b64enc,b64dec,sha256sum,sha1sum. - Lists:
list,first,last,len,has(contains),concat,uniq. - Math:
add,sub,mul,div,mod,max,min. - Date:
now,date "2006-01-02". - Generation:
uuidv4,randAlphaNum,genCA,genSignedCert.
Particularly useful:
# SHA256 над rendered ConfigMap data — для rollout на изменение config
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
Этот pattern заставляет Deployment рестарт-нуться при каждом изменении ConfigMap (потому что pod template hash меняется — Deployment делает rolling update). Stand-art для helm charts.
Helm hooks: координация lifecycle
Hooks — это обычные K8s resources (обычно Jobs) с annotation helm.sh/hook. Они выполняются на конкретных моментах lifecycle:
pre-install— до создания main resources в install.post-install— после.pre-upgrade/post-upgrade— аналогично для upgrade.pre-delete/post-delete— для uninstall.pre-rollback/post-rollback— для rollback.test— запускается черезhelm test <release>.
Пример: DB migration перед каждым app upgrade.
# templates/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "mychart.fullname" . }}-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:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./migrate", "up"]
Аннотации:
helm.sh/hook— на каком event выполняется (comma-separated).helm.sh/hook-weight— порядок выполнения hooks одного типа. Меньше — раньше. По умолчанию 0.helm.sh/hook-delete-policy:before-hook-creation— delete предыдущую hook resource перед созданием новой (нужно, потому что Job immutable — нельзя update).hook-succeeded— удалить hook resource после успешного выполнения.hook-failed— удалить hook resource после failure (по дефолту НЕ удаляется, чтобы можно было разобраться).
Hook idempotency: главный pitfall
Helm hooks не tracked как часть release. Это значит:
helm get manifestне покажет hooks (если они не оставлены черезdelete-policy).helm rollbackНЕ запускает обратные действия. Откат на старую ревизию — БЕЗ rollback migrations.
Это означает: DB migration через pre-upgrade hook должна быть idempotent и backward-compatible. Иначе после rollback приложение старой версии встретит схему БД новой версии и сломается.
Pattern для миграций:
- Expand: добавить колонку как nullable. Старый код игнорирует, новый — заполняет.
- Code change: deploy новый код, который читает/пишет колонку.
- Migrate data: backfill старых записей.
- Contract: через несколько релизов сделать колонку non-null.
Никогда не делай DROP COLUMN или RENAME через helm hook — это сломает rollback. Pattern expand-then-contract — единственный safe способ эволюционировать схему вместе с relations.
NOTES.txt: пост-инсталляционная инструкция
templates/NOTES.txt — это шаблонизированный текст, который выводится после helm install / helm upgrade. Полезно для показа пользователю, как получить доступ к новому release:
{{- if eq .Values.service.type "NodePort" }}
Get the URL by running:
export NODE_PORT=$(kubectl get svc {{ include "mychart.fullname" . }} -o jsonpath='{.spec.ports[0].nodePort}')
echo "http://$(minikube ip):$NODE_PORT"
{{- else if eq .Values.service.type "LoadBalancer" }}
Wait for external IP:
kubectl get svc {{ include "mychart.fullname" . }} -w
{{- end }}
helm get notes <release> — повторно показать notes уже установленного release-а.
Killer-моменты
toYaml | nindent N— главный pattern для embed values секции в template. Неправильный nindent — invalid YAML.{{- ... -}}режут whitespace. Без них в output появляются пустые строки. С обеих сторон — режут с обеих.includevstemplate:includeвозвращает string (можно pipe),templateсразу выводит. Всегда используйincludeв practice.- Hooks не tracked.
helm uninstallне удалит created-by-hook resources автоматически (DB rows и т.д.).helm rollbackне делает hook rollback. checksum/configannotation — стандартный pattern для рестарта Deployment при изменении ConfigMap.- DB migrations через pre-upgrade hook должны быть idempotent + backward-compatible. Иначе rollback ломает app.
if .Values.fooс foo: 0 — false (0 is falsy). ИспользуйhasKeyдля distinguishing missing vs zero.