Централизация пайплайнов GitLab CI и автоматизация версионирования Helm-чартов
В процессе масштабирования микросервисной архитектуры команды разработки неизбежно сталкиваются с проблемой поддержки CI/CD пайплайнов (конвейеров непрерывной интеграции и доставки). Когда количество микросервисов превышает десяток, ручное копирование конфигурационных файлов .gitlab-ci.yml превращается в административный кошмар. В каждом репозитории накапливаются сотни строк дублирующегося кода, а внесение изменений в процесс сборки или деплоя (например, обновление версии Docker или Helm) требует ручного редактирования десятков репозиториев.
Аналогичные проблемы возникают и с управлением Helm-чартами (пакетами для развертывания приложений в Kubernetes). Обновление параметров values или версий чартов вручную отнимает много времени и чревато человеческими ошибками.
Для решения этих проблем применяется методика централизации пайплайнов через директиву include в GitLab CI и автоматизация версионирования чартов с помощью утилиты yq и защищенных токенов авторизации.
Шаг 1. Создание центрального репозитория шаблонов
Первым шагом является вынос всей общей логики сборки, тестирования и развертывания в отдельный выделенный репозиторий. Назовем его infrastructure/ci-templates. Разработчики микросервисов больше не будут писать логику выполнения задач — они будут лишь подключать готовые шаблоны.
Рекомендуемая структура файлов в центральном репозитории infrastructure/ci-templates:
.gitlab/
├── templates/
│ ├── build.yml # Сборка Docker-образов
│ ├── test.yml # Тестирование кода
│ ├── deploy.yml # Развертывание в Kubernetes
│ └── helm.yml # Работа с Helm-чартами
└── jobs/
├── build-job.yml # Конкретные реализации джоб
└── deploy-job.yml
Шаг 2. Описание универсального шаблона сборки Docker-образов
В файле templates/build.yml центрального репозитория описывается скрытый джоб-шаблон (его имя начинается с точки, чтобы GitLab не запускал его самостоятельно):
.build_template:
stage: build
image: docker:24.0.7
services:
- docker:24.0.7-dind
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: "/certs/client"
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- docker build --tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: always
!IMPORTANT Если вы используете собственный сервер GitLab Runner (self-managed), для работы сборщика Docker-in-Docker (dind) в конфигурационном файле раннера
config.tomlобязательно должен быть настроен параметр монтирования томов:volumes = ["/certs/client", "/cache"]. Без этого клиент Docker внутри контейнера сборки не сможет получить доступ к TLS-сертификатам демона, и задача завершится с ошибкой соединения. Для общих раннеров на GitLab.com эта настройка уже сделана по умолчанию.
Шаг 3. Создание шаблона деплоя с помощью Helm
В файле templates/deploy.yml описывается логика развертывания приложения в кластер Kubernetes. В качестве базового образа используется официальный образ Helm:
.deploy_template:
stage: deploy
image: alpine/helm:3.12.0
script:
- helm upgrade --install "$CI_PROJECT_NAME" ./chart/ \
--set image.repository=$CI_REGISTRY_IMAGE \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--set environment=$DEPLOY_ENV \
--namespace $DEPLOY_ENV \
--create-namespace \
--wait \
--timeout 5m
environment:
name: $DEPLOY_ENV
url: https://$CI_PROJECT_NAME.$DEPLOY_ENV.example.com
rules:
- if: '$DEPLOY_ENV == "prod" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
- if: '$DEPLOY_ENV == "staging" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: on_success
Флаг --wait указывает Helm ожидать, пока все ресурсы Kubernetes (поды, сервисы) не перейдут в состояние готовности. Тайм-аут в 5 минут предотвращает вечное зависание пайплайна в случае сбоя при деплое. Окружение prod деплоится только вручную, а staging — автоматически при успешном завершении предыдущих этапов.
Шаг 4. Подключение шаблонов в микросервисах
Теперь в репозитории конкретного микросервиса файл .gitlab-ci.yml становится лаконичным. Разработчику достаточно импортировать внешние шаблоны с помощью include и расширить их (extends):
include:
- project: 'infrastructure/ci-templates'
ref: main
file: '/templates/build.yml'
- project: 'infrastructure/ci-templates'
ref: main
file: '/templates/deploy.yml'
variables:
SERVICE_NAME: $CI_PROJECT_NAME
stages:
- prepare
- build
- test
- deploy
build:
extends: .build_template
deploy_staging:
extends: .deploy_template
variables:
DEPLOY_ENV: staging
deploy_prod:
extends: .deploy_template
variables:
DEPLOY_ENV: prod
Шаг 5. Автоматическое вычисление версий Helm-чартов
Ручное обновление поля version в файле Chart.yaml часто приводит к конфликтам версий. Есть два сценария автоматизации этого процесса в GitLab CI.
Сценарий А. Если Helm-чарт лежит внутри репозитория самого сервиса
В этом случае перед этапом сборки добавляется задача предварительной подготовки, которая вычисляет версию чарта на основе тегов Git и коммита, а затем обновляет YAML-файл с помощью современной утилиты yq (версии 4+):
update_chart_version:
stage: prepare
image: alpine:3.18
variables:
GIT_DEPTH: 0 # Отключаем shallow clone, чтобы получить все теги Git
script:
- apk add --no-cache yq git
- |
RAW=$(git describe --tags --always --dirty 2>/dev/null || true)
VERSION=${RAW#v} # Убираем префикс "v", если он есть (v1.2.3 -> 1.2.3)
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+'; then
VERSION="0.0.0-${CI_COMMIT_SHORT_SHA}"
fi
echo "Chart version: $VERSION"
yq -i ".version = \"${VERSION}\"" chart/Chart.yaml
- cat chart/Chart.yaml
artifacts:
paths:
- chart/Chart.yaml
Обновленный файл Chart.yaml передается на следующие стадии как артефакт сборки.
Сценарий Б. Если Helm-чарты хранятся в общем центральном репозитории
Если все чарты лежат в отдельном проекте (например, infrastructure/helm-charts), пайплайн микросервиса должен скопировать свои файлы конфигурации чарта туда и сделать коммит.
Для безопасной авторизации в стороннем репозитории стандартный токен CI_JOB_TOKEN не подходит, так как он не дает прав на выполнение команды git push. Решением является использование Project Access Token (или Group Access Token) с правами Developer и областью видимости write_repository. Этот токен сохраняется в переменных CI/CD проекта под именем HELM_CHARTS_TOKEN (с флагами Masked и Protected).
Пример задачи для пуша изменений в репозиторий чартов:
update_helm_repo:
stage: deploy
image: alpine/git:2.40.1
script:
# Клонируем репозиторий чартов, используя access token. Имя пользователя — oauth2.
- git clone https://oauth2:${HELM_CHARTS_TOKEN}@gitlab.com/infrastructure/helm-charts.git
- cd helm-charts
- mkdir -p $SERVICE_NAME
- cp -r ../chart/* ./$SERVICE_NAME/
- git config user.email "[email protected]"
- git config user.name "GitLab CI"
- git add .
# Коммитим только при наличии изменений, чтобы не ломать пайплайн пустым коммитом
- git diff-index --quiet HEAD || git commit -m "Update $SERVICE_NAME to $CI_COMMIT_SHORT_SHA"
# Опция ci.skip предотвращает запуск бесконечного цикла пайплайнов в репозитории чартов
- git push -o ci.skip origin HEAD:main
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Использование опции -o ci.skip при пуше — важная мера предосторожности, которая указывает GitLab не запускать сборочные конвейеры для этого коммита в целевом репозитории, исключая бесконечные циклы запуска задач. Пайплайн запускается только при коммитах в ветку по умолчанию (main).
