Именованные шаблоны и _helpers.tpl

Перестаём копировать одинаковые блоки лейблов по всем манифестам — выносим их в переиспользуемые именованные шаблоны.

Именованный шаблон объявляется через define "имя" и вызывается через include "имя" контекст. Это функции Helm-чарта: написал раз — используешь везде.

В каждом манифесте чарта повторяются одни и те же блоки: имя ресурса, набор стандартных лейблов, selector-метки. Копировать их — нарушать DRY и плодить рассинхрон. Решение — _helpers.tpl.

Цена дублирования здесь не абстрактная. Представьте чарт из десятка манифестов, в каждом из которых руками прописан блок из четырёх стандартных лейблов. Понадобилось добавить пятый лейбл (скажем, app.kubernetes.io/managed-by) — и вы правите десять файлов, рискуя где-то опечататься или забыть один. А если в двух манифестах лейблы чуть разойдутся, отладка превращается в кошмар: ресурсы выглядят почти одинаково, но Service не находит поды по селектору. Именованный шаблон решает это радикально: набор лейблов описан в одном месте, правка автоматически расходится по всем вызовам.

Имя файла _helpers.tpl — соглашение, а не жёсткое требование. Подчёркивание в начале говорит Helm не рендерить файл как самостоятельный манифест (файлы на _ и . пропускаются), а расширение .tpl подчёркивает, что внутри только определения. Технически define можно положить в любой файл чарта — Helm всё равно соберёт все определения в общий реестр. Но держать их вместе в _helpers.tpl — устоявшаяся практика, и отступать от неё без причины не стоит: следующий человек будет искать хелперы именно там.

define: объявляем шаблон

В templates/_helpers.tpl:

{{- define "webapp.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "webapp.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
{{- end -}}

include: вызываем шаблон

В deployment.yaml:

metadata:
  name: {{ include "webapp.fullname" . }}
  labels: {{- include "webapp.labels" . | nindent 4 }}

Обратите внимание на . вторым аргументом include — это контекст, который получит шаблон. Без него внутри define не будет доступа к .Release и .Chart. Забыть передать контекст — ошибка номер один.

Стоит на минуту задержаться на причине, по которой контекст приходится передавать явно. Именованный шаблон не «видит» окружение того места, откуда его вызвали — у него нет доступа к точке вызывающего кода. Всё, что у него есть внутри, — это то, что вы передали вторым аргументом и что становится его точкой. Это сделано осознанно: шаблон должен быть самодостаточной функцией, а не куском, тайно зависящим от того, где его подставили. Цена такой чистоты — необходимость каждый раз дописывать . (или другой контекст). Привыкните воспринимать include "имя" . как «вызов функции с аргументом» — тогда забыть аргумент станет так же неестественно, как вызвать обычную функцию без скобок.

Соглашение об именах шаблонов

Имена шаблонов в Helm плоские и глобальные — реестр define общий на весь чарт и все его subchart-ы. Если два чарта объявят define "labels", они столкнутся. Поэтому имена принято префиксовать именем чарта: webapp.fullname, webapp.labels, webapp.selectorLabels. Точка в имени — лишь часть строки-идентификатора, никакой иерархии она не создаёт, но визуально группирует хелперы по чарту и предотвращает коллизии. Это та же логика, что и неймспейсы в языках программирования, только реализованная соглашением, а не механизмом языка. Когда подключаете чужой subchart, его хелперы автоматически получают префикс по имени subchart-а — ещё одна причина не полениться с префиксом в собственных определениях.

include против template: почему include

В Go есть встроенный template, но Helm рекомендует include. Причина: template — это действие, его результат нельзя передать в конвейер. А include возвращает строку, поэтому работает с | nindent, | indent, | quote:

# РАБОТАЕТ: include возвращает строку
labels: {{- include "webapp.labels" . | nindent 4 }}

# НЕ РАБОТАЕТ: template нельзя в конвейер
labels: {{- template "webapp.labels" . | nindent 4 }}

Правило простое: в Helm всегда используйте include, особенно если нужен отступ.

Селектор-метки отдельно от лейблов

Важный нюанс: метки в spec.selector Deployment иммутабельны — их нельзя менять после создания. Поэтому селектор-метки выносят в отдельный, минимальный и стабильный шаблон, а полный набор лейблов (с версией, которая меняется) — в другой:

{{- define "webapp.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}

В селектор кладут только стабильные метки (без версии), иначе апгрейд с новой версией сломается на иммутабельном поле.

Как include работает под капотом

Helm загружает все define из всех файлов чарта (и его subchart-ов с префиксами) в общий реестр шаблонов до рендера манифестов. include "имя" ctx исполняет шаблон имя с переданным контекстом ctx и возвращает результат как строку (внутри Helm оборачивает вызов так, чтобы перехватить вывод). Именно поэтому строку можно гнать через конвейер. Контекст ctx становится точкой внутри define — вот зачем передают .; иногда передают расширенный контекст через dict, чтобы прокинуть в шаблон дополнительные параметры.

Частые ошибки

  • Забыть контекст: include "x" без . — внутри шаблона пусто, .Release недоступен.
  • Использовать template там, где нужен отступ. Его нельзя в конвейер — берите include.
  • Версия в селекторе. Меняющаяся метка в spec.selector сделает апгрейд невозможным.

Итог

  • define/include выносят повторяющиеся имена и лейблы в _helpers.tpl — это DRY чарта.
  • Всегда include (возвращает строку, работает с конвейером), а не template.
  • Передавайте контекст вторым аргументом; селектор-метки держите стабильными и отдельно от полного набора.
Проверьте себя
1. Почему в Helm предпочитают include, а не template?
Ainclude короче
Binclude возвращает строку и работает в конвейере (| nindent), а template — нет
Ctemplate устарел
DРазницы нет
2. Что означает точка во include "webapp.labels" . ?
AЭто опечатка
BЭто контекст, передаваемый в шаблон; без него .Release и .Chart недоступны
CКонец строки
DТекущий файл
3. Почему версию приложения не кладут в spec.selector?
AОна длинная
BМетки селектора иммутабельны — меняющаяся версия сломает апгрейд
Cselector не поддерживает строки
DВерсия секретна