
Все найденные мной русскоязычные гайды не дают базового понимания того, как это работает, по большому счету это просто инструкции по настройке, причем под какой-то конкретный продукт и кейс: .net, Java, Node JS, etc.
Целью этой статьи является детальное и схематичное описание того, как все это устроено. Главная задача — вооружить читателя фундаментальным пониманием, что конкретно ему требуется сделать в его конкретном случае. Помимо самой инструкции по настройке, это будет так же справочник для погружения в DevOps, охватывающий:
Максимально много функционала, которыми обладает Gitlab (общие абстракции актуальны и для его аналогов).
Инструменты которые нужны: Bash, Docker, Kubernetes и другие.
Общую теорию и практику с конкретными сценариями.
Материал разбит на 3 части.
[Вы здесь → ] Настройка GitLab CI/CD: понимаем принципы работы и запускаем первый pipeline
[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 (continuous integration, непрерывная интеграция) — действия необходимые для интеграции новых изменений в имеющуюся кодовую базу.
Continuous 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 (continuous delivery, непрерывная доставка) — действия необходимые для доставки артефакта в место назначения (сервер где он будет выполняться). На этом этапе определяется куда будет доставлен артефакт, на dev, test, pre-prod(stage), prod.
Continuous delivery включает в себя следующее:
Получение артефакта на целевом сервере
CD (continuous deployment, непрерывное развертывание) — Действия необходимые для запуска уже доставленного артефакта.
Continuous 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 - deploy build-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" executor Using default image Using Docker executor with image ruby:3.1 ... Using default image Using effective pull policy of [always] for container ruby:3.1 Pulling 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 image Using 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.1 Using docker image sha256:9981... for ruby:3.1 with digest ruby@sha256:9162...
Информация об успешном скачивании docker образа ruby:3.1 из docker hub.
Нам говориться о подготовке экзекьютора и загрузке docker образа. Читаем дальше:
Preparing environment Using 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 repository Gitaly correlation ID: 73b6c9732c8240b7b5a1937a1eaac112 Fetching 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 environment Using 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 init git 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 script Using default image Using effective pull policy of [always] for container ruby:3.1 Using 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 variables Job succeeded
Подробно
Using default image Using effective pull policy of [always] for container ruby:3.1 Using 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 variables Job 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.
Спасибо, что дочитали! Тем, кто уже подписан на мой телеграм-канал — отдельная благодарность, это мотивирует делиться опытом дальше. Остальным — заходите, помимо технических статей поднимаю и социально важные темы.
Комментарии (7)

SHUstri
04.05.2026 23:402026й год.... похоже действительно пора уже поднимать первый CI\CD пайплайн и переименовываться из сисадмина в девопса, а тот как будто ЙТ ушло немного вперёд...
gtosss Автор
Продвигать технические статьи конкурируя с корпоративными блогами у которых на старте 10-15 плюсов трудно. Если материал был полезен не стесняйтесь плюсовать.
casssuzy
Вот да, прям в точку. Я как раз сейчас с этим столкнулся и не понимал, почему так сложно раскачать статью на старте. Не понимал почему у двух одинаково хороших статей, такая разная статистика.
house2008
А зачем вам лайки у статьи ? У вас была какая-то проблема боль, вы ее решили и поделились с сообществом. Когда люди выкладывают код на гитхаб большинство этого делают не ради звезд, а просто безвозмездный дух опенсорс.
IamNoMad
Объективно это нужно нам, тем кому интересен такого рода контент. Если не будет плюсов, вероятность того что мы это увидим крайне мала...
С кодом на гитхаб такая же проблема... Хороший код может лежать никем незамеченный очень долго, а то и вовсе быть невостребованным...