Всем привет! Я - Кирилл, DevOps компании sports.ru. Не так давно мы начали процесс переезда в Yandex Cloud, хочу рассказать, как это было.

Параллельно мы стали искать, где еще мы можем применить сильные стороны публичного облака. Сразу вспомнилась давняя проблема с периодическими всплесками активности разработчиков, которая приводила к исчерпанию gitlab runners и, следовательно, к долгому ожиданию в очереди. Раньше для решения этой задачи нужно было добавить новый сервер для раннеров. Но, поскольку, это было эпизодически, горизонтальное масштабирование было нецелесообразным. А вот Яндекс Облако дает нам возможность быстро получить ресурсы на короткий период. В Gitlab подобный функционал реализуется через Docker Machine Executor.

Стоит отметить, что Docker Machine сейчас находится в deprecated статусе, но команда gitlab продолжает поддерживать свой форк, по этому можно считать это решение вполне надежным. Однако, в будущем autoscale будет реализован на собственной технологии gitlab, но сроков перехода на данный момент нет. 

Подготовительный этап

Перед установкой runner будет несколько подготовительных шагов. 

В первую очередь, нам нужна утилита Docker Machine. Тут достаточно скачать последнюю версию бинарного файла с репозитория Gitlab и разметить его по пути /usr/bin

Для работы с Yandex Cloud нам так же потребуется официальный драйвер который тоже нужно разметить в /usr/bin. Тут стоит отметить, что название файла драйвера крайне важно, как и права на него, он должен быть исполняемым. 

Последним подготовительным этапом будет генерация "Авторизованного ключа" в Яндекс Облаке. Для начала создаем служебный аккаунт с правами "compute.admin", если для вашего раннера вдруг потребуется еще и внешний IP адрес, добавьте аккаунту права "vpc.admin". Далее генерируем ключ, сделать это мож��о через UI или через утилиту yc. Последний вариант более удобен, сама команда выглядит следующим образом yc iam key create --service-account-name <service-account-name> --output key.json --folder-id <folder-id>. На выходе получаем json, который стоит держать в зашифрованном виде, например в ansible vault. 

Установка gitlab runner

Итак, как же нам получить авто-масштабируемый раннер? Для начала потребуется поднять виртуальную машину на которой будет установлен GitlabRunner. Для этих целей мы используем готовую ansible-роль debops.gitlab_runner, которая служила верой и правдой долгие годы, но в этом случаи подвела. 

Мейнтейнеры забросили развитие роли (последний коммит был в 2018 году) и, ожидаемо, появились неподдерживаемые параметры, в частности, устарел "Off Peak time mode" и появился отдельный раздел в настройке ранера: "runners.machine.autoscaling". Именно по этому пришлось форкнуть роль и попутно разметить ее у нас на github.

В этой роли не было никаких серьезных изменений, поэтому для ее настройки можно воспользоваться официальной документацией. Мы лишь добавили настройку раздела autoscaling. Выглядит это так:

gitlab_runner__machine_autoscaling:
  - period: "* * 7-19 * * mon-fri *"
    idel_count: 0
    idel_time: 600
    timezone: "UTC"

Особое же внимание стоит обратить на настройку драйвера для Яндекс Облака.

gitlab_runner__machine_idle_count: 0
gitlab_runner__machine_idle_time: 900
gitlab_runner__machine_name: "auto-scale-%s"
gitlab_runner__machine_driver: "yandex"
gitlab_runner__machine_options: ["yandex-sa-key-file=/etc/gitlab-runner/key.json", "yandex-folder-id={{ yc_qa__folder_id }}", "yandex-cloud-id={{ yc_cloud_id }}", "yandex-subnet-id={{ yc_qa__subnet_id }}", "yandex-use-internal-ip=true", "yandex-image-family=ubuntu-2004-lts", "yandex-cores=4", "yandex-disk-type=network-ssd", "yandex-memory=8", "yandex-preemptible=true"]

gitlab_runner__machine_idle_count - Количество раннеров которые должны быть всегда доступны; 0 означает, что раннер будет запускаться только по требованию.

gitlab_runner__machine_idle_time - Время (в секундах) до того, как раннер будет удален; отсчитывается от последней выполненной джобы. 

gitlab_runner__machine_name - имя автоматически заданной VM.

gitlab_runner__machine_driver - имя драйвера для Docker Machine.

gitlab_runner__machine_options - список передаваемых параметров.

У драйвера достаточно много параметров, их список и значения можно посмотреть в официальном репозитории. Остановимся только на нескольких из них. Ключ, который мы сгенерировали на предварительном этапе, нужно указать в параметре yandex-sa-key-file. Если инстанс gitlab находится в одной сети с раннером, то можно использовать только внутренний IP адрес, для этого указываем yandex-use-internal-ip=true. В противном случае нужно указать yandex-nat=true, тогда VM получит белый IP адрес. И еще один параметр, который стоит указать, yandex-preemptible=true, это позволит создавать "прерываемые машины", которые значительно дешевле. 

После настройки и прокатки роли мы получим раннер. А его конфигурационный файл будет выглядеть примерно так:

concurrent = 50

[[runners]]
  name = "gitlab-runner-test-autoscale"
  url = "https://gitlab-test.test.ru/"
  token = "TOKEN"
  environment = [ "DOCKER_BUILDKIT=1", "DOCKER_DRIVER=overlay2" ]
  limit = 10
  executor = "docker+machine"
  [runners.docker]
    image = "docker:dind"
    privileged = true
    disable_cache = false
    cache_dir = "/home/gitlab-runner/cache"
    cap_drop = [ "NET_ADMIN", "SYS_ADMIN", "DAC_OVERRIDE" ]
    volumes = [ "/var/run/docker.sock:/var/run/docker.sock", "/home/gitlab-runner/cache" ]
  [runners.machine]
    IdleCount = 0
    IdleTime = 600
    MaxBuilds = 100
    MachineName = "auto-scale-%s"
    MachineDriver = "yandex"
    MachineOptions = [
      "yandex-sa-key-file=/etc/gitlab-runner/key.json",
      "yandex-folder-id=<ID>",
      "yandex-cloud-id=<ID>",
      "yandex-subnet-id=<ID>",
      "yandex-use-internal-ip=true",
      "yandex-image-family=ubuntu-2004-lts",
      "yandex-cores=4",
      "yandex-disk-type=network-ssd",
      "yandex-memory=8",
      "yandex-preemptible=true"
    ]
            [[runners.machine.autoscaling]]
          Periods = ['* * 7-19 * * mon-fri *']
          IdleCount = 2
          IdleTime  = 1800
          Timezone  = "UTC"
            [[runners.machine.autoscaling]]
          Periods = ['* * * * * sat,sun *']
          IdleCount = 0
          IdleTime  = 600
          Timezone  = "UTC"

Исходя из настроек, в буднии дни с 7 до 19 Gitlab будет держать на "горячем старте" две VM. А вот ночью и в выходные ради экономии все виртуалки будут удаляться. Кроме того время их ожидания сократится до 600 секунд. В случае большого наплыва задач раннер сможет создать до 10 виртуальных машин (определяется параметром limit = 10), которые будут удалены, если на эти раннеры больше не придет заданий в течении 30 минут. Следует отметить, что на создание одной машины уходит от 2 до 3 минут, они добавятся к общему времени выполнения pipeline. 

Заключение

Как видите, решение не требует каких-то серьезных усилий. По сути уже есть все нужные компоненты, их требуется только собрать вместе. 

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

Если у вас был опыт использования динамических раннеров в публичных облаках, то с радостью готовы обсудить это в комментариях.