Best Practices по GitLab CI/CD: от workflow:rules и кеша до OIDC, BuildKit, ревью-окружений и безопасных раннеров

Best Practices по GitLab CI/CD: от workflow:rules и кеша до OIDC, BuildKit, ревью-окружений и безопасных раннеров

Статья получилась большой: практик много, и каждая из них важна по-своему. Я собрал материал как набор best practices: не все пункты нужны каждому проекту, но почти каждый пункт однажды всплывает на ревью, при оптимизации медленного пайплайна, при разборе утечки секрета или после тяжелого инцидента.

Я старался писать для разных грейдов: от базовой гигиены вроде workflow:rules , cache , artifacts и needs до более продакшеновых тем вроде OIDC, Vault, CI_JOB_TOKEN , защищённых окружений, ревью-окружений, очередей слияния, BuildKit без root-прав, CI/CD-компонентов и усиления защиты раннеров.

Поэтому язык подачи здесь намеренно сухой, прямой и инженерный: без долгих заходов, без воды и без пересказа документации ради пересказа. Я хотел сделать не обзорную статью, а рабочую памятку, к которой можно вернуться при написании нового пайплайна, ревью .gitlab-ci.yml , переносе проекта в GitLab или наведении порядка в уже существующей CI/CD-платформе.

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

Оглавление:

Другие статьи из этой серии:

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

1. Зачем вообще думать о GitLab CI/CD

GitLab CI/CD часто начинается с простого файла:

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

Плохой GitLab CI/CD обычно ломается не сразу. Он сначала просто становится медленным, потом непонятным, потом дорогим, потом опасным. Сначала команда ждёт пайплайн 40 минут. Потом появляются дублирующиеся пайплайны веток и MR-пайплайнов. Потом один джоб случайно получает продакшен-секрет. Затем два деплоя одновременно пишут в одно окружение. Потом оказывается, что общий шаблон подключался по main , вчера изменился и сломал все проекты.

Хороший GitLab CI/CD решает несколько задач одновременно:

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

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

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

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

Безопасность. Пайплайн имеет доступ к исходникам, токенам, артефактам, реестру, облакам и окружениям. Его нужно защищать как часть цепочки поставки ПО, это не «просто автоматизация».

Безопасность. Пайплайн имеет доступ к исходникам, токенам, артефактам, реестру, облакам и окружениям. Его нужно защищать как часть цепочки поставки ПО, это не «просто автоматизация».

Управляемая доставка. Деплой должен быть отслеживаемым, сериализованным, ограниченным по правам и понятным в интерфейсе. Особенно если речь про стейджинг, продакшен и Kubernetes.

Управляемая доставка. Деплой должен быть отслеживаемым, сериализованным, ограниченным по правам и понятным в интерфейсе. Особенно если речь про стейджинг, продакшен и Kubernetes.

Сопровождаемость. .gitlab-ci.yml должен читать не только автор. Его должны понимать разработчики, DevOps, SRE, специалисты по безопасности и новый человек в команде.

Сопровождаемость. .gitlab-ci.yml должен читать не только автор. Его должны понимать разработчики, DevOps, SRE, специалисты по безопасности и новый человек в команде.

Правильная мысль тут такая: CI/CD — это не место, куда надо быстро накидать bash-команды. Это слой инженерной архитектуры проекта.

2. Архитектура пайплайна и базовая YAML-гигиена

Корневой .gitlab-ci.yml — это точка входа в CI/CD-конфигурацию. Через него мы должны быстро понять:

какие стадии есть в пайплайне;

какие стадии есть в пайплайне;

какие типы пайплайнов вообще создаются;

какие типы пайплайнов вообще создаются;

где находятся общие шаблоны;

где находятся общие шаблоны;

какие джобы относятся к lint/test/сборка/деплой;

какие джобы относятся к lint/test/сборка/деплой;

какие зависимости между джобами критичны;

какие зависимости между джобами критичны;

какие правила действуют для веток, MR, тегов и расписаний.

какие правила действуют для веток, MR, тегов и расписаний.

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

Лучше:

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

default нужен для значений, которые действительно общие для большинства джобов: базовый образ, политика повторных запусков, кеш, теги, before_script , timeout и так далее.

Пример:

Это лучше, чем копировать один и тот же image и before_script в каждый джоб. Но есть обратная крайность: складывать в default всё подряд. Чем больше глобальных настроек, тем выше шанс, что отдельный джоб начнёт наследовать то, что ему не нужно.

Если джоб не должен наследовать общий default , используйте явное отключение:

То же самое касается переменных:

Хорошая практика — не бороться с наследованием, а явно показывать, где джоб живёт отдельно от общего поведения.

Если шаблон или компонент должен переиспользоваться, не зашивайте в него main , master , конкретные имена групп, окружений и реестр без необходимости.

Плохо:

Лучше:

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

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

CI/CD часто превращается в bash-программу, спрятанную внутри YAML. Поэтому shell-часть надо писать так же аккуратно, как обычный код.

Плохо:

Лучше:

Такой джоб проще читать, проще ревьюить и проще отлаживать. Особенно когда речь про деплой, работу с секретами, внешние API или автоматизацию релизов.

Ошибки в .gitlab-ci.yml неприятны тем, что они ломают не приложение, а сам механизм проверки приложения. Поэтому CI Lint, проверка expressions и ревью изменений в CI-файлах должны быть частью процесса.

Отдельно стоит защищать изменения в CI-конфигурации через CODEOWNERS и политику защищённых веток. Если человек может поменять .gitlab-ci.yml , он фактически может поменять, какие команды будут выполнены и какие секреты увидят джобы.

Пример CODEOWNERS:

Для маленького проекта это может казаться избыточным. Для продакшен-системы это нормальная гигиена.

3. rules, workflow:rules и управление созданием пайплайна

Тут важно различать две вещи:

workflow:rules решает, будет ли создан пайплайн вообще;

workflow:rules решает, будет ли создан пайплайн вообще;

rules на уровне джоба решают, попадёт ли конкретный джоб в уже созданный пайплайн.

rules на уровне джоба решают, попадёт ли конкретный джоб в уже созданный пайплайн.

Если вы управляете только rules на уровне джоба, пайплайн всё равно может создаваться в ненужных ситуациях. Отсюда появляются дубли пайплайнов веток и MR, лишний расход раннеров и непонятная история проверок.

Базовый зрелый паттерн для ветки и процесса работы с MR:

Что здесь происходит:

если это MR-пайплайн — запускаем;

если это MR-пайплайн — запускаем;

если это push-based пайплайн ветки, но по ветке уже есть открытый MR — не запускаем;

если это push-based пайплайн ветки, но по ветке уже есть открытый MR — не запускаем;

если это обычная ветка без MR — запускаем пайплайн ветки.

если это обычная ветка без MR — запускаем пайплайн ветки.

Условие && $CI_PIPELINE_SOURCE == "push" здесь важно, потому что пайплайны, запущенные триггером, API, расписанием и downstream-процессами тоже могут иметь $CI_COMMIT_BRANCH , и без проверки источника их можно случайно заблокировать. Так GitLab не гоняет два пайплайна на один и тот же коммит: пайплайн ветки и MR-пайплайн одновременно, но не ломает остальные типы пайплайнов.

CI_PIPELINE_SOURCE показывает, откуда пришёл пайплайн: push, MR, schedule, API, trigger, parent-пайплайн и так далее.

В этой статье web -пайплайны для веток и тегов покрываются не отдельным правилом, а общими условиями по $CI_COMMIT_BRANCH и $CI_COMMIT_TAG . Источники external и external_pull_request_event намеренно не рассматриваются: если у вас есть интеграция с внешними pull request, их нужно разрешать отдельными правилами по CI_PIPELINE_SOURCE .

Отдельно стоит понимать, что есть и более редкие источники вроде chat , webide и security_orchestration_policy . В статье они не разбираются намеренно: это уже специализированные сценарии, и для них лучше писать отдельные правила под конкретный процесс.

Это лучше, чем строить сложные догадки только по ветке или тегу.

Пример:

Так пайплайн становится предсказуемым: джоб запускается не потому, что «так случайно совпали переменные», а потому что источник пайплайна явно подходит.

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

Плохо:

Такой джоб может попасть в разные типы пайплайнов, если вы не ограничили создание пайплайна на уровне workflow . В итоге push в ветку с открытым MR может породить и пайплайн ветки, и MR-пайплайн.

Лучше сначала ограничить создание пайплайна через workflow:rules , а потом уже настраивать джобы.

only/except — старый синтаксис. В старых проектах он встречается часто, но в новых конфигурациях лучше использовать rules и workflow:rules .

Проблема не только в том, что rules гибче. Проблема в смешении моделей. Джобы с rules и джобы с only/except могут вести себя по-разному по умолчанию. Когда таких джобов много, становится трудно понять, почему один джоб появился в пайплайне, а другой — нет.

Ещё один неочевидный нюанс: джобы без rules по умолчанию ведут себя как except: merge_requests . Поэтому если рядом есть джобы с rules , ориентированные на MR-пайплайны, один push в ветку с открытым MR легко порождает два пайплайна: пайплайн ветки для джобов без rules и MR-пайплайн для джобов с rules .

Целевое состояние:

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

Не каждый джоб должен запускаться на каждое изменение. Если поменялась документация, не всегда нужно гонять backend tests. Если поменялся frontend, не всегда нужно собирать Go-сервис.

Пример:

Для монорепозиториев это особенно важно. rules:changes позволяет не запускать половину пайплайна без причины.

Для больших монорепозиториев полезно помнить про лимиты: GitLab делает ограниченное число проверок по rules:changes , а на один блок rules:changes действует лимит по количеству путей и паттернов. На очень больших наборах изменений это может влиять на поведение правил, поэтому сложную логику лучше проверять отдельно и не превращать changes в огромный список из десятков разрозненных условий.

Если нужно сравнивать изменения с конкретной базой, используйте compare_to . Но учитывайте нюанс пайплайнов с результатом слияния: там база сравнения может быть временным merge-коммитом, и правила могут срабатывать шире, чем в обычном пайплайне ветки.

exists полезен для универсальных шаблонов:

Так один и тот же шаблон можно подключать в разные проекты, и джоб появится только там, где он имеет смысл.

Для больших проектов полезно не тратить тяжёлые проверки на черновой MR.

Пример:

Нюанс по версиям: $CI_MERGE_REQUEST_DRAFT появился в GitLab 17.10. Если у вас старая самостоятельно развёрнутая версия GitLab, используйте резервный вариант через $CI_MERGE_REQUEST_TITLE и регулярное выражение по заголовку MR.

Для ручных пайплайнов, переиспользуемых шаблонов и компонентов лучше использовать входные параметры CI/CD, а не набор произвольных переменных пайплайна без валидации.

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

Для новых конфигураций это особенно важно: GitLab постепенно смещает ручные запуски в сторону pipeline inputs, а не произвольных pipeline variables. Если вы строите контракт запуска для деплоя, отката или maintenance-процесса, inputs обычно безопаснее и предсказуемее, потому что их можно типизировать, ограничить и описать прямо в конфигурации.

тип значения;

тип значения;

значение по умолчанию;

значение по умолчанию;

список допустимых вариантов;

список допустимых вариантов;

валидацию регулярным выражением;

валидацию регулярным выражением;

описание для пользователя;

описание для пользователя;

проверку на этапе создания пайплайна.

проверку на этапе создания пайплайна.

Пример:

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

Важный момент по версиям: входные параметры CI/CD стали общедоступными в GitLab 17.0. Отдельные возможности вроде spec:inputs:rules , варианты для массивов и доступа к элементам массивов появились позже, поэтому для самостоятельно развёрнутого GitLab лучше проверять версию перед использованием свежих возможностей входных параметров.

4. DAG, needs, параллелизм, матрицы и быстрые проверки

Классическая модель стадий простая:

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

needs позволяет описывать реальные зависимости между джобами, а не только грубый порядок стадий.

needs: [] говорит GitLab: джоб не ждёт предыдущих стадий и может стартовать сразу. Это удобно для быстрых проверок с ранним падением.

Ошибки синтаксиса, форматирования, схемы, сообщения коммита и базовые smoke-проверки должны падать как можно раньше.

Плохо, когда пайплайн 30 минут собирает образ, запускает интеграционные тесты, а потом падает на prettier или YAML-lint.

Лучше:

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

Если пайплайн длится 40 минут, не обязательно оптимизировать все джобы. Нужно найти критический путь — цепочку зависимостей, которая определяет минимальное время до результата.

Иногда джоб длится 15 минут, но не блокирует ничего важного. А другой джоб длится 5 минут, но стоит в начале цепочки и задерживает весь деплой.

Смотрите:

граф пайплайна;

граф пайплайна;

граф needs ;

граф needs ;

длительность джобов и стадий;

длительность джобов и стадий;

время ожидания в очереди;

время ожидания в очереди;

нестабильные джобы;

нестабильные джобы;

время скачивания Docker-образов;

время скачивания Docker-образов;

время установки зависимостей;

время установки зависимостей;

хранилище и загрузку/скачивание артефактов.

хранилище и загрузку/скачивание артефактов.

Оптимизация без измерений часто превращается в угадайку.

Когда пайплайн построен только на стадиях, GitLab может автоматически передавать артефакты из предыдущих стадий. Но с DAG через needs поведение надо задавать явнее.

Пример:

Если артефакты не нужны, не скачивайте их:

Лишние артефакты — это сетевой шум, стоимость хранения и задержки в каждом джобе.

Если джоб зависит от другого джоба, который иногда не создаётся из-за rules , пайплайн может не стартовать.

Пример проблемы:

Если build-docs не попал в пайплайн, зависимость может стать проблемой. В таких случаях используйте опциональную зависимость:

Это особенно актуально для больших конфигураций с rules:changes .

parallel и parallel:matrix помогают ускорить тесты, сборки и проверки под разные версии среды выполнения.

Пример:

Но параллелизм не бесплатный. Если у вас два раннера, а вы создаёте 40 джобов, большая часть будет просто ждать в очереди. В таком случае вы увеличите сложность, но не сократите длительность.

Параллелизм должен соответствовать ёмкости раннеров.

Иногда downstream-джоб должен ждать не всю матрицу, а конкретную комбинацию.

Например, сборка образа для linux/amd64 должна ждать только тесты для linux/amd64 , а не всю матрицу тестов.

В таких случаях используйте needs:parallel:matrix . Это делает DAG точнее и уменьшает задержки.

retry полезен, если падает раннер, реестр, сеть или другой нестабильный инфраструктурный слой.

Плохая практика:

Так можно замаскировать реальные проблемы в тестах.

Лучше задавать повторный запуск адресно:

Если тесты нестабильны, их надо чинить, а не бесконечно перезапускать.

5. Переиспользование: extends, шаблоны, компоненты и входные параметры

Если несколько джобов используют одинаковую структуру, выносите её через скрытый джоб и extends .

Это лучше копипаста. Когда нужно обновить Node.js или способ установки зависимостей, вы меняете одно место.

Для более точечного переиспользования можно использовать !reference , но не стоит строить на нём нечитаемые YAML-фокусы. Правило простое: переиспользование должно уменьшать сложность, а не прятать её.

Шаблон пайплайна может задавать глобальные вещи: stages , workflow , default , общую архитектуру.

Job-шаблон должен быть максимально аккуратным: он встраивается в чужой пайплайн и не должен неожиданно менять глобальное поведение проекта.

Плохой job-шаблон:

Такой шаблон может конфликтовать с уже существующими stages и default в проекте.

Лучше:

Или ещё лучше — оформить это как CI/CD-компонент с входными параметрами.

Старый путь — общий репозиторий с подключаемыми файлами:

Он работает, но у него есть проблемы:

сложно понять, какие шаблоны вообще есть;

сложно понять, какие шаблоны вообще есть;

сложно версионировать;

сложно версионировать;

легко подключить плавающий main ;

легко подключить плавающий main ;

документация часто живёт отдельно или не живёт вообще;

документация часто живёт отдельно или не живёт вообще;

изменение общего шаблона может внезапно сломать десятки проектов.

изменение общего шаблона может внезапно сломать десятки проектов.

CI/CD-компоненты лучше подходят для платформенного переиспользования: у них есть каталог, версионирование, spec:inputs , документация и более явный контракт.

Пример компонента:

Подключение:

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

Плохо:

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

Лучше:

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

Плавающие ссылки — враг воспроизводимости.

Плохо:

Лучше:

Ещё строже:

~latest и частичное семантическое версионирование вроде 1 или 1.2 допустимы, если вы сознательно хотите получать автоматические обновления из каталога CI/CD. Но это всё равно движущаяся цель: ~latest может принести самый свежий релиз, включая ломающие изменения. Для критически важной для продакшена автоматизации лучше фиксированный релизный тег или SHA.

Если компонент внутри своего проекта включает дополнительные локальные файлы, лучше использовать include:local . Тогда связанные части конфигурации берутся из одного Git SHA.

Это уменьшает риск ситуации, когда один файл взяли из одной версии, а зависимый — из другой.

Пайплайн и компонент объединяются в итоговую конфигурацию. Если джоб компонента называется test , а в проекте уже есть джоб test , результат может быть неожиданным.

Плохо:

Лучше:

Если компонент можно подключить несколько раз, динамические имена через входные параметры почти обязательны.

Компонент — это код. Его надо тестировать.

Хороший паттерн: в CI самого компонентного проекта подключать компонент по текущему SHA коммита.

Так вы проверяете именно тот коммит, который собираетесь релизить.

Если компоненту нужны примерные файлы, тестовая нагрузка или тестовый Dockerfile, храните их рядом с компонентом. Тестируйте не только «джоб стартует», но и реальные побочные эффекты: создаётся ли артефакт, корректно ли публикуется отчёт, не ломается ли цепочка подключений.

Если общий компонент используется десятками проектов, ломающее изменение в нём — это не локальное изменение, а потенциальный инцидент.

Минимум:

README с примерами;

README с примерами;

описание входных параметров;

описание входных параметров;

журнал изменений;

журнал изменений;

заметки по миграции для ломающих изменений;

заметки по миграции для ломающих изменений;

подход к семантическому версионированию;

подход к семантическому версионированию;

тесты компонента;

тесты компонента;

понятная политика поддержки старых версий.

понятная политика поддержки старых версий.

CI/CD-платформа должна сопровождаться так же серьёзно, как библиотека или внутренний SDK.

Если вы подключаете чужой компонент, вы фактически разрешаете чужой YAML выполнить команды в вашем пайплайне.

Перед использованием смотрите:

какие команды выполняются;

какие команды выполняются;

какие секреты может увидеть джоб;

какие секреты может увидеть джоб;

какие кеши и артефакты используются;

какие кеши и артефакты используются;

какие теги раннеров нужны;

какие теги раннеров нужны;

нужен ли привилегированный Docker;

нужен ли привилегированный Docker;

какие токены и права доступа запрашиваются;

какие токены и права доступа запрашиваются;

как компонент версионируется;

как компонент версионируется;

есть ли тесты и документация.

есть ли тесты и документация.

Удобный компонент, который требует привилегированного раннера и широких токенов, может быть хуже простого локального джоба.

Для include:project и похожих механизмов переиспользования используйте полный 40-символьный SHA или релизный тег. Ветку main , ~latest и другие движущиеся ссылки стоит рассматривать как supply-chain-компромисс, а не как нейтральный дефолт.

include:remote удобен, когда CI-конфигурация лежит по внешнему URL. Но с точки зрения цепочки поставки ПО это самый рискованный вариант подключения: вы подтягиваете YAML снаружи и разрешаете ему влиять на пайплайн.

Отдельный нюанс безопасности: include:remote поддерживает только публичный HTTP/HTTPS GET без аутентификации. Вложенные include выполняются без контекста как public user, поэтому из них доступны только публичные проекты и шаблоны, а переменные в секции nested include недоступны. Это ещё один аргумент в пользу integrity и минимизации внешних include-зависимостей.

Если без include:remote нельзя, фиксируйте содержимое через integrity :

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

Нюанс по версиям: include:integrity появился в GitLab 17.9. Если у вас более старая самостоятельно развёрнутая версия GitLab, этот ключ не сработает.

Дополнительно для include:remote можно смотреть в сторону include:cache . Он кэширует содержимое внешнего подключения на заданный TTL и снижает количество HTTP-запросов к внешнему YAML. Но это компромисс между скоростью и свежестью: чем дольше кеш, тем выше шанс временно использовать устаревшую внешнюю конфигурацию.

Нюанс по версиям: include:cache появился в GitLab 18.9 как экспериментальная возможность, а общедоступной стала в GitLab 19.0.

6. Parent/child и multi-project пайплайны

Не нужно начинать новый проект сразу с parent/child-пайплайнов, то есть родительских и дочерних пайплайнов, multi-project пайплайнов, динамических child-пайплайнов и сложной матрицы подключаемых файлов.

Сначала простой пайплайн:

Потом workflow:rules , needs , кеши, отчёты. И только когда конфигурация реально стала большой, переходите к декомпозиции.

Сложная архитектура CI/CD оправдана, когда она решает реальную проблему: монорепозиторий, десятки сервисов, разные релизные циклы, отдельные команды, отдельные границы доверия.

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

Пример:

Так backend-пайплайн запускается только при изменениях backend, а frontend-пайплайн — только при изменениях frontend.

strategy: mirror делает статус trigger-джоба зеркалом downstream-пайплайна. В старых конфигурациях ещё часто встречается strategy: depend , но для новых пайплайнов лучше показывать именно mirror : так поведение trigger-джоба понятнее и ближе к реальному статусу downstream-процесса.

По версиям: strategy: mirror появился в GitLab 18.2. Если проект живёт на более старом самостоятельно развёрнутом GitLab, придётся временно использовать strategy: depend или сначала обновить GitLab.

Если child-пайплайн генерирует JUnit, качество кода, Terraform, метрики или отчёты по безопасности, отдельно проверьте, что эти отчёты реально видны в виджете MR. Для этого trigger-джоб должен ждать child-пайплайн через strategy: mirror или strategy: depend , иначе parent-пайплайн может завершиться раньше, а отчёты останутся спрятанными в child-пайплайне.

Так же по версиям: отчёты из child-пайплайнов в виджеты MR появились в GitLab 18.6, а отчёты по безопасности из child-пайплайнов — в GitLab 18.9.

Отдельный момент для покрытия: coverage_report из child-пайплайнов даёт аннотации в diff MR, но не становится обычным отчётом parent-пайплайна. Поэтому для child-пайплайнов полезно различать «виджет MR/дифф» и «отчёт, доступный на уровне parent-пайплайна».

Это намного лучше, чем один огромный .gitlab-ci.yml , где все джобы перемешаны.

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

Если граница стала межпроектной или организационной, чаще логичнее перейти на multi-project пайплайн.

Межпроектный пайплайн нужен, когда вышестоящий проект должен запускать пайплайн в другом проекте: например, библиотека запускает проверки потребителей, infra-репозиторий запускает деплой application-репозитория, релизная оркестрация связывает несколько сервисов.

Пример:

strategy: mirror здесь нужен по той же причине: upstream-trigger-джоб должен отражать реальный результат downstream-пайплайна, а не просто факт, что downstream-пайплайн удалось создать.

Если downstream-пайплайн должен забрать артефакты именно из MR-пайплайна, не подставляйте в needs:project обычное имя ветки. Для MR-пайплайнов нужно передавать CI_MERGE_REQUEST_REF_PATH , иначе легко случайно получить артефакты из последнего branch-пайплайна, а не из нужного MR.

Но здесь важно помнить про границу доверия. Downstream-пайплайн запускается в другом проекте, с его настройками, правами и правилами. Нужно понимать:

кто может запускать upstream-пайплайн;

кто может запускать upstream-пайплайн;

с какими правами стартует downstream;

с какими правами стартует downstream;

какие токены и артефакты передаются;

какие токены и артефакты передаются;

можно ли доверять downstream-коду;

можно ли доверять downstream-коду;

как трассировать сбои между проектами.

как трассировать сбои между проектами.

Межпроектный пайплайн — это уже не просто YAML-удобство, а часть организационной модели доступа.

7. Производительность, кеш, артефакты и стоимость

Фраза «CI медленный» ничего не объясняет. Нужно понять, где именно медленно:

получение исходников репозитория;

получение исходников репозитория;

скачивание Docker-образа;

скачивание Docker-образа;

установка зависимостей;

установка зависимостей;

тесты;

тесты;

сборка образа;

сборка образа;

загрузка и скачивание артефактов;

загрузка и скачивание артефактов;

очередь на раннеры;

очередь на раннеры;

медленный реестр;

медленный реестр;

сетевая задержка;

сетевая задержка;

нестабильные джобы и retries.

нестабильные джобы и retries.

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

Без измерений легко оптимизировать не то.

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

Для джобов, которые можно безопасно оборвать, ставьте:

Для деплоя и необратимых операций обычно наоборот:

Можно управлять автоотменой на уровне workflow:

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

Большие репозитории могут терять минуты просто на получение исходников.

В большинстве случаев лучше использовать fetch , а не полный clone . Shallow clone тоже помогает.

Но есть нюансы:

слишком маленький GIT_DEPTH может сломать генерацию журнала изменений, семантическое версионирование и инструменты, завязанные на Git;

слишком маленький GIT_DEPTH может сломать генерацию журнала изменений, семантическое версионирование и инструменты, завязанные на Git;

GIT_STRATEGY: fetch в общем окружении безопасен только если вы доверяете всем пользователям этой среды;

GIT_STRATEGY: fetch в общем окружении безопасен только если вы доверяете всем пользователям этой среды;

для очистки/stop-джобов после удаления ветки можно использовать GIT_STRATEGY: none или empty , если исходники не нужны.

для очистки/stop-джобов после удаления ветки можно использовать GIT_STRATEGY: none или empty , если исходники не нужны.

Пример stop-джоба, где очистка-команда доступна без получения исходников репозитория:

Если очистка-логика лежит в скрипте из репозитория вроде ./destroy-review.sh , не ставьте GIT_STRATEGY: none : без получения исходников этого файла может просто не быть в рабочей директории.

Кеш и артефакты решают разные задачи.

Кеш — для зависимостей и повторно используемых данных: npm кеш, bundler кеш, Maven repository, Gradle кеш и т.д.

Артефакты — для результатов конкретного джоба: результат сборки, JUnit-отчёт, отчёт о покрытии, бинарный файл, Terraform plan, пакет.

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

Лучше:

Для кеша зависимостей часто лучше ключ от lockfile, а не только от ветки.

Если lockfile не изменился, зависимости те же. Значит, кеш можно переиспользовать.

Для данных, специфичных для ветки, можно использовать ключ ветки:

Главное — не складывать разные типы данных под один и тот же key.

Первый пайплайн новой ветки часто медленный, потому что кеш ветки ещё нет. Резервные ключи помогают использовать кеш ветки по умолчанию.

Так джоб сначала ищет кеш текущей ветки, потом ветку по умолчанию, потом общий fallback.

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

Плохой пример:

А в другом джобе:

Оба джоба пишут в один и тот же архив кеша с разным содержимым. Потом начинаются странные misses, перезаписи и нестабильное поведение.

Лучше разделять:

Если джоб A создал кеш на одном хосте раннера, а джоб B ушёл на другой хост, локальный кеш может быть бесполезен.

Нормальные варианты:

один раннер для связанных джобов;

один раннер для связанных джобов;

несколько раннеров с распределённым кешем через объектное хранилище;

несколько раннеров с распределённым кешем через объектное хранилище;

общий сетевой кеш для одинаковых раннеров;

общий сетевой кеш для одинаковых раннеров;

автомасштабируемые раннеры с корректным бекендом кеша.

автомасштабируемые раннеры с корректным бекендом кеша.

Для объектного хранилища обязательно нужна политика жизненного цикла. Иначе хранилище кеша будет бесконтрольно расти.

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

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

Артефакты не должны жить вечно «на всякий случай».

Для тестовых отчётов часто нужен when: always , чтобы отчёт был доступен даже при падении джоба:

Слишком короткий срок хранения ломает отладку и интерфейс MR. Слишком длинный — раздувает хранилище. Нужен баланс.

Если джоб создаёт HTML-отчёт, Terraform plan, preview summary или другой файл, который должен открыть ревьюер, не заставляйте его искать файл в архиве артефактов.

Так результат становится частью цикла ревью, а не спрятанным файлом в выводе джоба.

8. Сборки Docker-образов, BuildKit, Dependency Proxy и кеш в реестре

Классический Docker-in-Docker удобен, но часто требует привилегированного контейнера и Docker-демона. Это повышает риск, особенно на общих или переиспользуемых раннерах.

Если проект чувствительный к безопасности, лучше смотреть в сторону BuildKit без root-прав, Buildah, Podman или другой модели без лишних привилегий.

Это не значит, что docker buildx всегда плох. Это значит, что модель раннеров должна соответствовать риску.

Пример BuildKit без root-прав:

Плюсы:

не нужен привилегированный Docker-демон;

не нужен привилегированный Docker-демон;

можно использовать кеш в реестре;

можно использовать кеш в реестре;

хорошо ложится в одноразовую CI-среду;

хорошо ложится в одноразовую CI-среду;

меньший радиус поражения по сравнению с классическим DinD на общем раннере.

меньший радиус поражения по сравнению с классическим DinD на общем раннере.

Минусы:

настройка auth менее привычная;

настройка auth менее привычная;

команде нужно понимать BuildKit;

команде нужно понимать BuildKit;

не все устаревшие Docker-процессы переносятся один в один.

не все устаревшие Docker-процессы переносятся один в один.

Встроенный кеш проще:

Но для сложных многоэтапных сборок часто лучше кеш в реестре:

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

Плохой пример:

Секрет может попасть в историю слоёв, логи сборки или кеш.

Лучше использовать секреты BuildKit:

В CI:

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

Базовые образы часто тянутся из Docker Hub или внешних реестров. Это даёт:

лимиты запросов;

лимиты запросов;

сетевые задержки;

сетевые задержки;

зависимость от внешней доступности;

зависимость от внешней доступности;

лишнее время скачивания.

лишнее время скачивания.

GitLab Dependency Proxy помогает кэшировать upstream-образы на уровне группы.

Пример:

Для Dockerfile:

Это особенно полезно, когда много проектов тянут одни и те же базовые образы.

Не делайте продакшен-деплой из latest .

Плохо:

Лучше:

Можно дополнительно пушить удобные теги:

Но деплой должен ссылаться на неизменяемый тег или digest, а не на плавающий latest .

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

Если у вас уже есть Kaniko-джобы и они работают, не обязательно ломать всё в один день. Но при плановом обновлении CI/CD-платформы стоит смотреть на BuildKit без root-прав, Buildah, Podman или другой поддерживаемый инструмент.

Главный принцип: не строить новую платформу на инструменте, который уже не является актуальной рекомендацией.

Сборка образа — это не конец цепочки поставки ПО. Если пайплайн просто собрал образ и сразу задеплоил его в продакшен, вы не отвечаете на несколько важных вопросов:

какие зависимости попали в образ;

какие зависимости попали в образ;

какие CVE есть в базовом образе и пакетах;

какие CVE есть в базовом образе и пакетах;

кто и в каком пайплайне собрал образ;

кто и в каком пайплайне собрал образ;

можно ли доказать происхождение артефакта;

можно ли доказать происхождение артефакта;

разрешён ли этот образ к деплою по внутренней политике.

разрешён ли этот образ к деплою по внутренней политике.

Минимальный зрелый процесс:

Собрать образ с неизменяемым тегом или digest.

Собрать образ с неизменяемым тегом или digest.

Сгенерировать SBOM, например в CycloneDX.

Сгенерировать SBOM, например в CycloneDX.

Прогнать сканирование контейнеров.

Прогнать сканирование контейнеров.

Подписать образ через Cosign, Notation или другой принятый в компании инструмент.

Подписать образ через Cosign, Notation или другой принятый в компании инструмент.

Сохранить provenance/attestation, если это требуется для цепочки поставки ПО.

Сохранить provenance/attestation, если это требуется для цепочки поставки ПО.

Перед продакшен-деплоем поставить проверку политик: не деплоить образ, который не прошёл минимальные проверки.

Перед продакшен-деплоем поставить проверку политик: не деплоить образ, который не прошёл минимальные проверки.

Пример с CycloneDX-отчётом:

Нюанс по тарифному уровню: GitLab-интеграция artifacts:reports:cyclonedx относится к Ultimate. Если нужного тарифного уровня нет, SBOM всё равно можно сохранять как обычный артефакт и использовать во внешней проверке политик, но интерфейс-интеграция GitLab может быть недоступна.

Конкретный набор инструментов зависит от компании. Важен принцип: продакшен-деплой должен опираться не только на факт успешной сборки, но и на проверяемую информацию об образе.

Dependency Proxy, кеш в реестре и теги образов ускоряют CI/CD, но без политики очистки они быстро превращаются в проблему хранилища.

Что стоит контролировать:

сколько живут временные и ревью-теги образов;

сколько живут временные и ревью-теги образов;

как часто чистится кеш сборки;

как часто чистится кеш сборки;

сколько хранится кеш-образ вроде $CI_REGISTRY_IMAGE:buildcache ;

сколько хранится кеш-образ вроде $CI_REGISTRY_IMAGE:buildcache ;

удаляются ли старые теги веток после закрытия веток;

удаляются ли старые теги веток после закрытия веток;

есть ли отдельная политика для релизных тегов и продакшен-образов;

есть ли отдельная политика для релизных тегов и продакшен-образов;

не растёт ли реестр контейнеров быстрее, чем команда это замечает.

не растёт ли реестр контейнеров быстрее, чем команда это замечает.

Хорошая модель простая: продакшен/релизные артефакты живут долго и управляются осознанно, а ревью-, временные и кеш-артефакты сборки имеют понятный TTL и автоматическую очистку.

9. Окружения, ревью-окружения и управляемые деплои

Деплой-джоб без environment — это просто команда в логах. Деплой-джоб с environment — это часть модели доставки GitLab.

Пример:

Так GitLab понимает, что и куда задеплоено. Это даёт:

историю деплоев;

историю деплоев;

ссылки на окружения;

ссылки на окружения;

панель окружений;

панель окружений;

защищённые окружения;

защищённые окружения;

согласования;

согласования;

очистку;

очистку;

визуализацию процесса доставки.

визуализацию процесса доставки.

Не стоит полагаться только на имя окружения. Лучше явно писать уровень:

Поддерживаемые значения:

production ;

production ;

staging ;

staging ;

testing ;

testing ;

development ;

development ;

other .

other .

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

Иногда URL окружения создаётся после деплоя: например, PaaS или Kubernetes ingress генерирует адрес динамически.

В таком случае джоб может записать URL в dotenv-отчёт:

Так ревью-окружение становится кликабельным в интерфейсе.

Ревью-окружение без очистки — это будущая свалка ресурсов.

Хороший паттерн:

В примере stop-review оставлен необязательным через allow_failure: true : это удобно для UI-очистки окружения и не блокирует пайплайн. Если же ваша политика требует обязательной очистки перед продолжением процесса, используйте блокирующий manual-джоб и не оставляйте его необязательным по умолчанию.

Важно: деплой и stop-джобы должны иметь согласованные rules . Если stop-джоб не попал в пайплайн, GitLab не сможет нормально остановить окружение.

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

Это защищает от странных гонок между деплоем и очисткой.

Для frontend, документации и статических сайтов ревью-окружение полезнее, если из MR можно перейти не только на корень приложения, но и на страницу, соответствующую изменённому файлу.

Пример .gitlab/route-map.yml :

Так ревьюер быстрее проверяет изменения в живом окружении.

Защищённая ветка защищает код. Защищённое окружение защищает деплой.

Для продакшена обычно нужно ограничить:

кто может деплоить;

кто может деплоить;

кто может согласовывать деплой;

кто может согласовывать деплой;

какие ветки/теги имеют доступ к продакшен-секретам;

какие ветки/теги имеют доступ к продакшен-секретам;

какие раннеры могут выполнять деплой-джоб.

какие раннеры могут выполнять деплой-джоб.

Пример:

А права на само окружение настраиваются в интерфейсе GitLab.

Два продакшен-деплоя одновременно — плохая идея.

resource_group гарантирует, что джобы с одной resource_group не будут выполняться параллельно.

Это особенно важно для:

деплоя в Kubernetes;

деплоя в Kubernetes;

Terraform apply;

Terraform apply;

миграций базы данных;

миграций базы данных;

публикации релизов;

публикации релизов;

изменяемых окружений.

изменяемых окружений.

У resource_group есть разные режимы обработки:

oldest_first — безопаснее для последовательного непрерывного деплоя;

oldest_first — безопаснее для последовательного непрерывного деплоя;

newest_first — быстрее выбрасывает старые деплой-джобы, но требует идемпотентности;

newest_first — быстрее выбрасывает старые деплой-джобы, но требует идемпотентности;

newest_ready_first — компромиссный вариант для готовых джобов.

newest_ready_first — компромиссный вариант для готовых джобов.

Если деплой не идемпотентный, агрессивные режимы могут привести к непредсказуемому состоянию.

Если деплой выполняется в downstream-пайплайне, блокировка должна жить до конца downstream-процесса.

Иначе trigger-джоб в parent-пайплайне быстро завершится, resource_group освободится, и следующий деплой стартует, пока downstream ещё работает.

Используйте trigger:strategy: mirror или подход, при котором parent-пайплайн ожидает downstream-пайплайн.

Опасный паттерн: parent/child-пайплайны конкурируют за один и тот же resource_group , особенно при oldest_first . Можно получить ситуацию, когда parent-пайплайн ждёт дочерний, а child-пайплайн ждёт ресурс, который удерживает parent-пайплайн.

Правило простое: если используете блокировки через родительские и child-пайплайны, проектируйте их явно. Не копируйте resource_group на разные уровни пайплайна без понимания очереди.

Старый деплой не должен приезжать в продакшен после нового.

Типичный сценарий:

Пайплайн A начал деплой.

Пайплайн A начал деплой.

Пайплайн B собрался быстрее и задеплоил новый коммит.

Пайплайн B собрался быстрее и задеплоил новый коммит.

Пайплайн A внезапно продолжил работу и откатил продакшен на старый коммит.

Пайплайн A внезапно продолжил работу и откатил продакшен на старый коммит.

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

Environment-scoped переменные удобны для разделения секретов по окружениям:

продакшен-секреты — только для production ;

продакшен-секреты — только для production ;

секреты ревью-окружений — только для review/* ;

секреты ревью-окружений — только для review/* ;

стейджинг-секреты — только для staging .

стейджинг-секреты — только для staging .

Но не стоит опираться на них в rules или include . На этапе валидации пайплайна они могут быть ещё не определены.

Если нужен доступ к environment-scoped переменным до полноценного деплоя, используйте environment actions:

prepare ;

prepare ;

verify ;

verify ;

access .

access .

Это лучше, чем пытаться заставить переменные работать там, где GitLab ещё не знает контекст окружения.

Manual-джоб сам по себе не всегда достаточная защита. Человек может нажать кнопку случайно, особенно если рядом несколько похожих деплой- и stop-джобов.

Для опасных действий добавляйте явное подтверждение:

Нюанс, который часто путают: when: manual вне rules по умолчанию создаёт необязательный manual-джоб ( allow_failure: true ), а when: manual внутри rules — блокирующее ( allow_failure: false ). Если нужен «необязательный manual» внутри rules , указывайте allow_failure: true явно.

Это особенно полезно для:

продакшен-деплоя;

продакшен-деплоя;

остановки продакшена;

остановки продакшена;

удаления ревью-окружения;

удаления ревью-окружения;

Terraform apply;

Terraform apply;

миграции базы данных;

миграции базы данных;

публикации релизов.

публикации релизов.

manual_confirmation не заменяет защищённые окружения и согласования. Это просто дополнительная защита от случайного действия в интерфейсе.

Нюанс по версиям: manual_confirmation появился в GitLab 17.1, а поддержка stop-джобов окружений появилась в GitLab 18.3. Если у вас более старая самостоятельно развёрнутая версия GitLab, проверяйте поддержку перед тем, как добавлять этот ключ в общие шаблоны.

Для продакшена важен не только сам деплой, но и правила, когда деплоить нельзя и как откатываться.

Заморозка деплоев нужна, если в проекте есть периоды, когда деплой запрещён: праздники, релизные окна, конец отчётного периода, миграции, внешние зависимости или freeze по договорённости с бизнесом.

Откат тоже нужно описать явно. Варианты бывают разные:

повторный запуск старого деплой-джоба;

повторный запуск старого деплой-джоба;

новый пайплайн со старым SHA коммита;

новый пайплайн со старым SHA коммита;

деплой предыдущего неизменяемого тега образа;

деплой предыдущего неизменяемого тега образа;

отдельный джоб отката;

отдельный джоб отката;

откат релиза через Git-тег или метаданные релиза.

откат релиза через Git-тег или метаданные релиза.

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

Если GitLab CI/CD деплоит в Kubernetes, не обязательно хранить долгоживущий kubeconfig как обычный CI/CD-секрет. Для многих проектов более правильная модель — GitLab Agent for Kubernetes.

Agent даёт пайплайн Kubernetes-контекст, а доступ можно ограничивать через ci_access , проекты, группы, RBAC и имперсонацию. В джобе появляется $KUBECONFIG , после чего можно выбрать нужный контекст и выполнить kubectl .

Пример идеи:

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

В примере образ намеренно указан с фиксированной версией. Для инструментов деплоя вроде kubectl , helm и kustomize лучше не использовать latest : иначе один и тот же деплой-джоб может начать вести себя по-разному после обновления образа.

10. MR, merged results pipelines и merge trains

Пайплайн ветки показывает состояние ветки. MR-пайплайн показывает состояние изменения в контексте MR.

Для команд с активным процессом ревью MR-пайплайн обычно важнее: именно он даёт интеграции с интерфейсом, виджеты, согласования и нормальную обратную связь ревьюеру.

Пример:

Так пайплайн ветки остаётся для веток без MR, а после открытия MR основной проверкой становится MR-пайплайн.

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

Пайплайн с результатом слияния создаёт временный merge-коммит между исходной и целевой ветками и проверяет именно его.

Это полезно, если:

много параллельных MR;

много параллельных MR;

ветка по умолчанию часто меняется;

ветка по умолчанию часто меняется;

интеграционные конфликты всплывают поздно;

интеграционные конфликты всплывают поздно;

важно ловить проблемы до merge.

важно ловить проблемы до merge.

Но есть нюанс: rules:changes:compare_to в merged results pipeline может работать иначе, потому что база сравнения — временный merge-коммит.

Если нужно различать обычный detached MR-пайплайн, merged results pipeline и merge train pipeline, смотрите на CI_MERGE_REQUEST_EVENT_TYPE : он принимает значения detached , merged_result и merge_train . Это удобно, когда тяжёлые проверки или публикация отчётов должны запускаться только в определённом типе MR-пайплайна.

Если в ветку по умолчанию часто вливаются изменения, даже merged results pipeline может быть недостаточно. Пока один MR проверяется, перед ним могут влиться другие MR.

Очередь слияния выстраивает MR в очередь и проверяет каждый MR в контексте тех изменений, которые уже стоят перед ним в очереди.

Это уменьшает риск ситуации «каждый MR отдельно зелёный, но main после merge красный».

Merge trains не стоит включать как отдельную магическую кнопку. Они работают нормально в связке с MR-пайплайнами и пайплайнами с результатом слияния.

Иначе MR могут вести себя неожиданно: застревать, проверяться не в том контексте или требовать ручных действий.

В процессе с большим количеством слияний удобно использовать Set to auto-merge . Тогда MR корректно попадает в очередь и GitLab сам выполнит merge, когда проверки пройдут.

Без этого люди начинают вручную ловить момент, когда пайплайн зелёный, а это возвращает хаос.

Release-джоб не должен быть случайным продолжением обычного деплой-джоба. У релиза отдельная ответственность: создать тег, метаданные релиза, журнал изменений, файлы релиза, package, тег образа или другую точку фиксации версии.

Частые варианты:

релиз создаётся при push Git-тега;

релиз создаётся при push Git-тега;

релиз создаётся после merge в ветку по умолчанию;

релиз создаётся после merge в ветку по умолчанию;

отдельный prepare-джоб собирает метаданные, а release-джоб публикует релиз;

отдельный prepare-джоб собирает метаданные, а release-джоб публикует релиз;

release-джоб создаёт файлы релиза и ссылки на артефакты;

release-джоб создаёт файлы релиза и ссылки на артефакты;

повышение версии и журнал изменений делаются отдельным контролируемым шагом.

повышение версии и журнал изменений делаются отдельным контролируемым шагом.

Пример релиза по тегу:

Отдельно проверьте верхнеуровневый workflow:rules : если он не разрешает tag pipelines, условие if: $CI_COMMIT_TAG на уровне джоба само по себе не поможет — теговый пайплайн просто не будет создан.

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

В официальных примерах GitLab часто встречается registry.gitlab.com/gitlab-org/cli:latest , но для критически важной для продакшена автоматизации релизов лучше зафиксировать конкретную версию образа или использовать внутренний зафиксированный образ с инструментами релиза.

Для продакшена лучше, чтобы релиз ссылался на неизменяемый тег образа или digest, а не на latest или плавающий тег ветки.

11. Безопасность, секреты, OIDC, Vault и CI_JOB_TOKEN

Пайплайн может:

читать исходный код;

читать исходный код;

собирать артефакты;

собирать артефакты;

публиковать Docker-образы;

публиковать Docker-образы;

получать секреты;

получать секреты;

ходить в облачные API;

ходить в облачные API;

деплоить в Kubernetes;

деплоить в Kubernetes;

пушить теги;

пушить теги;

создавать релизы;

создавать релизы;

запускать downstream-пайплайны.

запускать downstream-пайплайны.

Это не вспомогательный скрипт. Это часть цепочки поставки ПО.

Отсюда правила:

не доверять произвольным include и компонентам без ревью;

не доверять произвольным include и компонентам без ревью;

ограничивать токены;

ограничивать токены;

защищать раннеры;

защищать раннеры;

отделять защищённые и незащищённые процессы;

отделять защищённые и незащищённые процессы;

не давать продакшен-секреты обычным test-джобам;

не давать продакшен-секреты обычным test-джобам;

не запускать недоверенный код из форка с доступом к секретам родительского проекта.

не запускать недоверенный код из форка с доступом к секретам родительского проекта.

CI/CD-переменные удобны, но для важных секретов лучше использовать внешний менеджер секретов: Vault, облачный менеджер секретов или другой провайдер.

Базовый минимум для переменных GitLab:

замаскированные;

замаскированные;

скрытые;

скрытые;

защищённые;

защищённые;

с ограничением по окружению;

с ограничением по окружению;

минимальная область действия;

минимальная область действия;

регулярная ротация.

регулярная ротация.

Но целевое состояние для облачных и продакшен-секретов — краткоживущие учётные данные через OIDC или выдача секрета джобу по запросу.

Обычные переменные часто доступны джобу по умолчанию. А secrets: запрашиваются джобом явно.

Это важная разница. Чем явнее джоб просит секрет, тем проще ревьюить доступ.

Плохой подход:

Лучше не держать долгоживущий облачный ключ в GitLab вообще, а получать временный доступ через OIDC.

Статический облачный ключ в CI/CD — это долгий радиус поражения. Его надо хранить, ротировать, ограничивать и надеяться, что он не утечёт.

OIDC-подход лучше: джоб получает ID-токен, облачный провайдер или Vault проверяет claims и выдаёт временные учётные данные.

Пример для Vault:

В политике доверия лучше использовать стабильные claims вроде project_id и namespace_id , а не только path-based значения. Проект или группа могут переименоваться, а ID останется стабильнее.

OIDC полезен не только для Vault. Тот же принцип применим к облачной федерации: AWS, GCP, Azure, Yandex Cloud или другой провайдер проверяет claims токена и выдаёт временные учётные данные только конкретному проекту, ref или окружению. Это лучше, чем хранить в GitLab долгоживущий облачный ключ доступа.

Роль Vault не должна быть «всё всем».

Ограничивайте:

project/namespace;

project/namespace;

защищённые refs;

защищённые refs;

ref pattern;

ref pattern;

audience;

audience;

TTL;

TTL;

политику доступа только на нужное чтение.

политику доступа только на нужное чтение.

Если джоб должен только прочитать один секрет, не выдавайте ему write-доступ или wildcard-доступ.

Некоторые инструменты ожидают файл: kubeconfig, учётные данные Google, сертификат подписи, npmrc, SSH-ключ.

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

Пример:

CI_JOB_TOKEN удобен для автоматизации, но его нельзя считать безобидным.

Что делать:

включать список разрешений;

включать список разрешений;

добавлять только нужные проекты/группы;

добавлять только нужные проекты/группы;

использовать точечные права доступа там, где доступны;

использовать точечные права доступа там, где доступны;

не отключать список разрешений «навсегда ради удобства»;

не отключать список разрешений «навсегда ради удобства»;

пересматривать межпроектные доступы;

пересматривать межпроектные доступы;

не использовать job token как замену нормальной модели прав.

не использовать job token как замену нормальной модели прав.

Если пайплайн одного проекта может ходить в другой проект, это уже межпроектная граница доверия.

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

Если включаете:

делайте это только для конкретного проекта;

делайте это только для конкретного проекта;

ограничивайте защищённые ветки;

ограничивайте защищённые ветки;

учитывайте, что push через job token может не запускать новый пайплайн так, как вы ожидаете;

учитывайте, что push через job token может не запускать новый пайплайн так, как вы ожидаете;

документируйте релизный процесс.

документируйте релизный процесс.

В публичных и внутренних проектах логи и артефакты могут содержать чувствительные детали: пути, версии, ошибки, имена хостов, куски конфигов, имена пакетов, внутренние URLs.

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

Keystore, provisioning profile, сертификат подписи и похожие бинарные секреты не должны лежать в репозитории.

Для таких файлов используйте Secure Files или внешнее хранилище секретов.

Для проектов с требованиями безопасности не стоит надеяться, что каждая команда сама правильно подключит SAST, сканирование зависимостей, сканирование контейнеров или проверки политик.

Лучше использовать:

SAST;

SAST;

сканирование зависимостей;

сканирование зависимостей;

сканирование контейнеров;

сканирование контейнеров;

поиск секретов;

поиск секретов;

политики выполнения пайплайнов;

политики выполнения пайплайнов;

compliance-пайплайны;

compliance-пайплайны;

CODEOWNERS для чувствительных к безопасности файлов.

CODEOWNERS для чувствительных к безопасности файлов.

Чем критичнее проект, тем меньше безопасность должна быть добровольной настройкой.

MR из форка может содержать код, который пытается прочитать переменные, дёрнуть внутренний endpoint, украсть артефакт или использовать раннер родительского проекта.

Если запускаете MR-пайплайн из форка в родительском проекте, сначала ревьюйте код и понимайте, какие ресурсы он получит.

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

12. Раннеры, изоляция и усиление защиты инфраструктуры

Раннер можно зарегистрировать на уровне instance, group или project. Чем шире уровень, тем больше радиус поражения.

Общее правило:

общие раннеры — только для доверенных и хорошо изолированных нагрузок;

общие раннеры — только для доверенных и хорошо изолированных нагрузок;

групповые раннеры — для понятной группы проектов;

групповые раннеры — для понятной группы проектов;

проектные раннеры — для чувствительных проектов;

проектные раннеры — для чувствительных проектов;

выделенные защищённые раннеры — для продакшен-деплоев и чувствительных к безопасности джобов.

выделенные защищённые раннеры — для продакшен-деплоев и чувствительных к безопасности джобов.

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

Защитить только ветку недостаточно. Защитить только переменную недостаточно. Защитить только раннер тоже недостаточно.

Нормальная модель:

продакшен-деплой запускается только из защищённой ветки или тега;

продакшен-деплой запускается только из защищённой ветки или тега;

продакшен-секреты доступны только на защищённых refs;

продакшен-секреты доступны только на защищённых refs;

джоб выполняется на защищённом раннере;

джоб выполняется на защищённом раннере;

окружение защищено;

окружение защищено;

деплой требует ручного согласования или участия разрешённых деплоеров.

деплой требует ручного согласования или участия разрешённых деплоеров.

Если один слой открыт, вся цепочка становится слабее.

Идеальная модель для рискованных нагрузок — одноразовая среда: джоб выполнился, VM или контейнер раннера уничтожается.

Это снижает риск:

кражи данных между джобами;

кражи данных между джобами;

оставшихся секретов;

оставшихся секретов;

закрепления злоумышленника;

закрепления злоумышленника;

латерального перемещения;

латерального перемещения;

загрязнения рабочей директории.

загрязнения рабочей директории.

Для исполнителя Docker Machine с рискованными нагрузками используйте подход вроде MaxBuilds = 1 , чтобы одна машина не обслуживала много джобов подряд.

Shell-исполнитель выполняет команды прямо на хост раннера. Это мощно, но опасно.

Если джоб может выполнить произвольный shell на хосте, он может:

читать остатки файлов;

читать остатки файлов;

смотреть процессы;

смотреть процессы;

трогать сеть хоста;

трогать сеть хоста;

пытаться украсть учётные данные;

пытаться украсть учётные данные;

влиять на другие джобы.

влиять на другие джобы.

Shell-исполнитель допустим для полностью доверенных нагрузок на выделенных машинах. Для общих раннеров это плохая идея.

Docker-исполнитель в непривилегированном режиме обычно безопаснее Shell-исполнителя. Но привилегированные контейнеры резко меняют модель риска.

Дополнительно:

запускайте джоб от non-root-пользователя, где возможно;

запускайте джоб от non-root-пользователя, где возможно;

убирайте sudo , если он не нужен;

убирайте sudo , если он не нужен;

избегайте SETUID/SETGID внутри образа джоба;

избегайте SETUID/SETGID внутри образа джоба;

не включайте host PID namespace;

не включайте host PID namespace;

не монтируйте Docker socket без крайней необходимости.

не монтируйте Docker socket без крайней необходимости.

Монтирование Docker socket ( /var/run/docker.sock ) часто фактически равно root-доступу к хосту. Не используйте его как «простую замену DinD», если не понимаете последствия.

Если привилегированный режим действительно нужен:

отдельные раннеры;

отдельные раннеры;

отдельные хосты или VM;

отдельные хосты или VM;

запуск только на защищённых refs;

запуск только на защищённых refs;

минимум доступов;

минимум доступов;

короткая жизнь окружения раннера;

короткая жизнь окружения раннера;

мониторинг;

мониторинг;

понятный владелец.

понятный владелец.

Не смешивайте привилегированные джобы и обычные недоверенные джобы на одной переиспользуемой машине.

if-not-present может быть опасен на общем раннере: приватный образ, скачанный одним проектом, теоретически может быть переиспользован другим джобом на той же машине.

Для общих раннеров безопаснее не полагаться на уже скачанные приватные образы.

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

Если раннер используется недоверенными проектами, лучше предпочесть более изолированную модель, даже если она медленнее.

Раннер не должен иметь лишний доступ ко всей внутренней сети.

Хорошие меры:

отдельные сети/подсети;

отдельные сети/подсети;

запрет внешнего SSH;

запрет внешнего SSH;

фильтрация доступа к metadata endpoints;

фильтрация доступа к metadata endpoints;

ограничение east-west трафика;

ограничение east-west трафика;

отдельные раннеры для продакшен-деплоя;

отдельные раннеры для продакшен-деплоя;

egress-правила;

egress-правила;

аудит сетевых доступов.

аудит сетевых доступов.

Особенно это важно в облаках, где metadata endpoint может выдавать учётные данные.

На хосте раннера не должно быть лишних постоянных чувствительных данных:

отладочные учётные данные;

отладочные учётные данные;

старые SSH-ключи;

старые SSH-ключи;

временные токены;

временные токены;

артефакты прошлых джобов;

артефакты прошлых джобов;

лишние правила sudoers;

лишние правила sudoers;

ненужные сервисы;

ненужные сервисы;

открытые порты.

открытые порты.

Хост раннера — часть границы безопасности CI/CD. Его нельзя администрировать как случайную тестовую VM.

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

Лучше:

незащищённые джобы — отдельные обычные раннеры;

незащищённые джобы — отдельные обычные раннеры;

защищённые джобы — отдельные защищённые раннеры;

защищённые джобы — отдельные защищённые раннеры;

продакшен-деплой — отдельный выделенный раннер;

продакшен-деплой — отдельный выделенный раннер;

проверки безопасности с привилегированными требованиями — отдельный изолированный пул раннеров.

проверки безопасности с привилегированными требованиями — отдельный изолированный пул раннеров.

Это снижает риск, что недоверенная нагрузка повлияет на путь в продакшен.

13. Отчёты, качество, наблюдаемость и контроль

Не заставляйте разработчика читать сырые логи тестов. GitLab умеет показывать тестовые отчёты прямо в интерфейсе MR и пайплайна.

artifacts:when: always важен: если тесты упали, отчёт всё равно должен быть доступен.

coverage: вытаскивает процент покрытия из лога джоба.

coverage_report даёт построчные аннотации в diff MR.

Нюанс: GitLab использует RE2 для coverage , и группы в регулярном выражении должны быть незахватывающими. Поэтому используйте (?:...) , а не обычные захватывающие группы вроде (...) .

Для нормального пользовательского опыта часто нужны оба механизма.

Не надо строить качество только вокруг устаревших универсальных шаблонов. Лучше использовать реальные линтеры и статический анализ, которые команда уже понимает, и публиковать результат в формате отчёта Code Quality.

Идея простая: замечания должны появляться в MR-виджете, а не теряться в stdout.

Если падает браузерный тест, логов часто мало. Нужны:

скриншот;

скриншот;

видео;

видео;

trace;

trace;

HTML-отчёт;

HTML-отчёт;

сетевые логи.

сетевые логи.

Пример:

Но помните про хранилище. Видео и скриншоты быстро раздувают артефакты.

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

Хороший пайплайн показывает результат в MR:

тесты;

тесты;

покрытие;

покрытие;

качество кода;

качество кода;

замечания по безопасности;

замечания по безопасности;

выставленные артефакты;

выставленные артефакты;

ссылка на ревью-окружение;

ссылка на ревью-окружение;

статус деплоя.

статус деплоя.

Цель CI/CD — дать быстрый и понятный сигнал, а не просто выполнить команды.

Наблюдаемость пайплайна — это не только «зелёный/красный».

Смотрите:

долю успешных запусков;

долю успешных запусков;

среднюю и p95 длительность;

среднюю и p95 длительность;

время ожидания в очереди;

время ожидания в очереди;

длительность по стадиям и джобам;

длительность по стадиям и джобам;

нестабильные джобы;

нестабильные джобы;

долю повторных запусков;

долю повторных запусков;

утилизацию раннеров;

утилизацию раннеров;

рост хранилища;

рост хранилища;

рост реестра;

рост реестра;

частоту отменённых пайплайнов;

частоту отменённых пайплайнов;

инциденты, связанные с CI/CD.

инциденты, связанные с CI/CD.

Для долгосрочного контроля можно использовать GitLab Analytics, API, exporter-подходы, Prometheus/Grafana и внутренние дашборды.

CI/CD не должен жить только в голове платформенной команды.

Полезно держать в репозитории:

схему пайплайна;

схему пайплайна;

описание окружения;

описание окружения;

правила деплоя;

правила деплоя;

описание секретов и источников учётных данных;

описание секретов и источников учётных данных;

разбор типовых проблем;

разбор типовых проблем;

известные проблемы;

известные проблемы;

журнал изменений CI/CD-платформы.

журнал изменений CI/CD-платформы.

Не надо переписывать весь CI/CD за один MR.

Хороший порядок:

Измерить.

Измерить.

Найти узкое место.

Найти узкое место.

Внести маленькое изменение.

Внести маленькое изменение.

Сравнить эффект.

Сравнить эффект.

Повторить.

Повторить.

Так проще не сломать процесс доставки и показать пользу изменений.

Официальные примеры, компоненты и паттерны каталога полезны как старт. Но их нельзя бездумно копировать.

Проверяйте:

вашу версию GitLab;

вашу версию GitLab;

GitLab.com или самостоятельно развёрнутую инсталляцию;

GitLab.com или самостоятельно развёрнутую инсталляцию;

доступный уровень;

доступный уровень;

модель раннеров;

модель раннеров;

требования безопасности;

требования безопасности;

стратегия ветвления;

стратегия ветвления;

монорепозиторий или несколько репозиториев;

монорепозиторий или несколько репозиториев;

цель деплоя;

цель деплоя;

требования к аудиту и согласования.

требования к аудиту и согласования.

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

14. Финальный пример .gitlab-ci.yml

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

Нюанс по совместимости: финальный пример ниже ориентирован на относительно свежий GitLab, примерно 18.3+. Если у вас self-managed-инсталляция старее, отдельно проверьте поддержку workflow:auto_cancel:on_job_failure , переменной $CI_MERGE_REQUEST_DRAFT и manual_confirmation для stop-джобов. На старых версиях эти части примера нужно будет упростить или адаптировать под вашу версию GitLab.

Что здесь важно:

создание пайплайна управляется через workflow:rules ;

создание пайплайна управляется через workflow:rules ;

черновой MR можно не гонять;

черновой MR можно не гонять;

пайплайн ветки не дублирует MR-пайплайн;

пайплайн ветки не дублирует MR-пайплайн;

теги, расписания, API, trigger, multi-project и parent-child downstream-процессы явно разрешены на уровне workflow ;

теги, расписания, API, trigger, multi-project и parent-child downstream-процессы явно разрешены на уровне workflow ;

пайплайны, запущенные триггером, API, parent-child и multi-project downstream-процессами не блокируются правилом для пайплайнов веток, запущенных push-событием;

пайплайны, запущенные триггером, API, parent-child и multi-project downstream-процессами не блокируются правилом для пайплайнов веток, запущенных push-событием;

быстрые проверки стартуют через needs: [] ;

быстрые проверки стартуют через needs: [] ;

настройки Node.js вынесены в .node-job , поэтому деплой- и stop-джобы не наследуют npm ci ;

настройки Node.js вынесены в .node-job , поэтому деплой- и stop-джобы не наследуют npm ci ;

деплой- и stop-джобы используют отдельный зафиксированный образ с инструментами деплоя, а не случайный базовый alpine ;

деплой- и stop-джобы используют отдельный зафиксированный образ с инструментами деплоя, а не случайный базовый alpine ;

кеш зависимостей обновляется отдельным джобом deps через policy: pull-push ;

кеш зависимостей обновляется отдельным джобом deps через policy: pull-push ;

тесты публикуют JUnit и покрытие, а регулярное выражение покрытия использует незахватывающие группы;

тесты публикуют JUnit и покрытие, а регулярное выражение покрытия использует незахватывающие группы;

результат сборки передаётся через артефакты;

результат сборки передаётся через артефакты;

build-app не скачивает тестовые артефакты без необходимости;

build-app не скачивает тестовые артефакты без необходимости;

MR из форка проверяет Docker-сборку через build-image-check , но не пушит образ в реестр;

MR из форка проверяет Docker-сборку через build-image-check , но не пушит образ в реестр;

MR внутри того же проекта может собрать отдельный образ для ревью-окружения;

MR внутри того же проекта может собрать отдельный образ для ревью-окружения;

продакшен-образ собирается через BuildKit без root-прав только из ветки по умолчанию;

продакшен-образ собирается через BuildKit без root-прав только из ветки по умолчанию;

продакшен-образ тегируется SHA коммита;

продакшен-образ тегируется SHA коммита;

ревью-окружение имеет on_stop , auto_stop_in и resource_group ;

ревью-окружение имеет on_stop , auto_stop_in и resource_group ;

stop-review не использует GIT_STRATEGY: none , потому что вызывает ./destroy-review.sh из репозитория;

stop-review не использует GIT_STRATEGY: none , потому что вызывает ./destroy-review.sh из репозитория;

опасные manual-джобы имеют manual_confirmation ;

опасные manual-джобы имеют manual_confirmation ;

продакшен-деплой ручной, не прерываемый и сериализован через resource_group .

продакшен-деплой ручной, не прерываемый и сериализован через resource_group .

15. План внедрения

Если проект уже живой, не нужно внедрять всё сразу. Хорошая последовательность такая.

добавить workflow:rules ;

добавить workflow:rules ;

убрать дубли пайплайнов веток и MR;

убрать дубли пайплайнов веток и MR;

перестать смешивать only/except и rules ;

перестать смешивать only/except и rules ;

явно разделить push, MR, расписания, теги и trigger-процессы.

явно разделить push, MR, расписания, теги и trigger-процессы.

вынести линтинг/стиль/схему в ранние джобы;

вынести линтинг/стиль/схему в ранние джобы;

добавить needs: [] для быстрых проверок;

добавить needs: [] для быстрых проверок;

найти критический путь;

найти критический путь;

добавить interruptible там, где джобы можно отменять;

добавить interruptible там, где джобы можно отменять;

включить auto-cancel для лишних пайплайнов.

включить auto-cancel для лишних пайплайнов.

отделить кеш от артефактов;

отделить кеш от артефактов;

построить ключи кеша от lock-файлов;

построить ключи кеша от lock-файлов;

добавить резервные ключи;

добавить резервные ключи;

включить распределённый кеш для нескольких раннеров;

включить распределённый кеш для нескольких раннеров;

задать expire_in для артефактов;

задать expire_in для артефактов;

убрать лишнее скачивание артефактов через dependencies / needs:artifacts .

убрать лишнее скачивание артефактов через dependencies / needs:artifacts .

перестать собирать через опасный привилегированный процесс без причины;

перестать собирать через опасный привилегированный процесс без причины;

перейти на BuildKit без root-прав/Buildah/Podman или осознанный процесс на Docker Buildx;

перейти на BuildKit без root-прав/Buildah/Podman или осознанный процесс на Docker Buildx;

добавить кеш в реестре;

добавить кеш в реестре;

не передавать секреты через ARG / ENV / COPY ;

не передавать секреты через ARG / ENV / COPY ;

использовать Dependency Proxy;

использовать Dependency Proxy;

тегировать образы по SHA коммита или digest.

тегировать образы по SHA коммита или digest.

убрать секреты из репозитория;

убрать секреты из репозитория;

замаскированные/скрытые/защищённые переменные как минимум;

замаскированные/скрытые/защищённые переменные как минимум;

продакшен-секреты привязать к защищённым refs и окружениям;

продакшен-секреты привязать к защищённым refs и окружениям;

перейти на OIDC/Vault для облаков и внешних секретов;

перейти на OIDC/Vault для облаков и внешних секретов;

ограничить CI_JOB_TOKEN список разрешений;

ограничить CI_JOB_TOKEN список разрешений;

включить точечные права доступа, где это возможно.

включить точечные права доступа, где это возможно.

описать окружения;

описать окружения;

включить защищённые окружения;

включить защищённые окружения;

добавить согласования;

добавить согласования;

сериализовать деплой через resource_group ;

сериализовать деплой через resource_group ;

включить защиту от устаревших деплоев;

включить защиту от устаревших деплоев;

сделать ревью-окружения с очисткой;

сделать ревью-окружения с очисткой;

добавить карты маршрутов, если это frontend/документация/статический сайт.

добавить карты маршрутов, если это frontend/документация/статический сайт.

вынести повторяющийся YAML в extends / !reference ;

вынести повторяющийся YAML в extends / !reference ;

разделить job-шаблоны и шаблоны пайплайнов;

разделить job-шаблоны и шаблоны пайплайнов;

перейти к CI/CD-компонентам для массового переиспользования;

перейти к CI/CD-компонентам для массового переиспользования;

добавить spec:inputs ;

добавить spec:inputs ;

пинить версии;

пинить версии;

тестировать компоненты по $CI_COMMIT_SHA ;

тестировать компоненты по $CI_COMMIT_SHA ;

вести журнал изменений и заметки по миграции.

вести журнал изменений и заметки по миграции.

JUnit-отчёты;

JUnit-отчёты;

отчёты о покрытии;

отчёты о покрытии;

отчёт Code Quality;

отчёт Code Quality;

артефакты через expose_as ;

артефакты через expose_as ;

аналитика пайплайнов;

аналитика пайплайнов;

мониторинг раннеров;

мониторинг раннеров;

документация архитектуры пайплайна;

документация архитектуры пайплайна;

CODEOWNERS для CI/CD-файлов;

CODEOWNERS для CI/CD-файлов;

политики безопасности для критичных проектов.

политики безопасности для критичных проектов.

16. Заключение

GitLab CI/CD легко недооценить. Пока проект маленький, кажется, что достаточно пары джобов и трёх стадий. Но чем ближе пайплайн к продакшену, тем больше он становится похож на полноценную платформу доставки.

Хороший GitLab CI/CD — это не самый сложный YAML. Это понятная архитектура, где:

пайплайн создаётся только когда должен создаваться;

пайплайн создаётся только когда должен создаваться;

джобы запускаются только когда нужны;

джобы запускаются только когда нужны;

быстрые ошибки падают рано;

быстрые ошибки падают рано;

кеш ускоряет, а не ломает воспроизводимость;

кеш ускоряет, а не ломает воспроизводимость;

артефакты передают результат, а не заменяют хранилище;

артефакты передают результат, а не заменяют хранилище;

Docker-образы собираются безопасно;

Docker-образы собираются безопасно;

секреты не живут в коде и не раздаются всем джобам;

секреты не живут в коде и не раздаются всем джобам;

деплой сериализован и ограничен правами;

деплой сериализован и ограничен правами;

ревью-окружения сами очищаются;

ревью-окружения сами очищаются;

отчёты видны в MR, а не спрятаны в логах;

отчёты видны в MR, а не спрятаны в логах;

переиспользуемая логика версионируется и тестируется;

переиспользуемая логика версионируется и тестируется;

раннеры изолированы и не смешивают доверенные/недоверенные нагрузки.

раннеры изолированы и не смешивают доверенные/недоверенные нагрузки.

Главная мысль простая: CI/CD надо проектировать так же внимательно, как продакшен-инфраструктуру. Потому что для современного проекта CI/CD и есть одна из дорог к продакшену.

← Cybersecurity