Все уроки
DevOps и облакоСредний11 мин чтенияПолезно знать

GitLab CI/CD

Пайплайны в .gitlab-ci.yml: стадии, job-ы, раннеры и environments, встроенные в GitLab.

  • Pipelines
  • Stages
  • Runners
  • Environments
Шаг 1/6
Разработчик делает push. CI/CD pipeline просыпается, и первым делом linter проверяет стиль. Зелёный.

За 30 секунд

GitLab CI/CD — это движок автоматизации, встроенный в GitLab. Файл .gitlab-ci.yml в репозитории описывает пайплайн: job-ы, сгруппированные по стадиям, например build, test и deploy. GitLab Runner-ы — на инфраструктуре GitLab или на ваших серверах — выполняют эти job-ы, обычно внутри Docker-контейнеров. Environments, аппрувы и история деплоев входят в тот же продукт, так что код, merge request-ы и деплои находятся в одном месте.

Не в моём ежедневном стеке — я понимаю принципы и изучу детали, когда это понадобится проекту.

Почему это важно

  • Это один продукт на весь жизненный цикл: репозиторий, merge request-ы, пайплайны, container registry, environments и сканирование безопасности — удобно командам, которым нужно меньше инструментов.
  • Self-managed GitLab со своими раннерами — зрелый вариант для компаний, которые обязаны держать код и сборки на собственной инфраструктуре.
  • Пайплайны — это просто YAML с include, шаблонами и CI/CD components, так что платформенная команда может стандартизировать пайплайны в сотнях проектов.
  • Environments показывают, что где задеплоено, с ручными шлюзами, защищёнными environments и откатом к предыдущему деплою в один клик.

Как это работает

01Пайплайны, стадии и job-ы

Пайплайн — это полный запуск, инициированный push-ем, merge request-ом, тегом, расписанием или вызовом API. Он состоит из job-ов, и каждый job — это именованный блок в .gitlab-ci.yml с script: — shell-командами, которые нужно выполнить. Job-ы группируются в стадии, перечисленные по порядку в stages:. Все job-ы одной стадии идут параллельно; следующая стадия начинается, только когда все job-ы текущей завершились успешно. Представьте сборочный конвейер с постами: все рабочие на посту build должны закончить, прежде чем детали поедут на пост test. Каждый job выполняется в свежем окружении, часто в Docker-контейнере, выбранном через image:, поэтому файлы не переносятся, если не объявить их артефактами или кэшем. Если стадия не указана, job попадает в test. Предопределённые стадии .pre и .post всегда выполняются первой и последней.

yaml
stages: [build, test, deploy]
 
default:
  image: node:24-alpine
 
build:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths: [dist/]
    expire_in: 1 week
 
unit-tests:
  stage: test
  script:
    - npm ci
    - npm test

02Раннеры и executor-ы

GitLab Runner — это программа-агент, которая запрашивает у GitLab job-ы и выполняет их. На GitLab.com есть hosted-раннеры на Linux, Windows и macOS, которые тарифицируются в compute-минутах. Self-managed раннеры ставятся на ваши машины; их регистрируют токеном аутентификации, созданным в настройках проекта, группы или инстанса. Executor определяет, как выполняется job: shell запускает команды прямо на хосте (просто, но без изоляции), docker запускает контейнер на каждый job из его image (самый частый вариант по умолчанию), kubernetes создаёт под на каждый job в кластере, а docker-autoscaler или instance executor-ы создают облачные VM по запросу. Теги связывают job-ы с раннерами: job с tags: [gpu] выполнится только на раннерах, зарегистрированных с этим тегом. Раннеры бывают уровня инстанса, группы или конкретного проекта — так раннер в production-сети можно сделать доступным только deploy-проекту.

03Когда запускать job-ы: rules и workflow

rules: решает, добавляется ли job в пайплайн. Правила проверяются сверху вниз, побеждает первое совпадение; каждое может задать when: (on_success, manual, delayed, never) и allow_failure. В условиях используются предопределённые переменные — CI_PIPELINE_SOURCE, CI_COMMIT_BRANCH и CI_DEFAULT_BRANCH, — плюс changes: для путей к файлам и exists: для наличия файлов. workflow: rules работает уровнем выше и решает, создавать ли пайплайн вообще. Частый паттерн — запускать пайплайны для merge request-ов и для ветки по умолчанию, но не дублирующий branch-пайплайн для того же коммита. Предпочитайте rules старым ключевым словам only/except, которые больше не рекомендуются. needs: позволяет job-у стартовать, как только завершатся конкретные job-ы, игнорируя порядок стадий, — пайплайн превращается в более быстрый направленный граф.

yaml
workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
 
e2e-tests:
  stage: test
  needs: [build]
  rules:
    - changes: [src/**/*, package-lock.json]
  script:
    - npm run test:e2e

04Переменные, секреты и environments

CI/CD-переменные задаются в настройках проекта, группы или инстанса, либо в YAML для несекретных значений. Чувствительные помечайте как masked (скрыты в логах) и protected (доступны только пайплайнам защищённых веток и тегов), чтобы feature-ветка не могла прочитать production credentials. Переменные типа File записывают значение во временный файл и кладут в переменную путь к нему — удобно для kubeconfig и сертификатов. Environment — это именованная цель, например staging или production; job-ы объявляют environment: name и url, и GitLab записывает каждый деплой, так что видно, что где работает, и можно откатиться. Защищённые environments ограничивают, кто может деплоить, а deployment approvals добавляют обязательных аппруверов. Для доступа к облакам GitLab выдаёт OIDC ID-токены (id_tokens:), которые принимают облака и HashiCorp Vault, — это полностью избавляет от долгоживущих ключей.

05Сборка образов и деплой

GitLab предоставляет container registry для каждого проекта и предопределённые переменные для работы с ним: CI_REGISTRY, CI_REGISTRY_IMAGE и job-токен для входа. Job ниже собирает образ через Docker-in-Docker, тегирует его SHA коммита и пушит. Docker-in-Docker требует privileged-раннера; в Kubernetes или на shared-раннерах, где это запрещено, обычно используют rootless-сборщики вроде Buildah или BuildKit. Deploy-job ручной на ветке по умолчанию, привязан к environment production и использует resource_group, чтобы два деплоя никогда не шли одновременно. Команда деплоя здесь — заглушка: это может быть helm upgrade, SSH-команда, которая тянет образ, или вызов облачного CLI.

yaml
build-image:
  stage: build
  image: docker:28
  services: [docker:28-dind]
  script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
 
deploy-prod:
  stage: deploy
  environment: { name: production, url: https://example.com }
  resource_group: production
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual
  script:
    - ./scripts/deploy.sh "$CI_COMMIT_SHA"

Когда применять

  • Код хостится в GitLab — на GitLab.com или self-managed.
  • Всё должно быть на вашей инфраструктуре: self-managed GitLab и self-managed раннеры в вашей сети.
  • Платформенная команда хочет стандартизировать пайплайны во многих проектах через include и CI/CD components.
  • Вам важны встроенные environments, история деплоев, аппрувы и container registry на каждый проект.

Когда не стоит

  • Репозитории живут на GitHub — GitHub Actions интегрируется естественнее.
  • Крошечный статический сайт без тестов, где сборка по git push на хостинг-платформе проще.
  • Нужна экзотическая оркестрация сборок, с которой лучше справится специализированная система вроде Buildkite или Bazel remote execution.

Альтернативы

  • GitHub Actions

    Аналог для репозиториев GitHub с большим Marketplace action-ов.

  • Jenkins

    Self-hosted, на плагинах; часто встречается в старых или сильно кастомизированных системах.

  • Argo CD / Flux

    GitOps-инструменты, которые подтягивают деплои в Kubernetes; часто работают в паре с GitLab CI для сборок.

  • Tekton

    Kubernetes-native строительные блоки пайплайнов для команд, которые строят собственную CI-платформу.

Лучшие практики

  1. Используйте rules: и workflow: rules, а не only/except, и избегайте дублирующих branch- и merge request-пайплайнов.
  2. Помечайте credentials как masked и protected и защищайте ветки и environments, из которых идёт деплой.
  3. Стройте направленный граф через needs:, чтобы job-ы стартовали, как только готовы их входные данные.
  4. Делитесь логикой пайплайнов через include: или CI/CD components с типизированными inputs, а не копипастой.
  5. Фиксируйте теги образов (node:24-alpine, а не latest) и кэшируйте зависимости с ключом по lockfile.
  6. Используйте resource_group в deploy-job-ах, чтобы деплои в один environment никогда не пересекались.
  7. Запускайте deploy-job-ы только на выделенных раннерах с тегами, расположенных рядом с целью, и больше нигде.

Частые ошибки

  • Оставлять production-переменные незащищёнными, так что любой branch-пайплайн может их прочитать и слить.
  • Регистрировать раннер с shell executor на общем хосте, где job-ы могут трогать файлы друг друга и сам хост.
  • Путать кэш и артефакты, а потом удивляться, почему результата сборки нет на следующей стадии.
  • Выводить секреты через echo или запускать скрипты с set -x, печатая значения, которые маскирование не ловит.
  • Огромные однофайловые пайплайны со скопированными job-ами вместо extends:, якорей или include.

Глубже

06CI/CD components и типизированные inputs

CI/CD component — это переиспользуемая единица конфигурации пайплайна, которая хранится в проекте компонента в templates/, выпускается с тегами семантических версий и попадает в CI/CD Catalog. Файл начинается с заголовка spec:, где объявлены inputs с type (string, number, boolean, array), default, options и проверкой regex; от определений job-ов он отделён строкой ---. Значения подставляются через $[[ inputs.name ]] в момент создания пайплайна, до запуска любого job-а, так что неверное значение сразу падает с понятной ошибкой, а не посреди пайплайна. Потребители подключают компонент через include: component: с версией — точным тегом, частичной версией вроде ~latest или SHA коммита. Для production-пайплайнов используйте точные версии или SHA, а обновления пусть предлагает бот. Components — это аналог reusable workflows в GitLab и основа внутреннего каталога пайплайнов.

yaml
# templates/node-test/template.yml in the component project
spec:
  inputs:
    node_version:
      default: "24"
    stage:
      default: test
---
node-test:
  stage: $[[ inputs.stage ]]
  image: node:$[[ inputs.node_version ]]-alpine
  script:
    - npm ci
    - npm test
 
# consumer .gitlab-ci.yml
include:
  - component: $CI_SERVER_FQDN/your-group/ci-components/[email protected]
    inputs: { node_version: "22" }

07Parent-child и multi-project пайплайны

Большие репозитории упираются в ограничения одного пайплайна: слишком много job-ов, трудно читать, всё запускается на каждое изменение. Parent-child пайплайн позволяет job-у запустить отдельный дочерний пайплайн из другого YAML-файла того же проекта через trigger: include:. В сочетании с rules: changes: монорепозиторий может запускать только дочерний пайплайн изменившегося сервиса. Дочерние пайплайны можно даже генерировать динамически: job пишет YAML-файл как артефакт, а trigger-job его запускает — так команды создают пайплайны для каждого обнаруженного пакета. Multi-project пайплайны запускают пайплайн в другом проекте, например пайплайн приложения после публикации образа запускает пайплайн репозитория деплоя. strategy: depend (или более новая стратегия mirror) заставляет trigger-job отражать результат downstream, так что падение дочернего пайплайна видно в родительском.

08Масштабирование и изоляция парка раннеров

Время в очереди — скрытая цена CI. У каждого раннера в config.toml есть настройки concurrent (всего job-ов на процесс) и limit для отдельного раннера; задавайте их по CPU и памяти, а не наугад. Для всплесков нагрузки Kubernetes executor или docker-autoscaler executor с облачным fleeting-плагином масштабируются по запросу и обратно до нуля. Разделяйте парки по уровню доверия: shared-раннеры для обычных сборок, protected-раннеры (берут job-ы только из защищённых веток) для деплоев и изолированные по сети раннеры для всего, что касается production. Privileged-режим для Docker-in-Docker даёт job-у почти root-доступ к хосту; избегайте его на общих парках и предпочитайте rootless-сборщики. Распределённый кэш в S3-совместимом хранилище позволяет автомасштабируемым раннерам делить кэш. Следите за Prometheus-метриками раннера: длительность job-ов, время в очереди и ошибки.

Реальные ситуации

01

Монорепозиторий, где каждый коммит запускает всё

Проблема

В репозитории веб-приложение, API и три воркера. Каждый коммит гоняет все 60 job-ов по 40 минут, даже ради изменения одной строки в одном сервисе.

Как решить

Дайте каждому сервису свой файл дочернего пайплайна, например services/api/.gitlab-ci.yml. В корневом файле добавьте по trigger-job-у на сервис с trigger: include: этого файла и strategy: depend, плюс rules: changes: services/api/**/* и общие пути вроде lockfile. Оставьте полный прогон на ветке по умолчанию по ночам через пайплайн по расписанию, чтобы ловить поломки между сервисами. Добавьте needs: внутри дочерних пайплайнов, чтобы сократить критические пути. Типичное изменение теперь запускает один дочерний пайплайн за несколько минут, а в родительском по-прежнему наглядно виден статус каждого сервиса.

02

Feature-ветка слила production-токен

Проблема

Разработчик добавил в feature-ветке отладочный шаг, который печатал переменные окружения, и production API-токен оказался в логе job-а в base64.

Как решить

Немедленно отзовите и ротируйте токен, затем удалите лог job-а. Пометьте новую переменную как protected и masked, чтобы она подставлялась только в защищённых ветках и скрывалась в логах. Защитите ветку main и environment production и перенесите deploy credentials в scope защищённого environment. Ограничьте deploy-job-ы protected-раннером. В перспективе замените статический токен на id_tokens и короткоживущий credential из Vault или облачного провайдера. Добавьте короткую заметку для команды: никогда не печатать переменные окружения в CI.

03

Два деплоя пошли одновременно и сломали миграцию

Проблема

Два merge прошли с разницей в минуту. Оба deploy-job-а запустились параллельно, оба начали миграции базы данных, и production оказался в наполовину смигрированном состоянии.

Как решить

Добавьте resource_group: production в deploy-job, чтобы GitLab выполнял только один деплой в этот environment за раз; задайте process_mode oldest_first или newest_first в зависимости от того, должен ли деплоиться каждый коммит. Вынесите миграции в отдельный job перед раскаткой приложения и сделайте их обратно совместимыми (expand, затем contract), чтобы старый и новый код могли работать во время переключения. Включите опцию предотвращения устаревших deploy-job-ов, чтобы старый пайплайн не мог задеплоиться поверх нового. Записывайте каждый деплой в environment production для простого отката.

Шпаргалка

Проверить конфиг локально через API
glab ci lint .gitlab-ci.yml
Запуск только для merge request-ов
rules: [{ if: '$CI_PIPELINE_SOURCE == "merge_request_event"' }]
Ручной job на ветке по умолчанию
rules: [{ if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH', when: manual }]
Стартовать раньше, минуя порядок стадий
needs: [build]
Кэш с ключом по lockfile
cache: { key: { files: [package-lock.json] }, paths: [.npm/] }
Один деплой за раз
resource_group: production
OIDC-токен для job-а
id_tokens: { CLOUD_TOKEN: { aud: https://example.com } }
Переиспользовать скрытый job-шаблон
.base: { image: node:24-alpine } then test: { extends: .base }
Зарегистрировать раннер
gitlab-runner register --url https://gitlab.example.com --token $RUNNER_TOKEN

Вопросы и ответы

Q1Чем стадии отличаются от needs?

Стадии дают простой линейный порядок: все job-ы одной стадии должны завершиться, прежде чем начнётся следующая. Это легко читать, но медленно, когда один job в стадии намного дольше остальных, — все ждут его. needs: объявляет прямые зависимости между job-ами, так что job стартует, как только завершились нужные ему job-ы, даже если другие job-ы ранних стадий ещё идут. Пайплайн становится направленным ациклическим графом. needs также управляет тем, какие артефакты скачивает job. Я начинаю со стадий для наглядности и добавляю needs там, где критический путь длинный, например позволяю deploy-docs стартовать сразу после build-docs.

Q2Кэш или артефакты в GitLab?

Артефакты — это результаты job-ов, которые GitLab хранит и передаёт следующим job-ам того же пайплайна и которые можно скачать из UI: папки сборки, отчёты тестов, coverage. Они надёжны: следующий job, которому они нужны, их получит. Кэш — это ускорение по принципу best-effort для того, что можно сгенерировать заново, например node_modules или каталог загрузок пакетного менеджера; он разделяется между пайплайнами и хранится на раннере или в object storage. Job обязан корректно работать с пустым кэшем. Используйте cache:key:files с lockfile, чтобы кэш менялся при изменении зависимостей. Если корректность job-а зависит от файла — нужен артефакт.

Q3Как защитить production credentials в GitLab CI?

Храните их как CI/CD-переменные с пометками protected и masked, чтобы они подставлялись только в пайплайны защищённых веток или тегов и скрывались в логах. Защитите ветку main, чтобы пушить и мержить могли только maintainer-ы. Сделайте production защищённым environment с deployment approvals, чтобы deploy-job-ы могли запускать только указанные люди или группы. Запускайте deploy-job-ы на выделенном раннере с тегом в production-сети, а не на shared-раннерах. По возможности вообще избегайте статических credentials: используйте id_tokens, чтобы выдать job-у короткоживущий OIDC-токен, который облачный провайдер или Vault обменяет на временные credentials, ограниченные этим проектом и веткой.

Q4Что такое executor у GitLab Runner и какой бы вы выбрали?

Executor — это способ, которым раннер реально выполняет job. Shell запускает команды прямо на хосте раннера: быстрее всего настроить, но изоляции между job-ами нет, поэтому я использую его только на очень доверенных машинах одного назначения. Docker запускает каждый job в свежем контейнере из его image: хорошая изоляция, воспроизводимость, обычный выбор по умолчанию. Kubernetes создаёт под на каждый job — подходит командам, у которых уже есть кластер и нужно автомасштабирование. Docker-autoscaler или instance executor-ы поднимают облачные VM по запросу для сильной изоляции каждого job-а. Большинству команд я бы выбрал Docker executor на паре VM, а потом переходил на Kubernetes или автомасштабирование, когда время в очереди станет проблемой.

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

GitLab предлагает несколько уровней. Внутри одного файла extends: и YAML-якоря переиспользуют определения job-ов, а скрытые job-ы (имена с точкой в начале) работают как шаблоны. Между проектами include: подтягивает YAML из другого проекта, по удалённому URL или из шаблонов самого GitLab. Современный вариант — CI/CD components: версионированные переиспользуемые части пайплайна, опубликованные в CI/CD Catalog, со spec: inputs, которые типизированы и проверяются при создании пайплайна. Сервис пишет пару строк, чтобы подключить компонент нужной версии и передать inputs. Фиксируйте версии, чтобы изменение общего компонента не сломало все проекты разом.

Вопросы уровня senior

S1Как бы вы спроектировали платформу GitLab CI для 300 проектов с требованиями compliance?

Я бы дал небольшой каталог CI/CD components (build, test, scan, deploy), который поддерживает платформенная команда, с версиями по семантическим тегам и документацией. Критичные для compliance job-ы — SAST, dependency scanning и secret detection — навязывались бы централизованно через pipeline execution policies или compliance frameworks, чтобы проекты не могли их убрать. Парки раннеров разделены по доверию: автомасштабируемые shared-раннеры для сборок, protected-раннеры для деплоев, изолированные раннеры внутри production-сетей. Секреты приходят из Vault или облака через OIDC id_tokens, с claims, привязанными к пути проекта и защищённым ref-ам. Environments защищены аппрувами. Компромисс — автономия: команды должны иметь возможность расширять стандартный пайплайн своими job-ами, иначе они его форкнут, и платформа потеряет контроль.

S2Push-деплой из GitLab CI или pull-based GitOps с Argo CD?

Push-подход означает, что CI-job держит credentials кластера и запускает kubectl или helm. Это просто, даёт немедленную обратную связь в пайплайне и работает как для серверов, так и для кластеров. Минусы: CI нужны мощные credentials, а расхождение (drift) между Git и кластером остаётся незамеченным. Pull-based GitOps запускает агента внутри кластера, который следит за репозиторием конфигурации и применяет изменения. Credentials кластера никогда его не покидают, drift обнаруживается и исправляется, а история Git служит аудит-логом. Цена — ещё одна система в эксплуатации и непрямая обратная связь: CI только обновляет тег образа в Git. Для Kubernetes на масштабе я склоняюсь к GitOps, где CI собирает и тестирует образы; для нескольких VM push-деплой с protected-раннера вполне разумен.

S3Как сделать пайплайны и быстрыми, и надёжными по мере роста проекта?

Скорость — это делать меньше и параллельно: графы needs:, rules: changes:, чтобы пропускать незатронутую работу, шардирование тестов через parallel:, кэш зависимостей и слоёв Docker, заранее собранные CI-образы с готовым тулчейном. Надёжность — это детерминизм: зафиксированные версии образов, lockfile, отсутствие зависимости от внешних сервисов в unit-тестах и повторы только для известных временных ошибок инфраструктуры через retry: when:, но никогда для падающих тестов. Нестабильные тесты отслеживаются и помещаются в карантин с назначенным владельцем. Измеряйте через pipeline analytics и метрики раннеров и задайте бюджет, например медиана пайплайнов merge request-ов меньше десяти минут. Компромисс — поддержка: агрессивный пропуск может прозевать поломки между сервисами, поэтому дополняйте его полными пайплайнами по расписанию на ветке по умолчанию.

Глоссарий

Pipeline
Один запуск всех job-ов по push, merge request, тегу, расписанию или вызову API.
Stage
Группа job-ов, которые идут параллельно; стадии выполняются в указанном порядке.
Job
Именованная единица работы со скриптом, которую раннер выполняет в свежем окружении.
Runner
Агент, который берёт job-ы из GitLab и выполняет их с выбранным executor.
Executor
Способ выполнения job-ов раннером: shell, docker, kubernetes, автомасштабируемые VM и другие.
Artifact
Файлы, которые job сохраняет для следующих job-ов пайплайна или для скачивания.
Protected variable
CI/CD-переменная, которая подставляется только в пайплайны защищённых веток или тегов.
Environment
Именованная цель деплоя, которую отслеживает GitLab, с историей, аппрувами и откатом.
CI/CD component
Версионированная переиспользуемая часть конфигурации пайплайна с типизированными inputs, опубликованная в CI/CD Catalog.

Есть идея продукта?Давайте создадим его.

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