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

Создание пакетов для 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 3) можно почитать здесь. Напоминание: Разработчики Helm анонсировали, что в следующей крупной версии проекта — Helm 3 — появится поддержка Lua-скриптов, которые можно будет использовать одновременно с Go-шаблонами.

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

deployment.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata: name: } 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.

В отличие от template, функцию можно использовать в pipeline, т.е. Функция include, как и стандартная функция template, используется для переиспользования шаблонов. передавать результат в другую функцию:

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.

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


Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

Как распознавание лиц помогает находить тестовые телефоны

Привет, хабровчане! В EastBanc Technologies ведётся большое количество проектов, связанных с мобильной разработкой. В связи с чем необходим целый зоопарк устройств для тестирования на всех этапах. И, что характерно, каждый отдельный девайс постоянно оказывается нужен самым разным людям, а найти ...

Security Week 39: на смерть Google+

На прошлой неделе Google объявил (новость) о закрытии социальной сети Google+, но сделано это было достаточно необычно. Компания Google вообще не стесняется закрывать проекты, которые по разным причинам не взлетели. Многие до сих пор не могут простить компании отказа от ...