Pipeline, stage и job: порядок и параллельность
В первой лабораторной ты сделал самый маленький CI/CD-сценарий: один файл .gitlab-ci.yml, один или два jobs и несколько простых команд. Этого достаточно, чтобы увидеть главный цикл:
изменил файл -> commit -> push -> GitLab создал pipeline -> runner выполнил jobs
В этой главе примеры тоже будут простыми: echo, date, pwd, условные ./run-tests.sh и ./build-image.sh. Сейчас нам важно разобраться с самим написанием и выполнением pipeline. В реальном проекте на месте этих команд будут настоящие действия: тесты приложения, сборка, публикация Docker image, миграции, deploy и проверки после выкладки.
Теперь разберём, как из одного job получается нормальный pipeline для проекта. Нам нужно ответить на три вопроса:
- какие действия запускать;
- какие из них можно делать одновременно;
- какие должны ждать результат предыдущих действий.
Job: одно конкретное действие
Job - это отдельная работа внутри pipeline. Например:
- проверить код;
- запустить тесты;
- собрать приложение;
- собрать Docker image;
- выложить новую версию на тестовый стенд.
В .gitlab-ci.yml job выглядит как именованный блок:
lint:
script:
- echo "check code style"
test:
script:
- echo "run tests"
Здесь есть два jobs: lint и test. GitLab покажет их в интерфейсе как отдельные задачи. У каждого job будет свой статус и свой log.
Stage: крупный этап pipeline
Обычно jobs группируют по stages. Stage - это этап pipeline: сначала проверка, потом сборка, потом deploy.
stages:
- verify
- package
- deploy
lint:
stage: verify
script:
- echo "lint"
test:
stage: verify
script:
- echo "test"
build:
stage: package
script:
- echo "build"
GitLab выполняет jobs одного stage параллельно. Поэтому lint и test могут стартовать одновременно: они оба находятся в verify. А build начнётся только после успешного завершения всего stage verify.
Важно: место job внутри файла .gitlab-ci.yml почти не влияет на порядок выполнения. Job можно написать выше или ниже другого job, но GitLab всё равно будет смотреть на stage, needs, rules и другие настройки.
Например, этот вариант тоже корректный:
build:
stage: package
script:
- echo "build"
lint:
stage: verify
script:
- echo "lint"
test:
stage: verify
script:
- echo "test"
Хотя build написан первым, он не запустится раньше lint и test, потому что stage package идёт после stage verify. Поэтому при добавлении нового job думай не о том, куда именно вставить блок в файле, а о том, к какому stage он относится и должен ли он явно ждать другие jobs через needs.
Это первая идея параллельности: независимые проверки лучше класть в один stage. Если тесты и проверка кода не зависят друг от друга, им не нужно ждать друг друга.
Почему jobs не должны полагаться на общее состояние
Pipeline часто выглядит как один общий процесс, но на самом деле каждый job выполняется отдельно. GitLab организует pipeline, а команды из script выполняет runner.
В нашем курсе runner использует Docker executor. Это значит: для каждого job создаётся отдельный временный контейнер из image, указанного в .gitlab-ci.yml.
job lint -> контейнер из alpine:3.20 -> команды lint -> контейнер удалён
job test -> контейнер из alpine:3.20 -> команды test -> контейнер удалён
job build -> контейнер из docker:27 -> сборка image -> контейнер удалён
Контейнер job эфемерный: он живёт только во время выполнения job. Поэтому нельзя думать так: “в job build я создал файл, значит job test увидит его сам”. Не увидит. У соседнего job будет своё рабочее окружение.
Это касается не только Docker executor. У разных runner-ов есть свои особенности, но хороший pipeline не должен зависеть от случайно оставшихся файлов после предыдущего job.
Если результат одного job нужен следующему job, его нужно передать явно.
Как передавать результат дальше
Допустим, job build собрал папку dist, а job test-package должен проверить файлы из этой папки. Просто создать dist недостаточно: после завершения job контейнер исчезнет. Значит, результат нужно передать явно.
Один из основных способов такой передачи - artifacts: job сохраняет нужные файлы после выполнения, а следующие jobs могут их получить. Подробный синтаксис, срок хранения и cache разберём в следующей отдельной статье, а reports посмотрим позже как специальный вид результата, который GitLab умеет показывать в интерфейсе. Здесь важно запомнить только причину: jobs изолированы, поэтому важные результаты не должны жить “просто где-то в контейнере”.
В реальных pipeline результат можно передавать разными способами:
artifacts- файлы между jobs и stages;cache- ускорение повторных запусков, например зависимости;registry- Docker images, которые собрал pipeline;variables- параметры запуска;внешний сервис- например тестовый стенд, куда pipeline выложил приложение.
Когда stage недостаточно точный
Stages дают простой порядок: весь verify, потом весь package, потом весь deploy. Для маленького проекта этого хватает.
Но в большом pipeline не всегда нужно ждать весь предыдущий stage. Представь такую ситуацию:
build-imageсобрал Docker image;unit-testsдолго гоняют тесты;deploy-reviewдолжен выложить приложение на тестовый стенд сразу послеbuild-image;- ждать
unit-testsдля review-стенда команда не хочет, потому что стенд нужен быстро.
Тогда используется needs: job явно говорит, какие jobs ему нужны.
stages:
- package
- test
- deploy
build-image:
stage: package
script:
- ./build-image.sh
unit-tests:
stage: test
script:
- ./run-tests.sh
deploy-review:
stage: deploy
needs:
- job: build-image
script:
- ./deploy-review.sh
Что мы здесь изменили:
build-imageсобирает image;unit-testsнаходится в следующем stage и может выполняться долго;deploy-reviewнаходится ещё позже, в stagedeploy;- но у
deploy-reviewестьneeds, где указан толькоbuild-image.
Без needs GitLab выполнял бы pipeline в обычном порядке: сначала весь stage package, потом весь stage test, потом stage deploy. Значит deploy-review ждал бы, пока завершится unit-tests.
С needs GitLab получает более точную зависимость: deploy-review нужен результат именно от build-image. Поэтому после успешного build-image job deploy-review может стартовать, даже если unit-tests ещё выполняется.
Короткая запись делает то же самое:
deploy-review:
stage: deploy
needs: ["build-image"]
script:
- ./deploy-review.sh
Такую форму удобно использовать, когда нужно просто сказать: “этот job зависит от вот этих jobs”. Если дальше понадобятся дополнительные настройки, например как передавать artifacts через needs, запись раскрывают в длинный вариант:
deploy-review:
stage: deploy
needs:
- job: build-image
artifacts: true
script:
- ./deploy-review.sh
Есть и обратный случай: needs: []. Он означает, что job не ждёт предыдущие stages и может стартовать сразу после создания pipeline:
fast-info:
stage: deploy
needs: []
script:
- echo "this job can start immediately"
Так делают редко и осознанно. Например, если job просто печатает информацию, проверяет конфигурацию или запускает независимую быструю задачу. Для обычного deploy чаще всё-таки нужно явно указать, после чего он имеет право стартовать.
В этой статье главное запомнить не синтаксис каждой настройки, а логику:
- pipeline состоит из jobs;
- stages задают грубый порядок;
- jobs одного stage могут выполняться параллельно;
- jobs обычно изолированы и не должны полагаться на файлы друг друга;
- результат передают явно;
needsпомогает описать точные зависимости и ускорить большой pipeline.
