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.

Читайте также в нашем блоге:

Комментарии (0)