
Почти все причастные к разработке сталкивались с CI, и я уверен, что большинство знает про существование раннеров, на которых крутится наш CI. Но, как показывает практика, далеко не все знают о том, что раннеры бывают разных видов и что под разные сценарии DevOps в вашей команде подготовил разные окружения. Да и сами DevOps иногда оказываются в ступоре, когда на техническом интервью их спрашивают, чем docker от docker autoscaler отличается. Поэтому я решил собрать весь основной материал в виде большой шпаргалки. Сегодня разберём: какие раннеры бывают, какие плюсы и минусы у каждого, где какой лучше использовать, ну и на какие грабли вы можете наткнуться при работе.
Данная статья написана для версии GitLab / GitLab Runner 18.x. GitLab постоянно подчищает хвосты в работе раннеров, поэтому будьте внимательны, на других версиях что-то может быть деприкейтед, а что-то просто не существует.
На чём держится CI в GitLab?
Сам GitLab — это большой босс, которому не до наших мелких задач по типу запуска пайплайнов, он занят хранением кода, парсингом .gitlab-ci.yml, созданием очередей и рисованием галочек и крестиков. Главное тут запомнить: GitLab сам ничего не делает. А вот настоящий трудяга тут — GitLab Runner — это просто один бинарь на Golang, который можно накатить куда душа пожелает: от кубера до мини-ПК у вас под столом.
Теперь предлагаю глянуть, как между собой общаются GitLab и Runner:
Регистрация — подписание оффера. Раннер с помощью токена представляется GitLab и дальше уже работает привязанным к инстансу, группе или проекту.
Пулинг — просим у босса новую задачу. Раннер — назойливый сотрудник, который долбит GitLab дефолтно раз в 3 секунды (
check_interval) с вопросом «есть работа?». И самое главное, что соединение всегда исходящее от раннера, нам не нужно открывать порты или что-то подобное, наш сотрудник может также спокойно жить в закрытом контуре.Джоба — получаем задачу от босса. GitLab даёт работу раннеру и вместе с ней одноразовый
CI_JOB_TOKEN, с помощью которого раннер клонирует репу и забирает артефакты.Исполнение — работник делает всё по ТЗ. Раннер выполняет скрипты через заданный executor и отдаёт обратно логи.
Чтобы сматчить джобу и раннер, GitLab использует теги:
build: tags: - docker - linux script: - make build
Такая джоба прилетит раннеру, который содержит оба тега. Если джоба без тегов, то её получит раннер, которому разрешено запускать нетегированные джобы.
Ну и главное, ради чего мы тут собрались, по сути. Любой раннер, по сути, описывается двумя характеристиками:
Область видимости (проект, группа, инстанс)
Какая среда исполнения используется (shell, docker, kubernetes и т. п.)
Предлагаю разобрать обе по очереди. Начнём с той, что попроще.
Кому виден раннер
Instance runners
Эти работяги видны всем проектам на конкретном инстансе. Создаются администратором. Очередь работает по принципу fair usage: джоба уходит на раннер с наименьшим числом уже работающих джоб, таким образом один жирный проект не съест все ресурсы, застопорив остальные.
Лучше всего подходит для однотипных задач: сборка образов, тестирование, шаблонный деплой. Отличная оптимизация относительно десятков простаивающих раннеров под каждый проект.
Group runners
Эти раннеры уже более выборочны, они принадлежат конкретной группе и всем её подгруппам. Создаются владельцем группы. Для очереди используется стандартный механизм FIFO (First-In-First-Out). Золотая середина, когда не хочется дёргать админа, чтобы он настраивал раннер, и при этом отдельный раннер на каждый проект кажется избыточным.
Project runners
Самые придирчивые работяги, готовы работать только с конкретными проектами. Создаются Maintainer. При этом уже существующий project-runner можно подключать к другим проектам, если при создании это не заблокировано. Также важно понимать, что форки не наследуют раннеры родителя в целях безопасности.
Эти раннеры идеальны для специфичных задач: деплой прода с кредами, GPU или нужна стабильность без вероятности, что кто-то ещё будет пользоваться этими же ресурсами.
GitLab-hosted runners: когда лень — это стратегия
Свои раннеры — это, конечно, круто, но не все хотят этим заниматься, да и не каждому это нужно. Для этого GitLab.com предоставляет свои раннеры проектам, которые в нём живут. В результате каждая джоба получит свою эфемерную виртуалку, которая после завершения будет уничтожена. Эталонная изоляция, к которой должен стремиться self-hosted.
Однако бесплатный сыр только в мышеловке. Поэтому раннеры можно использовать не бесконечно. Мы платим за них compute minutes. Формула очень проста: минуты использования умножаем на cost factor раннера. После завершения потраченные compute minutes списываются из квоты namespace. Лимиты:
Free — 400 минут в месяц.
Premium — 10 000 минут в месяц.
Ultimate — 50 000 минут в месяц. При этом даже если мы держим проект на GitLab.com, свои раннеры не сжигают минуты, поэтому тяжёлые сборки лучше всего переносить на свои ресурсы.
GitLab предлагает несколько стандартных тегов:
Тег |
vCPU |
RAM |
Диск |
Тир |
Cost factor |
|---|---|---|---|---|---|
|
2 |
8 GB |
30 GB |
Free+ |
1 |
|
4 |
16 GB |
50 GB |
Free+ |
2 |
|
8 |
32 GB |
100 GB |
Premium/Ultimate |
3 |
|
16 |
64 GB |
200 GB |
Premium/Ultimate |
6 |
|
32 |
128 GB |
200 GB |
Premium/Ultimate |
12 |
|
2 |
8 GB |
30 GB |
Free+ |
1 |
|
4 |
16 GB |
50 GB |
Premium/Ultimate |
2 |
|
8 |
32 GB |
100 GB |
Premium/Ultimate |
3 |
|
4 |
15 GB |
50 GB + Tesla T4 (16 GB) |
Premium/Ultimate |
7 |
|
2 |
7.5 GB |
75 GB |
Free+ |
1 |
|
4 |
8 GB |
50 GB |
Premium/Ultimate |
6 |
|
6 |
16 GB |
50 GB |
Premium/Ultimate |
12 |
Здесь есть теги на любой вкус — от GPU до macOS.
Executor — где крутится джоба
Executor — это среда, в которой будет отрабатывать наша джоба. Сейчас полный список выглядит так:
Executor |
Изоляция |
Автоскейлинг |
Статус |
|---|---|---|---|
shell |
нет |
нет |
maintenance |
docker |
контейнер |
нет |
активно развивается |
docker autoscaler |
контейнер на эфемерной ВМ |
да |
активно развивается |
instance |
ВМ |
да |
активно развивается |
kubernetes |
под |
да, кластером |
активно развивается |
virtualbox / parallels |
ВМ |
нет |
maintenance |
ssh |
нет |
нет |
maintenance |
custom |
какую напишете |
нет |
maintenance |
Maintenance здесь указаны как в официальной документации. На них прилетают только фиксы по безопасности, нового функционала не планируется. Ну и король этого некролога — docker+machine — автоскейлер, который сам GitLab активно использовал, но с 17.5 признан deprecated, а в GitLab 20.0 будет полностью удалён.
Теперь пройдёмся по каждому.
shell: просто, но опасно
Самый простой работяга. Скрипты работают прямо на машине, от лица пользователя gitlab-runner. Никакой изоляции или чистой среды.
Первые грабли — раннер не ставит зависимости, всё надо преднастраивать самому, нет make — джоба fail. Типовой пример использования:
deploy: stage: deploy tags: [deploy-runner] script: - systemctl --user restart my-app environment: production rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Здесь мы деплоим прямо на сервер, на котором стоит наш раннер. Сам GitLab официально заявляет, что такой вариант небезопасен, так как в ходе работы можно получить доступ к файлам хоста, к рабочим директориям других проектов, к секретам с этого же хоста. Также много мусорит, а за собой не убирает. А если вы ещё добавили пользователя gitlab-runner в группу docker, чтобы развернуть всё на хосте в контейнерах, то через docker-сокет джоба получает root.
Однако несмотря на всё это, сценарии использования есть, и они вполне имеют место быть: деплой на конкретный хост, тачка под конкретный проект.
Ну и предлагаю в этом и в последующих разделах взглянуть на создание самого раннера. За последние несколько лет этот процесс поменялся. Раньше (до 2023) был
registration token — общий секрет группы или проекта, который при создании раннера вписывался в его переменные. Теперь подход другой: прямо через UI GitLab мы создаём раннер, указываем теги и настройки и в результате получаем authentication token с префиксом glrt-. Этот токен также можно скормить нескольким тачкам, и каждая станет отдельной нодой одного логического раннера.
Процесс установки:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash sudo apt install gitlab-runner
В результате будет создан пользователь gitlab-runner и systemd-сервис. Дальше регистрируем раннер через
sudo gitlab-runner register
Для shell executor это будет выглядеть примерно так:
sudo gitlab-runner register \ --non-interactive \ --url "https://gitlab.example.com/" \ --token "glrt-XXXX" \ --executor "shell" \ --description "shell-runner"
Ну и тривиальный config.toml:
concurrent = 1 [[runners]] name = "deploy-runner" url = "https://gitlab.example.com/" token = "glrt-XXXX" executor = "shell"
docker: хороший дефолт
В 90% случаев начинать лучше именно с него. Алгоритм работы прост: на каждую джобу создаём отдельный контейнер из образа, указанного в описании image:, при этом все вспомогательные задачи выполняет helper-container, он отвечает за clone, кэш и артефакты. Помимо основного контейнера, мы можем поднять дополнительные через директиву services:, это может быть какая-нибудь БД для тестов, например. После завершения джобы всё сносится, и следующая начинается с чистого листа. Также большой плюс — достаточно указать правильный image, который будет включать все нужные зависимости, таким образом получаем универсальный executor.
Примеры настройки:
concurrent = 4 [[runners]] name = "docker-runner" url = "https://gitlab.com/" token = "glrt-XXXX" executor = "docker" [runners.docker] image = "alpine:3.22" # дефолтный образ, если джоба не указала свой privileged = false pull_policy = "always" volumes = ["/cache"]
Пример использования:
test: tags: [docker-runner] image: python:3.13-slim services: - postgres:17 variables: POSTGRES_PASSWORD: test script: - pip install -r requirements.txt - pytest
Ещё один важный момент — сборка docker-образов внутри docker executor. Тут у нас 2 пути:
docker-in-docker (dind). Здесь мы прописываем в
config.tomlстрокуprivileged = true, тем самым мы отключили все механизмы безопасности, но при этом конкурентные сборки никак друг другу не мешают.проброс docker.sock. Здесь мы не выдаём привилегии, но прописываем проброс: для этого мы маппим
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]вconfig.toml. Таким образом, мы выдали полного рута для джобы, зато у нас общий кэш и без privileged. Также здесь есть конкуренция за имена среди всех контейнеров раннера. Ну и если прям паранойя по этому поводу и боишься, что кто-то пролезет в твой раннер, то rootless-сборщики вроде BuildKit приходят на помощь.
kubernetes: под на каждую джобу
Тут с выбором всё очень просто: если у вас уже есть Kubernetes, то просто используйте его. На каждую джобу будет создаваться отдельный под по принципу docker executor: основной + helper + service, все три будут иметь общий localhost в рамках пода. Гибкость за счёт autoscaler самого кластера.
Ставится через Helm-чарт:
helm repo add gitlab https://charts.gitlab.io kubectl create ns gitlab-runner kubectl -n gitlab-runner create secret generic runner-token \ --from-literal=runner-token="glrt-XXXX" helm install gitlab-runner gitlab/gitlab-runner -n gitlab-runner -f values.yaml
Минимальный рабочий values.yaml:
gitlabUrl: https://gitlab.example.com runners: secret: runner-token # тот самый secret с glrt-токеном config: | [[runners]] [runners.kubernetes] namespace = "gitlab-runner" image = "alpine:3.22" privileged = false cpu_request = "500m" memory_request = "512Mi" cpu_limit = "2" memory_limit = "4Gi" concurrent: 10 checkInterval: 30 rbac: create: true
Также хочу отметить, что лимиты на ресурсы можно передавать прямо из джобы:
build-fat-thing: tags: [k8s] variables: KUBERNETES_CPU_REQUEST: "4" KUBERNETES_MEMORY_REQUEST: "8Gi" script: - make build
Если у вас нет Kubernetes, то ради одного раннера это оверхед, но если уже используете, то отличное решение, но также будьте внимательны с dind.
docker autoscaler: облако под нагрузку
Если днём раннеры не успевают передохнуть от нагрузки, а ночью просто простаивают, то для этого есть docker autoscaler. Виртуалка сама будет создаваться в облаке, а при простое будет утилизироваться. Под капотом используется fleeting, плагинная абстракция над AWS ASG / GCP Instance Group / Azure VMSS. Официальная альтернатива великому docker+machine.
На AWS алгоритм будет следующий:
Создаём Auto Scaling Group с образом с предустановленным Docker и без своих политик скейлинга, так как это работа раннера
Ставим плагин:
gitlab-runner fleeting installГотовим конфиг
concurrent = 10 [[runners]] name = "docker-autoscaler" url = "https://gitlab.com" token = "glrt-XXXX" executor = "docker-autoscaler" [runners.docker] image = "alpine:3.22" [runners.autoscaler] plugin = "aws" capacity_per_instance = 1 # джоб на одну ВМ max_use_count = 1 # сколько джоб живёт ВМ; 1 = одноразовая max_instances = 10 [runners.autoscaler.plugin_config] name = "my-runner-asg" # имя ASG [runners.autoscaler.connector_config] username = "ec2-user" [[runners.autoscaler.policy]] idle_count = 2 # тёплый резерв idle_time = "20m0s"
Главная формула: concurrent = max_instances × capacity_per_instance. Иначе мы получим либо простаивающие ВМ, либо очередь.
Здесь решена проблема dind, так как тачка всё равно умрёт после завершения, а значит, прятать просто нет смысла.
Из минусов: самая сложная настройка и холодный старт хоста, так как первая джоба будет ждать, пока облако поднимет тачку.
instance: автоскейлящийся shell
Docker autoscaler, но без контейнеров, всё крутится прямо на ВМ. В конфиге просто заменяем executor на instance и убираем секцию [runners.docker]. Имеет место быть, например, когда собираем образы ОС или запускаем тесты, которые гоняют само железо. Ну и самое интересное — iOS-сборки на парке EC2 Mac.
virtualbox, parallels, ssh, custom: краснокнижные
Этих ребят мы встречаем всё реже, люди их почти добили.
virtualbox / parallels — раннер клонирует базовую ВМ из снапшота и по ssh подключается к ней. Можно использовать любую ОС плюс изоляция.
[[runners]] executor = "virtualbox" [runners.virtualbox] base_name = "win2019" # имя базовой ВМ base_snapshot = "clean" # снапшот, с которого клонируем disable_snapshots = false
ssh — раннер по ssh ходит на хост и исполняет скрипты. Никакого кэша, а для артефактов на целевом хосте нужен обязательно gitlab-runner:
[[runners]] executor = "ssh" [runners.ssh] host = "legacy.example.com" port = "22" user = "ci" identity_file = "/etc/gitlab-runner/id_ed25519"
Имеет место быть только для хостов, на которые ну никак не поставить раннер, а деплоить надо прямо на этот хост (но есть же Ansible).
custom — конструктор для взрослых. Через скрипты жизненного цикла описываем, как будет работать раннер:
[[runners]] executor = "custom" builds_dir = "/builds" cache_dir = "/cache" [runners.custom] prepare_exec = "/opt/ci-driver/prepare.sh" # например, lxc launch run_exec = "/opt/ci-driver/run.sh" # lxc exec cleanup_exec = "/opt/ci-driver/cleanup.sh" # lxc delete
Так, например, делают джобы в LXD или QEMU — всё, к чему сам GitLab не осмелился притронуться.
Выводы:
В итоге мы получили отличные результаты:
hosted — когда лень, да и не особо надо, и да, это легитимная стратегия,
docker — дефолт для всего остального,
kubernetes — когда кластер уже есть,
docker autoscaler / instance — когда нагрузка скачет, а privileged-джобам нужны одноразовые машины,
shell/ssh — только деплой и только с пониманием рисков.
Ну а ребята по типу custom, virtualbox и parallels пусть останутся для полноты картины. Знать о них полезно, но применять маловероятно.
© 2026 ООО «МТ ФИНАНС»