Всё было хорошо, пока не стало плохо. В какой‑то момент задачи в GitLab начали запускаться с задержкой в 5, 10, а иногда и 15 минут. Очередь в пайплайнах росла, DevOps нервничал, разработчики возмущались, а AWS молча выставлял счёт за сотни часов работы EC2-инстансов.

Вариант «давайте добавим ещё пару EC2» помогал ненадолго. Через какое‑то время мы снова получали те же симптомы: простаивающие (idle-) инстансы, лишние расходы и никакой нормальной изоляции задач. В итоге стало понятно, что латать это бесконечно бессмысленно, нужно настроить полноценное автомасштабирование GitLab Runner’ов.

Меня зовут Тимур Низамутдинов, я DevOps-инженер в DaaS‑подразделении «Фланта», которое поддерживает инфраструктуру разных компаний и помогает им внедрять DevOps-подходы. В этой статье я покажу, как можно отказаться от «вечно живущих» EC2-инстансов, настроить масштабируемые GitLab Runner’ы в AWS и при этом заметно сократить расходы на CI-инфраструктуру.

Наши цели

Мы хотим прийти к такому подходу, который одновременно:

  • масштабирует CI под меняющуюся нагрузку;

  • снижает расходы на AWS.

Вместо того чтобы держать раннеры на постоянно работающих EC2 («always-on»), мы можем поднимать и��стансы только под конкретные задачи (Job) или их группы. Задача пришла — создаётся EC2. Задача закончилась — инстанс либо берёт следующую из очереди либо автоматически останавливается, если работы больше нет.

Из «вечных» EC2 у нас остаётся только один управляющий инстанс с GitLab Runner, который занимается запуском и оркестрацией задач. Можно сделать его достаточно скромным по ресурсам, чтобы содержание почти ничего не стоило по сравнению с production-нагрузкой.

Итак, наши цели:

  • автоматическое создание EC2-инстансов при появлении задач в GitLab CI;

  • остановка или удаление инстанса, если он простоял без работы N минут;

  • обеспечение изоляции задач: одна виртуальная машина = одна задача или небольшой пул задач;

  • поддержка автоматизации сборки Amazon Machine Image (AMI) для быстрого создания одинаковых инстансов с одинаковым софтом;

  • управление раннерами через Terraform для описания инфраструктуры как кода.

Инструменты решения и что будем настраивать

Схема работы выглядит так: GitLab по тегу запускает задачу → управляющий Runner получает её и через Fleeting Plugin создаёт новый EC2-инстанс → на этом инстансе выполняется билд → после завершения задачи инстанс автоматически останавливается или удаляется.

Для решения мы будем использовать GitLab Runner версии 15.11+ с поддержкой Fleet Scaling.

Fleet Scaling — это механизм GitLab для масштабирования Runner’ов за счёт внешних ресурсов. Задачи при этом выполняются не на самом сервере с Runner’ом, а в облачной среде (в нашем случае — в AWS) на временных (ephemeral) виртуальных машинах.

Fleeting — это плагин, который связывает Runner с облаком. Он отвечает за то, чтобы:

  • по запросу Runner’а создавать нужные EC2-инстансы;

  • подключаться к ним (обычно по SSM);

  • передавать на них выполнение задачи;

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

Давайте по шагам разберём порядок настройки, которому будем следовать дальше:

  • Выбор executor’а — определяем, как GitLab Runner будет выполнять задачи: на самой виртуальной машине, в Docker или на временных EC2-инстансах.

  • Установка GitLab Runner на управляющий EC2-инстанс — этот постоянный раннер будет принимать задачи от GitLab и через Fleeting запускать под них временные инстансы.

  • Создание IAM-пользователя — отдельный AWS-аккаунт (например, gitlab-autoscaler), от имени которого Runner получает права на работу с EC2 и Auto Scaling.

  • Настройка IAM-политики — выдаём IAM-пользователю минимум необходимых прав: создание/удаление инстансов, работа с Auto Scaling Group, доступ к описаниям ресурсов.

  • Установка Fleeting Plugin — плагин, который связывает Runner с AWS и позволяет автоматически запускать и останавливать EC2.

  • Конфигурация GitLab Runner — правим config.toml: настраиваем executor’ы, параметры autoscaler’а, кеш в S3, политики простоя (idle_time) и максим��льное число инстансов.

  • Подготовка AMI для рабочих инстансов — собираем базовый образ с нужным софтом: gitlab-runner, docker, kubectl, helm, kubeconfig и так далее. Этот AMI потом будет использоваться в Launch Template.

  • Настройка Auto Scaling Group — создаём и настраиваем ASG и Launch Template: задаём тип инстансов, AMI, параметры масштабирования и обновляем образ при необходимости.

Какой executor использовать

Перед тем как перейти к конфигурациям и установке, немного теории: какие вообще есть executor’ы у GitLab Runner и чем они отличаются.

Executor

Изоляция

Где выполняется

Подходит для

shell

Локальная ВМ

Простые скрипты, быстрые тесты

docker

Docker на хосте

Frontend, unit-тесты, микросервисы

instance

✅✅

Отдельный EC2

Terraform, shell-задачи, Ansible, утилиты

docker-autoscaler

✅✅

Docker на EC2

Контейнерные задачи, сборки, frontend CI/CD

kubernetes

✅✅

Pod в Kubernetes

Крупные, масштабируемые CI/CD-инфраструктуры

В контексте статьи нас интересуют instance и docker-autoscaler, потому что они:

  • умеют автоматически запускать и останавливать EC2-инстансы под задачи;

  • обеспечивают хорошую изоляцию: одна ВМ или один контейнер на отдельном инстансе под задачу;

  • работают через Fleet Scaling API GitLab — «родной» механизм автоскейлинга Runner’ов.

Kubernetes executor тоже даёт изоляцию и масштабирование, но требует уже существующего кластера Kubernetes и его поддержки. Это отдельная большая тема.

Установка Runner на управляющий инстанс

Теперь, когда мы определились с компонентами, можно переходить к практике.

Сначала установим GitLab Runner на управляющий EC2-инстанс. Это «постоянная» машина, которая не уходит в автоскейл и отвечает за:

  • получение задач из GitLab;

  • общение с Fleeting-плагином;

  • запуск временных EC2-инстансов под задачи.

Все дальнейшие шаги по настройке — установку плагинов, правки config.toml, проверки — будем выполнять именно на этом управляющем инстансе.

curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt-get install -y gitlab-runner

Создание IAM-пользователя с выдачей прав 

Перед установкой Fleeting Plugin нужно подготовить для него доступ к AWS. Плагину нужны:

  • возможность подключаться к инстансам (обычно через SSM/SSH);

  • права на с��здание и удаление EC2;

  • доступ к Auto Scaling Group и Launch Template.

Всё это настраивается через IAM.

Проще всего создать отдельного IAM-пользователя, например gitlab-autoscaler. От его имени GitLab Runner будет обращаться к AWS. Данные этого пользователя попадут в AWS-профиль, который мы укажем в config.toml.

profile = default

На машине c GitLab Runner нужно добавить ключи пользователя в файл ~/.aws/credentials:

~/.aws/credentials:
[default] 
aws_access_key_id = ... 
aws_secret_access_key = ...

Чтобы пользователь gitlab-autoscaler мог управлять ресурсами, ему нужно выдать соответствующие права. Мы создаём и привязываем к нему IAM-политику, например gitlab-runner-autoscaling-policy. В этой политике даём разрешения на:

  • создание и удаление EC2-инстансов;

  • чтение описаний инстансов, образов и тегов;

  • работу с Auto Scaling Group gitlab-runner-ao-group (autoScalingGroupName/gitlab-runner-ao-group).

Именно через эту политику Fleeting Plugin получает право запускать и останавливать машины в нужной группе автоскейлинга.

Пример JSON-политики:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "autoscaling:SetDesiredCapacity",
        "autoscaling:TerminateInstanceInAutoScalingGroup"
      ],
      "Resource": "arn:aws:autoscaling:us-east-2:{$Account ID}:autoScalingGroup:4a22e664-5095-414b-9b8e-8dcc903f2a3d:autoScalingGroupName/gitlab-runner-ao-group"
    },
    {
      "Effect": "Allow",
      "Action": [
        "autoscaling:DescribeAutoScalingGroups",
        "ec2:DescribeInstances"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:GetPasswordData",
        "ec2-instance-connect:SendSSHPublicKey"
      ],
      "Resource": "arn:aws:ec2:us-east-2:{$Account ID}:instance/*",
      "Condition": {
        "StringEquals": {
          "ec2:ResourceTag/aws:autoscaling:groupName": "gitlab-runner-ao-group"
        }
      }
    }
  ]
}   

Обратите внимание: <ACCOUNT_ID> нужно заменить на реальный AWS Account ID.

Установка Fleeting Plugin для AWS

После того как IAM-пользователь создан, а ключи настроены, устанавливаем Fleeting Plugin:

# Install fleeting plugin for AWS
echo "Installing fleeting plugin..."
sudo gitlab-runner fleeting install aws:latest

# Create AWS credentials directory and files
sudo mkdir -p /home/gitlab-runner/.aws
sudo chown gitlab-runner:gitlab-runner /home/gitlab-runner/.aws

# Create AWS credentials file with S3 cache credentials
sudo tee /home/gitlab-runner/.aws/credentials > /dev/null <<EOF
[default]
aws_access_key_id = ${aws_s3_cache_access_key}
aws_secret_access_key = ${aws_s3_cache_secret_key}
EOF

sudo tee /home/gitlab-runner/.aws/config > /dev/null <<'EOF'
[default]
region = us-east-2
output = json
EOF

sudo chown -R gitlab-runner:gitlab-runner /home/gitlab-runner/.aws
sudo chmod 600 /home/gitlab-runner/.aws/credentials
sudo chmod 600 /home/gitlab-runner/.aws/config

Когда IAM настроен и Fleeting Plugin установлен, GitLab Runner получает возможность:

  • запускать и удалять EC2-инстансы под задачи через IAM-пользователя;

  • подключаться к этим инстансам через SSM (без прямого SSH-доступа);

  • выполнять на них задачи как в режиме instance, так и внутри Docker-контейнеров;

  • останавливать или удалять инстансы по правилам idle policy или при достижении max_use_count.

Про SSM отдельно. Каждый EC2-инстанс, который создаёт Fleeting Plugin, запускается с ролью AmazonSSMRoleForInstancesQuickSetup. Эта роль даёт права на безопасное подключение к инстансу через AWS Systems Manager (SSM) и позволяет Runner’у управлять им без открытого SSH и публичных ключей.

В итоге схема получается такой: Runner по мере появления задач динамически создаёт инстансы в нужной Auto Scaling Group, эти инстансы автоматически получают все необходимые права и настройки, а пос��е выполнения задач корректно завершаются по заданным политикам.

Конфигурация GitLab Runner

Следующий шаг — настройка самого GitLab Runner через файл /etc/gitlab-runner/config.toml.

Это центральное место, где мы задаём:

  • адрес GitLab и токены для регистрации Runner’а;

  • тип executor для каждой группы задач (instance, docker-autoscaler и так далее);

  • параметры автомасштабирования: максимальное число инстансов, idle policy, max_use_count и прочее;

  • настройки кеша (например, S3-бакет для кеша артефактов);

  • разные политики масштабирования для разных периодов времени (рабочие часы, ночь, выходные).

Ниже под спойлером — пример конфигурации Runner’а:

  • первая секция [[runners]] описывает instance runner, который выполняет задачи прямо на EC2-инстансах (через shell);

  • вторая секция [[runners]] — это runner с executor = "docker-autoscaler" для контейнерных задач;

  • блоки [runners.cache] и [runners.cache.s3] настраивают кеш (в нашем случае в S3), чтобы ускорить повторные сборки;

  • блок [runners.autoscaler] и вложенные [[runners.autoscaler.policy]] управляют автомасштабированием: сколько инстансов можно создавать одновременно, сколько задач может обработать один инстанс, как долго держать его в простое и как вести себя в разные периоды времени.

Конфигурация Runner'a
listen_address = ":9252"
concurrent = 200
check_interval = 0
connection_max_age = "30m0s"
shutdown_timeout = 0
log_level = "info"
log_format = "text"

[session_server]
  session_timeout = 1800
# Instance executor for default jobs
[[runners]]
  name = "${runner_name}"
  id = 400
  output_limit = 50000
  url = "${gitlab_url}"
  token = "${registration_token}"
  token_obtained_at = 2025-07-07T12:04:50Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "instance"

  [runners.cache]
    Type = "s3"
    Path = "cache"
    Shared = true
    MaxUploadedArchiveSize = 0
    [runners.cache.s3]
      ServerAddress = "s3.amazonaws.com"
      AccessKey = "${aws_s3_cache_access_key}"
      SecretKey = "${aws_s3_cache_secret_key}"
      BucketName = "walli-gitlab-runner-cache"
      BucketLocation = "us-east-2"

  [runners.autoscaler]
    capacity_per_instance = ${capacity_per_instance}
    max_use_count = ${max_use_count}
    max_instances = ${max_instances}
    plugin = "aws:latest"
    instance_acquire_timeout = "0s"
    update_interval = "0s"
    update_interval_when_expecting = "0s"
    [runners.autoscaler.plugin_config]
      name = "gitlab-runner-ao-group2"
      profile = "default"
    [runners.autoscaler.connector_config]
      protocol_port = 0
      username = "gitlab-runner"
      keepalive = "0s"
      timeout = "0s"

    [[runners.autoscaler.policy]]
      idle_count = ${idle_count}
      idle_time = "${idle_time}"
      scale_factor = 1.5
      scale_factor_limit = 10

      [[runners.autoscaler.policy]]
      periods = ["* 6-11 * * mon-fri"]
      idle_count = 20
      idle_time = "${idle_time}"
      scale_factor = 1.5
      scale_factor_limit = 10

# Docker autoscaler for docker jobs
[[runners]]
  name = "${runner_name}"
  id = 401
  url = "${gitlab_url}"
  token = "${docker_registration_token}"
  token_obtained_at = 2025-07-08T10:57:05Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "docker-autoscaler"
  environment = ["DOCKER_AUTH_CONFIG={\"auths\":{\"${docker_registry_url}\":{\"auth\":\"${docker_registry_auth}\"}}}"]
  [runners.cache]
    Type = "s3"
    Path = "cache"
    Shared = true
    MaxUploadedArchiveSize = 0
    [runners.cache.s3]
      ServerAddress = "s3.amazonaws.com"
      AccessKey = "${aws_s3_cache_access_key}"
      SecretKey = "${aws_s3_cache_secret_key}"
      BucketName = "walli-gitlab-runner-cache"
      BucketLocation = "us-east-2"

  [runners.docker]
    tls_verify = false
    image = "ubuntu:24.04"
    privileged = true
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
    extra_hosts = ${jsonencode(extra_hosts)}
    shm_size = 0
    network_mtu = 0

  [runners.autoscaler]
    capacity_per_instance = 5
    max_use_count = 30
    max_instances = 25
    plugin = "aws:latest"
    update_interval = "0s"
    update_interval_when_expecting = "0s"
    [runners.autoscaler.plugin_config]
      name = "gitlab-runner-ao-group-docker2"
      profile = "default"
    [runners.autoscaler.connector_config]
      username = "gitlab-runner"
      keepalive = "0s"
      timeout = "0s"

    [[runners.autoscaler.policy]]
      periods = ["* * * * *"]
      idle_count = 1
      idle_time = "20m0s"
      scale_factor = 1.5
      scale_factor_limit = 10

    [[runners.autoscaler.policy]]
      periods = ["* 6-11 * * mon-fri"]
      idle_count = 20
      idle_time = "20m0s"
      scale_factor = 1.5
      scale_factor_limit = 10

Подготовка AMI для GitLab Runner — базового образа для Launch Template

Чтобы Autoscaling Group могла поднимать «правильные» EC2-инстансы под задачи CI, ей нужен корректный AMI — базовый образ, в котором уже есть всё необходимое окружение.

Минимальный набор того, что должно быть внутри AMI:

  • Системные утилиты, которые будут использоваться в задачах (docker, kubectl, helm, terraform и так далее).

  • При необходимости — агенты/сервисы, но отдельный gitlab-runner как зарегистрированный Runner в AMI не нужен: роль Runner’а выполняет управляющий инстанс.

  • При необходимости — заранее положенный kubeconfig по пути /home/gitlab-runner/.kube/config.

  • Прогретые Docker-образы, если хотите ускорить первую сборку.

Самый простой путь — сделать первый AMI руками, а дальше уже автоматизировать с помощью Packer.

Условный минимальный сценарий:

  1. Берём базовый образ (например, Ubuntu 22.04 из AWS Marketplace).

  2. Запускаем из него временный EC2-инстанс.

  3. Заходим на инстанс по SSH и устанавливаем нужное ПО (gitlab-runner, docker, kubectl и так далее).

  4. Создаём пользователя gitlab-runner, настраиваем ему домашнюю директорию.

  5. При необходимости добавляем kubeconfig.

  6. Останавливаем инстанс и делаем из него образ через Actions → Create image.

  7. Полученный AMI используем в Launch Template для Auto Scaling Group.

После того как базовый процесс обкатан вручную, описываем те же шаги в Packer, чтобы:

  • не крутить руками EC2 для каждой новой версии;

  • гарантировать повторяемость и одинаковость образов;

  • иметь версионируемый шаблон AMI.

А уже этот AMI мы подставляем в Launch Template через Terraform.

Настройки Auto Scaling Group

В нашей конфигурации используются две группы автоскейлинга: gitlab-runner-ao-group и gitlab-runner-ao-group-docker. Для них настроены:

  • Launch Template: gitlab-runner-autoscaler;

  • AMI: ami-01f040934be890e5a — актуальный образ на момент написания и может обновляться в будущем;

  • тип инстанса: c7a.4xlarge — подбирается в зависимости от нагрузки и профиля задач.

Если нужно обновить AMI, например добавить новый kubeconfig или обновить версии утилит, порядок действий такой:

  1. Поднять EC2-инстанс из текущего AMI.

  2. Внести изменения, например обновить /home/gitlab-runner/.kube/config или установить дополнительный софт.

  3. Остановить инстанс и создать из него новый образ через Create image.

  4. Перейти в Launch Templates → gitlab-runner-autoscaler.

  5. Создать новую версию шаблона с вашим AMI и пометить её как используемую по умолчанию.

После этого Auto Scaling Group автоматически начнёт использовать свежий образ при создании новых EC2-инстансов.

Некоторое время мы делали это вручную, но затем автоматизировали процесс с помощью Packer и Terraform. Сейчас обновление шаблона сводится к:

  • пересборке AMI через Packer;

  • выполнению terraform plan / terraform apply для обновления Launch Template и связанных ресурсов.

В результате итоговая структура GitLab Runner-инфраструктуры выглядит так:

  • Две Auto Scaling Group под разные типы задач, каждая — со своим runner-токеном.

  • Один постоянно работающий Instance Runner, который управляет масштабированием и общается с GitLab.

  • Custom AMI, собранный Packer’ом и включающий весь нужный софт (docker, kubectl, helm и так далее).

  • IAM-роли для соответствующих прав.

  • CloudWatch Alarms для мониторинга состояния и нагрузки.

Остаётся одно слабое место — изменение конфигурации управляющего инстанса с Runner’ом. Если мы меняем config.toml или системные настройки, то нужно:

  • либо удалить текущий инстанс, чтобы Auto Scaling создал новый с актуальной конфигурацией;

  • либо аккуратно вносить изменения вручную на уже работающем экземпляре.

К счастью, это требуется нечасто. Если вы используете более элегантный подход для обновления конфигурации управляющих Runner’ов (например, Ansible, SSM, конфигурационный дрифт-контроль), будет интересно почитать в комментариях.

Полезные кейсы

Если ваши задачи используют kubectl (например, для helm install или деплой-скриптов), то kubeconfig должен быть заранее записан в AMI. Без корректного kubeconfig новый EC2-инстанс просто не сможет подключиться к вашему Kubernetes-кластеру.

Мы кладём конфиг по стандартному пути:

/home/gitlab-runner/.kube/config

Файл становится частью AMI и автоматически оказывается на каждом новом EC2-инстансе, который развёртывается из этого образа.

При подготовке AMI также имеет смысл заранее подтянуть (pull) базовые образы контейнеров, которые часто используются в CI. Это позволяет:

  • уменьшить время первой сборки на новом инстансе;

  • снизить зависимость от внешних registry по времени ответа.

Таким образом, инстанс с готовым kubeconfig и прогретыми Docker-образами стартует быстрее и сразу готов выполнять Kubernetes- и Docker-зависимые задачи.

Плюсы и минусы масштабируемых GitLab Runner’ов с кастомными AMI с нашим подходом

Плюсы:

  • Runner’ы запускаются только при наличии задач, что позволяет заметно экономить ресурсы в AWS.

  • Хорошая изоляция и безопасность: можно настроить схему «1 EC2 = 1 Job» или небольшой пул задач на инстанс.

  • AMI удобно обновлять и пересобирать через Packer — инфраструктура остаётся воспроизводимой.

  • Подходит для высоконагруженного CI: при росте нагрузки просто создаются дополнительные инстансы.

Минусы:

  • config.toml на управляющем инстансе не очень удобно править вручную: любые изменения требуют либо пересоздания инстанса, либо отдельного процесса доставки конфигурации.

  • AMI нужно регулярно обновлять: обновления ОС, пакетов, инструментов (docker, kubectl, helm и так далее).

  • Подключение через SSM зависит от корректной IAM-роли и SSM-агента: при ошибках в роли или настройке могут быть проблемы с доступом, если нет SSH-фоллбэка.

Что ещё можно развить и настроить:

  • несколько разных autoscaler’ов под разные теги в GitLab: разделить фронтенд, бэкенд, инфраструктурные задачи и тяжёлые пайплайны;

  • разные AMI под разные стеки: отдельные образы для Java, Node.js, Python, инфраструктурных утилит и так далее;

  • интеграцию с EFS или S3 для кеша, настройку общих volume’ов, включение/отключение shared runner’ов по расписанию (через CRON/policy);

  • полную автоматизацию через Terraform (ASG, Launch Template, IAM) и Packer (сборка AMI), чтобы любое изменение описывалось в коде и проходило через review.

Заключение

Autoscaler — отличный вариант, если вы не хотите держать постоянно работающие EC2-инстансы только ради GitLab Runner. Задача пришла — инстанс развернулся, задача отработала — ресурсы освободились. Всё прозрачно, управляемо и хорошо масштабируется.

В нашем случае запуск GitLab autoscaling с плагином Fleeting позволил:

  • полностью уйти от «вечных» EC2-инстансов под Runner’ы;

  • сократить расходы на CI в AWS и параллельно улучшить время прохождения пайплайнов;

  • повысить стабильность и предсказуемость CI-инфраструктуры за счёт изоляции и автомасштабирования;

  • упростить жизнь команде: появился единый пул Runner’ов с понятной конфигурацией вместо «зоопарка» ручных инстансов;

  • описать управление Runner’ами, AMI, Auto Scaling Group и IAM-ролями в виде кода через Terraform и Packer.

Полезные ссылки

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