Коротко:
- Плоский values.yaml без структуры - главная причина конфликтов при слиянии и непонятных переопределений в разных окружениях.
- Общие фрагменты шаблонов выносятся в
_helpers.tplчерез именованные шаблоны - это единственный способ не копипастить labels и annotations по десяти файлам. - Library chart - отдельный пакет с переиспользуемыми шаблонами для нескольких сервисов; subchart - зависимость внутри одного деплоя.
- Окружения разделяются через наложение values-файлов, а не через условия
if .Values.env == "prod"внутри шаблонов. - Проверяй шаблоны локально через
helm templateиhelm lintдо любого деплоя в кластер.
Откуда берется хрупкий чарт
Обычно всё начинается невинно: один values.yaml, несколько манифестов в папке templates/, и сервис деплоится. Проблемы появляются позже - когда нужно поддерживать три окружения, добавить второй сервис с похожей структурой, или когда в команду приходит новый человек и пытается понять, почему ingress.yaml содержит сто строк условий.
Монолитный чарт - это не проблема одного файла. Это накопленные решения «сделаем сейчас, разберемся потом»: дублированные labels в каждом манифесте, логика окружений прямо в шаблонах, values с плоской структурой без явного смысла. Когда такой чарт нужно обновить или переиспользовать, оказывается, что проще написать заново.
Дальше - конкретные приемы, которые не дают этому случиться.
Структура values.yaml: иерархия вместо плоского списка
Самая частая ошибка в организации values - это плоская структура, где все ключи живут на одном уровне:
replicaCount: 2
image: myapp
imageTag: 1.0.0
servicePort: 8080
ingressEnabled: true
ingressHost: myapp.example.com
resourcesLimitsCpu: 500m
resourcesLimitsMemory: 256MiПри десяти параметрах это терпимо. При пятидесяти файл превращается в свалку, и неочевидно, какие ключи связаны между собой. Вместо этого группируй параметры по сущностям:
replicaCount: 2
image:
repository: myapp
tag: 1.0.0
pullPolicy: IfNotPresent
service:
port: 8080
type: ClusterIP
ingress:
enabled: true
host: myapp.example.com
tls: []
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128MiТакой файл читается сверху вниз - понятно, что за что отвечает, и легко найти нужное место для переопределения. Вложенность хорошо работает с helm upgrade --set ingress.host=new.example.com и с наложением дополнительных values-файлов через -f.
Как разделять окружения
Антипаттерн, который ломает читаемость шаблонов быстрее всего - это условия вида:
{{- if eq .Values.env "production" }}
replicas: 5
{{- else }}
replicas: 1
{{- end }}Логика окружений ползет в шаблон, шаблон перестает быть декларативным, а новый инженер вынужден читать каждый блок, чтобы понять, что реально развернется в prod.
Правильный подход - базовый values.yaml с разумными дефолтами, и отдельные файлы переопределений для каждой среды:
values.yaml # базовые значения
values-staging.yaml # переопределения для staging
values-prod.yaml # переопределения для productionДеплой в конкретную среду выглядит так:
helm upgrade --install myapp ./chart \
-f values.yaml \
-f values-prod.yamlВторой файл перекрывает только то, что отличается. Шаблоны остаются чистыми - никаких условий на имя окружения.
_helpers.tpl: не просто labels
В большинстве чартов файл _helpers.tpl используется только для стандартных labels: app.kubernetes.io/name, app.kubernetes.io/version и аналогичных. Это хорошее начало, но возможности именованных шаблонов намного шире.
Любой повторяющийся фрагмент стоит выносить в хелпер. Например, блок с environment variables из ConfigMap и Secret часто одинаковый для всех контейнеров в чарте:
{{- define "myapp.envFrom" -}}
envFrom:
- configMapRef:
name: {{ include "myapp.fullname" . }}-config
- secretRef:
name: {{ include "myapp.fullname" . }}-secret
{{- end }}После этого в любом манифесте достаточно вызова:
containers:
- name: app
image: ...
{{- include "myapp.envFrom" . | nindent 4 }}Если структура envFrom изменится - меняешь одно место, а не пять файлов.
Правила хорошего хелпера
- Имя всегда начинается с префикса чарта:
myapp.labels, а не простоlabels. Без префикса возникают коллизии при использовании library charts. - Каждый хелпер делает одно дело. Не смешивай labels и annotations в одном шаблоне - их часто нужно использовать по отдельности.
- Избегай сложных вычислений прямо в хелпере. Если нужна нетривиальная логика, лучше разбить на два маленьких шаблона, чем держать один большой с вложенными условиями.
Когда нужен library chart
Представь ситуацию: у команды пять микросервисов, у каждого свой чарт, и в каждом - почти одинаковые шаблоны Deployment, Service и HorizontalPodAutoscaler. Синхронизировать их руками невозможно - через два месяца чарты разойдутся, и у одного сервиса окажутся устаревшие annotations.
Library chart решает эту задачу. Это специальный пакет с типом library в Chart.yaml, который содержит только именованные шаблоны и не деплоит никаких ресурсов самостоятельно. Сервисные чарты подключают его как зависимость и переиспользуют готовые шаблоны.
Минимальный Chart.yaml для библиотечного чарта:
apiVersion: v2
name: common
type: library
version: 1.2.0Подключение в сервисном чарте:
dependencies:
- name: common
version: "1.2.x"
repository: "oci://registry.example.com/charts"После helm dependency update шаблоны из common доступны через вызов include "common.deployment" ..
Library chart оправдан, когда больше двух-трех чартов разделяют одни и те же шаблонные паттерны. Для одного сервиса это избыточно.
Subcharts: зависимости внутри одного деплоя
Subchart - это вложенный чарт, который деплоится вместе с родительским. Типичный пример: приложение плюс Redis или PostgreSQL как зависимость. В Chart.yaml родителя:
dependencies:
- name: redis
version: "17.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabledКлючевой момент - поле condition. Оно позволяет включать или отключать зависимость через values.yaml:
redis:
enabled: true
architecture: standaloneValues для subchart передаются через пространство имен, совпадающее с именем зависимости. В примере выше все ключи внутри redis: попадают в Redis chart как его собственные values. Это важно знать, когда нужно переопределить, скажем, размер persistent volume для Redis в prod-окружении.
Подводные камни subcharts
Subchart создает жесткую связь между версиями. Если Bitnami обновил Redis chart и поменял структуру values - твой values-prod.yaml может молча перестать работать так, как ожидалось. Именно поэтому версии зависимостей стоит фиксировать точно, а не указывать "*".
Второй момент: глобальные values. В Helm есть специальная секция global: в values.yaml, которая видна и родителю, и всем subchart без явной передачи. Это удобно для общих параметров вроде registry или imagePullSecrets, но легко превращается в неявные зависимости, о которых не догадывается новый человек в команде.
Осторожно с global: значения в секции global доступны всем вложенным чартам автоматически. Это удобно, но создает неявные зависимости. Документируй использование global-values явно в README чарта.
Типичные ошибки при организации шаблонов
| Ошибка | Последствие | Как исправить |
|---|---|---|
| Дублирование labels в каждом манифесте вручную | Labels расходятся при изменении; сложно найти все ресурсы сервиса | Вынести в _helpers.tpl, использовать через include |
Условия if .Values.env == "prod" в шаблонах | Логика окружений разбросана по шаблонам; трудно отследить, что деплоится куда | Разделять через отдельные values-файлы |
| Плоская структура values без группировки | Конфликты при слиянии; непонятно, какие параметры связаны | Иерархическая структура по сущностям |
| Нефиксированные версии зависимостей | Обновление Bitnami ломает деплой без предупреждения | Фиксировать точные версии или диапазон x.y.x |
| Большой монолитный шаблон deployment.yaml с сотней строк | Сложно читать, тяжело тестировать, merge conflicts при командной работе | Разбить на отдельные файлы по ресурсам; общее в хелперы |
Проверка шаблонов до деплоя
Ни один шаблон не должен попасть в кластер непроверенным. Два базовых инструмента, которые должны быть в CI:
helm lint ./chart проверяет синтаксис и базовые правила оформления. Находит очевидные ошибки, undefined variables и нарушения структуры.
helm template ./chart -f values-prod.yaml рендерит шаблоны локально без подключения к кластеру. Вывод можно отправить в kubectl apply --dry-run=client -f -, чтобы проверить корректность Kubernetes-манифестов.
Для более глубокой проверки используй kubeconform - он валидирует манифесты против актуальных JSON Schema для нужной версии Kubernetes API. В отличие от kubeval, активно поддерживается и понимает CRD.
Пример пайплайна проверки в CI:
helm dependency update ./chart
helm lint ./chart
helm template ./chart -f values.yaml -f values-prod.yaml \
| kubeconform -strict -kubernetes-version 1.29.0Этот набор поймает синтаксические ошибки, undefined values и несовместимость с API нужной версии кластера.
Как масштабировать структуру при росте числа сервисов
Когда сервисов становится много, монорепозиторий чартов или отдельный репозиторий с общим library chart - это не вопрос вкуса, а инженерное решение.
Типичная структура монорепо для команды с несколькими сервисами:
charts/
common/ # library chart с общими шаблонами
Chart.yaml
templates/
_deployment.tpl
_service.tpl
_helpers.tpl
service-a/ # сервисный чарт
Chart.yaml
charts/ # здесь лежит common после helm dep update
templates/
values.yaml
values-prod.yaml
service-b/
...При таком подходе изменение в common требует обновления версии и запуска helm dependency update в каждом сервисном чарте. Это хорошо: изменения явные и контролируемые, нет магического наследования.
Если чарты публикуются в OCI-реестр (GitHub Container Registry, Harbor, AWS ECR), версионирование становится частью CI: push в main повышает патч-версию, релизный тег создает минорную или мажорную.
Чеклист: признаки здорового чарта
- В
values.yamlпараметры сгруппированы по сущностям, есть комментарии для неочевидных ключей - Общие labels, annotations и фрагменты вынесены в
_helpers.tpl, а не дублируются в каждом файле - Логика окружений реализована через отдельные values-файлы, а не через условия в шаблонах
- Версии всех зависимостей зафиксированы в
Chart.lock - В CI запускается
helm lintиhelm template | kubeconform - README содержит минимально необходимое: что деплоит чарт, какие values обязательны, как подключить окружения
- Нет шаблонных файлов длиннее 100-150 строк без явной необходимости
- Если несколько сервисов используют похожие паттерны - есть library chart или план его создания
Именование ресурсов и аннотации: системный подход
Когда чарт деплоит несколько ресурсов, именование становится отдельной задачей. Если называть Pod, ConfigMap и Secret по-разному в каждом манифесте, через несколько месяцев в кластере накапливаются ресурсы, связь которых непонятна без чтения исходников.
Хорошая практика: все ресурсы одного чарта получают имя через один хелпер. Обычно это {{ include "myapp.fullname" . }}, который собирает имя из release name и chart name. ConfigMap становится myapp-config, Secret - myapp-secret, ServiceAccount - myapp. Связь очевидна без документации.
С аннотациями та же логика. Если в проекте используется Argo CD, Flux или другой GitOps-инструмент, он часто добавляет собственные аннотации к ресурсам. Стоит заранее выделить блок для аннотаций в values.yaml и пробрасывать их через хелпер:
{{- define "myapp.annotations" -}}
{{- with .Values.commonAnnotations }}
{{- toYaml . }}
{{- end }}
{{- end }}Тогда добавить аннотацию для конкретного окружения можно через values-prod.yaml без изменения шаблонов.
Документирование values: минимум, который реально помогает
Одна из причин, по которой инженеры избегают чужих чартов - непонятные параметры без объяснений. Написать «что это» занимает десять секунд, а экономит двадцать минут разбирательств.
Вакансии для DevOps-инженеров
Несколько правил, которые работают на практике:
- Комментируй не очевидные ключи прямо над ними в values.yaml. Очевидный
replicaCountне нуждается в пояснении, ноterminationGracePeriodSecondsстоит прокомментировать. - Для булевых флагов вроде
autoscaling.enabledвсегда указывай, что именно включается и какие другие параметры становятся актуальными при значении true. - Если параметр обязателен и не имеет разумного дефолта, выстави пустую строку и добавь комментарий:
# required: DNS-имя сервиса для ingress. Это лучше, чем скрытый дефолт, который работает только в одном окружении.
Для более крупных чартов удобно держать раздел в README с таблицей параметров. Но только если её реально обновляют. Устаревшая документация хуже, чем её отсутствие.
Простой способ проверить документацию чарта: попроси нового члена команды задеплоить чарт в тестовое окружение только по README и values.yaml, без объяснений. Там, где он застрянет, документация неполная. Это занимает тридцать минут и находит реальные пробелы лучше любого ревью.
Сравнение подходов к переиспользованию шаблонов
Вопрос «как переиспользовать шаблоны между несколькими сервисами» имеет несколько ответов, и каждый подходит для разного масштаба.
| Подход | Когда подходит | Основной минус |
|---|---|---|
| Копирование шаблонов между чартами | Один-два сервиса, прототип | Расхождение версий со временем |
Общий _helpers.tpl внутри одного чарта | Один чарт с несколькими ресурсами | Не помогает при разных чартах |
| Library chart как зависимость | Три и больше сервисов с общими паттернами | Требует версионирования и реестра |
| Umbrella chart с subcharts | Группа сервисов, деплоящихся вместе | Жесткая связь версий всех компонентов |
На практике команды часто начинают с копирования, потом переходят к library chart, когда синхронизация руками становится болью. Umbrella chart выбирают, когда несколько сервисов образуют единый продукт и разворачиваются только вместе.
Версионирование чарта и совместимость с приложением
В Chart.yaml есть два поля версии: version и appVersion. Их часто путают или синхронизируют по умолчанию, что создает проблемы.
appVersion - это версия приложения, которое деплоит чарт. Обычно это тег Docker-образа. Это поле меняется при каждом релизе приложения.
version - это версия самого чарта, то есть шаблонов и структуры values.yaml. Она должна меняться, только если изменилась инфраструктурная часть: добавили новый параметр, изменили структуру шаблона, добавили зависимость.
Если привязать обе версии к одному тегу, каждый релиз приложения выглядит как изменение инфраструктуры. Это ломает возможность откатиться к предыдущей версии чарта независимо от версии приложения - а это именно то, что нужно при откате после неудачного изменения конфигурации.
Простое правило: appVersion меняет CI при сборке образа, version меняет инженер при изменении шаблонов или values. Изменение в разных пайплайнах.
FAQ
Что такое library chart и зачем он нужен?
Library chart - это Helm-пакет типа library, который содержит только именованные шаблоны и не создает никаких Kubernetes-ресурсов при деплое. Его подключают как зависимость в обычные чарты, чтобы переиспользовать общие шаблоны (Deployment, Service, labels) без копипаста между сервисами.
Чем subchart отличается от library chart?
Subchart - это обычный чарт, который деплоит реальные ресурсы и включается как зависимость в родительский чарт. Типичный пример - Redis или PostgreSQL рядом с приложением. Library chart не деплоит ничего сам по себе - он только предоставляет переиспользуемые шаблонные блоки.
Как правильно разделять values для разных окружений?
Держи в базовом values.yaml безопасные дефолты, подходящие для разработки. Для staging и prod создавай отдельные файлы переопределений и передавай их через -f values-prod.yaml при деплое. Не добавляй имя окружения как параметр внутрь шаблонов.
Нужно ли фиксировать версии зависимостей через Chart.lock?
Да. Файл Chart.lock генерируется автоматически при helm dependency update и фиксирует точные версии загруженных зависимостей. Его нужно коммитить в репозиторий - это гарантирует воспроизводимость сборки в CI и защищает от неожиданных обновлений upstream-чартов.
Как проверить шаблоны без реального кластера?
Связка helm template и kubeconform покрывает большинство случаев: рендеринг шаблонов с нужными values и валидация против JSON Schema нужной версии Kubernetes. Для проверки установки можно использовать helm install --dry-run --debug, но он требует доступа к кластеру для проверки некоторых ресурсов.
Когда стоит разбивать один большой чарт на несколько?
Когда в одном чарте появляются ресурсы для принципиально разных сервисов, или когда шаблоны разных частей нужно деплоить с разной частотой. Если один деплой всегда происходит целиком - разбивать не нужно. Если часть ресурсов меняется независимо от остальных - это сигнал к разделению.
Есть ли смысл использовать Helm для очень простых сервисов?
Для одного-двух манифестов без параметров Helm добавляет сложность без реальной пользы. Но как только появляется хотя бы два окружения с разными values или потребность в rollback через helm rollback, пакетирование в чарт начинает окупаться.
Итог
Хорошо организованный чарт - это не про красоту файловой структуры. Это про то, что изменение в одном месте не ломает пять других, новый инженер может разобраться за час, а деплой в prod отличается от staging только одним дополнительным файлом values.
Иерархические values, именованные шаблоны в хелперах, library charts для общих паттернов и фиксированные версии зависимостей - каждый из этих приемов решает конкретную проблему. Вместе они дают чарт, который не страшно обновлять через полгода и который выдержит рост команды и числа сервисов.