Всё было хорошо, пока не стало плохо. В какой‑то момент задачи в 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-пользователя — отдельный User (например,
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.
