19 июн. 2025 г.·7 мин чтения

Родительско-дочерние пайплайны GitLab для релизов нескольких сервисов

Родительско-дочерние пайплайны GitLab помогают командам выпускать несколько сервисов из одного репозитория без копипаста YAML, заблокированных деплоев или ещё одного CI-инструмента.

Родительско-дочерние пайплайны GitLab для релизов нескольких сервисов

Почему один большой пайплайн становится проблемой

Один пайплайн кажется удобным, когда в репозитории всего два или три сервиса. Потом продукт растёт. И вот уже тот же самый пайплайн должен собирать API, worker, web-приложение, фоновые задачи и ещё несколько внутренних инструментов. Маленькое изменение в одном месте может запустить работу по всему остальному дереву.

Это очень обычным образом замедляет команды. Если billing worker нужен срочный фикс, люди всё равно ждут web-тесты, упаковку mobile и проверки, которые к billing вообще не относятся. Десять лишних минут не звучат страшно, но на фоне нескольких релизов в день это превращается в часы потерянного времени.

Следующей проблемой обычно становится CI-файл. Команды начинают с одной общей настройки, а потом копируют её части, чтобы обработать исключения. Одному сервису нужен другой образ. Другому — другие правила деплоя. Третьему — дополнительные тестовые данные. Через несколько месяцев никто уже не уверен, какой блок был оригинальным, а какой дописали позже. Две задачи могут выглядеть почти одинаково и при этом вести себя по-разному.

В монорепозиториях такой дрейф особенно болезненный. Один сервис может использовать самые новые шаги деплоя, а другой — всё ещё запускать старую копию, которая пропускает проверку или берёт устаревшие переменные. Исправление означает редактировать одну и ту же логику в нескольких местах. Пропустите одну копию — и ловушка останется в репозитории.

Но ещё серьёзнее проблема релизного узкого места. Один упавший job может заблокировать всех, даже если ошибка произошла в сервисе, который никто не трогал. Нестабильный UI-тест может задержать срочный патч для API. Проблема в упаковке внутреннего инструмента может отложить исправление для клиента. В этот момент пайплайн уже не защищает релизы, а создаёт заторы.

Обычно тревожные признаки появляются довольно рано:

  • команды перезапускают пайплайны только чтобы обойти чужие сбои
  • небольшие изменения застревают за длинными стадиями тестов
  • правила деплоя понемногу расходятся от сервиса к сервису
  • люди выпускают обновления реже, потому что процесс кажется рискованным

Последний пункт особенно важен. Когда все сервисы живут по одному графику релизов, самая быстрая команда движется со скоростью самой медленной. Сервис, который уже готов сегодня, ждёт другой, который всё ещё на проверке. Команды начинают объединять изменения в крупные пакеты, а такие релизы сложнее отлаживать.

Стартапы сталкиваются с этим постоянно. Один репозиторий и один пайплайн сначала кажутся простым решением. Но позже эта простота оказывается ложной. Код вроде бы централизован, а в повседневной работе всё забито ожиданием, дрейфом и заблокированными релизами.

Что меняют родительско-дочерние пайплайны

Родительский пайплайн — это небольшой пайплайн наверху. Он проверяет, что изменилось, задаёт общие переменные, запускает несколько проверок, которые нужны всем, и решает, какие сервисные пайплайны стоит запустить.

Дочерний пайплайн — это отдельный пайплайн для одного сервиса или одной части репозитория. У него свои job, правила и путь деплоя. Web-приложение может собраться и выйти в релиз, не таща за собой worker, API и административную панель по тому же самому маршруту.

Это меняет саму форму CI. Вместо одного длинного файла, который пытается учесть все случаи, вы держите родительский пайплайн лёгким, а логику сервисов переносите в более маленькие дочерние файлы. Репозиторий по-прежнему остаётся единым, но релизы перестают вести себя как один поезд, где каждый вагон обязан тронуться одновременно.

В типичном монорепозитории это может означать отдельные пайплайны для web-приложения, API, фонового worker и сайта документации. Если меняется только документация, запускается только документационный пайплайн. Если меняется API, его пайплайн может протестировать код, собрать образ и задеплоить его в своё окружение, не дожидаясь чужих задач.

Правила тоже становятся понятнее. Вы держите условия рядом с тем сервисом, к которому они относятся. Пайплайн web может реагировать на изменения в services/web, а пайплайн worker — только на services/worker и запланированные задания.

Общие проверки всё равно должны жить где-то отдельно. Родительский пайплайн — хорошее место для lint на уровне репозитория, security checks или настройки версии перед запуском дочерних пайплайнов. Так у вас появляется одна точка управления для общей работы и отдельные дорожки для релиза каждого сервиса.

В этом и есть главный плюс. Вы сохраняете один репозиторий для кода, прав доступа и истории, но каждый сервис выходит по своему графику. Для большинства команд именно ради этого и выбирают монорепозиторий.

Практическая польза тоже очевидна: меньше скопированного YAML, а ошибки читаются проще. Когда пайплайн ломается, обычно видно, какой именно сервис упал и почему, без разбора страниц чужих job.

Когда такая схема уместна

Родительско-дочерние пайплайны имеют смысл, когда в одном репозитории живёт несколько сервисов, но они не ведут себя как одно приложение. Общий репозиторий удобен для переиспользования кода, общих библиотек и единого места для ревью. Проблемы начинаются тогда, когда каждый push будит один и тот же огромный пайплайн, даже если изменился только один сервис.

Боль усиливается, когда команды выкатывают изменения по разным графикам. Например, API выходит два раза в неделю, admin-приложение — раз в день, а worker меняется только раз в несколько недель. Если всем этим управляет один пайплайн, мелкие обновления ждут позади чужих job. Люди также начинают спорить, кто вообще владеет CI-файлом, потому что каждому сервису нужны чуть разные правила.

Такая структура полезна и тогда, когда часть проверок должна оставаться в одном центральном месте. Родитель может отвечать за правила lint, security scan, версии и логику веток. Каждый дочерний пайплайн затем запускает тесты сервиса и шаги деплоя, которые имеют смысл только для этого сервиса. Общие правила остаются вместе, но ни одно приложение не вынуждают жить в одном и том же релизном потоке.

Представьте монорепозиторий с четырьмя сервисами: web, API, billing worker и internal admin. Команде web нужны быстрые деплои после изменений в интерфейсе. Billing worker требует дополнительных тестов и более строгого времени релиза. Родительско-дочерние пайплайны позволяют сделать и то и другое без копирования всего CI-файла четыре раза.

Но это всё ещё не лучший выбор для всех. Если один простой пайплайн уже работает, оставьте его. Если у вас один небольшой продукт, один ритм релизов и короткий CI-файл, который всем понятен, лишние слои только добавят движущихся частей. Такая схема помогает тогда, когда сервисы действительно отличаются друг от друга, а не когда команде нужен более впечатляющий рисунок на слайде.

Хорошее совпадение обычно видно по нескольким признакам:

  • в одном репозитории живут несколько деплоемых сервисов
  • разные сервисы выходят в релиз в разное время
  • часть проверок должна оставаться в одном центральном пайплайне
  • командам нужно понятнее владение логикой сборки и деплоя

Если ваш CI-файл всё ещё помещается на один экран и никто не ненавидит в него лезть, скорее всего, вам это пока не нужно. Если он превратился в длинную цепочку скопированных правил и исключений, разделение обычно окупается быстро.

Как настроить всё шаг за шагом

Начните с самого маленького родительского файла, который сможете поддерживать. В большинстве репозиториев он должен делать три вещи: определять, что изменилось, запускать несколько общих проверок и триггерить нужные дочерние пайплайны. Держите его коротким намеренно. Если родитель снова вырастет в огромный .gitlab-ci.yml, вы просто переименовали ту же проблему.

Поместите общие проверки в родительский пайплайн. Lint общего кода, базовый набор тестов и простые policy checks должны жить там, потому что они нужны каждому сервису. Это убирает дублирующий YAML и не даёт одной и той же job запускаться три-четыре раза в разных сервисных пайплайнах.

Затем создайте по одному файлу дочернего пайплайна на каждый сервис. Если в вашем монорепозитории есть api, worker и web, дайте каждому свой файл и пусть этот файл отвечает за сборку, тесты и деплой. Логика релиза должна оставаться рядом с самим сервисом. Команде, которая работает над worker, не стоит редактировать web-пайплайн релиза.

Используйте правила по путям, чтобы правка документации не будила каждый deploy-job в репозитории. Часто достаточно простого сопоставления папок. Изменения в services/api/ должны запускать дочерний пайплайн API. Изменения в services/web/ должны запускать web-пайплайн.

Передавайте данные релиза сверху вниз каждый раз. Имя ветки, тег, SHA коммита и релизные метки должны уходить в каждый дочерний пайплайн как переменные. Так у всех сервисов будет один и тот же источник правды. Если вы ставите тег вроде v1.8.0, каждый дочерний пайплайн должен прочитать именно его, а не придумывать свою версию.

Сначала проверьте это на одном сервисе, прежде чем раскатывать по всему репозиторию. Возьмите сервис с самым простым путём релиза и сделайте сначала его. Посмотрите на граф пайплайна, убедитесь, что переменные доходят до дочернего пайплайна, и проверьте, что он запускается только при изменении своих файлов.

Когда первый сервис заработает, копируйте схему, а не весь файл. Переиспользуйте паттерн, держите правила сервиса рядом и не пытайтесь централизовать каждую мелочь. Обычно именно так многосервисные релизы остаются аккуратными, а не скатываются обратно в YAML-хаос.

Как делиться версиями и настройками

Выстроить единый поток версий
Сделайте теги, имена образов и настройки деплоя одинаковыми для всех сервисов.

Если каждый дочерний пайплайн начинает придумывать свои переменные, релизы быстро становятся грязными. Один сервис деплоит v1.8.2, другой использует сырой commit SHA, а третий называет то же окружение production вместо prod. Родительский пайплайн должен один раз решить общие значения и передать их дальше.

Для большинства команд достаточно небольшого набора переменных:

  • VERSION для номера релиза
  • IMAGE_TAG для тега контейнера
  • STAGE для целевого окружения, например staging или prod
  • REGION для локации, например us или eu

Оставляйте эти имена одинаковыми везде. Это важнее, чем кажется. Если один дочерний пайплайн использует ENV, другой — TARGET, а третий — DEPLOY_ENV, никто не поймёт, что правильно, пока не откроет три YAML-файла.

Для тегов образов пусть родитель один раз вычислит значение и отправит его в каждый дочерний пайплайн через trigger-job. Тогда все сервисы будут собираться, тестироваться и деплоиться с одной и той же релизной меткой. Откаты тоже станут проще, потому что весь релиз будет указывать на одну понятную версию, а не на смесь разных тегов.

Храните общие значения по умолчанию в одном месте. В GitLab это часто означает общий CI-файл с переменными по умолчанию, правилами именования и несколькими разумными запасными значениями. Каждый сервис по-прежнему может добавлять свои настройки, но базовые значения должны жить в одном файле, который все быстро находят.

Имена окружений заслуживают такой же дисциплины. Выберите один формат и сделайте его скучным. dev, staging и prod работают хорошо. Если вы ещё и деплоите по регионам, используйте такие же простые названия, например staging-us и prod-eu. Когда названия совпадают между сервисами, approvals, логи и история деплоев читаются намного легче.

Не стоит строить хитрые цепочки переменных. Если VERSION берётся из APP_VERSION, если он пустой — из RELEASE_VERSION, а если есть тег — ещё откуда-то, люди почти наверняка угадают неправильно. Вычисляйте общие значения в родителе и передавайте их явно. Дочерние пайплайны должны использовать эти значения, а не переосмысливать их.

Простой пример релиза

Представьте один репозиторий с тремя папками: web/, api/ и worker/. Команда выпускает все три сервиса из одной кодовой базы, но не хочет, чтобы каждое изменение запускало один огромный релизный пайплайн.

С родительско-дочерними пайплайнами родитель один раз делает общую работу. Он может запустить проверку секретов, проверку зависимостей и любую валидацию, которая относится ко всему репозиторию. После этого он решает, какие дочерние пайплайны нужно стартовать в зависимости от того, что изменилось.

Пример простой настройки может выглядеть так:

stages:
  - checks
  - triggers

security_checks:
  stage: checks
  script:
    - ./ci/run-security-checks.sh

web_release:
  stage: triggers
  trigger:
    include: web/.gitlab-ci.yml
  rules:
    - changes:
        - web/**/*

api_release:
  stage: triggers
  trigger:
    include: api/.gitlab-ci.yml
  rules:
    - changes:
        - api/**/*

worker_release:
  stage: triggers
  trigger:
    include: worker/.gitlab-ci.yml
  rules:
    - changes:
        - worker/**/*

Теперь представьте три обычных дня в этом репозитории.

Дизайнер обновляет checkout-страницу в web/. Родитель запускает общие security checks один раз, а затем стартует только дочерний web-пайплайн. GitLab собирает front end, запускает web-тесты и деплоит web-приложение. API и worker остаются в покое.

Позже backend-разработчик меняет api/. Этот коммит запускает только API-дочерний пайплайн. Он может прогнать contract tests, проверки миграций базы данных, собрать контейнер, поставить тег релиза и задеплоить API, не затрагивая поток релиза web.

Разделение особенно помогает, когда срочное исправление не может ждать. Если команда чинит баг в worker/, worker-пайплайн может протестировать и выпустить этот hotfix сразу. Никто не ждёт медленный web-набор тестов, который к изменению вообще не относится.

Именно здесь многосервисные релизы перестают ощущаться спутанными. У каждого сервиса остаётся свой ритм, но у репозитория всё ещё есть одна понятная точка входа. Родитель даёт общее управление. Дочерние пайплайны дают каждому сервису пространство для движения.

Ошибки, которые создают путаницу

Спланировать лёгкие пути деплоя
Спланируйте релизы web, API, worker и admin без одного огромного CI-файла.

Большинство проблем с родительско-дочерними пайплайнами начинается с хороших намерений. Команда хочет, чтобы каждый сервис релизился отдельно, но схема довольно быстро становится такой сложной, что никто уже не верит, что именно запустится.

Первая ошибка — копировать один и тот же файл дочернего пайплайна для каждого сервиса и менять в нём несколько строк. На первый взгляд это быстро. Через месяц один сервис использует новые правила, у другого всё ещё старые теги, а у третьего деплой идёт чуть другим скриптом. Такой дрейф съедает часы, потому что YAML выглядит достаточно похоже, чтобы ввести людей в заблуждение.

Лучше работает простой подход. Держите один общий шаблон для типовых шагов релиза, а каждый файл сервиса оставляйте тонким. Файл сервиса должен описывать, что в нём уникально, а не повторять весь релизный процесс.

Ещё одна частая ошибка — запускать каждый дочерний пайплайн на каждый коммит. Это превращает аккуратный монорепозиторий в шумный. Если кто-то правит документацию или меняет только billing-сервис, API, worker и admin-приложение должны спать. Когда каждый коммит запускает всё, люди перестают обращать внимание на результаты пайплайна.

Команды также застревают, когда смешивают общие правила и правила сервиса в одном месте. Тогда на простой вопрос трудно ответить: этот дочерний пайплайн запустился из-за совпадения ветки, из-за изменения файлов или из-за глобального правила? Держите общее поведение в одном файле. Условия сервиса — рядом с триггером этого сервиса.

Логика релиза становится трудной для понимания, когда она растворяется в слишком большом числе include-файлов. Два уровня ещё допустимы. Пять — обычно уже нет. Если людям нужно десять минут, чтобы найти, где задаются теги, окружения или релизные ветки, схема слишком глубоко спрятана.

Имена переменных тоже могут создать собственный хаос. Передача размытых значений вроде VERSION, NAME, TARGET и ENV между родительским и дочерним пайплайнами приглашает ошибки. Используйте имена, которые говорят правду: SERVICE_NAME вместо NAME, SERVICE_VERSION вместо VERSION, DEPLOY_ENV вместо ENV и IMAGE_TAG вместо TAG.

Понятные имена снижают число плохих релизов. Когда у каждого правила, файла и переменной есть одна задача, процесс перестаёт казаться загадочным и начинает ощущаться как рутина.

Что проверить перед внедрением

Привлечь фракционного CTO
Получите второе мнение перед тем, как переписывать GitLab-процесс релизов.

Эта схема работает лучше всего, когда границы очевидны. Если людям нужно гадать, какой файл за какой сервис отвечает, конфигурация начнёт дрейфовать.

Начните с владения. У каждого сервиса должен быть свой файл дочернего пайплайна, а имя файла должно быть настолько близко к названию сервиса, чтобы никому не нужна была карта. Если billing выходит отдельно, дайте ему отдельный CI-файл. Если три сервиса всё ещё делят один дочерний файл, вы, скорее всего, ещё не разделили их достаточно сильно.

Правила по путям тоже нужно проверить на здравый смысл. Команды часто пишут аккуратные changes-правила и забывают, что структура папок изменилась несколько месяцев назад. Сравните каждое правило с реальной структурой репозитория. Один пропущенный путь означает, что сервис не соберётся, когда должен. Один неаккуратный путь разбудит лишние job и потратит время раннеров.

Держите родительский пайплайн маленьким. Пусть он выполняет только те проверки, которые нужны всем, например базовый lint, общие security checks или валидацию для всего репозитория. Если родитель начинает владеть тестами сервисов и логикой деплоя, вы снова вернулись к одному большому пайплайну, только уже с лишним церемониалом.

Перед запуском проверьте несколько базовых вещей:

  • у каждого сервиса есть одна понятная точка входа в дочерний пайплайн
  • правила по путям совпадают с текущими папками репозитория
  • общие проверки остаются в родительском пайплайне
  • теги, версии образов и названия релизов следуют одному правилу
  • сбои в одном дочернем пайплайне случайно не блокируют другие сервисы

Поток версий требует отдельного внимания. Один раз решите, как теги переходят из родительского пайплайна в дочерние, и придерживайтесь этого правила везде. Если один сервис читает Git-тег, другой — ручную переменную, а третий сам собирает строку версии, люди перестанут доверять релизам.

Сделайте один пилотный коммит, который затронет два сервиса и один общий файл. Вы должны увидеть, что общие проверки запускаются один раз, нужные дочерние пайплайны стартуют, переменные проходят дальше без искажений, а лишняя работа остаётся в стороне. Если этого не происходит, исправьте правила до того, как на них кто-то начнёт полагаться.

Что делать дальше

Выберите один сервис и проверьте идею там, где боль ощущается сильнее всего. Не начинайте с самого аккуратного или самого маленького приложения только потому, что так спокойнее. Начните с сервиса, который тормозит всех, чаще всего ломает основной пайплайн или выходит гораздо чаще остальных.

Такой пилот даст вам полезное сравнение до и после. Если родительско-дочерние пайплайны подходят, вы должны увидеть меньше ожидания, меньше лишних перезапусков и меньше споров о том, кто владеет упавшим job.

Обычно лучше всего работает простой план внедрения, а не полная переписка CI:

  • выберите сначала самый шумный сервис
  • зафиксируйте текущее время пайплайна, частоту ошибок и число перезапусков
  • переведите только этот сервис в дочерний пайплайн
  • сравните цифры через один или два релизных цикла

Сохраняйте сравнение простым. Если старый путь занимал 28 минут, а новый — 11 для того же сервиса, этого уже достаточно само по себе.

Имена лучше зафиксировать заранее. Команды быстро путаются, когда в одном проекте используется SERVICE_NAME, в другом APP, а в третьем MODULE. То же самое касается имён job. Если у каждого deploy-job своё название, люди тратят время на поиски по графам пайплайна.

Обычно достаточно короткой заметки в репозитории. Опишите, как вы называете дочерние пайплайны, deploy-job, окружения, переменные версий и релизные теги. Оставьте всё простым. Новые участники команды должны понимать это за пару минут.

Если вам нужен второй взгляд на то, где лучше разделить родительский пайплайн или как держать GitLab runners и инфраструктуру деплоя лёгкими, Oleg Sotnikov на oleg.is занимается таким Fractional CTO и startup advisory. Это может помочь, когда CI-файл уже стал настоящим узким местом и команде нужен более чистый план до работы с production.

Один удачный пилот лучше, чем полный план миграции. Сделайте так, чтобы один сервис выпускался чисто, а потом повторяйте этот шаблон только там, где он действительно приносит пользу.

Часто задаваемые вопросы

Что такое родительско-дочерний пайплайн в GitLab?

Родительский пайплайн выполняет общую работу в начале потока. Он проверяет, что изменилось, задаёт общие переменные и запускает нужные дочерние пайплайны. Каждый дочерний пайплайн затем отвечает за сборку, тесты и деплой одного сервиса.

Когда стоит заменить один большой пайплайн на родительско-дочерние?

Разделяйте пайплайн, когда в одном репозитории живёт несколько деплоемых сервисов и одно изменение постоянно будит лишние джобы. Если люди ждут тесты для приложений, которые не трогали, перезапускают пайплайны из-за нестабильных задач в других сервисах или спорят из-за одного огромного CI-файла, такое разделение обычно помогает.

Стоит ли использовать такую схему для одного небольшого приложения?

Нет. Если один простой пайплайн уже работает и команда его понимает, лучше оставить всё как есть. Родительско-дочерние пайплайны полезны, когда сервисы выходят по разным графикам или требуют разных правил, а не тогда, когда простая схема уже справляется.

Как сделать так, чтобы запускался только изменённый сервис?

Используйте rules:changes в родительском пайплайне по путям. Привяжите каждый триггер к папкам, которые относятся к одному сервису, например web/**/* или api/**/*, чтобы GitLab запускал только тот дочерний пайплайн, который нужен для изменённых файлов.

Что нужно оставлять в родительском пайплайне?

Оставьте там проверки уровня репозитория: общий lint, security scans, правила для веток и настройку версий. Тесты сервисов, сборку образов и логику деплоя держите внутри дочернего пайплайна, чтобы родительский оставался маленьким.

Как делиться версиями и настройками окружения между дочерними пайплайнами?

Пусть родительский пайплайн один раз вычислит значения вроде VERSION, IMAGE_TAG и DEPLOY_ENV, а потом передаст их в каждый дочерний пайплайн как переменные. Так все сервисы будут использовать один и тот же релизный маркер, а откаты станет проще отслеживать.

Может ли один упавший дочерний пайплайн заблокировать другие сервисы?

Да, если правила настроены неудачно. В хорошей схеме каждый дочерний пайплайн должен падать или проходить сам по себе, чтобы сломанная job worker не останавливала срочный фикс для API. Перед запуском проверьте правила триггеров и зависимостей.

Как избежать дрейфа YAML из-за копипаста?

Не копируйте полный CI-файл для каждого сервиса. Оставьте общие шаги в одном шаблоне, а каждый дочерний файл сделайте тонким и сфокусированным на том, что отличает этот сервис от остальных.

Как безопаснее всего внедрить такую схему?

Начните с одного шумного сервиса, а не со всего репозитория. Переведите его в дочерний пайплайн, сравните время сборки и число перезапусков за один-два релизных цикла, а потом повторяйте схему только там, где она действительно экономит время и снижает путаницу.

Какие ошибки чаще всего создают путаницу в такой схеме?

Команды обычно сталкиваются с проблемами, когда запускают каждый дочерний пайплайн на каждый коммит, прячут правила в слишком большом числе include-файлов или используют размытые имена переменных вроде ENV и TAG. Делайте владение файлами очевидным, имена — простыми, а родительский пайплайн — коротким.