Вечер DevOps инженера
Вечер DevOps инженера

Почти все причастные к разработке сталкивались с 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

saas-linux-small-amd64 (default)

2

8 GB

30 GB

Free+

1

saas-linux-medium-amd64

4

16 GB

50 GB

Free+

2

saas-linux-large-amd64

8

32 GB

100 GB

Premium/Ultimate

3

saas-linux-xlarge-amd64

16

64 GB

200 GB

Premium/Ultimate

6

saas-linux-2xlarge-amd64

32

128 GB

200 GB

Premium/Ultimate

12

saas-linux-small-arm64

2

8 GB

30 GB

Free+

1

saas-linux-medium-arm64

4

16 GB

50 GB

Premium/Ultimate

2

saas-linux-large-arm64

8

32 GB

100 GB

Premium/Ultimate

3

saas-linux-medium-amd64-gpu-standard

4

15 GB

50 GB + Tesla T4 (16 GB)

Premium/Ultimate

7

saas-windows-medium-amd64 (beta)

2

7.5 GB

75 GB

Free+

1

saas-macos-medium-m1 (beta)

4

8 GB

50 GB

Premium/Ultimate

6

saas-macos-large-m2pro (beta)

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 ООО «МТ ФИНАНС»

Комментарии (0)