Практическое руководство по созданию Helm чарта или как избавиться от рутины при работе с YAML манифестами

от автора

Вступление

Я люблю манифесты Kubernetes. Правда, мне приносит большое удовольствие создавать по отдельности каждый ресурс командой kubectl apply. Но это только в начале… Когда у вас таких ресурсов больше пяти, а микросервисов и того больше, то управлять всем этим зоопарком становится болью. Вам необходимо манипулировать отдельными манифестами, если у вас несколько схожих сервисов, отличающихся небольшими деталями, то вам придётся для каждого создавать свою пачку манифестов. Про разные окружения я молчу.

Можно скинуть весь деплой ресурсов на CI/CD Pipeline и забыть о манифестах навсегда. Но если вам понадобится развернуть или, наоборот, «свернуть» приложение, то вышеперечисленных проблем не избежать. Таким образом, в этой статье я покажу свой опыт создания Helm чарта и его запуска, но перед этим изучив методы деплоя приложения без Helm.

Предисловие

После деплоя нескольких сервисов при помощи YAML манифестов и командыkubectl apply, я решил создать свой первый Helm чарт и подумал, почему бы не написать об этом статью! У автора довольно небольшой опыт работы с Helm, поэтому в статье могут быть допущены ошибки. Поэтому если вы обнаружите опечатку/грубую (и не только) ошибку, то просьба выделить текст и нажать на Ctrl+Enter. Спасибо!

Оглавление

Цель

Я уже создал репозитории с исходным кодом. Там можно найти две папки — kubectl, содержащий yaml манифесты и helm — чарт с теми же самыми манифестами. В статье будет минимум теории — основная часть это практика. Сначала я покажу из каких манифестов состоит приложение, как они запускаются, а затем попробуем создать свой Helm чарт, сделав манифесты более универсальными, и развернуть релиз.

Немного о Helm

Helm — это пакетный менеджер Kubernetes. Он позволяет легко запускать, обновлять и откатывать приложения. Основная сущность Helm — это чарты.

Чарт (Chart) — это коллекция связанных манифестов Kubernetes. При помощи чарта вы можете запускать приложения других разработчиков или же создать свой чарт и развернуть его.

Релиз (Release) — это установленный чарт. В один кластер можно установить сколько угодно релизов одного чарта. У каждого релиза есть своё название, которое можно использовать для нейминга ресурсов Kubernetes.

В первые месяцы использования Kubernetes я использовал Helm исключительно для разворачивания сервисов от сторонних разработчиков. Так, я активно использую ingress-nginx и loki-stack (кстати, использовал в прошлой статье). Но недавно мне пришла идея использовать Helm также для собственных сервисов. Причин для этого несколько:

  1. Helm позволяет управлять несколькими манифестами как единым целым. В наших сервисах по 4-7 манифестов, некоторые требуют подстановки переменных (через envsubst), что также осложняет деплой вне CI/CD пайплайна.

  2. Все наши сервисы строятся почти по одному принципу, следовательно, много кода повторяется. При помощи хелмовского values.yaml (об этом чуть позже) получится вынести меняющиеся значения из манифестов и использовать один чарт для нескольких приложении (релизов) сразу.

  3. Helm становится (или уже стал) «маст хев» технологией. В последнее время в вакансиях я чаще стал видеть Helm для Dev-Ops инженеров, требующих знания K8s.

Манифесты и их назначения

Сначала следует указать, что деплоить будем в Yandex Managed Kubernetes. В сервисах используется Lockbox (сервис от Yandex Cloud для хранения секретов) и External Secret Operator для синхронизации Kubernetes со сторонними провайдерами (в нашем случае с сервисами Yandex.Cloud). Для развертывания приложения предусмотрены следующие манифесты:

  • cert-external-secret.yaml — ресурс типа ExternalSecret. Манифест необходим, чтобы получить TLS сертификат из Yandex Certificate Manager и его приватный ключ. В дальнейшем, ExternalSecret создаст секрет с данными значениями, которые будут использоваться в ingress.yaml.

  • cert-secret-store.yaml — ресурс типа SecretStore. Он указывает к какому стороннему API обращаться за получением данных. В текущем манифесте провайдером указан yandexcertificatemanager.

  • clusterip.yaml — ресурс типа Service(ClusterIP). Обеспечивает доступ к запущенному сервису внутри кластера.

  • deploy.yaml — ресурс типа Deployment . Управляет подами и следит за тем, чтобы все реплики были развернуты. В нём указано запустить одну реплику с образом Java (Spring) и некоторыми переменными, взятых из секрета со значениями из Yandex Lockbox. Использует образ из Yandex Container Registry.

  • ingress.yaml — ресурс типа Ingress. Служит для обеспечения доступа к сервису через HTTP(-S). Одного Ingress не хватит, нужно отдельно развернуть Ingress Controller (обычно ingress-nginx). Использует данные из секрета, созданного cert-external-secret.yaml манифестом, для обеспечения доступа по HTTPS.

  • lockbox-external-secret.yaml — также ресурс типа ExternalSecret, в нём описываются все переменные, которые нужно получить из Lockbox.

  • lockbox-secret-storage.yaml — также ресурс типа SecretStore, провайдером служит yandexlockbox.

Содержимое манифестов

Теперь давайте развёрнем всю эту махину! Вы, возможно, предложите воспользоваться командойkubectl apply -f ./kubectl, чтобы по отдельности не выполнять команды для каждого файла. Дело в том, что в некоторых конфигурационных файлах используются переменные, подставляемые через envsubst. Это очень удобная утилита для вставки значении в файл. Она очень полезна для подстановки переменных в CI/CD пайплайне. С другой стороны, при локальном поднятии ресурсов это добавляет сложность.

Ниже, в порядке выполнения, будут указаны содержимое манифестов и команды для их развертывания. Также обращайте внимание на комментарии.
P.S. содержимое манифестов указано лишь с целью показать как изменятся манифесты после создания Helm чарта. Вам не обязательно разворачивать те же самые ресурсы/использовать сервисы Yandex.Cloud

Чуть не забыл! Сперва создадим отдельное пространство имён и добавим секрет с авторизированным ключом для доступа к Yandex Cloud:

$ kubectl create ns kubectl-ns namespace/kubectl-ns created $ kubectl --namespace kubectl-ns create secret generic yc-auth \       --from-file=authorized-key=authorized-key.json  secret/yc-auth created 

SecretStore (TLS Certificate)

cert-secret-store.yaml
apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata:   name: spring-app-certificate-secret-store   namespace: kubectl-ns spec:   provider:     yandexcertificatemanager:       auth:         authorizedKeySecretRef:           name: yc-auth           key: authorized-key

$ kubectl apply -f ./kubectl/cert-secret-store.yaml secretstore.external-secrets.io/spring-app-certificate-secret-store created $ kubectl -n kubectl-ns get ss/spring-app-certificate-secret-store NAME                                  AGE   STATUS   CAPABILITIES   READY spring-app-certificate-secret-store   20s   Valid    ReadOnly       True 

ExternalSecret (TLS Certificate)

cert-external-secret.yaml
apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata:   name: spring-app-certificate-external-secret   namespace: kubectl-ns spec:   refreshInterval: 1h   secretStoreRef:     name: spring-app-certificate-secret-store     kind: SecretStore   target:     name: spring-app-certificate-secret     template:       type: kubernetes.io/tls   data:   - secretKey: tls.crt     remoteRef:       key: $CERTIFICATE_ID       property: chain   - secretKey: tls.key     remoteRef:       key: $CERTIFICATE_ID       property: privateKey

Обратите внимание на $CERTIFICATE_ID. Так как ID сертификата из Certificate Manager может периодический изменяться, то хранить его в коде плохая практика. Поэтому сначала необходимо узнать ID сертификата, записать его в переменную окружения CERTIFICATE_ID и передать её через envsubst:

$ export CERTIFICATE_ID=<your_certificate_id_here> $ envsubst \$CERTIFICATE_ID < ./kubectl/cert-external-secret.yaml | kubectl apply -f - $ kubectl -n kubectl-ns get externalsecret/spring-app-certificate-external-secret NAME                                     STORE                                 REFRESH INTERVAL   STATUS         READY spring-app-certificate-external-secret   spring-app-certificate-secret-store   1h                 SecretSynced   True 

SecretStore (Lockbox secret)

lockbox-secret-store.yaml
apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata:   name: spring-app-lockbox-secret-store   namespace: kubectl-ns spec:   provider:     yandexlockbox:       auth:         authorizedKeySecretRef:           name: yc-auth           key: authorized-key

$ kubectl apply -f ./kubectl/lockbox-secret-store.yaml secretstore.external-secrets.io/spring-app-lockbox-secret-store created $ kubectl -n kubectl-ns get ss/spring-app-lockbox-secret-store   NAME                              AGE   STATUS   CAPABILITIES   READY spring-app-lockbox-secret-store   51s   Valid    ReadOnly       True 

ExternalSecret (Lockbox secret)

lockbox-external-secret.yaml
apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata:   name: spring-app-lockbox-external-secret   namespace: kubectl-ns spec:   refreshInterval: 1h   secretStoreRef:     name: spring-app-lockbox-secret-store     kind: SecretStore   target:     name: spring-app-lockbox-secret   data:   - secretKey: JDBC_URL     remoteRef:       key: $SECRET_ID       property: JDBC_URL   - secretKey: DB_USERNAME     remoteRef:       key: $SECRET_ID       property: DB_USERNAME   - secretKey: DB_PASSWORD     remoteRef:       key: $SECRET_ID       property: DB_PASSWORD

Теперь же следует передать в файл $SECRET_ID — ID Yandex Lockbox секрета.

$ export SECRET_ID=<your_lockbox_secret_id_here> $ envsubst \$SECRET_ID < ./kubectl/lockbox-external-secret.yaml | kubectl apply -f - externalsecret.external-secrets.io/spring-app-lockbox-external-secret created $ kubectl -n kubectl-ns get externalsecret/spring-app-lockbox-external-secret  NAME                                 STORE                             REFRESH INTERVAL   STATUS         READY spring-app-lockbox-external-secret   spring-app-lockbox-secret-store   1h                 SecretSynced   True 

Service (type: ClusterIP)

clusterip.yaml
apiVersion: v1 kind: Service metadata:   name: spring-app   namespace: kubectl-ns   labels:     app-label: spring-app-clusterip-label spec:   ports:   - name: http     protocol: TCP     port: 80     targetPort: http   selector:     app-label: spring-app-label

$ kubectl apply -f ./kubectl/clusterip.yaml service/spring-app created 

Deployment

deploy.yaml
apiVersion: apps/v1 kind: Deployment metadata:   name: spring-app   namespace: kubectl-ns   labels:     app-label: spring-app-label spec:   replicas: 1   selector:     matchLabels:       app-label: spring-app-label   template:     metadata:       labels:         app-label: spring-app-label     spec:       containers:       - name: spring-app-app         image: cr.yandex/$REGISTRY_ID/spring-app:$VERSION         ports:         - name: http           containerPort: 8080         env:         # --- variables from Yandex Lockbox         - name: JDBC_URL           valueFrom:             secretKeyRef:               name: spring-app-lockbox-secret               key: JDBC_URL         - name: DB_USERNAME           valueFrom:             secretKeyRef:               name: spring-app-lockbox-secret               key: DB_USERNAME         - name: DB_PASSWORD           valueFrom:             secretKeyRef:               name: spring-app-lockbox-secret               key: DB_PASSWORD

Для данного манифеста необходимо передать два значения: $REGISTRY_ID — ID реестра и $VERSION — версия образа.

$ export REGISTRY_ID=<your_container_registry_id_here> $ export VERSION=<your_image_version_here> $ envsubst \$REGISTRY_ID,\$VERSION < ./kubectl/deploy.yaml | kubectl apply -f - deployment.apps/spring-app created $ kubectl -n kubectl-ns get deploy/spring-app NAME         READY   UP-TO-DATE   AVAILABLE   AGE spring-app   1/1     1            1           17m 

Ingress

ingress.yaml
apiVersion: networking.k8s.io/v1 kind: Ingress metadata:   name: spring-app-ingress   namespace: kubectl-ns spec:   tls:     - hosts:       - spring-app.dev.example.com       secretName: spring-app-certificate-secret   ingressClassName: spring-app-class-resource   rules:     - host: spring-app.dev.example.com       http:         paths:         - path: /           pathType: Prefix           backend:             service:               name: spring-app               port:                 name: http

Создание ingress.yaml пропустим, так как для его настройки дополнительно нужно разворачивать Ingress Controller.

Вывод: рутина

Мы развернули все нужные ресурсы. А теперь посчитайте количество выполненных команд с учетом назначении переменных окружения. А теперь представьте, что вам нужно их выполнять чаще, чем раз в жизни? Конечно, можно использовать bash скрипты, но тогда придется для каждого сервиса создавать свой bash скрипт. Рутина, не так ли? И тут к нам на помощь приходит Helm!

Перед следующим разделом удалим все созданные ранее ресурсы:

$ kubectl delete -f ./kubectl/ externalsecret.external-secrets.io "spring-app-certificate-external-secret" deleted secretstore.external-secrets.io "spring-app-certificate-secret-store" deleted service "spring-app" deleted deployment.apps "spring-app" deleted ingress.networking.k8s.io "spring-app-ingress" deleted externalsecret.external-secrets.io "spring-app-lockbox-external-secret" deleted secretstore.external-secrets.io "spring-app-lockbox-secret-store" deleted 

Создание чарта

Для начала создадим чарт:

$ helm create helm-chart Creating helm-chart 

Взглянем на структуру только что созданного чарта:

Как видим, команда helm create создала несколько папок и вложенных в них файлов. Коротко о каждом:

  • charts/ — папка, содержащая сторонние чарты, от которых зависит текущий

  • Chart.yaml — файл, содержащий основные сведения о чарте.

  • templates/ — папка, содержащая шаблоны Kubernetes с возможностью форматирования и вставки значении Helm.

  • templates/*.tpl — файлы, содержащие именнованые шаблоны. Вы можете создавать файлы с расширением tpl и размещать в них собственные шаблоны, а затем использовать в манифестах.

  • values.yaml — файл, содержащий переменные, используемые в шаблонах. Содержит стандартные значения, при установке релиза можно указать собственные.

Переносим манифесты в чарт и форматируем их

Теперь наша цель перенести все манифесты из kubectl в helm чарт. Основное требование для чарта — возможность иметь более одного релиза. Для этого мы воспользуемся возможностями форматирования от Helm, а также файлом values.yaml

Встроенные объекты

В Helm, помимо использования собственных переменных, можно использовать встроенные. Таким образом, вы можете использовать в шаблонах (дальше) и манифестах такие переменные как Release.Name (название релиза),Values.example(значение example из values.yaml),Chart.Version(версия чарта) и т.д. С полным списком Built-in объектов можно ознакомиться тут.

values.yaml

Главным помощником для того, чтобы сделать чарт более гибким и универсальным, является упомянутый ранее файл values.yaml. Он содержит переменные, которые назначаются перед установкой релиза. Изначально оставим его пустым и по ходу переноса манифестов будем его наполнять.

_helpers.tpl

_helpers.tpl
{{/* Expand the name of the chart. */}} {{- define "helm-chart.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }}  {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "helm-chart.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }}  {{/* Create chart name and version as used by the chart label. */}} {{- define "helm-chart.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }}  {{/* Common labels */}} {{- define "helm-chart.labels" -}} helm.sh/chart: {{ include "helm-chart.chart" . }} {{ include "helm-chart.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }}  {{/* Selector labels */}} {{- define "helm-chart.selectorLabels" -}} app.kubernetes.io/name: {{ include "helm-chart.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }}  {{/* Create the name of the service account to use */}} {{- define "helm-chart.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "helm-chart.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }}  

В данном файле уже определены некоторые именнованые шаблоны. В данных шаблонах используются Built-In переменные:

  • «helm-chart.name» — название чарта

  • «helm-chart.fullname» — полное название чарта

  • «helm-chart.chart» — название чарта + его версия

  • «helm-chart.labels» — общие для всех ресурсов чарта метки. Я их буду использовать везде, так как по документации Helm они используются для идентификации ресурса.

  • «helm-chart.selectorLabels» — метки, используемые для селектов в ресурсах ReplicaSet и Deployment. Также эти метки входят в шаблон «helm-chart.labels»

Шаблонные функции

Помимо использования переменных, вы можете вызывать шаблонные функции. Подробнее в документации: тык.

Стартуем! Начнём заполнение шаблонов в том же порядке, как и в случае с манифестами из kubectl.

cert-secret-store.yaml & lockbox-secret-store.yaml

Скопируем манифесты из kubectl и перенесем в папку templates:

$ cp -r ./kubectl/*-secret-store.yaml ./helm-chart/templates  

Произведем некоторые изменения. Во-первых, удалим из названия ресурса название приложения и заменим его названием релиза:

metadata:   name: {{ .Release.Name }}-certificate-secret-store 

Во-вторых, вставим шаблон helm-chart.labels с помощью include и передадим вывод (символ | ) в шаблонную функцию nident, добавляющую переданное количество пробелов в начало строки. Я передаю в функцию 4, так как необходимо именно столько пробелов:

metadata:   labels:     {{- include "helm-chart.labels" . | nindent 4 }} 

Теперь, чтобы узнать как будет выглядеть наш манифест после всех манипуляции хелма, воспольуемся командой helm install с параметром --dry-run, который «понарошку» установит чарт:

$ helm install --dry-run test-release ./helm-chart NAME: test-release STATUS: pending-install MANIFEST: --- # Source: helm-chart/templates/cert-secret-store.yaml apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata:   name: test-release-certificate-secret-store   labels:     helm.sh/chart: helm-chart-0.1.0     app.kubernetes.io/name: helm-chart     app.kubernetes.io/instance: test-release     app.kubernetes.io/version: "1.16.0"     app.kubernetes.io/managed-by: Helm   ... 

Название релиза и метки были успешно вставлены в манифест.
Так как не все наши развернутые приложения требуют доступ к TLS сертификатам и Lockbox секретам, сделаем SecretStore и ExternalSecret манифесты опциональными. Добавим следующие значения в values.yaml:

lockboxSecretStore:   enabled: true  certificateSecretStore:   enabled: true 

По умолчанию эти ресурсы будут со значениями true.
Теперь давайте добавим логику активации манифеста, исходя из ранее добавленных значении. Для этого воспользуемся условным выражением:

{{- if .Values.certificateSecretStore.enabled -}} apiVersion: external-secrets.io/v1beta1 kind: SecretStore ... {{- end }} 

Снова выполним helm install --dry-run, но переопределим значение certificateSecretStore.enabled, сделав его false. Это можно сделать двумя способами: создать свой values.yaml и определить значения там или воспользоваться параметром --set. Убедимся, что список манифестов окажется пустым:

$ helm install --dry-run --set certificateSecretStore.enabled=false test-release ./helm-chart NAME: test-release STATUS: pending-install MANIFEST: 

Полный код манифестов:

cert-secret-store.yaml
{{- if .Values.certificateSecretStore.enabled -}} apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata:   name: {{ .Release.Name }}-certificate-secret-store   namespace: {{ .Release.Namespace }}   labels:     {{- include "helm-chart.labels" . | nindent 4 }} spec:   provider:     yandexcertificatemanager:       auth:         authorizedKeySecretRef:           name: yc-auth           key: authorized-key {{- end }}

lockbox-secret-store.yaml
{{- if .Values.lockboxSecretStore.enabled -}} apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata:   name: {{ .Release.Name }}-lockbox-secret-store   namespace: {{ .Release.Namespace }}   labels:     {{- include "helm-chart.labels" . | nindent 4 }} spec:   provider:     yandexlockbox:       auth:         authorizedKeySecretRef:           name: yc-auth           key: authorized-key {{- end }}

cert-external-secret.yaml

Так как SecretStore и ExternalSecret связанные сущности, создадим аналогичные условия деплоя для ExternalSecret манифестов. Но сначала добавим новое значение для ID сертификата в values.yaml:

certificateSecretStore:   enabled: true   externalSecret:     certificateId: "" 

И затем в манифест:

...   - secretKey: tls.crt     remoteRef:       key: {{ .Values.certificateSecretStore.externalSecret.certificateId }}       property: chain   - secretKey: tls.key     remoteRef:       key: {{ .Values.certificateSecretStore.externalSecret.certificateId }}       property: privateKey 

Полный код манифестов:

cert-external-secret.yaml
{{- if .Values.certificateSecretStore.enabled -}} apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata:   name: {{ .Release.Name }}-certificate-external-secret   namespace: {{ .Release.Namespace }}   labels:     {{- include "helm-chart.labels" . | nindent 4 }} spec:   refreshInterval: 1h   secretStoreRef:     name: {{ .Release.Name }}-certificate-secret-store     kind: SecretStore   target:     name: {{ .Release.Name }}-certificate-secret     template:       type: kubernetes.io/tls   data:   - secretKey: tls.crt     remoteRef:       key: {{ .Values.certificateSecretStore.externalSecret.certificateId }}       property: chain   - secretKey: tls.key     remoteRef:       key: {{ .Values.certificateSecretStore.externalSecret.certificateId }}       property: privateKey {{- end }}

lockbox-external-secret.yaml

Все секреты отличаются между собой значениями. Поэтому аналогично с другими меняющимися значениями вынесем поля из файла и поместим их в values.yaml. Не забудем также вынести и $SECRET_ID:

lockboxSecretStore:   enabled: true   externalSecret:     secretId: ""     data:     - secretKey: JDBC_URL       property: JDBC_URL     - secretKey: DB_USERNAME       property: DB_USERNAME     - secretKey: DB_PASSWORD       property: DB_PASSWORD 

Теперь в манифесте нужно перебрать все заданные значения. Для этого воспользуемся циклом — в Helm для этого используется оператор range:

... spec:   ...     data:    {{- range .Values.lockboxSecretStore.externalSecret.data }}     - secretKey: {{ .secretKey }}       remoteRef:         key: {{ $.Values.lockboxSecretStore.externalSecret.secretId }}         property: {{ .property }}   {{- end }} 

Обратите внимание на знак доллара в remoteRef.key. Операторы range и with создают свою область видимости. В данном случае . указывает на текущую область видимости, которая задана оператором range. Поэтому, чтобы получить значение из values.yaml необходимо в начало добавить $., указывающий шаблонизатору обращаться к корневой области видимости.

Полный код манифестов:

lockbox-external-secret.yaml
{{- if .Values.lockboxSecretStore.enabled -}} apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata:   name: {{ .Release.Name }}-lockbox-external-secret   namespace: {{ .Release.Namespace }}   labels:     {{- include "helm-chart.labels" . | nindent 4 }} spec:   refreshInterval: 1h   secretStoreRef:     name: {{ .Release.Name }}-lockbox-secret-store     kind: SecretStore   target:     name: {{ .Release.Name }}-lockbox-secret   data:   {{- range .Values.lockboxSecretStore.externalSecret.data }}     - secretKey: {{ .secretKey }}       remoteRef:         key: {{ $.Values.lockboxSecretStore.externalSecret.secretId }}         property: {{ .property }}           {{- end }} {{- end }}

clusterip.yaml

Редактирование манифеста для сервиса не отличается от остальных. Вынесем в values.yaml значения spec.ports[0].port и spec.ports[0].targetPort:

clusterip:   port: 80   targetPort: http 

Также вместо собственных селекторов воспользуемся шаблоном helm-chart.selectorLabels, предлагаемым из коробки:

... spec:   ports:   - name: http     protocol: TCP     port: {{ .Values.clusterip.port }}     targetPort: {{ .Values.clusterip.targetPort }}   selector:     {{- include "helm-chart.selectorLabels" . | nindent 4 }} 

Полный код манифестов:

clusterip.yaml
apiVersion: v1 kind: Service metadata:   name: {{ .Release.Name }}   namespace: {{ .Release.Namespace }}   labels:     {{- include "helm-chart.labels" . | nindent 4 }} spec:   ports:   - name: http     protocol: TCP     port: {{ .Values.clusterip.port }}     targetPort: {{ .Values.clusterip.targetPort }}   selector:     {{- include "helm-chart.selectorLabels" . | nindent 4 }}

deploy.yaml

Добавим в values.yaml следующие значения:

deployment:   replicaCount: 1   image: ""   containerPort: 8080   resources:     requests:       cpu: "150m"       memory: "400Mi"     limits:       cpu: "250m"       memory: "600Mi"  

Так как в контейнер передаются в качестве переменных окружения переменные из Lockbox секрета, добавим такой же цикл, как в lockbox-external-secret.yaml, но с немного другой структурой. Не забудем добавить перед циклом условие, что Lockbox используется в релизе. Чуть не забыл самое главное! Вставим ресурсы (лимиты и запросы) для контейнера при помощи функции toYaml:

spec:   ...   template:     ...     spec:       containers:       - name: {{ .Release.Name }}-app image: {{ .Values.deployment.image }} ports: - name: {{ .Values.clusterip.targetPort }}   containerPort: {{ .Values.deployment.containerPort }} resources:           {{- toYaml .Values.deployment.resources | nindent 10 }}     {{- if .Values.lockboxSecretStore.enabled }}         env:         {{- range .Values.lockboxSecretStore.externalSecret.data }}         - name: {{ .secretKey }}           valueFrom:             secretKeyRef:               name: {{ $.Release.Name }}-lockbox-secret               key: {{ .secretKey }}         {{- end }}         {{- end }} 

Полный код манифестов:

deploy.yaml
apiVersion: apps/v1 kind: Deployment metadata:   name: {{ .Release.Name }}   namespace: {{ .Release.Namespace }}   labels:     {{- include "helm-chart.labels" . | nindent 4 }} spec:   replicas: {{ .Values.deployment.replicaCount }}   selector:     matchLabels:       {{- include "helm-chart.selectorLabels" . | nindent 6 }}   template:     metadata:       labels:         {{- include "helm-chart.selectorLabels" . | nindent 8 }}     spec:       containers:       - name: {{ .Release.Name }}-app         image: {{ .Values.deployment.image }}         ports:         - name: {{ .Values.clusterip.targetPort }}           containerPort: {{ .Values.deployment.containerPort }}         resources:           {{- toYaml .Values.deployment.resources | nindent 10 }}         {{- if .Values.lockboxSecretStore.enabled }}         env:         {{- range .Values.lockboxSecretStore.externalSecret.data }}         - name: {{ .secretKey }}           valueFrom:             secretKeyRef:               name: {{ $.Release.Name }}-lockbox-secret               key: {{ .secretKey }}         {{- end }}         {{- end }}

ingress.yaml

Добавим также ресурс типа Ingress. Также как и с ExternalSecret, сделаем создание ингресса на усмотрение пользователя. В values.yaml добавим значения:

ingress:   enabled: true   host: "" 

Полный код манифестов:

ingress.yaml
{{- if .Values.ingress.enabled -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata:   name: {{ .Release.Name }}   namespace: {{ .Release.Namespace }}   labels:     {{- include "helm-chart.labels" . | nindent 4 }} spec:   tls:     - hosts:       - {{ .Values.ingress.host }}       secretName: {{ .Release.Name }}-certificate-secret   ingressClassName: {{ .Release.Name }}-class-resource   rules:     - host: {{ .Values.ingress.host }}       http:         paths:         - path: /           pathType: Prefix           backend:             service:               name: {{ .Release.Name }}               port:                 name: {{ .Values.clusterip.port }} {{- end }}

А также values.yaml со всеми добавленными значениями:

values.yaml
lockboxSecretStore:   enabled: true   externalSecret:     secretId: ""     data:     - secretKey: JDBC_URL       property: JDBC_URL     - secretKey: DB_USERNAME       property: DB_USERNAME     - secretKey: DB_PASSWORD       property: DB_PASSWORD  certificateSecretStore:   enabled: true   externalSecret:     certificateId: ""  clusterip:   port: 80   targetPort: http  deployment:   replicaCount: 1   image: ""   containerPort: 8080      resources:     requests:       cpu: "150m"       memory: "400Mi"     limits:       cpu: "250m"       memory: "600Mi"  ingress:   enabled: true   host: ""

Запускаем чарт

И так, мы создали свой Helm чарт с довольно гибким values.yaml файлом. Вы можете использовать стандартный values.yaml, но вряд-ли он будет соотвествовать всем вашим потребностям. Поэтому вы можете назначить значения, воспользовавшись параметром --set при установке релиза или же, если их много, написать yaml файл со своими значениями и указать путь с помощью параметра -f. Так как для развертывания моих сервисов нужно перезаписать достаточно много значении, я создал файлик my-app-values.yaml. Установим чарт:

$ helm install -n kubectl-ns -f ./my-app-values.yaml my-app ./helm-chart  NAME: my-app LAST DEPLOYED: Fri Oct 20 17:21:18 2023 NAMESPACE: kubectl-ns STATUS: deployed REVISION: 1 TEST SUITE: None 

Получим список всех ресурсов:

$ kubectl -n kubectl-ns get all                                      NAME                                 READY   STATUS   RESTARTS      AGE pod/my-app-84c8d4cfdd-mhqb4   0/1    Error   1 (36s ago)  106s  NAME                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE service/my-app          ClusterIP   10.96.167.201   <none>        80/TCP    107s  NAME                            READY   UP-TO-DATE   AVAILABLE   AGE deployment.apps/my-app          0/1     1            0           107s  NAME                                       DESIRED   CURRENT   READY   AGE replicaset.apps/my-app-84c8d4cfdd          1         1         0       107s 

Под не запустился! Выясним в чём причина, выполнив kubectl describe:

$ kubectl -n kubectl-ns describe pods/my-app-84c8d4cfdd-mhqb4 Events:   Type     Reason     Age                    From               Message   ----     ------     ----                   ----               -------   Normal   Scheduled  5m40s                  default-scheduler  Successfully assigned kubectl-ns/my-app-84c8d4cfdd-mhqb4 to cl12s2vrpmu4of6it02q-itys   Warning  Failed     5m40s (x2 over 5m40s)  kubelet            Error: secret "my-app-lockbox-secret" not found 

secret not found. Но почему? Посмотрим список всех секретов:

$ kubectl -n kubectl-ns get secret                                    NAME                                  TYPE                                  DATA   AGE my-app-certificate-secret             kubernetes.io/tls                     2      7m40s my-app-lockbox-secret                 Opaque                                4      7m39s 

Так вот же они! Я был в небольшом ступоре, когда обнаружил эту проблему, но быстро смог понять в чем дело. Давайте выполним ту же самую команду для установки чарта, но используем параметр --dry-run. В этой статье я уже использовал эту команду: она не по настоящему устанавливает чарт и выводит список всех манифестов в порядке их установки. Чтобы не смотреть содержимое всех манифестов, «грепнем» вывод команды и получим только названия манифестов:

$ helm install --dry-run -n kubectl-ns -f ./my-app-values.yaml config-server ./helm-chart | grep "Source:" # Source: helm-chart/templates/clusterip.yaml # Source: helm-chart/templates/deploy.yaml # Source: helm-chart/templates/ingress.yaml # Source: helm-chart/templates/cert-external-secret.yaml # Source: helm-chart/templates/lockbox-external-secret.yaml # Source: helm-chart/templates/cert-secret-store.yaml # Source: helm-chart/templates/lockbox-secret-store.yaml 

Заметили? ExternalSecret и SecretStore создаются после Deployment.

Порядок запуска ресурсов

Helm сортирует все ресурсы чарта и выполняет их в такой очерёдности:

Очерередность запуска ресурсов

Исходя из этого, Secret(5) создается раньше Deployment(21). Но дело в том, что секрет со значениями из Lockbox создает ресурс типа ExternalSecret, которого в списке нет. Поэтому Helm выполняет неизвестные ему типы ресурсов последними (SecretStore и ExternalSecret). Но повлиять на очередь загрузки можно при помощи хуков чарта.

Хуки

Хуки позволяют выполнять манифесты в какой-то определённый момент, например перед установкой релиза или после его удаления. Чтобы указать в какой момент выполнять хук, на ресурс накидывается аннотация helm.sh/hook. Список всех возможных хуков привёден в документации:

Нам понадобится pre-install. Помимо аннотации с указанием хука, мы можем также указать его вес аннотацией helm.sh/hook-weight, чтобы назначить конкретный порядок выполнения ресурсов, выполняемых в рамках одного хука. По умолчанию всем ресурсам назначается вес, равный «0». Значение аннотации должно быть строковым и может быть как отрицательным, так и положительным. Helm в дальнейшем сортирует ресурсы в порядке возрастания веса. Кроме двух вышеуказанных аннотации, можно воспользоваться аннотацией helm.sh/hook-delete-policy, определяющую политику удаления хука:

По умолчанию указан before-hook-creation, что означает, что ресурс, созданный хуком, не удалится до тех пор, пока не будет запущен новый хук.

Определяем свой порядок создания ресурсов

Снова залезем в код. Зададим для всех ресурсов типа ExternalSecret и SecretStore аннотацию с хуком pre-install. Так как ресурсы типа SecretStore должен создаваться раньше, назначим им вес «-2». Тогда ExternalSecret будут иметь вес «-1». Так как секреты после выполнения хука нам будут нужны для запуска Deployment и настройки Ingress по HTTPS, оставим значение аннотации helm.sh/hook-deletion-policy по умолчанию.

cert-secret-store.yaml | lockbox-secret-store.yaml:

apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata:   ...   annotations:     "helm.sh/hook": pre-install     "helm.sh/hook-weight": "-2" ... 

cert-external-secret.yaml | lockbox-external-secret:

apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata:   ...   annotations:     "helm.sh/hook": pre-install     "helm.sh/hook-weight": "-1" ... 

Снова выполним команду для установки чарта и убедимся, что под находится в статусе Running:

$ kubectl -n kubectl-ns get pods NAME                             READY   STATUS    RESTARTS      AGE my-app-84c8d4cfdd-hksz5          1/1     Running   0             49s 

Под был успешно запущен!

Запускаем второй релиз

Теперь попробуем запустить второй релиз. Возьмем стандартный образ nginx. Всё что нам понадобится это Deployment и Service(ClusterIP). На этот раз, для назначения кастомных значении релизу воспользуемся параметром --set:

$ kubectl create namespace nginx-helm namespace/nginx-helm created $ helm install \     -n nginx-helm \     --set lockboxSecretStore.enabled=false \     --set certificateSecretStore.enabled=false \     --set deployment.image=nginx:1.25.2 \     --set deployment.containerPort=80 \     --set ingress.enabled=false \     nginx ./helm-chart NAME: nginx LAST DEPLOYED: Fri Oct 20 20:10:58 2023 NAMESPACE: nginx-helm STATUS: deployed REVISION: 1 TEST SUITE: None $ kubectl -n nginx-helm get all NAME                         READY   STATUS    RESTARTS   AGE pod/nginx-658dbf5895-5rrmm   1/1     Running   0          14s  NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE service/nginx   ClusterIP   10.96.171.141   <none>        80/TCP    14s  NAME                    READY   UP-TO-DATE   AVAILABLE   AGE deployment.apps/nginx   1/1     1            1           14s  NAME                               DESIRED   CURRENT   READY   AGE replicaset.apps/nginx-658dbf5895   1         1         1       14s 

Также мы можем посмотреть список всех релизов в пространстве имен:

$ helm -n nginx-helm ls NAME    NAMESPACE       REVISION        UPDATED                                 STATUS       CHART                   APP VERSION nginx   nginx-helm      1               2023-10-20 20:10:58.163471204 +0300 MSK deployed     helm-chart-0.1.0        1.16.0  

Удалим релиз:

$ helm -n nginx-helm uninstall nginx      release "nginx" uninstalled 

Итоги и что дальше

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Используете Helm в своём Kubernetes кластере?

69.39% Да, использую как свои чарты, так и чарты от сторонних разработчиков34
16.33% Да, но только свои чарты8
8.16% Нет, разворачиваю все ресурсы через kubectl apply4
6.12% Я не использую Kubernetes3

Проголосовали 49 пользователей. Воздержались 5 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/769046/


Комментарии

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

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