Именованные шаблоны и _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. - Передавайте контекст вторым аргументом; селектор-метки держите стабильными и отдельно от полного набора.