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.
Читайте также в нашем блоге: