Вступление
Я люблю манифесты 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 также для собственных сервисов. Причин для этого несколько:
Helm позволяет управлять несколькими манифестами как единым целым. В наших сервисах по 4-7 манифестов, некоторые требуют подстановки переменных (через envsubst), что также осложняет деплой вне CI/CD пайплайна.
Все наши сервисы строятся почти по одному принципу, следовательно, много кода повторяется. При помощи хелмовского values.yaml (об этом чуть позже) получится вынести меняющиеся значения из манифестов и использовать один чарт для нескольких приложении (релизов) сразу.
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). Спасибо за прочтение!
