Создание пакетов для Kubernetes с Helm: структура чарта и шаблонизация

от автора

Про Helm и работу с ним «в общем» мы рассказали в прошлой статье. Теперь подойдём к практике с другой стороны — с точки зрения создателя чартов (т.е. пакетов для Helm). И хотя эта статья пришла из мира эксплуатации, она получилась больше похожей на материалы о языках программирования — такова уж участь авторов чартов. Итак, чарт — это набор файлов…

Файлы чарта можно разделить на две группы:

  1. Файлы, необходимые для генерации манифестов Kubernetes-ресурсов. К ним относятся шаблоны из директории templates и файлы со значениями (по умолчанию значения хранятся в values.yaml). Также к данной группе относятся файл requirements.yaml и директория charts — всё это используется для организации вложенных чартов.
  2. Сопроводительные файлы, содержащие информацию, которая может быть полезна при поиске чартов, знакомстве с ними и их использовании. Большая часть файлов этой группы является необязательной.

Подробнее о файлах обеих групп:

  • Chart.yaml — файл с информацией о чарте;
  • LICENSE — необязательный текстовый файл с лицензией чарта;
  • README.md — необязательный файл с документацией;
  • requirements.yaml — необязательный файл со списком чартов-зависимостей;
  • values.yaml — файл со значениями по умолчанию для шаблонов;
  • charts/ — необязательная директория со вложенными чартами;
  • templates/ — директория с шаблонами манифестов Kubernetes-ресурсов;
  • templates/NOTES.txt — необязательный текстовый файл с примечанием, которое выводится пользователю при инсталяции и обновлении.

Чтобы лучше разобраться в содержимом этих файлов, можно обратиться к официальному руководству разработчика чарта или поискать соответствующие примеры в официальном репозитории.

Создание чарта по большому счёту сводится к организации правильно оформленного набора файлов. И главная сложность в этом «оформлении» — использование достаточно продвинутой системы шаблонов для достижения нужного результата. Для рендера манифестов Kubernetes-ресурсов используется стандартный Go-шаблонизатор, расширенный функциями Helm.

Напоминание: Разработчики Helm анонсировали, что в следующей крупной версии проекта — Helm 3 — появится поддержка Lua-скриптов, которые можно будет использовать одновременно с Go-шаблонами. Останавливаться gодробнее на этом моменте не буду — об этом (и других изменениях в Helm 3) можно почитать здесь.

К примеру, вот так в Helm 2 выглядит шаблон Kubernetes-манифеста Deployment‘а блога на WordPress из прошлой статьи:

deployment.yaml

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. Большая часть приведённых ниже примеров взята из чартов официального репозитория.

Шаблонизация

Шаблоны: {{ }}

Всё, что связано с шаблонизацией, оборачивается в двойные фигурные скобки. Текст вне фигурных скобок при рендере остаётся неизменным.

Значение контекста: .

При рендере файла или partial’а (подробнее о переиспользовании шаблонов рассказывается в следующих разделах статьи) прокидывается значение, которое становится доступным внутри через переменную контекста — точку. При передаче в качестве аргумента структуры точка используется для доступа к полям и методам этой структуры.

Значение переменной изменяется в процессе рендера в зависимости от контекста, в котором она используется. Большинство блочных операторов переопределяет переменную контекста внутри основного блока. (Основные операторы и их особенности будут рассмотрены ниже, после знакомства с базовой структурой Helm.)

Базовая структура Helm

При рендере манифестов в шаблоны прокидывается структура со следующими полями:

  • Поле .Values — для доступа к параметрам, которые определяются при инсталяции и обновлении релиза. К ним относятся значения опций --set, --set-string и --set-file, а также параметры файлов со значeниями, файл values.yaml и файлы, соответствующие значениям опций --values:
    containers: - name: main   image: "{{ .Values.image }}:{{ .Values.imageTag }}"   imagePullPolicy: {{ .Values.imagePullPolicy }} 
  • .Release — для использования данных релиза о выкате, инсталяции или обновлении, имени релиза, namespace и значений ещё нескольких полей, которые могут пригодиться при генерации манифестов:
    metadata:   labels:     heritage: "{{ .Release.Service }}"     release: "{{ .Release.Name }}" subjects: - namespace: {{ .Release.Namespace }} 
  • .Chart — для доступа к информации о чарте. Поля соответствуют содержимому файла Chart.yaml:
    labels:   chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 
  • Структура .Files — для работы с хранящимися в директории чарта файлами; со структурой и доступными методами можно ознакомиться по ссылке. Примеры:
    data:   openssl.conf: | {{ .Files.Get "config/openssl.conf" | indent 4 }} 

    data: {{ (.Files.Glob "files/docker-entrypoint-initdb.d/*").AsConfig | indent 2 }} 
  • .Capabilities — для доступа к информации о кластере, в котором выполняется выкат:
    {{- 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’ов:

  • Объявленные partial’ы являются глобальными и могут использоваться во всех файлах директории templates.
  • Основной чарт компилируется вместе с зависимыми чартами, поэтому при существовании двух одноимённых partial’ов будет использоваться последний загруженный. При именовании partial’а принято добавлять имя чарта для избежания подобных конфликтов: 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 

Встроенные функции

С функциями, доступными в шаблонах, можно ознакомиться по следующей ссылке. Здесь же я расскажу только о некоторых из них.

Функция 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 — библиотека, состоящая из 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. Конечно, никто не обещает вам идеальных реализаций в уже существующих пакетах, однако они отлично подойдут как отправная точка. Остальное же приходит с практикой, в которой вам помогут команды отладки helm template и helm lint, а также запуск инсталяции с опцией --dry-run.

Для получения более обширного представления о разработке Helm-чартов, лучших практиках и используемых технологиях предлагаю ознакомиться с материалами по следующим ссылкам (все на английском языке):

А в конце очередного материала про Helm прикрепляю опрос, который поможет лучше понять, какие ещё статьи о Helm ждут (или не ждут?) читатели хабры. Спасибо за внимание!

P.S.

Читайте также в нашем блоге:


ссылка на оригинал статьи https://habr.com/post/423239/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *