Helm: функции и pipelines

TL;DR: Helm-шаблоны используют Go template engine + библиотеку Sprig (~70 функций). Pipeline {{ value | func1 | func2 }} — цепочка трансформаций, где результат предыдущей функции передаётся последним аргументом следующей. _helpers.tpl — место для именованных шаблонов (define/include), переиспользуемых во всём чарте.

Зачем это знать

Без функций шаблоны Helm — просто подстановка переменных. С функциями — полноценная генерация манифестов: валидация входных данных (required, fail), трансформация строк и структур (toYaml, indent), условная логика, переиспользуемые блоки. Разница между чартом, который ломается на каждом втором values.yaml, и чартом, который корректно обрабатывает edge cases.

Pipelines

Pipeline — цепочка функций, соединённых |. Результат левой части передаётся последним аргументом правой:

# Без pipeline (вложенные вызовы — читается тяжело)
{{ trunc 63 (trimSuffix "-" (printf "%s-%s" .Release.Name .Chart.Name)) }}
 
# С pipeline (читается как поток данных слева направо)
{{ printf "%s-%s" .Release.Name .Chart.Name | trimSuffix "-" | trunc 63 }}

Оба варианта эквивалентны. Pipeline предпочтителен — он читается линейно.

Многошаговый pipeline

# Сформировать имя ресурса: lowercase, обрезать до 63 символов (лимит K8s),
# убрать trailing дефис
{{ printf "%s-%s" .Release.Name .Chart.Name | lower | trunc 63 | trimSuffix "-" }}

Это настолько частый паттерн, что его выносят в _helpers.tpl (см. секцию «Именованные шаблоны»).

Helm использует Go template engine + библиотеку Sprig (~70 функций). Полный справочник всех функций с примерами — в helm-functions.

toYaml + indent — самый частый паттерн

Проблема: values.yaml содержит вложенную структуру (resources, tolerations, nodeSelector), и её нужно вставить в шаблон как есть, с правильным отступом.

# values.yaml
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 250m
    memory: 256Mi
 
tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "gpu"
    effect: "NoSchedule"
# templates/deployment.yaml
spec:
  template:
    spec:
      containers:
        - name: {{ .Chart.Name }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
      tolerations:
        {{- toYaml .Values.tolerations | nindent 8 }}

Результат после рендеринга:

spec:
  template:
    spec:
      containers:
        - name: payment-service
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 250m
              memory: 256Mi
      tolerations:
        - key: "dedicated"
          operator: "Equal"
          value: "gpu"
          effect: "NoSchedule"

Важно: indent добавляет отступ к уже существующему тексту. nindent добавляет перенос строки перед отступом. С nindent используй {{- (trim whitespace слева), иначе будет лишняя пустая строка.

indent vs nindent

# indent — НЕ добавляет newline, нужен перенос в шаблоне
data: |
{{ .Files.Get "config.yml" | indent 4 }}
 
# nindent — добавляет newline, используй с {{-
data:
  {{- .Files.Get "config.yml" | nindent 4 }}

Управление пробелами

{{- и -}} — trim пробелов и переносов слева/справа от блока:

# Без trim — лишние пустые строки в output
metadata:
  labels:
    {{ if .Values.team }}
    team: {{ .Values.team }}
    {{ end }}
 
# С trim — чистый output
metadata:
  labels:
    {{- if .Values.team }}
    team: {{ .Values.team }}
    {{- end }}

Правило: {{- ставь почти всегда на if, range, end, define, include. Не ставь на строках, которые должны рендерить значение с переносом.

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

define создаёт переиспользуемый блок, include вызывает его. Все именованные шаблоны принято хранить в _helpers.tpl (файлы с _ не рендерятся как манифесты).

_helpers.tpl

# templates/_helpers.tpl
 
# Имя ресурса: release-name + chart-name, обрезать до 63 символов
{{- define "myapp.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
 
# Стандартные labels (рекомендация Kubernetes docs)
{{- define "myapp.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: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
{{- end -}}
 
# Selector labels (подмножество labels для matchLabels)
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
 
# Имя image с учётом registry
{{- define "myapp.image" -}}
{{- if .Values.image.registry -}}
  {{- printf "%s/%s:%s" .Values.image.registry .Values.image.repository .Values.image.tag -}}
{{- else -}}
  {{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}
{{- end -}}
{{- end -}}

Использование в шаблонах

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: {{ include "myapp.image" . }}

include vs template

# template — вставляет как есть, НЕ работает с pipeline
{{ template "myapp.labels" . }}
 
# include — возвращает строку, РАБОТАЕТ с pipeline
{{ include "myapp.labels" . | nindent 4 }}

Всегда используй include. template не поддерживает pipeline, поэтому невозможно контролировать отступы — результат ломает YAML.

Функция tpl — динамический рендеринг

tpl рендерит строку из values.yaml как Go template. Позволяет использовать шаблонные выражения внутри values.

# values.yaml
config:
  greeting: "Hello from {{ .Release.Name }}"
  dbHost: "{{ .Release.Name }}-postgresql"
# templates/configmap.yaml — БЕЗ tpl (строка как есть, не отрендерена)
data:
  greeting: {{ .Values.config.greeting }}
  # Результат: greeting: Hello from {{ .Release.Name }}  ← литерал, не значение
 
# С tpl (строка рендерится как шаблон)
data:
  greeting: {{ tpl .Values.config.greeting . }}
  dbHost: {{ tpl .Values.config.dbHost . }}
  # Результат: greeting: Hello from my-release
  #            dbHost: my-release-postgresql

Важно: Второй аргумент tpl — контекст (обычно .). Без него шаблонные выражения внутри строки не имеют доступа к объектам.

Условная логика и циклы

if / else

# Создать Ingress только если включён
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "myapp.fullname" . }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  rules:
    - host: {{ .Values.ingress.host | required "ingress.host is required when ingress is enabled" }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ include "myapp.fullname" . }}
                port:
                  number: {{ .Values.service.port }}
{{- end }}

range — итерация

# Несколько environment variables из map
env:
  {{- range $key, $value := .Values.env }}
  - name: {{ $key | upper }}
    value: {{ $value | quote }}
  {{- end }}
# values.yaml
env:
  database_url: "postgres://db:5432/app"
  redis_url: "redis://cache:6379"
  log_level: "info"

Результат:

env:
  - name: DATABASE_URL
    value: "postgres://db:5432/app"
  - name: LOG_LEVEL
    value: "info"
  - name: REDIS_URL
    value: "redis://cache:6379"

with — сменить контекст

# with перенаправляет . на указанный объект
{{- with .Values.nodeSelector }}
nodeSelector:
  {{- toYaml . | nindent 2 }}
{{- end }}

Если .Values.nodeSelector пустой — весь блок не рендерится. Внутри with контекст . = .Values.nodeSelector, для доступа к корню используй $.

Подводные камни

ПроблемаСимптомРешение
toYaml без nindentСломанный YAML — неправильные отступыВсегда toYaml .Values.x | nindent N
template вместо includeНевозможно контролировать отступыИспользовать include + nindent
tpl без второго аргументаnil pointer evaluatingtpl .Values.str . — точка обязательна
Число из values рендерится как floatport: 8.08e+03{{ .Values.port | int }} или "{{ .Values.port }}"
{{ без - в if/rangeПустые строки в output{{- на управляющих конструкциях
required в optional-блокеОшибка даже когда блок не нуженОбернуть в if: {{- if .Values.x }}{{ required ... }}{{- end }}
Имя define конфликтует с subchartSubchart переопределяет шаблонПрефикс имени чарта: define "myapp.labels", не define "labels"

Связанные материалы

  • helm-functions — справочник всех функций: строковые, числовые, списки, словари, крипто, валидация
  • helm-objects — встроенные объекты: Release, Values, Chart, Files, Capabilities, Template
  • helm-basics — установка Helm, CLI команды, values.yaml, создание чарта
  • yaml-templates — шаблоны K8s-манифестов