Compilar pacotes para o Kubernetes com o Helm: estrutura e modelo de gráfico



Falamos sobre Helm e trabalhamos com ele "em geral" em um artigo anterior . Agora, vamos abordar a prática do outro lado - do ponto de vista do criador dos gráficos (ou seja, pacotes para o Helm). E, embora este artigo tenha vindo do mundo da exploração, acabou sendo mais semelhante aos materiais sobre linguagens de programação - esse é o destino dos autores das tabelas. Portanto, um gráfico é uma coleção de arquivos ...

Os arquivos de gráfico podem ser divididos em dois grupos:

  1. Arquivos necessários para gerar manifestos de recursos do Kubernetes. Isso inclui modelos do diretório de templates e arquivos com valores (os valores padrão são armazenados em values.yaml ). Também neste grupo estão o arquivo requirements.yaml e o diretório de charts - tudo isso é usado para organizar gráficos aninhados.
  2. Acompanha arquivos contendo informações que podem ser úteis para encontrar gráficos, conhecê-los e usá-los. A maioria dos arquivos deste grupo é opcional.

Mais informações sobre os arquivos dos dois grupos:

  • Chart.yaml - arquivo com informações sobre o gráfico;
  • LICENSE - um arquivo de texto opcional com uma licença de gráfico;
  • README.md - um arquivo opcional com documentação;
  • requirements.yaml - um arquivo opcional com uma lista de gráficos de dependência;
  • values.yaml - arquivo com valores padrão para modelos;
  • charts/ - um diretório opcional com gráficos aninhados;
  • diretório templates/ - com modelos de manifesto de recursos do Kubernetes;
  • templates/NOTES.txt - um arquivo de texto opcional com uma nota exibida ao usuário durante a instalação e atualização.

Para entender melhor o conteúdo desses arquivos, você pode consultar o guia oficial do desenvolvedor de gráficos ou procurar exemplos relevantes no repositório oficial .

A criação geral de um gráfico se resume a organizar um conjunto de arquivos projetado corretamente. E a principal dificuldade nesse "design" é o uso de um sistema de modelos bastante avançado para alcançar o resultado desejado. Para renderizar manifestos de recursos do Kubernetes, é usado um mecanismo Go template padrão, estendido pelas funções Helm .

Lembrete : os desenvolvedores do Helm anunciaram que na próxima versão principal do projeto - Helm 3 - haverá suporte para scripts Lua, que podem ser usados simultaneamente com os modelos Go. Não vou me debruçar sobre esse ponto com mais detalhes - isso (e outras mudanças no Helm 3) pode ser lido aqui .

Por exemplo, veja como o Helm 2 se parece com o modelo de manifesto Kubernetes da implantação para um blog WordPress de um artigo anterior :

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 }} 

Agora - sobre os princípios e características básicas da padronização no Helm. A maioria dos exemplos abaixo é retirada dos gráficos do repositório oficial .

Templating


Modelos: {{ }}


Tudo relacionado à modelagem é envolto em chaves duplas. O texto fora dos colchetes permanece inalterado durante a renderização.

Valor de contexto :.


Ao renderizar um arquivo ou parcial (para obter mais informações sobre a reutilização de modelos, consulte as próximas seções do artigo) , o valor que é acessível internamente através da variável de contexto - o ponto é lançado. Quando passado como argumento para uma estrutura, o ponto é usado para acessar os campos e métodos dessa estrutura.

O valor da variável muda durante o processo de renderização, dependendo do contexto em que é usada. A maioria das instruções de bloco substitui a variável de contexto dentro do bloco principal. Os principais operadores e seus recursos serão discutidos abaixo, após a familiarização com a estrutura básica do Helm.

A estrutura básica do Helm


Ao renderizar manifestos, uma estrutura com os seguintes campos é lançada nos modelos:

  • Field .Values - para acessar os parâmetros que são determinados durante a instalação e atualização do release. Eles incluem os valores das opções --set , --set-string e --set-file , bem como os parâmetros dos arquivos com valores, o arquivo values.yaml e os arquivos correspondentes aos valores das opções --values :

     containers: - name: main image: "{{ .Values.image }}:{{ .Values.imageTag }}" imagePullPolicy: {{ .Values.imagePullPolicy }} 
  • .Release - para usar dados da versão sobre lançamento, instalação ou atualização, nome da versão, espaço para nome e os valores de vários outros campos que podem ser úteis ao gerar manifestos:

     metadata: labels: heritage: "{{ .Release.Service }}" release: "{{ .Release.Name }}" subjects: - namespace: {{ .Release.Namespace }} 
  • .Chart - para acessar informações do gráfico . Os campos correspondem ao conteúdo do arquivo Chart.yaml :

     labels: chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 
  • Estrutura .Files - para trabalhar com arquivos armazenados no diretório do gráfico ; a estrutura e os métodos disponíveis podem ser encontrados aqui . Exemplos:

     data: openssl.conf: | {{ .Files.Get "config/openssl.conf" | indent 4 }} 

     data: {{ (.Files.Glob "files/docker-entrypoint-initdb.d/*").AsConfig | indent 2 }} 
  • .Capabilities - para acessar informações sobre o cluster no qual a .Capabilities é realizada:

     {{- 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


Começamos, é claro, com as else if , else if e 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 }} 

O operador de range foi projetado para trabalhar com matrizes e mapas. Se uma matriz é passada como argumento e contém elementos, para cada elemento um bloco é executado sequencialmente (nesse caso, o valor dentro do bloco fica disponível através da variável de contexto):

 {{- range .Values.ports }} - name: {{ .name }} port: {{ .containerPort }} targetPort: {{ .containerPort}} {{- else }} ... {{- end}} 

 {{ range .Values.tolerations -}} - {{ toYaml . | indent 8 | trim }} {{ end }} 

Para trabalhar com mapas, é fornecida uma sintaxe com variáveis:

 {{- range $key, $value := .Values.credentials.secretContents }} {{ $key }}: {{ $value | b64enc | quote }} {{- end }} 

Um comportamento semelhante ocorre with operador:: se o argumento passado existir, o bloco será executado e a variável de contexto no bloco corresponderá ao valor do argumento. Por exemplo:

 {{- with .config }} config: {{- with .region }} region: {{ . }} {{- end }} {{- with .s3ForcePathStyle }} s3ForcePathStyle: {{ . }} {{- end }} {{- with .s3Url }} s3Url: {{ . }} {{- end }} {{- with .kmsKeyId }} kmsKeyId: {{ . }} {{- end }} {{- end }} 

Para reutilizar os modelos, um pacote configurável de define [name] e template [name] [variable] pode ser usado, onde o valor passado é disponibilizado através da variável de contexto no bloco 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 -}} 

Alguns recursos a serem considerados ao usar define , ou, mais simplesmente, parcial'ov:

  • Parcialmente declarado são globais e podem ser usados ​​em todos os arquivos do diretório de templates .
  • O gráfico principal é compilado junto com os gráficos dependentes; portanto, se houver dois nomes parciais parciais do mesmo tipo, o último carregado será usado. Ao nomear um parcial, é habitual adicionar um nome de gráfico para evitar tais conflitos: define "chart_name.partial_name" .

Variáveis: $


Além de trabalhar com o contexto, você pode armazenar, modificar e reutilizar dados usando variáveis:

 {{ $provider := .Values.configuration.backupStorageProvider.name }} ... {{ if eq $provider "azure" }} envFrom: - secretRef: name: {{ template "ark.secretName" . }} {{ end }} 

Ao renderizar um arquivo ou parcial, $ tem o mesmo significado que o ponto. Mas, diferentemente da variável de contexto (point), o valor de $ não muda no contexto das instruções de bloco , o que permite trabalhar simultaneamente com o valor de contexto da instrução de bloco e a estrutura básica do Helm (ou o valor passado para parcial, se falarmos sobre o uso de $ inside parcial'a) . Ilustração da diferença:

 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 do processamento deste modelo, será apresentado o seguinte (para maior clareza, na saída da estrutura serão substituídos pelos pseudominios correspondentes):

 context: #  helm dollar: #  helm with: context: #.Chart dollar: #  helm template: context: #.Chart dollar: #.Chart with: context: habr dollar: #.Chart 

E aqui está um exemplo real do uso desse recurso:

 {{- 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 }} 

Indentação


Ao desenvolver modelos, margens extras podem permanecer: espaços, guias, feeds de linha. Com eles, o arquivo simplesmente parece mais legível. Você pode abandoná-los ou usar uma sintaxe especial para remover o recuo dos padrões usados:

  • {{- variable }} trunca espaços anteriores;
  • {{ variable -}} trunca espaços subsequentes;
  • {{- variable -}} são as duas opções.

Um exemplo de arquivo, cujo processamento será a linha habr flant helm :

 habr {{- " flant " -}} helm 

Funções incorporadas


Todas as funções incluídas no modelo podem ser encontradas no seguinte link . Aqui vou contar apenas sobre alguns deles.

A função de index foi projetada para acessar elementos de uma matriz ou mapas:

 definitions.json: | { "users": [ { "name": "{{ index .Values "rabbitmq-ha" "rabbitmqUsername" }}", "password": "{{ index .Values "rabbitmq-ha" "rabbitmqPassword" }}", "tags": "administrator" } ] } 

A função aceita um número arbitrário de argumentos, o que permite trabalhar com elementos aninhados:

$map["key1"]["key2"]["key3"] => index $map "key1" "key2" "key3"

Por exemplo:

 httpGet: {{- if (index .Values "pushgateway" "extraArgs" "web.route-prefix") }} path: /{{ index .Values "pushgateway" "extraArgs" "web.route-prefix" }}/#/status {{- end }} 

Operações booleanas são implementadas no mecanismo de modelo como funções (e não como operadores). Todos os argumentos para eles são avaliados ao passar:

 {{ if and (index .Values field) (eq (len .Values.field) 10) }} ... {{ end }} 

Se não houver campo de field renderização field modelo falhará ( error calling len: len of untyped nil ): a segunda condição é verificada, apesar do primeiro não ter sido atendido. Vale a pena fazer uma anotação e resolver essas consultas dividindo-as em várias verificações:

 {{ if index . field }} {{ if eq (len .field) 10 }} ... {{ end }} {{ end }} 

O pipeline é um recurso exclusivo dos modelos Go que permite declarar expressões executadas como um pipeline em um shell. Formalmente, o pipeline é uma cadeia de comandos separados pelo símbolo | . Um comando pode ser um valor simples ou uma chamada de função . O resultado de cada comando é passado como o último argumento para o próximo comando , e o resultado do comando final no pipeline é o valor de todo o pipeline. Exemplos:

 data: openssl.conf: | {{ .Files.Get "config/openssl.conf" | indent 4 }} 

 data: db-password: {{ .Values.externalDatabase.password | b64enc | quote }} 

Funções adicionais


O Sprig é uma biblioteca de 70 recursos úteis para resolver uma ampla variedade de tarefas. Por motivos de segurança, o Helm exclui as funções env e expandenv que fornecem acesso às variáveis ​​de ambiente do Tiller.

A função de include , como a função de template padrão, é usada para reutilizar modelos. Ao contrário do template , a função pode ser usada no pipeline, ou seja, passe o resultado para outra função:

 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 -}} 

A função required permite que os desenvolvedores declarem valores obrigatórios necessários para renderizar um modelo: se um valor existir, ele será usado ao renderizar um modelo; caso contrário, a renderização será encerrada com a mensagem de erro indicada pelo desenvolvedor:

 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 }} 

A função tpl permite renderizar uma string como modelo. Ao contrário do template e da include , a função permite executar modelos passados ​​em variáveis, bem como renderizar modelos armazenados não apenas no diretório de templates . Como é isso?

Executando modelos de variáveis:

 containers: {{- with .Values.keycloak.extraContainers }} {{ tpl . $ | indent 2 }} {{- end }} 

... e em values.yaml , temos o seguinte 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 

Renderização de um arquivo armazenado fora do diretório 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 }} 

... no gráfico, no caminho files/job.tpl , existe o seguinte modelo:

 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 

É aqui que a introdução aos conceitos básicos da padronização no Helm chegou ao fim ...

Conclusão


O artigo descreve a estrutura dos gráficos Helm e examina detalhadamente as principais dificuldades de sua criação - modelo: princípios básicos, sintaxe, funções e operadores de modelo Go, funções adicionais.

Como começar a trabalhar com tudo isso? Como o Helm já é um ecossistema inteiro, você sempre pode ver exemplos de gráficos para pacotes semelhantes. Por exemplo, se você deseja compactar uma nova fila de mensagens, consulte o gráfico público do RabbitMQ . Obviamente, ninguém promete implementações ideais em pacotes existentes, mas elas são perfeitas como ponto de partida. O restante vem com a prática, na qual os helm template e helm lint debugging o ajudarão, além de iniciar a instalação com a opção --dry-run .

Para uma compreensão mais ampla do desenvolvimento de gráficos Helm, melhores práticas e tecnologias usadas, sugiro que você se familiarize com os materiais nos seguintes links (todos em inglês):


E no final do próximo material de Helm, estou anexando uma pesquisa que ajudará a entender melhor que outros artigos sobre Helm os leitores de Habr estão esperando (ou não estão esperando?). Obrigado pela atenção!

PS


Leia também em nosso blog:

Source: https://habr.com/ru/post/pt423239/


All Articles