Трендовые github проекты в нашем телеграм канале. Подпишись → Как подобрать среду выполнения для GitLab CI
GitLab хранит код, разбирает .gitlab-ci.yml, строит очередь и показывает статус пайплайна. Команды из job выполняет отдельный процесс — GitLab Runner. Это бинарный сервис, который периодически запрашивает работу у GitLab, получает одноразовый CI_JOB_TOKEN, забирает репозиторий и артефакты, запускает скрипты через выбранный executor, затем отправляет логи и результат.
У такой схемы есть важное свойство для закрытой инфраструктуры: соединение инициирует Runner. Серверу с runner обычно не требуется принимать входящие подключения из GitLab. Но безопасность и стоимость CI определяются тем, где именно исполняется job и кому доступна эта машина.
Сначала определить область видимости
Runner можно привязать к проекту, группе или всему GitLab-инстансу. Это решение влияет на изоляцию, использование ресурсов и права доступа к секретам.
Instance runner доступен проектам всего инстанса. Он удобен для повторяющихся задач: тестов, сборки контейнеров, публикации типовых артефактов. Такой пул позволяет не держать отдельную машину для каждого проекта. При этом ему нужны строгие ограничения: к нему потенциально попадёт код из большого числа репозиториев.
Group runner обслуживает группу и её подгруппы. Это практичный вариант для команды или направления, которым требуется общий набор образов, кэшей и правил доступа, но нет смысла ждать настройки от администратора инстанса.
Project runner предназначен для одного проекта. Его стоит выбирать для деплоя в production, задач с отдельными учётными данными, GPU и долгих сборок, которым требуется гарантированная ёмкость. Форки не наследуют runners родительского проекта, что уменьшает риск запуска недоверенного кода с доступом к его секретам.
Теги дополняют эту модель. Job с tags: [docker, linux] получит только runner, у которого есть оба тега. Для нетегированных job у runner должно быть явно разрешено выполнение таких задач. Полезно заранее разделить теги для сборки, тестов и деплоя: это делает маршрутизацию пайплайна наблюдаемой и не даёт опасной job случайно попасть на deploy-хост.
Shell: узкая специализация и высокий уровень доверия
Shell executor выполняет команды на хосте от имени пользователя gitlab-runner. Он прост в установке и хорошо подходит для контролируемого деплоя на конкретный сервер:
deploy:
tags: [deploy-runner]
script:
- systemctl --user restart my-app
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Цена простоты — отсутствие изоляции. Job видит файловую систему хоста, может оставить зависимости и рабочие каталоги, а также пересечься с другими проектами. Добавление пользователя runner в группу docker фактически открывает job доступ уровня root через Docker socket. Поэтому shell разумно выделять под один доверенный проект и понятную операцию, например деплой. Для общего пула сборок он создаёт слишком большую поверхность атаки.
Docker как базовый вариант
Docker executor запускает каждую job в отдельном контейнере из образа, указанного в image:. Вспомогательный контейнер получает исходники, работает с кэшем и артефактами; сервисы из services: позволяют поднять рядом PostgreSQL, Redis или другой компонент для тестов. После job контейнеры удаляются, поэтому следующая сборка получает чистую среду.
test:
tags: [docker-runner]
image: python:3.13-slim
services:
- postgres:17
variables:
POSTGRES_PASSWORD: test
script:
- pip install -r requirements.txt
- pytest
Ключевая эксплуатационная задача здесь — поддерживать образы с закреплёнными версиями инструментов и регулярно обновлять их. В config.toml полезно задать дефолтный образ, ограничение параллелизма и кэш. Контейнерная изоляция делает Docker хорошим выбором для тестов и обычных сборок, однако она не отменяет риски привилегированного режима.
Сборка образов внутри job требует отдельного решения. Docker-in-Docker обычно включает privileged = true; это удобно для изолированных параллельных сборок, но ослабляет защиту. Проброс /var/run/docker.sock даёт job полный контроль над Docker на хосте и также не подходит для кода с низким уровнем доверия. Для чувствительных pipelines лучше рассмотреть rootless BuildKit либо одноразовые машины.
Kubernetes: job в отдельном pod
Если Kubernetes уже используется в инфраструктуре, Kubernetes executor естественно продолжает эту модель. Для каждой job создаётся pod с основным контейнером, helper и сервисами. Контейнеры разделяют сетевое пространство pod, а ресурсы можно задавать через requests и limits.
build-fat-thing:
tags: [k8s]
variables:
KUBERNETES_CPU_REQUEST: "4"
KUBERNETES_MEMORY_REQUEST: "8Gi"
script:
- make build
Runner обычно разворачивают Helm chart в отдельном namespace и передают токен через Kubernetes Secret. Важно ограничить RBAC, namespace и допустимые образы, иначе CI получит больше прав в кластере, чем требуется. Кластерный autoscaler позволяет масштабировать ресурсы вместе с очередью job. Разворачивать Kubernetes только ради одного runner редко оправданно: поддержка control plane, сети и хранения будет дороже выигрыша.
Переменная нагрузка и одноразовые ВМ
Docker autoscaler создаёт виртуальные машины под нагрузку и удаляет простаивающие экземпляры. Для провайдеров поддерживаются механизмы наподобие AWS Auto Scaling Group, GCP Instance Group и Azure VMSS через плагины fleeting. На ВМ запускаются Docker job, а max_use_count = 1 позволяет уничтожать машину после одной задачи. Это особенно полезно для привилегированных сборок: состояние не переходит к следующему запуску.
При проектировании такого пула надо согласовать лимиты. Верхняя граница параллелизма равна max_instances × capacity_per_instance. Если эти параметры расходятся с concurrent, часть мощности будет простаивать или job начнут ждать в очереди. Нужен также тёплый резерв: несколько idle-инстансов уменьшают холодный старт, но увеличивают расходы.
Executor instance решает близкую задачу без контейнеров: скрипты выполняются прямо на эфемерной ВМ. Он подходит для сборок образов ОС, тестов, зависящих от хоста, и задач на специализированном железе. Для большинства прикладных тестов Docker executor проще в сопровождении.
Практичная схема выбора
Начните с Docker runner для изолированных тестов и сборок. Выделите отдельный project runner для production-деплоя и не смешивайте его с общим пулом. Подключайте Kubernetes executor, когда кластер уже есть и команда умеет управлять его политиками и ресурсами. При выраженных пиках нагрузки переносите тяжёлые или привилегированные job на autoscaler с одноразовыми ВМ.
Перед запуском проверьте четыре вещи: область видимости runner, теги job, доступные секреты и способ сборки контейнеров. Эти решения важнее конкретной команды установки: они определяют, кто сможет выполнить код на вашей инфраструктуре, сколько времени job проведёт в очереди и насколько легко будет расследовать инцидент.