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) определяет, где и насколько изолированно выполняется джоба.