Автомасштабирование GitLab Runner в AWS: экономим на EC2

Эта статья описывает наш опыт отказа от постоянно работающих инстансов 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

Секция [runners.autoscaler] и элементы — ключевые для управления автомасштабированием. В них задаётся, сколько инстансов создавать одновременно, сколько джобов может обработать каждый инстанс, как долго держать их в простое и как масштабироваться в разное время суток.

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), чтобы любое изменение было описано как код и прошло ревью.

Возможно, ваш конкретный сценарий потребует других доработок, чтобы извлечь максимум из этого подхода — делитесь своими мыслями и опытом в комментариях!

© 2026 meganuke