- PVSM.RU - https://www.pvsm.ru -
Про Helm и работу с ним «в общем» мы рассказали в прошлой статье [1]. Теперь подойдём к практике с другой стороны — с точки зрения создателя чартов (т.е. пакетов для Helm). И хотя эта статья пришла из мира эксплуатации, она получилась больше похожей на материалы о языках программирования — такова уж участь авторов чартов. Итак, чарт — это набор файлов…
Файлы чарта можно разделить на две группы:
templates
и файлы со значениями (значения по умолчанию хранятся в values.yaml
). Также к данной группе относятся файл requirements.yaml
и директория charts
— всё это используется для организации вложенных чартов.Подробнее о файлах обеих групп:
Chart.yaml
— файл с информацией о чарте;LICENSE
— необязательный текстовый файл с лицензией чарта;README.md
— необязательный файл с документацией;requirements.yaml
— необязательный файл со списком чартов-зависимостей;values.yaml
— файл со значениями по умолчанию для шаблонов;charts/
— необязательная директория со вложенными чартами;templates/
— директория с шаблонами манифестов Kubernetes-ресурсов;templates/NOTES.txt
— необязательный текстовый файл с примечанием, которое выводится пользователю при инсталяции и обновлении.Чтобы лучше разобраться в содержимом этих файлов, можно обратиться к официальному руководству разработчика чарта [2] или поискать соответствующие примеры в официальном репозитории [3].
Создание чарта по большому счёту сводится к организации правильно оформленного набора файлов. И главная сложность в этом «оформлении» — использование достаточно продвинутой системы шаблонов для достижения нужного результата. Для рендера манифестов Kubernetes-ресурсов используется стандартный Go-шаблонизатор [4], расширенный функциями Helm.
Напоминание: Разработчики Helm анонсировали, что в следующей крупной версии проекта — Helm 3 — появится поддержка Lua-скриптов, которые можно будет использовать одновременно с Go-шаблонами. Останавливаться подробнее на этом моменте не буду — об этом (и других изменениях в Helm 3) можно почитать здесь [5].
К примеру, вот так в Helm 2 выглядит шаблон Kubernetes-манифеста Deployment'а блога на WordPress из прошлой статьи [1]:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: {{ template "fullname" . }}
labels:
app: {{ template "fullname" . }}
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
spec:
replicas: {{ .Values.replicaCount }}
template:
metadata:
labels:
app: {{ template "fullname" . }}
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
release: "{{ .Release.Name }}"
spec:
{{- if .Values.image.pullSecrets }}
imagePullSecrets:
{{- range .Values.image.pullSecrets }}
- name: {{ . }}
{{- end}}
{{- end }}
containers:
- name: {{ template "fullname" . }}
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy | quote }}
env:
- name: ALLOW_EMPTY_PASSWORD
{{- if .Values.allowEmptyPassword }}
value: "yes"
{{- else }}
value: "no"
{{- end }}
- name: MARIADB_HOST
{{- if .Values.mariadb.enabled }}
value: {{ template "mariadb.fullname" . }}
{{- else }}
value: {{ .Values.externalDatabase.host | quote }}
{{- end }}
- name: MARIADB_PORT_NUMBER
{{- if .Values.mariadb.enabled }}
value: "3306"
{{- else }}
value: {{ .Values.externalDatabase.port | quote }}
{{- end }}
- name: WORDPRESS_DATABASE_NAME
{{- if .Values.mariadb.enabled }}
value: {{ .Values.mariadb.db.name | quote }}
{{- else }}
value: {{ .Values.externalDatabase.database | quote }}
{{- end }}
- name: WORDPRESS_DATABASE_USER
{{- if .Values.mariadb.enabled }}
value: {{ .Values.mariadb.db.user | quote }}
{{- else }}
value: {{ .Values.externalDatabase.user | quote }}
{{- end }}
- name: WORDPRESS_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
{{- if .Values.mariadb.enabled }}
name: {{ template "mariadb.fullname" . }}
key: mariadb-password
{{- else }}
name: {{ printf "%s-%s" .Release.Name "externaldb" }}
key: db-password
{{- end }}
- name: WORDPRESS_USERNAME
value: {{ .Values.wordpressUsername | quote }}
- name: WORDPRESS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "fullname" . }}
key: wordpress-password
- name: WORDPRESS_EMAIL
value: {{ .Values.wordpressEmail | quote }}
- name: WORDPRESS_FIRST_NAME
value: {{ .Values.wordpressFirstName | quote }}
- name: WORDPRESS_LAST_NAME
value: {{ .Values.wordpressLastName | quote }}
- name: WORDPRESS_BLOG_NAME
value: {{ .Values.wordpressBlogName | quote }}
- name: WORDPRESS_TABLE_PREFIX
value: {{ .Values.wordpressTablePrefix | quote }}
- name: SMTP_HOST
value: {{ .Values.smtpHost | quote }}
- name: SMTP_PORT
value: {{ .Values.smtpPort | quote }}
- name: SMTP_USER
value: {{ .Values.smtpUser | quote }}
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "fullname" . }}
key: smtp-password
- name: SMTP_USERNAME
value: {{ .Values.smtpUsername | quote }}
- name: SMTP_PROTOCOL
value: {{ .Values.smtpProtocol | quote }}
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
livenessProbe:
httpGet:
path: /wp-login.php
{{- if not .Values.healthcheckHttps }}
port: http
{{- else }}
port: https
scheme: HTTPS
{{- end }}
{{ toYaml .Values.livenessProbe | indent 10 }}
readinessProbe:
httpGet:
path: /wp-login.php
{{- if not .Values.healthcheckHttps }}
port: http
{{- else }}
port: https
scheme: HTTPS
{{- end }}
{{ toYaml .Values.readinessProbe | indent 10 }}
volumeMounts:
- mountPath: /bitnami/apache
name: wordpress-data
subPath: apache
- mountPath: /bitnami/wordpress
name: wordpress-data
subPath: wordpress
- mountPath: /bitnami/php
name: wordpress-data
subPath: php
resources:
{{ toYaml .Values.resources | indent 10 }}
volumes:
- name: wordpress-data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ .Values.persistence.existingClaim | default (include "fullname" .) }}
{{- else }}
emptyDir: {}
{{ end }}
{{- if .Values.nodeSelector }}
nodeSelector:
{{ toYaml .Values.nodeSelector | indent 8 }}
{{- end -}}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
Теперь — об основных принципах и особенностях шаблонизации в Helm. Большая часть приведённых ниже примеров взята из чартов официального репозитория [3].
{{ }}
Всё, что связано с шаблонизацией, оборачивается в двойные фигурные скобки. Текст вне фигурных скобок при рендере остаётся неизменным.
.
При рендере файла или partial'а (подробнее о переиспользовании шаблонов рассказывается в следующих разделах статьи) прокидывается значение, которое становится доступным внутри через переменную контекста — точку. При передаче в качестве аргумента структуры точка используется для доступа к полям и методам этой структуры.
Значение переменной изменяется в процессе рендера в зависимости от контекста, в котором она используется. Большинство блочных операторов переопределяет переменную контекста внутри основного блока. Основные операторы и их особенности будут рассмотрены ниже, после знакомства с базовой структурой Helm.
При рендере манифестов в шаблоны прокидывается структура со следующими полями:
.Values
— для доступа к параметрам, которые определяются при инсталяции и обновлении релиза. К ним относятся значения опций --set
, --set-string
и --set-file
, а также параметры файлов со значeниями, файл values.yaml
и файлы, соответствующие значениям опций --values
:
containers:
- name: main
image: "{{ .Values.image }}:{{ .Values.imageTag }}"
imagePullPolicy: {{ .Values.imagePullPolicy }}
.Release
— для использования данных релиза [6] о выкате, инсталяции или обновлении, имени релиза, namespace и значений ещё нескольких полей, которые могут пригодиться при генерации манифестов:
metadata:
labels:
heritage: "{{ .Release.Service }}"
release: "{{ .Release.Name }}"
subjects:
- namespace: {{ .Release.Namespace }}
.Chart
— для доступа к информации о чарте [7]. Поля соответствуют содержимому файла Chart.yaml
:
labels:
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
.Files
— для работы с хранящимися в директории чарта файлами; со структурой и доступными методами можно ознакомиться по ссылке [8]. Примеры:
data:
openssl.conf: |
{{ .Files.Get "config/openssl.conf" | indent 4 }}
data:
{{ (.Files.Glob "files/docker-entrypoint-initdb.d/*").AsConfig | indent 2 }}
.Capabilities
— для доступа к информации о кластере [9], в котором выполняется выкат:
{{- if .Capabilities.APIVersions.Has "apps/v1beta2" }}
apiVersion: apps/v1beta2
{{- else }}
apiVersion: extensions/v1beta1
{{- end }}
{{- if semverCompare "^1.9-0" .Capabilities.KubeVersion.GitVersion }}
apiVersion: apps/v1
{{- else }}
Начнём, конечно, с операторов if
, else if
и else
:
{{- if .Values.agent.image.tag }}
image: "{{ .Values.agent.image.repository }}:{{ .Values.agent.image.tag }}"
{{- else }}
image: "{{ .Values.agent.image.repository }}:v{{ .Chart.AppVersion }}"
{{- end }}
Оператор range
предназначен для работы с массивами и картами. Если в качестве аргумента передаётся массив и он содержит элементы, то для каждого элемента последовательно выполняется блок (при этом значение внутри блока становится доступным через переменную контекста):
{{- range .Values.ports }}
- name: {{ .name }}
port: {{ .containerPort }}
targetPort: {{ .containerPort}}
{{- else }}
...
{{- end}}
{{ range .Values.tolerations -}}
- {{ toYaml . | indent 8 | trim }}
{{ end }}
Для работы с картами предусмотрен синтаксис с переменными:
{{- range $key, $value := .Values.credentials.secretContents }}
{{ $key }}: {{ $value | b64enc | quote }}
{{- end }}
Похожее поведение — у оператора with
: eсли переданный аргумент существует, то выполняется блок, а переменная контекста в блоке соответствует значению аргумента. Например:
{{- with .config }}
config:
{{- with .region }}
region: {{ . }}
{{- end }}
{{- with .s3ForcePathStyle }}
s3ForcePathStyle: {{ . }}
{{- end }}
{{- with .s3Url }}
s3Url: {{ . }}
{{- end }}
{{- with .kmsKeyId }}
kmsKeyId: {{ . }}
{{- end }}
{{- end }}
Для переиспользования шаблонов может быть задействована связка из define [name]
и template [name] [variable]
, где переданное значение становится доступным через переменную контекста в блоке define
:
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ template "kiam.serviceAccountName.agent" . }}
...
{{- define "kiam.serviceAccountName.agent" -}}
{{- if .Values.serviceAccounts.agent.create -}}
{{ default (include "kiam.agent.fullname" .) .Values.serviceAccounts.agent.name }}
{{- else -}}
{{ default "default" .Values.serviceAccounts.agent.name }}
{{- end -}}
{{- end -}}
Пара особенностей, которые стоит учитывать при использовании define
, или, проще говоря, partial'ов:
templates
.define "chart_name.partial_name"
.$
Помимо работы с контекстом можно хранить, изменять и переиспользовать данные, используя переменные:
{{ $provider := .Values.configuration.backupStorageProvider.name }}
...
{{ if eq $provider "azure" }}
envFrom:
- secretRef:
name: {{ template "ark.secretName" . }}
{{ end }}
При рендере файла или partial'а $
имеет такое же значение, что и точка. Но в отличие от переменной контекста (точки), значение $
не изменяется в контексте блочных операторов, что позволяет одновременно работать со значением контекста блочного оператора и базовой структурой Helm (или значением, переданным в partial, если говорить об использовании $
внутри partial'а). Иллюстрация отличия:
context: {{ . }}
dollar: {{ $ }}
with:
{{- with .Chart }}
context: {{ . }}
dollar: {{ $ }}
{{- end }}
template:
{{- template "flant" .Chart -}}
{{ define "flant" }}
context: {{ . }}
dollar: {{ $ }}
with:
{{- with .Name }}
context: {{ . }}
dollar: {{ $ }}
{{- end }}
{{- end -}}
В результате обработки этого шаблона получится следующее (для наглядности в выводе структуры заменены на соответствующие псевдоимена):
context: #Базовая структура helm
dollar: #Базовая структура helm
with:
context: #.Chart
dollar: #Базовая структура helm
template:
context: #.Chart
dollar: #.Chart
with:
context: habr
dollar: #.Chart
А вот реальный пример использования данной особенности:
{{- if .Values.ingress.enabled -}}
{{- range .Values.ingress.hosts }}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{ template "nats.fullname" $ }}-monitoring
labels:
app: "{{ template "nats.name" $ }}"
chart: "{{ template "nats.chart" $ }}"
release: {{ $.Release.Name | quote }}
heritage: {{ $.Release.Service | quote }}
annotations:
{{- if .tls }}
ingress.kubernetes.io/secure-backends: "true"
{{- end }}
{{- range $key, $value := .annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
spec:
rules:
- host: {{ .name }}
http:
paths:
- path: {{ default "/" .path }}
backend:
serviceName: {{ template "nats.fullname" $ }}-monitoring
servicePort: monitoring
{{- if .tls }}
tls:
- hosts:
- {{ .name }}
secretName: {{ .tlsSecret }}
{{- end }}
---
{{- end }}
{{- end }}
При разработке шаблонов могут оставаться лишние отступы: пробелы, табуляции, переводы строк. С ними файл попросту выглядит более читабельным. Можно либо отказаться от них, либо использовать специальный синтаксис для удаления отступов вокруг используемых шаблонов:
{{- variable }}
обрезает предшествующие пробелы;{{ variable -}}
обрезает последующие пробелы;{{- variable -}}
— оба варианта.
Пример файла, результатом обработки которого будет строка habr flant helm
:
habr
{{- " flant " -}}
helm
Со всеми функциями, встроенными в шаблон, можно ознакомиться по следующей ссылке [10]. Здесь же я расскажу только о некоторых из них.
Функция index
предназначена для доступа к элементам массива или карт:
definitions.json: |
{
"users": [
{
"name": "{{ index .Values "rabbitmq-ha" "rabbitmqUsername" }}",
"password": "{{ index .Values "rabbitmq-ha" "rabbitmqPassword" }}",
"tags": "administrator"
}
]
}
Функция принимает произвольное количество аргументов, что позволяет работать с вложенными элементами:
$map["key1"]["key2"]["key3"] => index $map "key1" "key2" "key3"
Например:
httpGet:
{{- if (index .Values "pushgateway" "extraArgs" "web.route-prefix") }}
path: /{{ index .Values "pushgateway" "extraArgs" "web.route-prefix" }}/#/status
{{- end }}
Булевые операции реализованы в шаблонизаторе как функции (а не как операторы). Все аргументы для них вычисляются при передаче:
{{ if and (index .Values field) (eq (len .Values.field) 10) }}
...
{{ end }}
При отсутствии поля field
рендер шаблона завершится с ошибкой (error calling len: len of untyped nil
): второе условие проверяется, несмотря на то, что первое не выполнилось. Стоит взять это на заметку, а подобные запросы решать за счёт разбиения на несколько проверок:
{{ if index . field }}
{{ if eq (len .field) 10 }}
...
{{ end }}
{{ end }}
Pipeline — это уникальная функция Go-шаблонов, позволяющая объявлять выражения, которые выполняются подобно конвейеру в shell. Формально конвейер представляет собой цепочку команд, разделенных символом |
. Команда может быть простым значением или вызовом функции. Результат каждой команды передаётся в качестве последнего аргумента следующей команде, а результатом конечной команды в конвейере является значение всего конвейера. Примеры:
data:
openssl.conf: |
{{ .Files.Get "config/openssl.conf" | indent 4 }}
data:
db-password: {{ .Values.externalDatabase.password | b64enc | quote }}
Sprig [11] — библиотека, состоящая из 70 полезных функций для решения широкого спектра задач. Из соображений безопасности в Helm исключены функции env
и expandenv
, которые предоставляли бы доступ к переменным окружения Tiller.
Функция include
, как и стандартная функция template
, используется для переиспользования шаблонов. В отличие от template
, функцию можно использовать в pipeline, т.е. передавать результат в другую функцию:
metadata:
labels:
{{ include "labels.standard" . | indent 4 }}
{{- define "labels.standard" -}}
app: {{ include "hlf-couchdb.name" . }}
heritage: {{ .Release.Service | quote }}
release: {{ .Release.Name | quote }}
chart: {{ include "hlf-couchdb.chart" . }}
{{- end -}}
Функция required
даёт разработчикам возможность объявлять обязательные значения, необходимые для рендеринга шаблона: если значение существует, при рендере шаблона оно используется, в противном же случае рендер завершается с указанным разработчиком сообщением об ошибке:
sftp-user: {{ required "Please specify the SFTP user name at .Values.sftp.user" .Values.sftp.user | b64enc | quote }}
sftp-password: {{ required "Please specify the SFTP user password at .Values.sftp.password" .Values.sftp.password | b64enc | quote }}
{{- end }}
{{- if .Values.svn.enabled }}
svn-user: {{ required "Please specify the SVN user name at .Values.svn.user" .Values.svn.user | b64enc | quote }}
svn-password: {{ required "Please specify the SVN user password at .Values.svn.password" .Values.svn.password | b64enc | quote }}
{{- end }}
{{- if .Values.webdav.enabled }}
webdav-user: {{ required "Please specify the WebDAV user name at .Values.webdav.user" .Values.webdav.user | b64enc | quote }}
webdav-password: {{ required "Please specify the WebDAV user password at .Values.webdav.password" .Values.webdav.password | b64enc | quote }}
{{- end }}
Функция tpl
позволяет рендерить строку как шаблон. В отличие от template
и include
, функция позволяет выполнять шаблоны, которые передаются в переменных, а также рендерить шаблоны, хранящиеся не только в директории templates
. Как это выглядит?
Выполнение шаблонов из переменных:
containers:
{{- with .Values.keycloak.extraContainers }}
{{ tpl . $ | indent 2 }}
{{- end }}
… а в values.yaml
имеем следующее значение:
keycloak:
extraContainers: |
- name: cloudsql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.11
command:
- /cloud_sql_proxy
args:
- -instances={{ .Values.cloudsql.project }}:{{ .Values.cloudsql.region }}:{{ .Values.cloudsql.instance }}=tcp:5432
- -credential_file=/secrets/cloudsql/credentials.json
volumeMounts:
- name: cloudsql-creds
mountPath: /secrets/cloudsql
readOnly: true
Рендер файла, хранящегося вне директории templates
:
apiVersion: batch/v1
kind: Job
metadata:
name: {{ template "mysqldump.fullname" . }}
labels:
app: {{ template "mysqldump.name" . }}
chart: {{ template "mysqldump.chart" . }}
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
spec:
backoffLimit: 1
template:
{{ $file := .Files.Get "files/job.tpl" }}
{{ tpl $file . | indent 4 }}
… в чарте, по пути files/job.tpl
, имеется следующий шаблон:
spec:
containers:
- name: xtrabackup
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy | quote }}
command: ["/bin/bash", "/scripts/backup.sh"]
envFrom:
- configMapRef:
name: "{{ template "mysqldump.fullname" . }}"
- secretRef:
name: "{{ template "mysqldump.fullname" . }}"
volumeMounts:
- name: backups
mountPath: /backup
- name: xtrabackup-script
mountPath: /scripts
restartPolicy: Never
volumes:
- name: backups
{{- if .Values.persistentVolumeClaim }}
persistentVolumeClaim:
claimName: {{ .Values.persistentVolumeClaim }}
{{- else -}}
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ template "mysqldump.fullname" . }}
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}
- name: xtrabackup-script
configMap:
name: {{ template "mysqldump.fullname" . }}-script
На этом знакомство с азами шаблонизации в Helm подошло к концу…
В статье рассказано о структуре Helm-чартов и подробно разобрана главная сложность в их создании — шаблонизация: основные принципы, синтаксис, функции и операторы Go-шаблонизатора, дополнительные функции.
Как начать со всем этим работать? Поскольку Helm — это уже целая экосистема, всегда можно посмотреть на примеры чартов схожих пакетов. Например, если вы хотите запаковать новый message queue, взгляните на публичный чарт RabbitMQ [12]. Конечно, никто не обещает вам идеальных реализаций в уже существующих пакетах, однако они отлично подойдут как отправная точка. Остальное же приходит с практикой, в которой вам помогут команды отладки helm template
и helm lint
, а также запуск инсталяции с опцией --dry-run
.
Для получения более обширного представления о разработке Helm-чартов, лучших практиках и используемых технологиях предлагаю ознакомиться с материалами по следующим ссылкам (все на английском языке):
А в конце очередного материала про Helm прикрепляю опрос, который поможет лучше понять, какие ещё статьи о Helm ждут (или не ждут?) читатели Хабра. Спасибо за внимание!
Читайте также в нашем блоге:
Автор: Алексей Игрычев
Источник [18]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/open-source/292965
Ссылки в тексте:
[1] прошлой статье: https://habr.com/company/flant/blog/420437/
[2] официальному руководству разработчика чарта: https://docs.helm.sh/chart_template_guide/
[3] официальном репозитории: https://github.com/helm/charts
[4] Go-шаблонизатор: https://golang.org/pkg/text/template/
[5] здесь: https://habr.com/company/flant/blog/417079/
[6] данных релиза: https://github.com/helm/helm/blob/e8d80729ac20f402a80c5107b8b2513008de13fc/pkg/chartutil/values.go#L362-L370
[7] информации о чарте: https://github.com/helm/helm/blob/250d25fdceaf5546f29228f3711cedd8d6776fcd/pkg/proto/hapi/chart/metadata.pb.go#L75-L112
[8] ссылке: https://github.com/helm/helm/blob/f7f686f7d065218ef6df3fbb75ce6348e699a0f3/pkg/chartutil/files.go
[9] информации о кластере: https://github.com/helm/helm/blob/89c29f3ff107bd107a3a115c69e41bcf89422bc0/pkg/chartutil/capabilities.go#L42
[10] следующей ссылке: https://golang.org/pkg/text/template/#hdr-Functions
[11] Sprig: http://masterminds.github.io/sprig/
[12] публичный чарт RabbitMQ: https://github.com/helm/charts/tree/master/stable/rabbitmq
[13] Лучшие практики: https://docs.helm.sh/chart_best_practices/
[14] Документация Go-шаблонизатора: https://golang.org/pkg/text/template
[15] Практика с dapp. Часть 2. Деплой Docker-образов в Kubernetes с помощью Helm: https://habr.com/company/flant/blog/336170/
[16] Сборка и дeплой приложений в Kubernetes с помощью dapp и GitLab CI: https://habr.com/company/flant/blog/345580/
[17] Лучшие практики CI/CD с Kubernetes и GitLab: https://habr.com/company/flant/blog/345116/
[18] Источник: https://habr.com/post/423239/?utm_source=habrahabr&utm_medium=rss&utm_campaign=423239
Нажмите здесь для печати.