Настройка GitLab CI/CD: понимаем принципы работы и запускаем первый pipeline

Все найденные мной русскоязычные гайды не дают базового понимания того, как это работает, по большому счету это просто инструкции по настройке, причем под какой-то конкретный продукт и кейс: .net, Java, Node JS, etc.
Целью этой статьи является детальное и схематичное описание того, как все это устроено. Главная задача — вооружить читателя фундаментальным пониманием, что конкретно ему требуется сделать в его конкретном случае. Помимо самой инструкции по настройке, это будет так же справочник для погружения в DevOps, охватывающий:
-
Максимально много функционала, которыми обладает Gitlab (общие абстракции актуальны и для его аналогов).
-
Инструменты которые нужны: Bash, Docker, Kubernetes и другие.
-
Общую теорию и практику с конкретными сценариями.
Материал разбит на 3 части.
-
[Вы здесь → ] Простейший пример
ci-cd -
[in progress 80%] Более зрелый CI/CD (выход в продакшн, self-hosted runner и работа с registry)
-
[todo] Горизонтальное масштабирование CI/CD (высоко-нагруженный продакшн)
Оглавление
-
В чем идея CI/CD?
-
Терминология
-
Простейший CI/CD (разработка, MVP)
-
Простой пример
.gitlab-ci.yml -
Схема работы простейшего CI/CD
-
Практика с простейшим шаблоном CI/CD
-
В чем идея CI/CD
Даже начинающему разработку интуитивно понятно что такое CI/CD и для чего оно нужно. Отправили коммиты с изменениями в репозиторий — нужно исходный код превратить в собранное приложение и развернуть его на dev/test/prod стенде. Можно зайти на VPS и все сделать вручную: git clone, запустить автотесты, сделать сборку и запустить собранное приложение, при этом старое запущенное приложение, нужно заменить новым билдом. Автоматизацию этого процесса и назвали CI/CD. В эту аббревиатуру навалили сразу несколько значений которые всегда делаются рядом друг с другом и часто эти действия трудно однозначно классифицировать.
Но я все же попробую классифицировать и достичь какой-то однозначности по терминологии.
Терминология
CI (continues integration, непрерывная интеграция) — действия необходимые для интеграции новых изменений в имеющуюся кодовую базу.
Continues integration включает в себя следующее:
-
объедение всех изменений в одну точку
-
установку зависимостей
-
статистические проверки кода (типизация, линтеры)
-
запуск тестов (Unit, интеграционные, e2e)
-
проверка работоспособности сборки (убедиться что сборка возможна)
-
сборка кодовой базы. Это может быть как сборка библиотеки в соответствующий формат (например, npm-модуль, PyPI-пакет, Go-модуль), так и сборка в конечный исполняемый формат (.jar, .whl, bundle, Docker-контейнер, .exe-файл и т. д.) Результат сборки принято называть артефактом.
-
загрузка артефакта в специализированное хранилище для дальнейшего использования и версионирования. Такие хранилища часто называют registry (npm registry, Docker Registry, PyPI) или hub (Docker Hub), а также repository manager (Nexus, Artifactory, GitHub Packages).
CD (continues delivery, непрерывная доставка) — действия необходимые для доставки артефакта в место назначения (сервер где он будет выполняться). На этом этапе определяется куда будет доставлен артефакт, на dev, test, pre-prod(stage), prod.
Continues delivery включает в себя следующее:
-
Получение артефакта на целевом сервере
CD (continues deployment, непрерывное развертывание) — Действия необходимые для запуска уже доставленного артефакта.
Continues deployment включает в себя следующее:
-
Запуск артефакта
-
Миграция БД (если наш артефакт ее использует)
-
Запуск e2e тестов. Именно тех чья функция заключается в том, что бы проверить целостность работы системы. Некоторые e2e могли быть запущены еще на этапе CI.
-
Тут может быть так же механизм отката к предыдущей версии.
Delivery и Deployment на практике часто смешивается в одно целое “доставили и сразу запустили”. А если приблизиться к реальности еще больше и отдалиться от академических определений — часто на практике все 3 части смешиваются в один большой конвеер который и называется единым словом CI/CD или CICD. Таким образом, на практике все может быть свалено в одну большую кучу и например запуск тестов или миграций можно встретить совсем не в той последовательности, которая встречается в учебниках и теоритических статьях.
Pipeline (пайплайн) — это цепочка всех шагов необходимых для реализации CI/CD, каждый шаг это stage (стадия, этап), внутри каждого stage запускаются jobs (джобы, задачи). Если один этап падает, дальнейшие шаги отменяются. Дословно с английского переводиться как “трубопровод”, соответственно для носителя языка это ассоциируется с каким-то промышленным трубопроводом, который построен с целью доставки чего-то из точки A в точку B.
Stage (стадия, этап) — группа конкретных действий (джобов) из которых состоит пайплайн.
job (джоба) — конкретное действие или скрипт.
Runner (раннер) GitLab — runner выполняет джобы в пайплайне. Runner умеет выполнять джобы параллельно друг другу, использовать разные экзекьютеры (executors). Раннер можно установить отдельно на вашу машину и тогда gitlab будет просить его выполнять все инструкции.
Executor (экзекьютер) — способ выполнения скриптов описанных в джобах, например может работать в docker или сразу на целевой машине в bash.
Все варианты экзекьютеров
Всего есть такие варианты:
-
Docker — Самый популярный, все джобы изолировано в одноразовых контейнерах.
-
Docker Machine (deprecated в пользу Autoscaler) — будет удален в 2027-ом.
-
Docker Autoscaler — Замена Docker Machine, автоматически управляет виртуальными машинами с Docker.
-
Custom — Для нестандартных случаев, когда какой-то самописный или малоизвестный механизм оркестрации. Полная кастомизация и контроль, но и все сопустсвующие подводные камни.
-
Instance — Каждая джоба, отдельная преднастроенная виртуалка.
-
Kubernetes — Джобы запускаются как поды в кластере. Можно использовать если уже настроен кластер k8s.
-
Shell — Простейшая реализация где джобы выполняются прям на хосте сервера. При таком подходе не будет никакой изоляции и легко заафектить что-то на сервере во время работы джоб.
-
SSH — Тоже как Shell, но через SSH подключение.
-
Parallels — Адаптация под Parallels Desktop for Mac, это коммерческий гипервизор позволяющий запускать Windows/Linux на маке. Короче решение для тех у кого слишком много лишних денег (возможно полезен тем кто специализируется на iOS/macOS разработке).
-
VirtualBox — Полная виртуализация для каждой джобы в виртуалках VirtualBox.
Более детально и схематично каждый термин будет описан дальше в статье.
Сколько есть специалистов и компаний, столько и разных реализацией CI/CD. Важно не просто следовать какому-то гайду, а делать осознанно именно то, что вам действительно требуется.
Простейший CI/CD (разработка, MVP)
В этом разделе рассмотрим конфигурацию и схему простой реализации CI/CD которая подойдет на этапе разработки или MVP.
Простой пример .gitlab-ci.yml
Все начинается с репозитория, а точнее с описанного в нем файла .gitlab-ci.yml (документация GitLab по gitlab-ci.yml). Этот файл содержит все инструкции, которые нужно выполнить для целей CI/CD.
Рассмотрим простейший пример, который GitLab предлагает в качестве стартового шаблона .gitlab-ci.yml:
Вместо реальных запусков тестов используется команда echo, которая просто выводит сообщение в консоль, а также команда sleep, которая создает искусственную задержку.
stages: # Список stages (стадий) для jobs и порядок их выполнения - build - test - deploybuild-job: # Джоба build, выполняемая в первую очередь stage: build script: - echo "Compiling the code..." - echo "Compile complete."unit-test-job: # Запуск тестов stage: test # Запустится только если build прошел успешно script: - echo "Имитация запуска unit-тестов... с задержкой 60 секунд" - sleep 60 - echo "Покрытие кода в районе 90%"lint-test-job: # Еще одна джоба, которая относится к стадии "test" stage: test # Так как она тоже относится к стадии "test", будет запущена параллельно script: - echo "Проверка кода линтером... Задержка 10 секунд." - sleep 10 - echo "Нет ошибок линтера."deploy-job: # Само развертывание (deploy) stage: deploy # Запустится только если все джобы из стадии "test" завершились успешно environment: production script: - echo "Развертывание приложения..." - echo "Приложение развернуто."
Когда GitLab отслеживает изменение в репозитории, он передает инструкции из .gitlab-ci.yml раннеру. Раннер, в свою очередь, использует указанный executor для выполнения команд. Подробнее о поддерживаемых executors — в документации GitLab.
Runner (раннер) GitLab — это программа, которая выполняет инструкции в указанном окружении. Executor определяет, в каком именно окружении будут выполнены команды. Так как в примере используются только базовые утилиты echo и sleep, подойдет практически любой executor:
-
Docker — каждая джоба запускается в изолированном Docker-контейнере на основе указанного образа.
-
Shell — команды выполняются напрямую в оболочке хост-системы, где установлен раннер.
-
Kubernetes — джобы выполняются в подах кластера Kubernetes.
-
SSH — команды выполняются на удаленной машине через SSH-соединение.
Для большинства реальных проектов рекомендуется использовать Docker, так как он обеспечивает изоляцию и воспроизводимость окружения.
Если вы используете не Self Hosted Gitlab (который поднят в вашей инфраструктуре), а GitLab.com (SaaS-инстанс) по умолчанию будет использован shared runners, который в стандартной конфигурации использует Docker executor — каждая джоба запускается в изолированном контейнере.
Схема работы простейшего CI/CD
Схема на примере когда:
-
Используется gitlab.com (не self hosted)
-
Runner установлен на вашем сервере
-
Используется docker executor
-
Используется вышеописанный шаблон
.gitlab-ci.yml(4 джобы)

Если build-job создает какой-то артефакт, его можно расположить в каком-то хранилище. Если довольствоваться самой простой и быстрой реализацией, можно исключить работу с registry (хранилищем артефактов). Часто на начальных этапах разработки так и поступают, вместо дополнительных действий с registry происходит следующее: раннер поднимает контейнер в котором происходит подключение по SSH к целевому серверу, на целевом сервере уже установлен docker и git, по этому в рамках подключения SSH достаточно сделать git pull, docker run, а старый контейнер просто гасят. Это работает когда в репозитории разработчика есть Dockerfile который делает все необходимое для запуска приложения: установка зависимостей, сборка и запуск.
Но этот сценарий имеет сразу множество недостатков и подходит для этапа разработки или MVP, но не для поддержки серьезного продакшена. Недостатки такого подхода по пунктам:
-
Downtime — нужно положить старый контейнер, поднять новый, приложение недоступно пока происходит сборка нового контейнера.
-
Сборка нагружает production сервер
-
Нет истории артефактов, нет возможности откатиться к прошлому артефакту, если с новым что-то пошло не так.
-
Не подходит для горизонтального масштабирования.
-
Уязвимая инфраструктура — доступ к серверу прода попадает в gitlab (ssh ключи), а на сервере прода есть доступ к репозиторию.
-
Исходный код присутствует на сервере прода.
-
Docker хранит свои слои и кэш на сервере прода.
При этом у многих такой подход живет в проде и ничего страшного не случается (до поры до времени).
Практика с простейшим шаблоном CI/CD
Закоммитим и запушим данный .gitlab-ci.yml файл gitlab и посмотрим что произойдет.
При просмотре пайплайна, gitlab визуализирует наши stages и jobs. Всего у нас 4 джобы. 2 параллельных lint-test-job и unit-test-job они находятся в стадии test. Перейдем в unit-test-job и посмотрим что gitlab нам отобразит:
В .gitlab-ci.yml это описано так:
unit-test-job: # Запуск тестов stage: test # Запустится только если build прошел успешно script: - echo "Имитация запуска unit-тестов... с задержкой 60 секунд" - sleep 60 - echo "Покрытие кода в районе 90%"
Всего 3 команды, но в логах мы видим много всего. По этому разберем каждую строчку для лучшего понимания.
Running with gitlab-runner 18.10.0~pre.705.ge11dde90 (e11dde90) on green-4.saas-linux-small-amd64.runners-manager.gitlab.com/default ntHFEtyXQ, system ID: s_8990de21c550 feature flags: FF_USE_GIT_PROACTIVE_AUTH:true
Подробно
Running with gitlab-runner 18.10.0~pre.705.ge11dde90 (e11dde90)
Используется gitlab-runner версии 18.10.0 (pre-release сборка).
e11dde90 — Судя по всему хэш-коммита.
on green-4.saas-linux-small-amd64.runners-manager.gitlab.com/default ntHFEtyXQ
Имя хоста runner’а: green-4.saas-linux-small-amd64.runners-manager.gitlab.com:
green-4 — конкретная нода
saas-linux-small-amd64 — тип машины: SaaS (облако GitLab), Linux, small (малый размер ресурсов), архитектура amd64
ntHFEtyXQ — видимо просто ID раннера (но почему-то в UI мы увидим этот ID без последнего символа).
Исходя из всего этого складывается впечатление, что это shared runner от GitLab (не self-hosted). system ID: s_8990de21c550 — Уникальный системный идентификатор машины, на которой работает runner. Узнал из документации gitab API.
feature flags: FF_USE_GIT_PROACTIVE_AUTH:true — Определяет стратегию авторизации которую использует Gitaly. Gitaly — это программный модуль разработанный gitlab для удаленного управления репозиториями GIT.
Документация по Gitaly.
Документация гитлаб о feature flags.
Документация proactiveAuth GIT.
Нам сообщается версия runner’а который занимался выполнением джобы. Я не нашел прямой ссылки для перехода к нему, по этому просто подставил в url его номер, так мне удалось просмотреть информацию о нем.
Это один из множества раннеров который запустил gitlab для своих пользователей. Он выполняется где-то на серверах гитлаба. Позже мы настроим свой собственным раннер для своих задач.
Двигаемся по логам дальше и видим:
Preparing the "docker+machine" executorUsing default imageUsing Docker executor with image ruby:3.1 ...Using default imageUsing effective pull policy of [always] for container ruby:3.1Pulling docker image ruby:3.1 ...Using docker image sha256:9981... for ruby:3.1 with digest ruby@sha256:9162...
Подробно
Preparing the "docker+machine" executor
Тип executor’а docker+machine — Он кстати deprecated уже года 4 и gitlab выбрал другое направление развития — Autoscaler. Странно почему до сих пор используется в shared runners. Подробнее docker+machine. В двух словах этот экзекьютор умеет поднимать полноценные виртуальные машины, а внутри уже запускать docker. Зачем гитлабу виртуализация, если есть контейнеризация? Коротко — безопасность, фикс проблемы известной под именем container escape.
Using default imageUsing Docker executor with image ruby:3.1 ...
В .gitlab-ci.yml не указано какой image докера использовать, по этому используется образ по умолчанию из настроек раннера. В UI к сожалению не выводиться информация о конфигурации раннера, она лежит в конфигурационных файлах формата toml и скрыта от нас.
Using effective pull policy of [always] for container ruby:3.1
Политика скачивания образа always — то есть всегда скачивать, даже если есть локально.
Pulling docker image ruby:3.1Using docker image sha256:9981... for ruby:3.1 with digest ruby@sha256:9162...
Информация об успешном скачивании docker образа ruby:3.1 из docker hub.
Нам говориться о подготовке экзекьютора и загрузке docker образа. Читаем дальше:
Preparing environmentUsing effective pull policy of [always] for container sha256:c11...Running on runner-nthfetyxq-project-79632759-concurrent-0 via runner-nthfetyxq-s-l-s-amd64-1775396763-e03183f3...Getting source from Git repositoryGitaly correlation ID: 73b6c9732c8240b7b5a1937a1eaac112Fetching changes with git depth set to 20...Initialized empty Git repository in /builds/webstap/self-server-config/.git/Created fresh repository.Checking out 3575359a as detached HEAD (ref is main)...Skipping Git submodules setup$ git remote set-url origin "${CI_REPOSITORY_URL}" || echo 'Not a git repository; skipping'
Подробно
Preparing environmentUsing effective pull policy of [always] for container sha256:c11...
Runner поднимает вспомогательный контейнер (это уже другой, не тот который ruby:3.1). Контейнер ruby:3.1 имеет другой хэш 9981, он предназначен для наших jobs. Контейнер о котором сейчас идет речь имеет другой хэш c11, он нужен раннеру для клонирования git репозитория, работой с кэшем/артефактами и всем тем что происходит до и после нашего пользовательского скрипта.
Running on runner-nthfetyxq-project-79632759-concurrent-0 via runner-nthfetyxq-s-l-s-amd64-1775396763-e03183f3...
Тут указывается 2 имени:
Первое имя — контейнер который будет запускаться.
Второе имя — виртуальная машина на которой будет запускаться контейнер, в ней есть Docker daemon за счет которого и запуститься контейнер.
Имя контейнера состоит из:
runner-nthfetyxq-project-79632759-concurrent-0 │ │ │ │ │ └─ Слот конкурентности (первый из N) │ └─ ID проекта в GitLab └─ Токен/ID раннера (короткий хеш)
Имя виртуальной машины состоит из:
via runner-nthfetyxq-s-l-s-amd64-1775396763-e03183f3 │ │ │ │ │ │ │ │ │ │ │ │ │ └─ ID инстанса (VM) │ │ │ │ │ └─ Похоже на Timestamp или порядковый номер │ │ │ │ └─ Архитектура: amd64 │ s-l-s = shared/linux/small (тип машины) └─ Токен/ID раннера (короткий хеш)
Gitaly correlation ID: 73b6c9732c8240b7b5a1937a1eaac112
Ранее мы уже говорили о Gitaly. Тут просто указан ID для трассировки. Простыми словами это конкретный ID операции, который позволит отыскать ее в логах.
Fetching changes with git depth set to 20...
Информация о том, что runner выполнит что-то вроде:
git initgit remote add <наш репозиторий>git fetch origin --depth 20
Иными словами, вместо загрузки всех коммитов и полной копии репозитория будут подтянуты последние 20 коммитов. Это нужно для экономии трафика и ускорение работы. Это значение можно конфигурировать через переменную GIT_DEPTH в .gitlab-ci.yml, подробнее о других конфигурационных переменных можно найти тут.
Initialized empty Git repository in /builds/webstap/self-server-config/.git/Created fresh repository.
Инициализирован пустой git репозиторий в файловой системе внутри контейнера.
Checking out 3575359a as detached HEAD (ref is main)...
Было выполнено git checkout 3575359a — переключение на конкретный коммит (не на ветку). Переключение именно на коммит важно в условиях CI — пока запускался пайплайн, в ветку могли прилететь сверху еще коммиты, но пайплайн предназначен строго для конкретного коммита, по этому раннер ориентируется строго по коммитам, а не по веткам.
Skipping Git submodules setup
Подмодули не настроены (нет GIT_SUBMODULE_STRATEGY в .gitlab-ci.yml). Подробнее.
git remote set-url origin "${CI_REPOSITORY_URL}" || echo 'Not a git repository; skipping'
CI_REPOSITORY_URL — predefined переменная. Содержит полный путь к репозиторию. Ее значение:
https://gitlab-ci-token:$CI_JOB_TOKEN@gitlab.example.com/my-group/my-project.git
Это команда перезаписывает URL remote origin, чтобы последующие git операции были авторизованы.
|| echo 'Not a git repository; skipping
Если не удалось выполнить git remote будет просто написано “Not a git repository; skipping”, это нужно что бы джоба не упала, на случай если работа не с git репозиторием.
GitLab Runner на виртуальной машине скачал два Docker-образа — ruby:3.1 для выполнения пользовательского кода и gitlab-runner-helper для служебных операций, затем helper-контейнер склонировал Git-репозиторий webstap/self-server-config (shallow clone, глубина 20 коммитов) в директорию /builds/webstap/self-server-config, переключился на конкретный коммит 3575359a из ветки main и подготовил рабочую среду для выполнения скриптов из .gitlab-ci.yml.
Осталось разобрать последнюю часть:
Executing "step_script" stage of the job scriptUsing default imageUsing effective pull policy of [always] for container ruby:3.1Using docker image sha256:9981... for ruby:3.1 with digest ruby@sha256:916... ...$ echo "Имитация запуска unit-тестов... с задержкой 60 секунд"Имитация запуска unit-тестов... с задержкой 60 секунд$ sleep 60$ echo "Покрытие кода в районе 90%"Покрытие кода в районе 90%Cleaning up project directory and file based variablesJob succeeded
Подробно
Using default imageUsing effective pull policy of [always] for container ruby:3.1Using docker image sha256:9981... for ruby:3.1 with digest ruby@sha256:916... ...
Подготовка контейнера для скрипта. Раннер поднимает новый контейнер из образа ruby:3.1, в котором будет выполняться наш скрипт из .gitlab-ci.yml.
$ echo "Имитация запуска unit-тестов... с задержкой 60 секунд"
Далее мы видим строку которая начинается с $ — гитлаб показывает саму команду, которую он запустит в контейнере. А затем stdout это команды (результат ее выполнения):
Имитация запуска unit-тестов... с задержкой 60 секунд
Аналогично далее:
$ sleep 60$ echo "Покрытие кода в районе 90%"Покрытие кода в районе 90%
Завершение:
Cleaning up project directory and file based variablesJob succeeded
“Cleaning up project directory” — Удаляются файлы проекта из рабочей директории /builds/webstap/self-server-config/.
“file based variables” — Удаляются временные файлы, созданные из CI/CD переменных типа File (для безопасности — чтобы секреты не остались на диске).
Job succeeded
Все команды вернули exit code 0 — то есть выполнены без ошибок.
Выполнен блок script из .gitlab-ci.yml джобы unit-test-job:
unit-test-job: # Запуск тестов stage: test # Запустится только если build прошел успешно script: - echo "Имитация запуска unit-тестов... с задержкой 60 секунд" - sleep 60 - echo "Покрытие кода в районе 90%"
Ранее мы говорили об артефактах, в результате выполнения пайплайнов мы не создавали артефакты в скриптах наших джобов, но все же некоторые файлы были созданы и размещены в хранилище артефактов gitlab. Речь идет о файлах с логами. Просмотреть их можно если перейти в Build → Artifacts.
Мы рассмотрели как себя ведет шаблон CI-CD предложенный gitlab. Данную реализацию можно доработать таким образом, что одна из наших джобов подключалась через SSH к нашему VPS, заходила в определенную директорию, обновляла git репозиторий и поднимала контейнер. Но как я уже писал: у этого есть ряд недостатков.
С вами был Тимофей. Кто я?
Разрабатываю с 2015 года. Стартовал как front-end разработчик на React, после 6-лет переключился на full-stack, последние годы — чаще DevOps. Мой публичный WakaTime.
Спасибо, что дочитали! Тем, кто уже подписан на мой телеграм-канал — отдельная благодарность, это мотивирует делиться опытом дальше. Остальным — заходите, помимо технических статей поднимаю и социально важные темы.
ссылка на оригинал статьи https://habr.com/ru/articles/1031452/