*Все технические решения описаны в обезличенном виде и адаптированы под публичное изложение. Проект находится под НДА, поэтому часть информации и детали реализации были изменены или обобщены.
Изначальный расчет был на то, что данное решение позволит существенно сократить обслуживание «железной» инфраструктуры. А вместе с ним 一 и расходы на IT-отдел в принципе. Дополнительно планировались привести в порядок текущий парк, который в тот момент насчитывал большое количество неуправляемых раннеров. Часть из них была просто забыта и заброшена, другая часть 一 в один прекрасный момент зависла и так и не пришла в себя. И, разумеется, никому не хотелось с этим разбираться, ведь гораздо проще было просто создать новый раннер.
Все это выливалось в то, что длительность сборок проектов составляла по несколько часов 一 долго, нудно, тяжело и нерационально.
Итого, наша мотивация для перехода в облако включала в себя:
Масштабирование в зависимости от нагрузки;
Значительное сокращение времени сборки;
Возможность оперативной адаптации под новый проект или задачу.
Сравним текущее решение on-premises с облаком
On-premises не увеличивает ресурсы по щелчку пальцев: приходится поднимать виртуалочку, ставить туда раннер и регистрировать его в Gitlab. И только после этого танца с бубнами им можно наконец-то пользоваться. Другое дело облако, в котором все происходит автоматически, по запросу от Gitlab-менеджера.
Наш выбор и ход действий
В итоге пал на одного из крупных российских облачных провайдеров с его autoscale-группами. Немного поговорим и об его инфраструктуре и подготовке.
Нами была создана сегментированная сеть с ограничением исходящего трафика, чтобы у раннеров не было прямого доступа в Сеть. Также был подготовлен образ Gitlab-менеджера(по совместительству и linux раннера) с заранее установленным плагином для беспроблемной интеграции с Облаком, а также образ Windows-раннера, настроенный средствами автоматизации конфигурации.
И вот у нас на руках, казалось бы, готовое решение. Но нет. Все оказалось не так просто.
После первого запуска и быстрого теста вылезла проблема 一 плагин переставал скейлить раннеры как вверх, так и вниз. В результате до них не доходили задачи с Gitlab, не создавались новые и не удалялись старые виртуалки.
Анализ этой ситуации показал, что так происходит из-за того, что в конфигурации Gitlab-runner было описано сразу 2 группы раннеров: Windows и Linux. По какой-то причине автоскеллер сначала инициировал масштабирование Linux-виртуальных машин, после чего пытался их удалить, но терялся и начинал всю эту бессмысленную процедуру по новой. Итог был один: постоянная попытка заскейлиться в ту или иную сторону без какой-либо практической пользы.
Мы решили это разделением 2-х групп раннеров используя 2 сервиса: отдельно зарегистрировали сервис для Windows и сервис для Linux.
concurrent = 10
check_interval = 0
connection_max_age = "15m0s"
shutdown_timeout = 0
#########
# Linux runner group
#########
[[runners]]
name = "gitlab-runner-linux-autoscaler"
url = "${url}"
id = ${id_linux}
token = "${token_linux}"
executor = "docker-autoscaler"
output_limit = 20480 # 20MB
[runners.feature_flags]
FF_USE_FLEETING_ACQUIRE_HEARTBEATS = true
[runners.cache]
Type = "s3"
Path = "/"
Shared = true
[runners.cache.s3]
ServerAddress = "storage.yandexcloud.net"
AccessKey = "${s3_bucket_ak}"
SecretKey = "${s3_bucket_sk}"
BucketName = "${s3_bucket_runner_cache}"
BucketLocation = "ru-central1"
Insecure = false
[runners.docker]
pull_policy = "if-not-present"
tls_verify = false
image = "alpine:latest"
privileged = true
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = true # use s3 cache for runners
volumes = ["/cache","/var/run/docker.sock:/var/run/docker.sock"]
shm_size = 0
network_mtu = 0
# https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersautoscaler-section
[runners.autoscaler]
plugin = "fleeting-plugin-yc"
capacity_per_instance = 2
max_use_count = 100
max_instances = 5
instance_ready_command = "sudo cloud-init status --wait"
[runners.autoscaler.state_storage]
enabled = true
[runners.autoscaler.plugin_config]
name = "gitlab-linux-runners"
folder_id = "${folder_id}"
config_file = "/etc/gitlab-runner/template_linux.yml"
[runners.autoscaler.connector_config]
username = "ubuntu"
# https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersautoscalerpolicy-sections
[[runners.autoscaler.policy]]
idle_count = 1
idle_time = "15m0s"
# periods = ["* * * * sat-sun"]
concurrent = 10
check_interval = 0
connection_max_age = "15m0s"
shutdown_timeout = 0
#########
# Windows runner group
#########
[[runners]]
name = "gitlab-runner-windows-autoscaler"
url = "${url}"
id = ${id_windows}
token = "${token_windows}"
executor = "docker-autoscaler"
output_limit = 20480 # 20MB
builds_dir = "C:/builds"
[runners.feature_flags]
FF_USE_FLEETING_ACQUIRE_HEARTBEATS = true
FF_USE_POWERSHELL_PATH_RESOLVER = true
[runners.cache]
Type = "s3"
Path = "/"
Shared = true
[runners.cache.s3]
ServerAddress = "storage.yandexcloud.net"
AccessKey = "${s3_bucket_ak}"
SecretKey = "${s3_bucket_sk}"
BucketName = "${s3_bucket_runner_cache}"
BucketLocation = "ru-central1"
Insecure = false
[runners.docker]
pull_policy = "if-not-present"
tls_verify = false
image = "windows/servercore:ltsc2019"
# privileged = true
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["//./pipe/docker_engine://./pipe/docker_engine"]
shm_size = 0
network_mtu = 0
# https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersautoscaler-section
[runners.autoscaler]
plugin = "fleeting-plugin-yc"
capacity_per_instance = 2
max_use_count = 100
max_instances = 5
[runners.autoscaler.state_storage]
enabled = true
[runners.autoscaler.plugin_config]
name = "gitlab-windows-runners"
folder_id = "${folder_id}"
config_file = "/etc/gitlab-runner/template_windows.yml"
[runners.autoscaler.connector_config]
os = "windows"
protocol = "ssh"
username = "Administrator"
# https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersautoscalerpolicy-sections
[[runners.autoscaler.policy]]
idle_count = 1
idle_time = "15m0s"
# periods = ["* * * * sat-sun"]
И это сработало! Бонусом это внесло удобство и для администрирования: стало возможно спокойно зайти, перезагрузить нужную группу раннеров, не трогая вторую.
Историю можно было бы на этом закончить, если бы проблема была одна. Другая заключалась в длительности сборок. Тут все было завязано на том, что пакеты и артефакты локально лежали на Gitlab-мастере, а скорости между ним и раннерами оставляли желать лучшего.
Решено было переносить артефакты в объектное хранилище совместимое с S3 API, чтобы сократить скорость скачивания и скорость загрузки. В совокупности бы это позволило прокачать скорость сборки.
gitlab_rails['object_store']['enabled'] = true
gitlab_rails['object_store']['proxy_download'] = false
gitlab_rails['object_store']['connection'] = {
'provider' => 'AWS',
'endpoint' => '{{ gitlab_s3_endpoint }}',
'path_style' => true,
'region' => '{{ gitlab_s3_region }}',
'aws_access_key_id' => '{{ gitlab_s3_access_key }}',
'aws_secret_access_key' => '{{ gitlab_s3_secret_key }}'
}
gitlab_rails['object_store']['objects']['lfs']['enabled'] = false
gitlab_rails['object_store']['objects']['artifacts']['bucket'] = '{{ gitlab_artifacts_s3_bucket }}'
gitlab_rails['object_store']['objects']['packages']['bucket'] = '{{ gitlab_packages_s3_bucket }}'
Но и на этом не все. При сборке на Windows-раннерах были специфические окружения, например, тот же SDK. Все это приходилось пересобирать на Docker-контейнерах.
Все не было гладко и с холодным запуском первого раннера: согласно нашей политике, минимальное количество раннеров = 0. Поэтому первый раннер создает тот, кто запустил задачу. При этом приходится потратить некоторое количество времени. Зато следующие джобы выполняются уже мгновенно. Здесь, к сожалению, конкретного решения у нас не случилось. В итоге посчитали, что лучше всего будет оставить один раннер в ожидании задачи.
Про интеграцию в пайплайны
Новые проекты собирались в Docker, поэтому было удобно и не запарно просто поменять тэг раннера и на этом успокоиться. Но были и специфические проекты 一 пусть и устаревшие, но из той категории, которые могут в любой момент пересобраться.
Вот там потребовалась уже более глобальная переделка с использованием Docker контейнеров. И, несмотря на то, что без небольших багов и застоев не обошлось, переход на облачные решения прошел гладко. Гораздо более гладко, чем мы изначально предполагали, приготовившись к самому настоящему завалу.
Промежуточные результаты
Чем мы можем похвастаться на данный момент?
Часть команд перешла на использование облаков и отзываются о наших раннерах более, чем положительно: удобно, понятно и просто.
Часть же команды пока пребывает на старых раннерах. Тут все завязано на специфике проектов. За один подход это не решишь.
Нет полной картинки и возможности ее получить из-за регулярных изменений конфигурации автоскеллера, которые то увеличивают, то уменьшают инстанс — мы еще в поисках оптимальных значений для себя
Но уже сейчас можно сказать, что и нагрузка на IT-отдел, и затраты на обслуживание «железа» поползли вниз, что не могло не понравится руководству компании.