Вступление
Я люблю манифесты 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). Спасибо за прочтение!
Комментарии (8)
past
22.10.2023 16:48+2В голосовании не хватает опции "использую только для сторонних чартов. Многие вендоров распространяют свое ПО только в виде чартов.
past
Я в свое время написал 100+ чартов и сейчас считаю, что Хелм это зло. Он приносит кучу добавленной сложности в замен так называемых "релизов" которые, как правило, оказываются сломаны в тот момент, когда они особенно нужны, например, когда надо срочно чинить прод. Мы года 4 назад перешли на kustomize и с тех пор забыли про Хелм как про страшный сон, чего и вам желаю.
AzamatKomaev Автор
Добрый вечер! По поводу опции в голосовании вы правы - упустил этот момент, но уже поздно(
Пока Helm кажется идеальным решением, чтобы облегчить возню с деплоем приложении. Это открывает большие возможности - сейчас у нас десяток микросервисов и для каждого своя папка с YAML манифестами, с хелмом получится удалить все эти манифесты и обойтись одной командой
helm install
, только лишь изменив некоторые значения параметром--set
. С kustomize признаюсь почти не работал, единственное для чего пытался его использовать, так это для создания секрета со значениями из .env файлика, но впоследствии отказался от этого. В любом случае, спасибо за совет, но я пока остановлюсь на Helm :)ToomIm
Кастомайз всё же лучше как мне кажется, попробуйте!
- Делаете для ci шаблон делоя, сервиса, конфиг-мапы и тд.
- Дальше в зафисимости от среды деплоя(можно папками разделить) главный(например dev). В нем описываете откуда секреты тянуть и тд.
- Делаете папки с наименованиями стендов
- Делаете там kustomization.yml и переопределяете то что нужно и отличается от dev
В целом по основному всё :)
Есть моменты, не спорю, когда helm может порешать. Но я их вижу только в рамках официальных чартов того же vault, например.
ElectricPigeon
Подскажите, пожалуйста, один момент на Вашем опыте. Я для себя открыл helm, когда столкнулся с необходимостью параметризовать манифесты в CI/CD. Конкретнее, в случае, когда я хотел деплоить приложения на dev и prod окружения. Я использовал
values.yaml
, чтобы указать там имя секрета, доменное имя для ingress и прочие вещи, которые могут различаться в разных окружениях.Я согласен с вашим мнением про добавленную сложность, но как тогда решить проблему, когда тебе действительно нужна параметризация?
koala1101
тут есть два варината:
1) можно при вызове helm указывать флаги
--set ingress.baseHost=example.domain
2) держать 2 values.yaml по типу values-dev.yaml и values-prod.yaml, указывая также при вызове helm какой файлик вы хотите использовать
helm -f values-dev.yaml
ElectricPigeon
Спасибо за ответ) Я как раз так и делал.
Я хотел поинтересоваться у автора комментария, как он в случае с Kustomize решает такую же проблему, если вообще с ней сталкивается
past
В kustomize есть такая вещь, как слои.
Обычно делают базовый слой, где лежит не параметризованный набор манифестов и оверлеи, которые ссылаются на базовый слой как родительский и в которых накладываются патчи с конкретными значениями, которые нужно поменять для окружений. Вот годная статья от Фланта https://habr.com/ru/companies/flant/articles/469179/