
Hablamos sobre Helm y trabajamos con él "en general" en un
artículo anterior . Ahora acerquémonos a la práctica desde el otro lado, desde el punto de vista del creador de los gráficos (es decir, paquetes para Helm). Y aunque este artículo proviene del mundo de la explotación, resultó ser más similar a los materiales sobre lenguajes de programación, tal es el destino de los autores de las listas. Entonces, un gráfico es una colección de archivos ...
Los archivos de gráficos se pueden dividir en dos grupos:
- Archivos necesarios para generar manifiestos de recursos de Kubernetes. Estos incluyen plantillas del directorio de
templates
y archivos con valores (los valores predeterminados se almacenan en values.yaml
). También en este grupo se encuentran el archivo require.yaml y el directorio de charts
: todo esto se utiliza para organizar gráficos anidados. - Acompañando archivos que contienen información que puede ser útil para encontrar gráficos, conocerlos y usarlos. La mayoría de los archivos en este grupo son opcionales.
Más información sobre los archivos de ambos grupos:
Chart.yaml
: archivo con información sobre el gráfico;LICENSE
: un archivo de texto opcional con una licencia de gráfico;README.md
: un archivo opcional con documentación;requirements.yaml
- un archivo opcional con una lista de gráficos de dependencia;values.yaml
: archivo con valores predeterminados para plantillas;charts/
- un directorio opcional con gráficos anidados;templates/
- directorio con plantillas de manifiesto de recursos de Kubernetes;templates/NOTES.txt
: un archivo de texto opcional con una nota que se muestra al usuario durante la instalación y actualización.
Para comprender mejor el contenido de estos archivos, puede consultar la
guía oficial para desarrolladores de gráficos o buscar ejemplos relevantes en el
repositorio oficial .
En general, crear un gráfico se reduce a organizar un conjunto de archivos correctamente diseñado. Y la principal dificultad en este "diseño" es el uso de un sistema de plantillas bastante avanzado para lograr el resultado deseado. Para representar los manifiestos de recursos de Kubernetes,
se utiliza un motor de plantillas Go estándar, ampliado por las funciones de Helm .
Recordatorio : los desarrolladores de Helm anunciaron que en la próxima versión principal del proyecto, Helm 3, habrá soporte para los scripts Lua, que pueden usarse simultáneamente con las plantillas Go. No me detendré en este punto con más detalle: este (y otros cambios en Helm 3) se pueden leer aquí .Por ejemplo, así es como Helm 2 se parece a la plantilla de manifiesto Kubernetes de la
implementación para un blog de WordPress de un
artículo anterior :
despliegue.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 }}
Ahora, sobre los principios básicos y las características de la estandarización en Helm. La mayoría de los ejemplos a continuación están tomados de los gráficos del
repositorio oficial .
Templar
Plantillas: {{ }}
Todo lo relacionado con la plantilla está envuelto en llaves dobles. El texto fuera de los corchetes permanece sin cambios durante el renderizado.
Valor de contexto :.
Al representar un archivo o parcial
(para obtener más información sobre la reutilización de plantillas, consulte las siguientes secciones del artículo) , el valor que se puede acceder internamente a través de la variable de contexto: se lanza el punto. Cuando se pasa como argumento a una estructura, el punto se usa para acceder a los campos y métodos de esta estructura.
El valor de la variable cambia durante el proceso de representación
dependiendo del contexto en el que se usa. La mayoría de las declaraciones de bloque anulan la variable de contexto dentro del bloque principal.
Los operadores principales y sus características se analizarán a continuación, después de familiarizarse con la estructura básica de Helm.La estructura básica de Helm
Al representar manifiestos, se arroja una estructura con los siguientes campos en las plantillas:
.Values
campo: para acceder a los parámetros que se determinan durante la instalación y actualización de la versión. Estos incluyen los valores de las opciones --set
, --set-string
y --set-file
, así como los parámetros de los archivos con valores, el archivo values.yaml
y los archivos correspondientes a los valores de las opciones --values
:
containers: - name: main image: "{{ .Values.image }}:{{ .Values.imageTag }}" imagePullPolicy: {{ .Values.imagePullPolicy }}
.Release
- para usar datos de lanzamiento sobre despliegue, instalación o actualización, nombre de lanzamiento, espacio de nombres y los valores de varios campos más que pueden ser útiles al generar manifiestos:
metadata: labels: heritage: "{{ .Release.Service }}" release: "{{ .Release.Name }}" subjects: - namespace: {{ .Release.Namespace }}
.Chart
: para acceder a la información del gráfico . Los campos corresponden al contenido del archivo Chart.yaml
:
labels: chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
- Estructura.
.Files
: para trabajar con archivos almacenados en el directorio de gráficos ; La estructura y los métodos disponibles se pueden encontrar aquí . Ejemplos:
data: openssl.conf: | {{ .Files.Get "config/openssl.conf" | indent 4 }}
data: {{ (.Files.Glob "files/docker-entrypoint-initdb.d/*").AsConfig | indent 2 }}
.Capabilities
: para acceder a información sobre el clúster en el que se realiza la .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 }}
Operadores
Comenzamos, por supuesto, con las
else
if
,
else if
y
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 }}
El operador de
range
está diseñado para trabajar con matrices y mapas. Si se pasa una matriz como argumento y contiene elementos, entonces para cada elemento se ejecuta un bloque secuencialmente (en este caso, el valor dentro del bloque está disponible a través de la variable de contexto):
{{- range .Values.ports }} - name: {{ .name }} port: {{ .containerPort }} targetPort: {{ .containerPort}} {{- else }} ... {{- end}}
{{ range .Values.tolerations -}} - {{ toYaml . | indent 8 | trim }} {{ end }}
Para trabajar con mapas, se proporciona sintaxis con variables:
{{- range $key, $value := .Values.credentials.secretContents }} {{ $key }}: {{ $value | b64enc | quote }} {{- end }}
Un comportamiento similar es
with
operador: si existe el argumento pasado, el bloque se ejecuta y la variable de contexto en el bloque corresponde al valor del argumento. Por ejemplo:
{{- with .config }} config: {{- with .region }} region: {{ . }} {{- end }} {{- with .s3ForcePathStyle }} s3ForcePathStyle: {{ . }} {{- end }} {{- with .s3Url }} s3Url: {{ . }} {{- end }} {{- with .kmsKeyId }} kmsKeyId: {{ . }} {{- end }} {{- end }}
Para reutilizar las plantillas, se puede usar un paquete de
define [name]
y
template [name] [variable]
, donde el valor pasado está disponible a través de la variable de contexto en el bloque 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 -}}
Un par de características a tener en cuenta al usar
define
o, más simplemente, parcial'ov:
- Los declarados parcialmente son globales y se pueden usar en todos los archivos del directorio de
templates
. - El gráfico principal se compila junto con los gráficos dependientes, por lo que si hay dos nombres parciales parciales del mismo tipo, se utilizará el último cargado. Al nombrar un parcial, es habitual agregar un nombre de gráfico para evitar tales conflictos:
define "chart_name.partial_name"
.
Variables: $
Además de trabajar con el contexto, puede almacenar, modificar y reutilizar datos utilizando variables:
{{ $provider := .Values.configuration.backupStorageProvider.name }} ... {{ if eq $provider "azure" }} envFrom: - secretRef: name: {{ template "ark.secretName" . }} {{ end }}
Al representar un archivo o parcial,
$
tiene el mismo significado que el punto. Pero a diferencia de la variable de contexto (punto), el valor de
$
no cambia en el contexto de las declaraciones de bloque , lo que le permite trabajar simultáneamente con el valor de contexto de la declaración de bloque y la estructura básica de Helm (o el valor pasado a parcial, si hablamos de usar
$
inside partial'a) . Ilustración de diferencia:
context: {{ . }} dollar: {{ $ }} with: {{- with .Chart }} context: {{ . }} dollar: {{ $ }} {{- end }} template: {{- template "flant" .Chart -}} {{ define "flant" }} context: {{ . }} dollar: {{ $ }} with: {{- with .Name }} context: {{ . }} dollar: {{ $ }} {{- end }} {{- end -}}
Como resultado del procesamiento de esta plantilla, resultará lo siguiente (para mayor claridad, en la salida de la estructura se reemplazan con los seudo-nombres correspondientes):
context: # helm dollar: # helm with: context: #.Chart dollar: # helm template: context: #.Chart dollar: #.Chart with: context: habr dollar: #.Chart
Y aquí hay un ejemplo real del uso de esta función:
{{- 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 }}
Sangría
Al desarrollar plantillas, pueden quedar márgenes adicionales: espacios, pestañas, avances de línea. Con ellos, el archivo simplemente parece más legible. Puede abandonarlos o usar una sintaxis especial para eliminar la sangría alrededor de los patrones utilizados:
{{- variable }}
trunca espacios anteriores;{{ variable -}}
trunca los espacios subsiguientes;{{- variable -}}
son ambas opciones.
Un ejemplo de un archivo, cuyo procesamiento será la línea
habr flant helm
:
habr {{- " flant " -}} helm
Funciones incorporadas
Todas las funciones integradas en la plantilla se pueden encontrar en el
siguiente enlace . Aquí solo contaré algunos de ellos.
La función de
index
está diseñada para acceder a elementos de una matriz o mapas:
definitions.json: | { "users": [ { "name": "{{ index .Values "rabbitmq-ha" "rabbitmqUsername" }}", "password": "{{ index .Values "rabbitmq-ha" "rabbitmqPassword" }}", "tags": "administrator" } ] }
La función toma un número arbitrario de argumentos, lo que le permite trabajar con elementos anidados:
$map["key1"]["key2"]["key3"] => index $map "key1" "key2" "key3"
Por ejemplo:
httpGet: {{- if (index .Values "pushgateway" "extraArgs" "web.route-prefix") }} path: /{{ index .Values "pushgateway" "extraArgs" "web.route-prefix" }}/#/status {{- end }}
Las operaciones booleanas se implementan en el motor de plantillas como funciones (y
no como operadores). Todos los argumentos para ellos se evalúan al pasar:
{{ if and (index .Values field) (eq (len .Values.field) 10) }} ... {{ end }}
Si no hay campo de
field
representación de
field
plantilla fallará (
error calling len: len of untyped nil
): se verifica la segunda condición, a pesar de que la primera no se ha cumplido. Vale la pena tomar nota y resolver tales consultas dividiéndolos en varias comprobaciones:
{{ if index . field }} {{ if eq (len .field) 10 }} ... {{ end }} {{ end }}
Pipeline es una característica única de Go-templates que le permite declarar expresiones que se ejecutan como una tubería en un shell. Formalmente, la tubería es una cadena de comandos separados por el símbolo
|
. Un comando puede ser un
valor simple
o una llamada a una función . El resultado de cada comando se pasa como el
último argumento al siguiente comando , y el resultado del comando final en la tubería es el valor de toda la tubería. Ejemplos:
data: openssl.conf: | {{ .Files.Get "config/openssl.conf" | indent 4 }}
data: db-password: {{ .Values.externalDatabase.password | b64enc | quote }}
Funciones adicionales
Sprig es una biblioteca de
70 funciones útiles para resolver una amplia gama de tareas. Por razones de seguridad, Helm excluye las funciones
env
y
expandenv
que proporcionan acceso a las variables de entorno de Tiller.
La función de
include
, como la función de
template
estándar, se utiliza para reutilizar plantillas. A diferencia de la
template
, la función se puede usar en la tubería, es decir pasar el resultado a otra función:
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 función
required
brinda a los desarrolladores la oportunidad de declarar los valores necesarios para representar la plantilla: si el valor existe, se utiliza al representar la plantilla; de lo contrario, la representación finaliza con el mensaje de error indicado por el desarrollador:
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 función
tpl
permite representar una cadena como plantilla. A diferencia de
template
e
include
, la función le permite ejecutar plantillas que se pasan en variables, así como renderizar plantillas que se almacenan no solo en el directorio de
templates
. ¿Cómo se ve?
Ejecutar plantillas desde variables:
containers: {{- with .Values.keycloak.extraContainers }} {{ tpl . $ | indent 2 }} {{- end }}
... y en
values.yaml
tenemos el siguiente valor:
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
Renderizado de un archivo almacenado fuera del directorio de
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 }}
... en el gráfico, a lo largo de la ruta
files/job.tpl
, está la siguiente plantilla:
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
Aquí es donde la introducción a los conceptos básicos de la estandarización en Helm llegó a su fin ...
Conclusión
El artículo describe la estructura de los gráficos de Helm y examina en detalle la dificultad principal en su creación: plantilla: principios básicos, sintaxis, funciones y operadores Go-template, funciones adicionales.
¿Cómo empezar a trabajar con todo esto? Como Helm ya es un ecosistema completo, siempre puedes ver ejemplos de gráficos para paquetes similares. Por ejemplo, si desea empaquetar una nueva cola de mensajes, eche un vistazo al
gráfico público de RabbitMQ . Por supuesto, nadie le promete implementaciones ideales en los paquetes existentes, pero son perfectos como punto de partida. El resto viene con la práctica, en la que la
helm template
y los
comandos de depuración de helm lint
te ayudarán, así como comenzar la instalación con la opción
--dry-run
.
Para una comprensión más amplia del desarrollo de los gráficos de Helm, las mejores prácticas y las tecnologías utilizadas, le sugiero que se familiarice con los materiales en los siguientes enlaces (todos en inglés):
Y al final del próximo material de Helm, adjunto una encuesta que ayudará a comprender mejor qué otros artículos sobre Helm esperan los lectores de Habr (¿o no esperan?) Gracias por su atencion!
PS
Lea también en nuestro blog: