
Nous avons parlé de Helm et de travailler avec lui «en général» dans un
article précédent . Maintenant, approchons la pratique de l'autre côté - du point de vue du créateur des graphiques (c'est-à-dire des packages pour Helm). Et bien que cet article provienne du monde de l'exploitation, il s'est avéré être plus similaire aux documents sur les langages de programmation - tel est le sort des auteurs des graphiques. Ainsi, un graphique est une collection de fichiers ...
Les fichiers graphiques peuvent être divisés en deux groupes:
- Fichiers requis pour générer des manifestes de ressources Kubernetes. Il s'agit notamment des modèles du répertoire des
templates
et des fichiers avec des valeurs (les valeurs par défaut sont stockées dans values.yaml
). Ce fichier requirements.yaml
fichier requirements.yaml
et le répertoire des charts
- tout cela est utilisé pour organiser les graphiques imbriqués. - Fichiers d'accompagnement contenant des informations qui peuvent être utiles pour trouver des graphiques, les connaître et les utiliser. La plupart des fichiers de ce groupe sont facultatifs.
Plus d'informations sur les fichiers des deux groupes:
Chart.yaml
- fichier contenant des informations sur le graphique;LICENSE
- un fichier texte optionnel avec une licence graphique;README.md
- un fichier optionnel avec documentation;requirements.yaml
- un fichier facultatif avec une liste de graphiques de dépendance;values.yaml
- fichier avec les valeurs par défaut pour les modèles;charts/
- un répertoire optionnel avec des graphiques imbriqués;- répertoire
templates/
- avec les modèles de manifeste de ressources Kubernetes; templates/NOTES.txt
- un fichier texte facultatif avec une note qui est affichée à l'utilisateur pendant l'installation et la mise à jour.
Pour mieux comprendre le contenu de ces fichiers, vous pouvez vous référer au
guide officiel du développeur de graphiques ou rechercher des exemples pertinents dans le
référentiel officiel .
La création d'un graphique revient en gros à organiser un ensemble de fichiers correctement conçu. Et la principale difficulté de cette «conception» est l'utilisation d'un système de modèles assez avancé pour obtenir le résultat souhaité. Pour le rendu des manifestes de ressources Kubernetes, un
moteur de modèle Go standard est utilisé , étendu par les fonctions Helm .
Rappel : les développeurs de Helm ont annoncé que dans la prochaine version majeure du projet - Helm 3 - il y aura un support pour les scripts Lua, qui peuvent être utilisés simultanément avec les Go-templates. Je ne m'attarderai pas sur ce point plus en détail - ceci (et d'autres changements dans Helm 3) peuvent être lus ici .Par exemple, voici à quoi ressemble Helm 2 comme le modèle de manifeste Kubernetes du
déploiement pour un blog WordPress d'un
article précédent :
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 }}
Maintenant - sur les principes de base et les caractéristiques de la normalisation dans Helm. La plupart des exemples ci-dessous sont tirés des graphiques du
référentiel officiel .
Templating
Modèles: {{ }}
Tout ce qui concerne les modèles est enveloppé de doubles accolades. Le texte en dehors des accolades reste inchangé pendant le rendu.
Valeur de contexte :.
Lors du rendu d'un fichier ou partiel
(pour plus d'informations sur la réutilisation des modèles, voir les sections suivantes de l'article) , la valeur qui est accessible en interne via la variable de contexte - le point est levé. Lorsqu'il est passé comme argument à une structure, le point est utilisé pour accéder aux champs et aux méthodes de cette structure.
La valeur de la variable change pendant le processus de rendu en
fonction du contexte dans lequel elle est utilisée. La plupart des instructions de bloc remplacent la variable de contexte à l'intérieur du bloc principal.
Les principaux opérateurs et leurs caractéristiques seront discutés ci-dessous, après avoir pris connaissance de la structure de base de Helm.La structure de base de Helm
Lors du rendu des manifestes, une structure avec les champs suivants est jetée dans les modèles:
- Champ
.Values
- pour accéder aux paramètres qui sont déterminés lors de l'installation et de la mise à jour de la version. Il s'agit notamment des valeurs des options --set
, --set-string
et --set-file
, ainsi que des paramètres des fichiers avec valeurs, du fichier values.yaml
et des fichiers correspondant aux valeurs des options --values
:
containers: - name: main image: "{{ .Values.image }}:{{ .Values.imageTag }}" imagePullPolicy: {{ .Values.imagePullPolicy }}
.Release
- pour utiliser les données de version sur le déploiement, l'installation ou la mise à jour, le nom de la version, l'espace de noms et les valeurs de plusieurs autres champs qui peuvent être utiles lors de la génération de manifestes:
metadata: labels: heritage: "{{ .Release.Service }}" release: "{{ .Release.Name }}" subjects: - namespace: {{ .Release.Namespace }}
.Chart
- pour accéder aux informations du graphique . Les champs correspondent au contenu du fichier Chart.yaml
:
labels: chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
- Structure
.Files
- pour travailler avec des fichiers stockés dans le répertoire du graphique ; la structure et les méthodes disponibles peuvent être trouvées ici . Exemples:
data: openssl.conf: | {{ .Files.Get "config/openssl.conf" | indent 4 }}
data: {{ (.Files.Glob "files/docker-entrypoint-initdb.d/*").AsConfig | indent 2 }}
.Capabilities
- pour accéder aux informations sur le cluster dans lequel le .Capabilities
est effectué:
{{- 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 }}
Les opérateurs
Nous commençons, bien sûr, par les
else
if
,
else if
et
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 }}
L'opérateur de
range
est conçu pour fonctionner avec des tableaux et des cartes. Si un tableau est passé en argument et qu'il contient des éléments, alors pour chaque élément un bloc est exécuté séquentiellement (dans ce cas, la valeur à l'intérieur du bloc devient disponible via la variable de contexte):
{{- range .Values.ports }} - name: {{ .name }} port: {{ .containerPort }} targetPort: {{ .containerPort}} {{- else }} ... {{- end}}
{{ range .Values.tolerations -}} - {{ toYaml . | indent 8 | trim }} {{ end }}
Pour travailler avec des cartes, une syntaxe avec des variables est fournie:
{{- range $key, $value := .Values.credentials.secretContents }} {{ $key }}: {{ $value | b64enc | quote }} {{- end }}
Un comportement similaire se produit
with
opérateur:: si l'argument passé existe, le bloc est exécuté et la variable de contexte dans le bloc correspond à la valeur de l'argument. Par exemple:
{{- with .config }} config: {{- with .region }} region: {{ . }} {{- end }} {{- with .s3ForcePathStyle }} s3ForcePathStyle: {{ . }} {{- end }} {{- with .s3Url }} s3Url: {{ . }} {{- end }} {{- with .kmsKeyId }} kmsKeyId: {{ . }} {{- end }} {{- end }}
Pour réutiliser les modèles, un ensemble de
define [name]
et
template [name] [variable]
peut être utilisé, où la valeur transmise est rendue disponible via la variable de contexte dans le bloc de
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 -}}
Quelques fonctionnalités à considérer lors de l'utilisation de
define
, ou, plus simplement, de partial'ov:
- Les partial'y déclarés sont globaux et peuvent être utilisés dans tous les fichiers du répertoire des
templates
. - Le graphique principal est compilé avec les graphiques dépendants, donc s'il y a deux noms partiels partiels du même type, le dernier chargé sera utilisé. Lorsque vous nommez un partiel, il est habituel d'ajouter un nom de graphique pour éviter de tels conflits:
define "chart_name.partial_name"
.
Variables: $
En plus de travailler avec le contexte, vous pouvez stocker, modifier et réutiliser des données à l'aide de variables:
{{ $provider := .Values.configuration.backupStorageProvider.name }} ... {{ if eq $provider "azure" }} envFrom: - secretRef: name: {{ template "ark.secretName" . }} {{ end }}
Lors du rendu d'un fichier ou partiel,
$
a la même signification que le point. Mais contrairement à la variable de contexte (point), la valeur de
$
ne change pas dans le contexte des instructions de bloc , ce qui vous permet de travailler simultanément avec la valeur de contexte de l'instruction de bloc et la structure de base de Helm (ou la valeur passée à partial, si nous parlons d'utiliser
$
intérieur de partial'a) . Illustration de la différence:
context: {{ . }} dollar: {{ $ }} with: {{- with .Chart }} context: {{ . }} dollar: {{ $ }} {{- end }} template: {{- template "flant" .Chart -}} {{ define "flant" }} context: {{ . }} dollar: {{ $ }} with: {{- with .Name }} context: {{ . }} dollar: {{ $ }} {{- end }} {{- end -}}
À la suite du traitement de ce modèle, les éléments suivants se révéleront (pour plus de clarté, dans la sortie de la structure sont remplacés par les pseudo-noms correspondants):
context: # helm dollar: # helm with: context: #.Chart dollar: # helm template: context: #.Chart dollar: #.Chart with: context: habr dollar: #.Chart
Et voici un véritable exemple d'utilisation de cette fonctionnalité:
{{- 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 }}
Indentation
Lors du développement de modèles, des marges supplémentaires peuvent rester: espaces, tabulations, sauts de ligne. Avec eux, le fichier semble simplement plus lisible. Vous pouvez soit les abandonner, soit utiliser une syntaxe spéciale pour supprimer l'indentation autour des modèles utilisés:
{{- variable }}
tronque les espaces précédents;{{ variable -}}
tronque les espaces suivants;{{- variable -}}
sont les deux options.
Un exemple de fichier dont le traitement sera la ligne
habr flant helm
:
habr {{- " flant " -}} helm
Fonctions intégrées
Toutes les fonctions intégrées au modèle se trouvent sur le
lien suivant . Ici, je ne parlerai que de certains d'entre eux.
La fonction d'
index
est conçue pour accéder aux éléments d'un tableau ou de cartes:
definitions.json: | { "users": [ { "name": "{{ index .Values "rabbitmq-ha" "rabbitmqUsername" }}", "password": "{{ index .Values "rabbitmq-ha" "rabbitmqPassword" }}", "tags": "administrator" } ] }
La fonction prend un nombre arbitraire d'arguments, ce qui vous permet de travailler avec des éléments imbriqués:
$map["key1"]["key2"]["key3"] => index $map "key1" "key2" "key3"
Par exemple:
httpGet: {{- if (index .Values "pushgateway" "extraArgs" "web.route-prefix") }} path: /{{ index .Values "pushgateway" "extraArgs" "web.route-prefix" }}/#/status {{- end }}
Les opérations booléennes sont implémentées dans le moteur de modèle en tant que fonctions (et
non en tant qu'opérateurs). Tous les arguments pour eux sont évalués lors du passage:
{{ if and (index .Values field) (eq (len .Values.field) 10) }} ... {{ end }}
S'il n'y a pas de
field
rendu
field
modèle échouera (
error calling len: len of untyped nil
): la deuxième condition est vérifiée, malgré le fait que la première n'a pas été remplie. Cela vaut la peine de prendre une note et de résoudre ces requêtes en se divisant en plusieurs contrôles:
{{ if index . field }} {{ if eq (len .field) 10 }} ... {{ end }} {{ end }}
Le pipeline est une fonctionnalité unique des modèles Go qui vous permet de déclarer des expressions qui s'exécutent comme un pipeline dans un shell. Formellement, le pipeline est une chaîne de commandes séparées par le symbole
|
. Une commande peut être une
valeur simple
ou un appel de fonction . Le résultat de chaque commande est transmis comme
dernier argument à la commande suivante , et le résultat de la commande finale dans le pipeline est la valeur de l'ensemble du pipeline. Exemples:
data: openssl.conf: | {{ .Files.Get "config/openssl.conf" | indent 4 }}
data: db-password: {{ .Values.externalDatabase.password | b64enc | quote }}
Fonctions supplémentaires
Sprig est une bibliothèque de
70 fonctionnalités utiles pour résoudre un large éventail de tâches. Pour des raisons de sécurité, Helm exclut les fonctions
env
et
expandenv
qui donnent accès aux variables d'environnement Tiller.
La fonction
include
, comme la fonction de
template
standard, est utilisée pour réutiliser les modèles. Contrairement au
template
, la fonction peut être utilisée dans le pipeline, c'est-à-dire passer le résultat à une autre fonction:
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 -}}
La fonction
required
donne aux développeurs la possibilité de déclarer les valeurs requises nécessaires au rendu du modèle: si la valeur existe, elle est utilisée lors du rendu du modèle, sinon le rendu se termine avec le message d'erreur indiqué par le développeur:
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 }}
La fonction
tpl
vous permet de rendre une chaîne en tant que modèle. Contrairement au
template
et à l'
include
, la fonction vous permet d'exécuter des modèles transmis dans des variables, ainsi que des modèles de rendu stockés non seulement dans le répertoire des
templates
. À quoi ça ressemble?
Exécution de modèles à partir de variables:
containers: {{- with .Values.keycloak.extraContainers }} {{ tpl . $ | indent 2 }} {{- end }}
... et dans
values.yaml
nous avons la valeur suivante:
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
Rendu d'un fichier stocké en dehors du répertoire des
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 }}
... sur le graphique, le long du chemin
files/job.tpl
, se trouve le modèle suivant:
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
C'est là que l'introduction aux bases de la normalisation dans Helm a pris fin ...
Conclusion
L'article décrit la structure des graphiques Helm et examine en détail la principale difficulté de leur création - modèle: principes de base, syntaxe, fonctions et opérateurs Go-template, fonctions supplémentaires.
Comment commencer à travailler avec tout ça? Étant donné que Helm est déjà un écosystème complet, vous pouvez toujours consulter des exemples de graphiques pour des packages similaires. Par exemple, si vous souhaitez emballer une nouvelle file d'attente de messages, jetez un œil au
graphique public RabbitMQ . Bien sûr, personne ne vous promet des implémentations idéales dans des packages existants, mais elles sont parfaites comme point de départ. Le reste est livré avec de la pratique, dans laquelle le
helm template
et les
helm lint
débogage de helm lint
vous aideront, ainsi que de démarrer l'installation avec l'option
--dry-run
.
Pour une compréhension plus large du développement des cartes Helm, des meilleures pratiques et des technologies utilisées, je vous suggère de vous familiariser avec les documents sur les liens suivants (tous en anglais):
Et à la fin du prochain article Helm, je joins une enquête qui aidera à mieux comprendre quels autres articles sur Helm les lecteurs Habr attendent (ou n'attendent pas?) Merci de votre attention!
PS
Lisez aussi dans notre blog: