Всё было хорошо, пока не стало плохо. В какой‑то момент задачи в 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.
Условный минимальный сценарий:
Берём базовый образ (например, Ubuntu 22.04 из AWS Marketplace).
Запускаем из него временный EC2-инстанс.
Заходим на инстанс по SSH и устанавливаем нужное ПО (gitlab-runner, docker, kubectl и так далее).
Создаём пользователя
gitlab-runner, настраиваем ему домашнюю директорию.При необходимости добавляем kubeconfig.
Останавливаем инстанс и делаем из него образ через Actions → Create image.
Полученный 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 или обновить версии утилит, порядок действий такой:
Поднять EC2-инстанс из текущего AMI.
Внести изменения, например обновить
/home/gitlab-runner/.kube/configили установить дополнительный софт.Остановить инстанс и создать из него новый образ через Create image.
Перейти в Launch Templates → gitlab-runner-autoscaler.
Создать новую версию шаблона с вашим 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.