Эта статья описывает наш опыт отказа от постоянно работающих инстансов EC2, настройки масштабируемых GitLab Runner в AWS и существенного сокращения расходов на CI-инфраструктуру. Как мы к этому пришли?
Поначалу всё работало как часы — до тех пор, пока не начало сыпаться. В какой-то момент GitLab Jobs стали тормозить на 5, 10, а то и 15 минут. Очереди пайплайнов росли, DevOps-инженеры нервничали, разработчики злились, а AWS спокойно выставляла счёт за сотни часов работы EC2-инстансов.
Старый трюк «просто добавим ещё EC2» помогал лишь на время. Вскоре мы снова оказывались лицом к лицу с теми же проблемами: простаивающие инстансы, деньги на ветер и отсутствие нормальной изоляции джобов. Мы поняли, что бесконечно «чинить» одно и то же — тупик. Нужно было наладить настоящее автомасштабирование GitLab Runner.
Цели: автоматическое масштабирование и снижение расходов
Мы хотели реализовать подход, который позволял бы CI масштабироваться под переменную нагрузку и сокращать затраты на AWS.
Вместо того чтобы держать раннеры на «вечно включённых» EC2, идея заключалась в автоматическом поднятии инстансов под конкретные джобы или группы джобов. Когда приходил новый джоб, создавался EC2-инстанс. Как только джоб завершался, инстанс либо брал следующий из очереди, либо автоматически останавливался, если работы больше не было.
В итоге единственным «вечно включённым» EC2 оставался управляющий инстанс с GitLab Runner, который принимал джобы и координировал их выполнение. Он потреблял совсем немного ресурсов, так что его содержание обходилось практически бесплатно по сравнению с реальными рабочими нагрузками.
Итак, наши цели были следующими:
-
автоматически поднимать EC2-инстансы при появлении GitLab CI Jobs;
-
останавливать или удалять инстанс после N минут простоя;
-
обеспечивать изоляцию джобов по принципу «одна VM — один джоб» (или небольшая пачка джобов);
-
автоматизировать сборку образов Amazon Machine Image (AMI), чтобы быстро разворачивать одинаковые инстансы с нужным набором ПО;
-
управлять раннерами через Terraform в рамках подхода «инфраструктура как код» (infrastructure as code).
Обзор рабочего процесса и схемы GitLab + AWS
В общих чертах ожидаемый рабочий процесс выглядит так:
-
GitLab запускает джоб по конкретному тегу →
-
управляющий Runner подхватывает его и с помощью Fleeting Plugin поднимает новый EC2-инстанс →
-
сборка выполняется на этом инстансе →
-
после завершения джоба инстанс автоматически останавливается или удаляется.
Мы реализовали это с использованием GitLab Runner версии 15.11+ с поддержкой масштабирования Fleet (Fleet scaling) на Ubuntu Linux. Fleet scaling — это встроенный механизм GitLab для масштабирования раннеров с использованием внешних ресурсов. Джобы выполняются не на сервере раннера, а на эфемерных виртуальных машинах в облаке (в нашем случае — AWS).
Fleeting — это библиотека/плагин, реализующий данный подход путём подключения раннера к облаку. Он отвечает за создание EC2-инстансов по запросу раннера, подключение к ним (как правило, через SSM), передачу джобов на исполнение и удаление инстансов после завершения работы.
Шаги, которые мы будем выполнять для достижения поставленных целей:
-
Выбрать экзекьютор (executor). Определить, как GitLab Runner будет запускать джобы — прямо на VM, внутри Docker-контейнера или на эфемерных EC2.
-
Установить GitLab Runner на управляющий EC2. Этот «вечно включённый» раннер будет принимать джобы от GitLab и использовать Fleeting для создания временных инстансов.
-
Создать IAM-пользователя. Завести отдельного пользователя (например,
gitlab-autoscaler); раннер будет использовать его учётные данные для взаимодействия с EC2 и Auto Scaling. -
Настроить IAM-политику. Выдать IAM-пользователю ровно те права, которые ему нужны: создание и удаление инстансов, работа с Auto Scaling Group и чтение сведений о ресурсах.
-
Установить Fleeting Plugin. Этот плагин подключает раннер к AWS и позволяет ему автоматически запускать и останавливать EC2-инстансы.
-
Настроить GitLab Runner. Отредактировать
config.toml: задать экзекьюторы, параметры автоскейлера, кеширование в S3, политики простоя (idle_time) и максимальное количество инстансов. -
Подготовить AMI для рабочих инстансов. Собрать базовый образ со всем необходимым ПО: gitlab-runner, docker, kubectl, helm и т. д. На этот AMI будем ссылаться в Launch Template.
-
Подготовить Auto Scaling Group. Создать и настроить ASG и Launch Template: указать тип инстанса, AMI, параметры масштабирования, предусмотреть обновление образа при необходимости.
Реализация
1. Выбор экзекьютора GitLab Runner
Прежде чем переходить к конфигам и настройке, немного теории: какие экзекьюторы GitLab Runner существуют и чем они отличаются. Краткое техническое сравнение:
| Экзекьютор | Изоляция | Среда выполнения | Лучше всего подходит для |
|---|---|---|---|
shell |
❌ |
Локальная VM |
Простые скрипты, быстрые тесты |
docker |
✅ |
Docker на хосте |
Frontend, юнит-тесты, микросервисы |
instance |
✅✅ |
Выделенный EC2-инстанс |
Terraform, shell-джобы, Ansible, утилиты |
docker-autoscaler |
✅✅ |
Docker на EC2 |
Контейнеризованные джобы, сборки, frontend CI/CD |
kubernetes |
✅✅ |
Kubernetes Pod |
Масштабные CI/CD-инфраструктуры |
Мы сосредоточимся на экзекьюторах instance и docker-autoscaler, поскольку они:
-
умеют автоматически поднимать и останавливать EC2-инстансы под конкретные джобы;
-
обеспечивают хорошую изоляцию: одна VM или один контейнер на выделенном инстансе на джоб;
-
используют Fleet Scaling API GitLab — нативный механизм автомасштабирования раннеров.
А как же экзекьютор kubernetes? Он тоже даёт изоляцию и масштабирование, но требует наличия и поддержки Kubernetes-кластера. Это большая отдельная тема, которая заслуживает собственного материала, поэтому в данном руководстве мы её опустим.
2. Установка Runner на управляющий инстанс
Разобравшись с компонентами, переходим к практике.
Сначала установим GitLab Runner на управляющий EC2-инстанс. Этот «вечно включённый» инстанс отвечает за:
-
получение джобов от GitLab;
-
взаимодействие с плагином Fleeting;
-
создание временных EC2-инстансов для выполнения джобов.
Обратите внимание: все дальнейшие шаги — установка плагинов, правка config.toml и запуск тестов — выполняются именно на этом управляющем инстансе.
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" -o gitlab-repo-add.sh
less gitlab-repo-add.sh # проверьте содержимое скрипта, прежде чем запускать
sudo bash gitlab-repo-add.sh
sudo apt-get install -y gitlab-runner
3. Создание IAM-пользователя
Прежде чем устанавливать Fleeting Plugin, нужно подготовить доступ к AWS. Плагину необходимо:
-
подключаться к инстансам (обычно через SSM/SSH);
-
создавать и удалять EC2-инстансы;
-
работать с Auto Scaling Group и Launch Template.
Всё это настраивается через IAM в AWS. Проще всего создать отдельного IAM-пользователя, например gitlab-autoscaler. GitLab Runner будет использовать его для взаимодействия с AWS. Учётные данные этого пользователя откроют AWS-профиль, указанный в config.toml:
profile = default
На машине с GitLab Runner нужно добавить ключи пользователя в файл ~/.aws/credentials:
[default]
aws_access_key_id = ...
aws_secret_access_key = ...
4. Выдача прав IAM-пользователю
Чтобы пользователь gitlab-autoscaler мог управлять ресурсами, ему нужно выдать соответствующие права. Создадим IAM-политику — например, gitlab-runner-autoscaling-policy — и прикрепим её к пользователю. Эта политика позволяет:
-
создавать и удалять EC2-инстансы;
-
читать описания инстансов, образов и тегов;
-
работать с Auto Scaling Group
gitlab-runner-ao-group(то есть сautoScalingGroupName/gitlab-runner-ao-group).
Данная политика позволяет Fleeting Plugin запускать и останавливать инстансы в указанной группе автомасштабирования.
Пример JSON-политики (замените $ACCOUNT_ID на свой реальный идентификатор AWS-аккаунта):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"autoscaling:SetDesiredCapacity",
"autoscaling:TerminateInstanceInAutoScalingGroup"
],
"Resource": "arn:aws:autoscaling:eu-central-1:{$ACCOUNT_ID}:autoScalingGroup:...: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:eu-central-1:{$ACCOUNT_ID}:instance/*",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/aws:autoscaling:groupName": "gitlab-runner-ao-group"
}
}
}
]
}
5. Установка AWS Fleeting Plugin
Теперь, когда IAM-пользователь готов и ключи настроены, устанавливаем Fleeting Plugin:
# Устанавливаем Fleeting Plugin для AWS
echo "Installing Fleeting Plugin..."
sudo gitlab-runner fleeting install aws:latest
# Создаём директорию и файлы для AWS credentials
sudo mkdir -p /home/gitlab-runner/.aws
sudo chown gitlab-runner:gitlab-runner /home/gitlab-runner/.aws
# Создаём файл credentials с ключами для S3-кеша
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 = eu-central-1
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-пользователя;
-
подключаться к этим инстансам через AWS Systems Manager (SSM) без необходимости в прямом SSH-доступе;
-
запускать джобы на них через экзекьютор instance или внутри Docker-контейнеров;
-
останавливать или удалять инстансы согласно
idle policyили при достижении лимитаmax_use_count.
Краткое замечание про SSM: каждый EC2-инстанс, создаваемый Fleeting Plugin, работает под ролью AmazonSSMRoleForInstancesQuickSetup. Эта роль позволяет Runner безопасно подключаться к инстансу через SSM — без SSH и управления публичными ключами.
Итоговый рабочий процесс выглядит так:
-
Runner динамически создаёт инстансы в указанной Auto Scaling Group по мере поступления джобов →
-
инстансам автоматически назначаются нужные права и настройки →
-
после завершения джобов инстансы корректно завершают работу согласно заданным политикам.
6. Настройка GitLab Runner
Следующий шаг — настройка GitLab Runner через файл /etc/gitlab-runner/config.toml. Это центральный конфигурационный файл, в котором задаются:
-
URL GitLab и токены регистрации раннера;
-
тип экзекьютора для каждой группы джобов (instance, docker-autoscaler и т. д.);
-
параметры автомасштабирования: максимальное количество инстансов,
idle policy,max_use_countи другие; -
настройки кеша (например, S3-бакет для кеширования артефактов);
-
различные политики масштабирования для разных временных промежутков (рабочие часы, ночь, выходные).
Пример конфигурации Runner:
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 — запускает джобы прямо на EC2-инстансах (через shell)
[[runners]]
name = "${runner_name}"
id = 400
output_limit = 50000
url = "${gitlab_url}"
token = "${registration_token}"
token_obtained_at = 2026-02-01T12:42:42Z
token_expires_at = 0001-01-01T00:00:00Z
executor = "instance"
# S3-кеш для ускорения сборок
[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 = "eu-central-1"
[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 = ["* 10-18 * * mon-fri"]
idle_count = 20
idle_time = "${idle_time}"
scale_factor = 1.5
scale_factor_limit = 10
# Экзекьютор docker-autoscaler — для контейнерных джобов
[[runners]]
name = "${runner_name}"
id = 401
url = "${gitlab_url}"
token = "${docker_registration_token}"
token_obtained_at = 2026-02-01T12:43:43Z
token_expires_at = 0001-01-01T00:00:00Z
executor = "docker-autoscaler"
environment = ["DOCKER_AUTH_CONFIG={\"auths\":{\"${docker_registry_url}\":{\"auth\":\"${docker_registry_auth}\"}}}"]
# S3-кеш для ускорения сборок
[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 = "eu-central-1"
[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 = ["* 10-18 * * mon-fri"]
idle_count = 20
idle_time = "20m0s"
scale_factor = 1.5
scale_factor_limit = 10
7. Подготовка AMI для GitLab Runner
Чтобы Auto Scaling Group поднимала правильные EC2-инстансы для CI-джобов, нужен подходящий AMI — базовый образ со всем необходимым предустановленным окружением. Минимальный набор требований к AMI:
-
системные утилиты для выполнения джобов (docker, kubectl, helm, terraform и т. д.);
-
дополнительные агенты или сервисы при необходимости; регистрировать отдельный gitlab-runner в AMI не нужно — управляющий инстанс берёт на себя роль раннера;
-
при необходимости — заранее настроенный
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.
8. Настройка Auto Scaling Group
В нашей конфигурации используются две Auto Scaling Group: gitlab-runner-ao-group и gitlab-runner-ao-group-docker. Их параметры:
-
Launch Template:
gitlab-runner-autoscaler; -
AMI:
ami-01f040934be890e5a— актуальный образ на момент написания статьи, в будущем может обновляться; -
тип инстанса:
c7a.4xlarge— выбран исходя из нашей нагрузки и типов выполняемых джобов.
(К слову, библиотека GitLab Fleeting работает и с другими облаками. Например, тот же подход можно реализовать с помощью Virtual Machine Scale Sets в Azure или групп инстансов в Google Cloud.)
Вот как можно обновить AMI — например, чтобы добавить новый kubeconfig или обновить версии инструментов:
-
Запустить EC2-инстанс на основе существующего AMI.
-
Внести изменения: обновить
/home/gitlab-runner/.kube/configили установить новое ПО. -
Остановить инстанс и создать из него новый образ.
-
Перейти в 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, занимающийся масштабированием и связью с GitLab;
-
кастомный AMI, собранный с помощью Packer, со всем необходимым ПО;
-
IAM-роли для выдачи нужных прав;
-
CloudWatch Alarms для мониторинга состояния кластера и нагрузки.
Единственное оставшееся неудобство — правка конфигурации управляющего инстанса Runner. Если нужно изменить config.toml или другие системные настройки, приходится либо:
-
удалить текущий инстанс, чтобы Auto Scaling Group подняла новый с обновлённой конфигурацией, либо
-
аккуратно внести изменения вручную на работающем инстансе.
К счастью, это требуется нечасто. Если у вас есть более изящное решение для обновления конфигурации управляющего Runner (например, Ansible, SSM или контроль дрейфа конфигурации), буду рад услышать об этом в комментариях.
Полезные рекомендации
Если ваши джобы используют kubectl (например, для helm install или деплойных скриптов), файл kubeconfig должен быть предустановлен в AMI. Без рабочего kubeconfig новый EC2-инстанс не сможет подключиться к Kubernetes-кластеру.
Файл конфигурации можно хранить по стандартному пути /home/gitlab-runner/.kube/config и включить его в AMI. Тогда любой новый EC2-инстанс, созданный из этого образа, сразу сможет им воспользоваться.
При сборке AMI также стоит предзагрузить базовые контейнерные образы, которые часто используются в CI. Это позволяет:
-
сократить время первой сборки на новом инстансе;
-
снизить зависимость от внешних реестров.
В результате инстанс с настроенным kubeconfig и предзагруженными Docker-образами запускается быстрее и готов к обработке джобов, зависящих от Kubernetes и Docker, с первых секунд.
Итоги: плюсы и минусы
Автоскейлер — отличный вариант, если не хочется держать EC2-инстансы запущенными круглосуточно ради GitLab Runner. Появился джоб — инстанс поднялся; джоб завершён — ресурсы освобождены. Всё прозрачно, управляемо и хорошо масштабируется.
Наш подход с масштабируемыми GitLab Runner и кастомными AMI имеет следующие преимущества:
-
Раннеры запускаются только при наличии джобов — существенная экономия на ресурсах AWS.
-
Хорошая изоляция и безопасность: можно реализовать модель «1 EC2 = 1 джоб» или назначить небольшой пул джобов на каждый инстанс.
-
AMI легко обновлять и пересобирать с помощью Packer, что обеспечивает воспроизводимость инфраструктуры.
-
Отлично подходит для высоконагруженных CI: при пиковой нагрузке просто создаётся больше инстансов.
Вместе с тем у подхода есть ограничения:
-
Редактировать
config.tomlна управляющем инстансе вручную — то ещё удовольствие. Любое изменение означает либо пересоздание инстанса, либо ручную доставку конфигурации. -
AMI требует регулярного обновления: ОС, пакеты, инструменты.
-
Подключение через SSM требует правильной IAM-роли и наличия SSM-агента. При неверной конфигурации можно потерять доступ, если нет резервного SSH.
Что ещё можно улучшить?
-
Вместо хранения в AMI
kubeconfigможно получать из AWS Secrets Manager при запуске инстанса. -
Несколько автоскейлеров под разные теги GitLab: раздельные раннеры для frontend-, backend-, инфраструктурных и ресурсоёмких пайплайнов.
-
Разные AMI под разные стеки: выделенные образы для Java, Node.js, Python, инфраструктурных инструментов и т. д.
-
Интеграция с EFS или S3 для кеширования, настройка общих томов, включение и отключение shared-раннеров по расписанию (через cron/policy).
-
Полная автоматизация через Terraform (ASG, Launch Template, IAM) и Packer (сборка AMI), чтобы любое изменение было описано как код и прошло ревью.
Возможно, ваш конкретный сценарий потребует других доработок, чтобы извлечь максимум из этого подхода — делитесь своими мыслями и опытом в комментариях!