GitLab – это мощный и в то же время простой инструмент для организации проектов. Как и любой крупный и самодостаточный продукт, GitLab постоянно развивается и дорабатывается. И сегодня хотелось бы обсудить новый функционал, который пока ещё находится в разработке, но уже доступен для использования. Речь идёт о поддержке размещения Helm-чартов в GitLab Package Registry. Для простоты далее я буду называть его GitLab Helm repo.

Зачастую основой для описания инфраструктуры, запускаемой в Kubernetes, являются Helm-чарты. Поэтому при работе команды инженеров с большим количеством проектов невольно приходят мысли о стандартизации подходов работы с этими чартами. С появлением GitLab 14.1 появилась возможность настраивать хранение общих чартов для всех проектов, с которыми ведется работа.
Немного про концепцию
До появления соответствующего функционала в GitLab для подобных задач можно было использовать, например, ChartMuseum или вовсе не иметь единого хранилища для чартов, а хранить исходники манифестов прямо в репозитории с кодом приложения. Конечно, с точки зрения функциональности это не имело каких-то явных недостатков, но в смысле стандартизации подхода оформления проектов со временем — по мере увеличения количества проектов — всё превращалось в большую кашу.
С использованием же GitLab Helm repo можно иметь один репозиторий в корпоративном GitLab, в котором хранить общие Helm-чарты для часто используемых компонентов инфраструктуры и элементов «кубернетизации» приложений. Например, Helm-чарт для stateless-приложений, который из коробки включает в себя манифесты для полной настройки следующих ресурсов Kubernetes:
Deployment;
ConfigMap;
Secret;
Service;
HPA;
VPA;
PDB.
Представьте: у вас есть только values.yaml, где описана вся логика работы приложения, а все нужные манифесты отрендерятся сами при деплое.
Как это работает? Когда вы устанавливаете и настраиваете GitLab для нового проекта, у него создаётся «дочерний» репозиторий. Его CI завязан на подключение к вашему корпоративному хранилищу, скачивание из него последних актуальных версий Helm-чартов и повторный push в локальный репозиторий проекта. Такое простое взаимодействие даёт возможность модернизировать, дорабатывать и фиксить все компоненты в одном месте, а дочерние репозитории сами подтянут актуальные изменения. Кроме того, таким образом намного ближе становится решение вопроса о стандартизации подхода при оформлении новых проектов.
Реализация основного репозитория

Пошагово поднимем несколько репозиториев, и я наглядно покажу, как можно организовать взаимодействие между ними. Скажу наперёд, в CI будем использовать werf.
Использование werf для реализации идеи, рассматриваемой в статье, не является обязательным, но, так как этот инструмент обеспечивает комплексный подход для работы с сабчартами и мы владеем огромной экспертизой в нём, то весь процесс становится проще.
Приступим к созданию «главного» репозитория, в котором будут храниться общие Helm-чарты.
1. Создаём новый репозиторий:

2. Создаём структуру каталогов и базовые файлы для организации CI/CD:
. ├── .gitlab-ci.yml ├── .helm │ ├── Chart.lock │ ├── charts │ │ └── my-chart │ │ ├── Chart.yaml │ │ ├── templates │ │ │ └── main.yaml │ │ └── values.yaml │ └── Chart.yaml ├── README.md └── werf.yaml
Для простоты в качестве main.yaml я буду использовать примитивный манифест ConfigMap, который просто печатает название чарта:
#---- .helm/charts/my-chart/templates/main.yaml apiVersion: v1 kind: ConfigMap metadata: name: {{ .Chart.Name }} data: file: | {{ .Chart.Name }} #---- werf.yaml project: charts-repo configVersion: 1
Хочется уделить больше внимания файлу .gitlab-ci.yml. Он может читаться сложно, поэтому я постарался прокомментировать длинные и необычные команды:
stages: - publish-charts variables: REPO_URL: "${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/helm/api/stable/charts" before_script: - set -eo pipefail # Активируем werf. - type trdl && . $(trdl use werf 1.2 stable) - type werf && source $(werf ci-env GitLab --as-file) - | # Обновляем доступные Helm repo через werf. werf helm repo update # Ищем все файлы с описанием чартов и используем их для построения зависимостей. find . -type f -regex '.*/\(Chart.ya?ml\|requirements.ya?ml\)' -exec \ sh -c 'werf helm dependency build $(dirname "{}") --skip-refresh' \; "publish charts": stage: publish-charts script: - | # Пробегаем по всем директориям с чартами и упаковываем чарты, помещая их в директорию .packages. mkdir -p .packages while read chart; do echo "[PACKAGING CHART $chart]" werf helm package "$chart" -d .packages done < <(find .helm/charts -mindepth 1 -maxdepth 1 -type d) - | # Заполняем 2 переменные: CHART_NAME и CHART_VERSION. find .packages -mindepth 1 -maxdepth 1 -type f -name '*.tgz' -exec sh -c 'basename "$0"' '{}' \; | while read package; do CHART_NAME=$(echo $package | sed -e 's/-[0-9]\.[0-9]\.[0-9]\.tgz$//g') CHART_VERSION=$(echo $package | sed -e 's/^[a-zA-Z-].*-//g' | sed -e 's/.tgz$//g') # используя переменные выше, устанавливаем, есть ли уже чарт с таким именем и версией в Helm repo CHART_EXISTS=$(werf helm search repo -l $REPO_NAME/$CHART_NAME | { egrep "$REPO_NAME/$CHART_NAME\s"||true; } | { egrep "$CHART_VERSION\s"||true; } | wc -l) # если его нет, то пушим в package registry, иначе выводим сообщение, что чарт уже присутствует в Helm repo if [ $CHART_EXISTS = 0 ]; then curl -sSl --post301 --form "chart=@.packages/$package" --user "$REPO_PUSH:$REPO_PUSH_SECRET" "$REPO_URL" else echo "Chart package $package already exists in Helm repo! Skip!" fi done only: - tags
Для использования этого CI (то есть для push’а чарта в package registry) нам нужно создать некоторые ключи и оформить их в виде переменных окружения. Рассмотрим их подробнее на следующем шаге.
3. Переходим в настройки Repository (Settings -> Repository -> Deploy tokens) и создаём токен с правами read_package_registry и write_package_registry.
4. Переходим в настройки CI/CD (Settings -> CI/CD -> Variables) и создаём переменные окружения:
REPO_NAME— алиас (например,my-charts);REPO_PUSH— название токена из п.3;REPO_PUSH_SECRET— Secret для токена из п.3.
5. Заходим на машину с GitLab Runner’ом, на котором будет запускаться CI данного проекта, и регистрируем на нём Helm repo (команды werf helm здесь аналогичны обычным командам helm):
werf helm repo add --username $REPO_PUSH --password $REPO_PUSH_SECRET $REPO_NAME ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/helm/stable werf helm repo update
Не забудьте заменить переменные окружения реальными значениями!
Примечание: для своего удобства я не стал создавать отдельную пару токен/Secret для GitLab Runner’а и поэтому буду использовать те, что получил в п.3. Но в реальных кейсах мы рекомендуем для настройки GitLab Runner’ов генерировать отдельный токен с правами только на
read_package_registry.
6. Commit’им, push’им изменения, создаем новый тег (например, my-chart-1.0.0) и переходим в созданный pipeline. После окончания Job’а переходим в Packages & Registries -> Package Registry и проверяем, что наш Helm-чарт там присутствует.


Примечание: я использую деплой по тегу, потому что в такой вариации мы всегда сможем быстро найти, с какого коммита был запущен тот или иной Helm-чарт. К тому же, бывают случаи, когда package registry утерян: в такой ситуации, когда после восстановления package registry он пустой, мы сможем за-push’ить только последние версии Helm-чартов, которые будут в main-ветке. Если какое-то приложение использует устаревшую версию Helm-чарта, то возникнут трудности. Имея на каждую версию чарта свой тег, мы сможем быстро восстановить для приложения необходимый компонент.
7. Проверим состояние репозитория на GitLab Runner’е через werf:
werf helm repo update werf helm search repo my-charts NAME CHART VERSION APP VERSION DESCRIPTION my-charts/my-chart 1.0.0
Видим, что Helm-чарт появился в репозитории. Полдела сделано!
Реализация дочернего репозитория
Репозиторий на стороне проекта, который будет использовать общий чарт, настраиваем немного иначе.
1. Создаём пустой репозиторий и приводим его структуру к следующему виду:
. ├── .gitlab-ci.yml ├── .helm │ └── Chart.yaml ├── README.md └── werf.yaml
2. Рассмотрим содержимое ключевых файлов:
#---- .helm/Chart.yaml apiVersion: v2 name: client-charts-repo version: 1.0.0 dependencies: - name: my-chart version: ~1.0 repository: "@my-charts"
#---- werf.yaml project: client-charts-repo configVersion: 1
#---- .gitlab-ci.yml stages: - publish-charts variables: REPO_URL: "${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/helm/api/stable/charts" HELM_URL: "${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/helm/stable" default: before_script: - set -eo pipefail - type trdl && . $(trdl use werf 1.2 stable) - type werf && source $(werf ci-env GitLab --as-file) .base_publish_charts: stage: publish-charts script: | werf helm repo add --force-update --username $MAIN_REPO_PULL --password $MAIN_REPO_PULL_SECRET $MAIN_REPO_NAME $MAIN_HELM_URL werf helm repo update werf helm dependency update .helm/ find .helm/charts -mindepth 1 -maxdepth 1 -type f -name '*.tgz' -exec sh -c 'basename "$0"' '{}' \; | while read package; do CHART_NAME=$(echo $package | sed -e 's/-[0-9]\.[0-9]\.[0-9]\.tgz$//g') CHART_VERSION=$(echo $package | sed -e 's/^[a-zA-Z-].*-//g' | sed -e 's/.tgz$//g') CHART_EXISTS=$(werf helm search repo $REPO_NAME | { egrep "$REPO_NAME/$CHART_NAME\s" || true; } | { egrep "$CHART_VERSION\s" || true; } | wc -l) if [ $CHART_EXISTS = 0 ]; then curl -sSl --post301 --form "chart=@.helm/charts/$package" --user "$REPO_PUSH:$REPO_PUSH_SECRET" "$REPO_URL" else echo "Chart package $package already exists in Helm repo! Skip!" fi done werf helm repo add --username $REPO_PULL --password $REPO_PULL_SECRET $REPO_NAME $HELM_URL werf helm repo update echo "Настройка на ПК инженера" echo "REPO_URL: $REPO_URL" echo "werf helm repo add --username $REPO_PULL --password $REPO_PULL_SECRET $REPO_NAME $HELM_URL" rules: - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' when: on_success allow_failure: true "publish charts": extends: - .base_publish_charts tags: - werf
3. Теперь разберём переменные окружения, которые используются в настройках проекта:
MAIN_REPO_PULL— название токена с правамиread_package_registryв главном репозитории.MAIN_REPO_PULL_SECRET— Secret токена с правамиread_package_registryв главном репозитории.MAIN_REPO_NAME— алиас главного репозитория.MAIN_HELM_URL— URL для доступа в главный репозиторий.CLIENT_REPO_NAME— алиас дочернего репозитория.CLIENT_REPO_PUSH— название токена с правамиwrite_package_registryв дочернем репозитории.CLIENT_REPO_PUSH_SECRET— Secret токена с правамиwrite_package_registryв дочернем репозитории.CLIENT_REPO_PULL— название токена с правамиread_package_registryв дочернем репозитории.CLIENT_REPO_PULL_SECRET— Secret токена с правамиread_package_registryв дочернем репозитории.
4. Настроим переменные окружения дочернего репозитория для доступа в главный репозиторий:
Переходим в главный репозиторий во вкладку
Settings->Repository->Deploy tokensи создаём новый токен с правамиread_package_registry.Помещаем полученные значение в переменные окружения
MAIN_REPO_PULLиMAIN_REPO_PULL_SECRETдочернего репозитория.MAIN_REPO_NAMEберём изREPO_NAME, который задавали в п.4 при настройке главного репозитория,MAIN_HELM_URLдолжна соответствовать значению${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/helm/stableглавного репозитория.
5. В дочернем репозитории переходим во вкладку Settings -> Repository -> Deploy tokens и создаём два токена.
Первый — с правами write_package_registry. Полученные данные помещаем в переменные окружения:
CLIENT_REPO_PUSH— название токена,CLIENT_REPO_PUSH_SECRET— Secret токена.
Второй — с правами read_package_registry. Полученные данные помещаем в переменные окружения:
CLIENT_REPO_PULL— название токена,CLIENT_REPO_PULL_SECRET— Secret токена.
Также придумываем алиас для Helm repo и помещаем его в переменную CLIENT_REPO_NAME. Например, CLIENT_REPO_NAME = client-charts-repo.
На этом этапе мы закончили настройку дочернего репозитория. Если запустить pipeline на основной ветке в такой конфигурации, Job скачает из главного Helm repo все чарты, которые указаны в качестве зависимостей в файле .helm/Chart.yaml. Это добавляет дополнительной гибкости, так как позволяет включать в проект только те Helm-чарты, которые в нём требуются.
В данной реализации CI для дочернего репозитория из главного Helm repo скачивается только последний актуальный Helm-чарт. Можно доработать CI таким образом, чтобы он повторно push’ил все доступные версии Helm-чартов. Можно добавить какую-нибудь логику, которой вам будет удобно пользоваться, но так как это выходит за рамки статьи, я воспользуюсь правом не приводить примеры (кто сказал про рендер и валидацию Helm-чарта перед push’ем?..).
На этом этапе мы закончили настройку всех компонентов. Теперь при push’е пакета с Helm-чартом в родительский Package registry его можно будет скачать через CI дочерних (клиентский) репозиториев. В дополнение можно настраивать CI по своим нуждам — например, сделать так, чтобы дочерние CI запускались по расписанию и автоматом подтягивали обновления чартов к себе (без явного запуска пайплайна руками).
Выводы
В статье мы рассмотрели, как научить GitLab нужного проекта выкачивать Helm-чарты из централизованного хранилища в другом репозитории. Это очень удобная и полезная функция в GitLab. Нам давно не хватало подобной возможности, и вот наконец мы можем управлять Helm-чартами, как говорится, «не отходя от кассы»!
P.S.
Читайте также в нашем блоге:
