GitLab Runner: shared, specific и executor'ы

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

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

Зачем нужен отдельный агент

Сервер GitLab — это веб-приложение; запускать на нём чужой код было бы небезопасно и тяжело. Поэтому исполнение вынесено в раннеры — лёгкие агенты, которых можно поставить хоть на ноутбук, хоть на кластер из сотни машин. Раннер периодически спрашивает сервер: «есть работа для меня?» — и, получив джобу, выполняет её.

За этой архитектурой стоит фундаментальное разделение ответственности. Сервер занимается тем, в чём он силён: хранит код в git-репозиториях, рисует интерфейс, ведёт очередь джоб, считает права доступа и складывает логи. А вот тяжёлую и потенциально опасную работу — компиляцию, запуск тестов, выполнение скриптов из репозитория любого пользователя — он не трогает. Если бы сервер исполнял присланный в коммите код прямо у себя, один вредоносный .gitlab-ci.yml мог бы прочитать секреты соседних проектов или положить весь инстанс. Раннер же изолирован: он живёт на отдельной машине, общается с сервером только по защищённому каналу и получает ровно ту работу, которую ему разрешено выполнять.

Важная деталь: связь инициирует раннер, а не сервер. Раннер сам, изнутри своей сети, открывает исходящее соединение и спрашивает: «есть для меня джоба?» (это называют long polling). Благодаря этому раннер можно поставить за NAT или внутри закрытого периметра компании, не открывая на нём ни одного входящего порта — серверу не нужно «дозваниваться» до агента. Такая модель радикально упрощает безопасность: вам не приходится пробивать дыры в корпоративном фаерволе ради CI. Один сервер при этом обслуживает сколько угодно раннеров, и наоборот — один раннер может обслуживать несколько проектов, если ему это разрешено.

Shared и specific раннеры

На GitLab.com есть shared runners — общие раннеры, предоставленные платформой. Вы пишете .gitlab-ci.yml, и джобы крутятся «из коробки» на инфраструктуре GitLab (с лимитом минут на бесплатном тарифе). Это самый быстрый старт.

Specific (project/group) runners — раннеры, которые вы регистрируете сами на своих серверах и привязываете к проекту или группе. Их выбирают, когда нужна особая среда (свои зависимости, мощное железо, доступ во внутреннюю сеть, GPU) или хочется сэкономить на минутах shared-раннеров.

На практике выбор между общим и собственным раннером — это компромисс между удобством и контролем. Shared-раннеры не требуют обслуживания: GitLab сам их патчит, масштабирует и гарантирует чистое окружение. Но вы платите за это минутами, не управляете версиями софта на хосте и не можете дотянуться из джобы до приватной базы данных в вашем VPC. Собственный раннер снимает эти ограничения, но взамен вы отвечаете за его обновления, безопасность и доступность: если ваш единственный раннер упал ночью, утром у всей команды «висят» пайплайны. Поэтому в зрелых командах раннеры держат пулом — несколько одинаковых агентов за одним тегом, чтобы падение одного не останавливало конвейер.

Есть и третий, промежуточный уровень — group runners, привязанные не к одному проекту, а к целой группе. Это удобно, когда десятки репозиториев одной команды должны делить общий мощный раннер: регистрируете его один раз на уровне группы, и все вложенные проекты получают к нему доступ без дублирования настройки. Так выстраивают иерархию: shared для черновых экспериментов, group для повседневной работы команды, project — для проектов с особыми требованиями к секретам или железу.

Executor — как именно выполняется джоба

Раннер сам по себе только координирует; реальное выполнение делегируется executor'у. Самые ходовые:

ExecutorГде выполняется джобаИзоляция
dockerв свежем контейнере из образа image:высокая, чистое окружение каждый раз
shellпрямо в оболочке хоста раннеранизкая, состояние накапливается
kubernetesв эфемерном поде кластеравысокая, авто-масштабирование

Executor docker — самый популярный: каждая джоба получает чистый контейнер из указанного образа, поэтому окружение предсказуемо и повторяемо. shell прост, но состояние между джобами накапливается, что приводит к «работает у меня, падает на CI». kubernetes хорош для больших нагрузок: под на каждую джобу, автоскейл.

Конкурентность: сколько джоб тянет один раннер

Раннер — это не «одна джоба за раз». У него есть параметр concurrent: сколько джоб он готов выполнять одновременно. Раннер с docker-executor и значением concurrent = 4 поднимет до четырёх контейнеров параллельно, и четыре джобы из разных пайплайнов будут крутиться бок о бок на одной машине. Это объясняет, почему параллельные джобы в стадии исполняются действительно параллельно: либо их разбирают несколько раннеров, либо один раннер с запасом конкурентности. Если же все слоты заняты, лишние джобы честно ждут в очереди в статусе pending — это нормально, а не поломка.

Отсюда практический вывод: производительность CI упирается не в один большой сервер, а в сумму мощностей раннеров. Когда пайплайны стали медленными и всё дольше висят в очереди, первое, что стоит проверить, — хватает ли свободных слотов на раннерах, а не «тормозит ли GitLab».

Сравнение с GitHub Actions

В GitHub Actions раннеры тоже бывают hosted (общие) и self-hosted (свои), но понятие executor скрыто: hosted-раннер — это виртуалка с предустановленным софтом. В GitLab executor выбирается явно при настройке раннера, что даёт больше контроля над тем, как именно изолируется джоба.

Разница глубже, чем просто терминология. В GitHub Actions джоба по умолчанию выполняется прямо на виртуальной машине-раннере, а контейнеры — это опция (через container: или отдельные service-контейнеры). В GitLab всё ровно наоборот для самого ходового сценария: docker-executor делает контейнер основным местом выполнения, а указанный в джобе image: — это и есть окружение скрипта. Иными словами, в GitHub «контейнер внутри ВМ», а в GitLab с docker-executor — «контейнер и есть рабочая среда». Это меняет интуицию: в GitLab вы почти всегда думаете в терминах образов, а слой ВМ остаётся невидимым. Зато GitHub из коробки даёт богатые предустановленные образы раннеров (с node, python, браузерами), тогда как в GitLab вы сами выбираете базовый образ и доустанавливаете нужное — больше явного контроля ценой пары лишних строк в before_script.

Как работает под капотом

Раннер с docker-executor'ом на каждую джобу делает примерно следующее: запрашивает у сервера задачу и её image, поднимает контейнер из этого образа, клонирует репозиторий нужного коммита внутрь, прокидывает переменные окружения, выполняет команды before_script и script, собирает артефакты и кеш, затем удаляет контейнер. Чистое окружение каждый раз — ключевое свойство, дающее воспроизводимость.

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

  • Удивляться, что файлы между джобами не сохраняются. С docker-executor каждая джоба — новый контейнер; передавать файлы нужно через artifacts или cache.
  • Использовать shell-executor в проде и ловить «грязное» состояние от предыдущих запусков.
  • Забыть, что у shared-раннеров на бесплатном тарифе ограниченное число минут.

Итоги

  • Раннер — агент, который забирает джобы из очереди и выполняет их.
  • Shared — общие от платформы, specific — ваши собственные для особых сред.
  • Executor (docker/shell/kubernetes) определяет, где и насколько изолированно выполняется джоба.
Проверьте себя
1. Почему с docker-executor файлы не сохраняются между джобами автоматически?
AGitLab запрещает запись на диск
BКаждая джоба выполняется в новом чистом контейнере, который удаляется после завершения
CРаннер работает только в памяти
DФайлы всегда удаляются вручную
2. Когда стоит зарегистрировать specific (свой) раннер вместо shared?
AНикогда, shared всегда лучше
BКогда нужна особая среда, мощное железо или доступ во внутреннюю сеть
CТолько для деплоя на production
DКогда в проекте больше пяти джоб