Использование Yandex Serverless Containers как GitLab-раннера: экономия, ограничения и настройка пайплайнов
Как оптимизировать инфраструктуру CI/CD с помощью Yandex Serverless Containers. Реальный кейс: настройка GitLab-раннеров, Kaniko и ускорение pipeline
Мы, команда Спектр, протестировали Yandex Serverless Containers в роли GitLab-раннера. Идея подхода выглядит рационально: не держать постоянный сервер под CI, а запускать runner только на время выполнения job.
На практике решение оказалось рабочим, но не бесшовным. Serverless-раннеры меняют модель выполнения пайплайнов: окружение становится одноразовым, локальный Docker cache исчезает, а привычный Docker-in-Docker перестает подходить. Поэтому переход требует не только настройки интеграции с GitLab, но и пересмотра build jobs.
Делимся тем, как этот переход прошел у нас: что получилось хорошо, где появились ограничения и какие изменения в CI/CD потребовались, чтобы сборки оставались предсказуемыми.
Зачем мы смотрели в сторону serverless-раннеров
Классический GitLab Runner обычно живет на отдельном сервере или рядом с self-managed GitLab. Подход выглядит понятным и стабильным, но у этой медали есть обратная сторона — эксплуатационная стоимость:
- Сервер нужно обслуживать: обновлять ОС, следить за местом на диске, чистить Docker cache, контролировать нагрузку и доступность. Если раннер стоит на том же сервере, что и GitLab, тяжелые сборки начинают конкурировать с основным сервисом за CPU, RAM и диск.
- Неравномерная нагрузка. В одни дни CI почти не используется, в другие нужно запустить несколько сборок параллельно. Для постоянного сервера это неудобно, поскольку ресурсы либо простаивают, либо их не хватает в пиковые моменты.
Serverless-подход решает именно эту инфраструктурную задачу. Runner не существует постоянно, он появляется на время job, выполняет работу и исчезает.
Как устроен подход с Yandex Serverless Containers
Официальная схема Яндекса строится вокруг webhook и одноразового runner-процесса.
Когда в GitLab появляется job с нужным тегом, GitLab отправляет webhook. Yandex Serverless Containers запускает контейнер, внутри которого стартует GitLab Runner. Runner подключается к GitLab, забирает job, выполняет ее и завершает работу. После этого serverless-контейнер удаляется.
Ключевое свойство такой схемы - эфемерность. Между запусками не сохраняется состояние окружения. У следующей job будет новый контейнер, новая файловая система и пустой локальный cache.
Это дает плюсы на уровне инфраструктуры:
- не нужно держать отдельный runner-сервер;
- ресурсы оплачиваются только во время выполнения job;
- несколько сборок можно запускать параллельно без ручного масштабирования серверов;
- CI перестает нагружать сервер, на котором работает GitLab.
Но та же эфемерность становится главным источником технических ограничений.
Где начинается отличие от обычного раннера
На постоянном раннере со временем накапливается полезное состояние: Docker images, layer cache, зависимости, временные файлы, прогретые сетевые соединения. Не все это нужно хранить бесконечно, но для повторных сборок такой cache часто дает существенный выигрыш.
В serverless-среде этого нет. Каждый запуск начинается с чистого окружения. Это влияет на все build jobs, которые раньше неявно опирались на локальный Docker daemon и его cache.
Самое заметное ограничение - невозможность использовать привычный Docker-in-Docker-подход.
Почему пришлось отказаться от Docker-in-Docker
До перехода Docker-образы собирались обычным способом: docker:dind, docker login, docker build, docker push.
Для serverless-окружения этот сценарий не подходит. У job нет привычного доступа к хосту и локальному Docker daemon. Значит, сборка образов должна выполняться инструментом, которому не нужен привилегированный Docker daemon.
Смена технологического стека для сборки
Отсутствие постоянного хоста потребовало перехода на утилиты, предназначенные для изолированной сборки образов в пользовательском пространстве (user-space).
Мы выбрали Kaniko. Он собирает Docker-образы внутри контейнера и подходит для окружений, где DinD недоступен.
Минимальный шаблон job выглядит так:
build:
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
tags:
- yandex-serverless
script:
- mkdir -p /kaniko/.docker
- printf '{"auths":{"%s":{"username":"%s","password":"%s"}}}\n' "$CI_REGISTRY" "$CI_REGISTRY_USER" "$CI_REGISTRY_PASSWORD" > /kaniko/.docker/config.json
- /kaniko/executor
--context "$CI_PROJECT_DIR"
--dockerfile "$CI_PROJECT_DIR/docker/Dockerfile"
--destination "$IMAGE_NAME"
--snapshot-mode=redo
--use-new-run
Где возникли сложности
Основные проблемы с Kaniko появляются там, где во время сборки идет активная работа с файловой системой: создается много файлов, меняются директории с зависимостями, выполняется очистка временных данных.
Для Docker daemon на постоянном раннере это привычная задача. Он работает с файловой системой через свои механизмы слоев и может переиспользовать локальный cache. Если runner живет долго, повторные сборки получают преимущество от уже скачанных образов и ранее собранных слоев.
Kaniko работает иначе. Он не обращается к Docker daemon, а сам анализирует изменения файловой системы и делает снапшоты. В эфемерном serverless-окружении это особенно заметно: локального cache нет, а каждая job стартует с чистого состояния.
Backend-сборки обычно переносят это проще. Там тоже есть зависимости и файловые операции, но чаще меньше массовой работы с мелкими файлами, а сам Dockerfile остается достаточно предсказуемым: скачать базовый образ, установить зависимости, собрать приложение, отправить результат в registry.
Самым заметным примером стали frontend-сборки. На первый взгляд frontend-образ должен быть простым: собрать статику и положить ее в nginx. В финальный runtime-образ действительно попадает только результат сборки. node_modules там быть не должно.
Но проблема возникает раньше — в builder-stage. Чтобы получить статику, нужно выполнить yarn install и yarn build. На этом этапе внутри контейнера появляется node_modules, а это десятки или сотни тысяч мелких файлов.
Из-за этого дорогими становятся операции, которые раньше почти не замечались:
- установка зависимостей;
- этап Linking dependencies в Yarn;
- снапшот слоя после появления node_modules;
- удаление node_modules;
- удаление файловой системы builder-stage перед финальным nginx-stage.
Именно поэтому фраза “node_modules не попадает в итоговый образ” не решает проблему. Он не попадает в runtime, но все равно существует во время сборки, а Kaniko должен пройти через эту файловую систему.
Что можно сделать, чтобы ускорить frontend-сборку
Универсального набора флагов для ускорения Kaniko нет. Эффект зависит от Dockerfile, пакетного менеджера, количества файлов, размера базового образа и того, насколько сборка опиралась на локальный cache раннера.
Но есть несколько направлений, которые стоит проверить в первую очередь.
Уменьшить builder-stage
Чем меньше файловая система на этапе сборки, тем меньше Kaniko распаковывает, анализирует и удаляет. Для frontend-сборок это особенно важно: если в builder-образе есть инструменты, которые не участвуют в production build, их стоит вынести из этого этапа или заменить более легким базовым образом.
Не кэшировать все автоматически
Registry cache Kaniko может помочь на простых слоях, но для слоев с большим количеством мелких файлов эффект бывает обратным. Например, cache слоя с node_modules может стоить дороже, чем повторная установка зависимостей: слой нужно сохранить, проверить и скачать обратно. Поэтому cache лучше включать точечно и сравнивать время сборки на реальных pipeline.
Сократить лишнюю работу с зависимостями
Для Yarn можно проверить отключение optional dependencies и контроль install scripts. Это уменьшает объем необязательных действий во время install:
yarn install --immutable --ignore-optional
Держать runtime-образ минимальным
Multi-stage build остается обязательным. В финальный nginx-образ должна попадать только собранная статика, а не зависимости и временные файлы:
RUN yarn install --immutable --ignore-optional \
&& yarn build \
&& rm -rf node_modules /root/.cache /tmp/*
FROM nginx:1.21 AS runner
COPY --from=builder /app/build /usr/share/nginx/html
Оценить другой пакетный менеджер
Для тяжелых frontend-проектов можно рассмотреть, например, переход с Yarn на pnpm. У pnpm другая модель установки зависимостей: content-addressable store и более эффективная работа с повторяющимися пакетами. Но это отдельная миграция, а не быстрая настройка CI.
Нюансы self-managed GitLab
В документации Яндекса не указана минимальная версия GitLab, с которой сценарий работает без дополнительных настроек. На нашем инстансе проявились две несовместимости.
Первая - workflow создания раннеров. GitLab поддерживает старый способ через registration token и новый способ через runner authentication token. Для Yandex Serverless Containers нужен новый workflow, но в нашей версии GitLab он не был доступен в интерфейсе по умолчанию. Но его можно включить поменяв feature flags через Rails console:
sudo gitlab-rails runner "Feature.enable(:create_runner_workflow_for_admin)"
sudo gitlab-rails runner "Feature.enable(:create_runner_workflow_for_namespace)"
Названия feature flags зависят от версии GitLab, поэтому перед применением нужно сверяться со своим релизом.
Вторая проблема - Custom headers для webhook. Для настройки со стороны Yandex Cloud потребовались заголовки, а в нашей версии GitLab их нельзя было задать штатно в интерфейсе webhook.
Мы решили это через встроенный Nginx GitLab: добавили proxy route в nginx['custom_gitlab_server_config'] в gitlab.rb. Route принимал запрос, добавлял нужные заголовки и проксировал его дальше в Yandex Serverless Containers подставляя нужные заголовки.
Пример такого proxy route:
nginx['custom_gitlab_server_config'] = "
location = /yandex-runner-webhook {
if ($request_method != POST) {
return 405;
}
if ($http_x_gitlab_token != '<GITLAB_WEBHOOK_SECRET>') {
return 403;
}
proxy_pass https://<YANDEX_CONTAINER_ID>.containers.yandexcloud.net/webhook;
proxy_ssl_server_name on;
proxy_ssl_name <YANDEX_CONTAINER_ID>.containers.yandexcloud.net;
proxy_set_header Host <YANDEX_CONTAINER_ID>.containers.yandexcloud.net;
proxy_set_header Authorization 'Api-Key <YANDEX_API_KEY>';
proxy_set_header X-Ycf-Container-Integration-Type 'async';
proxy_set_header Content-Type $content_type;
proxy_set_header X-Gitlab-Event $http_x_gitlab_event;
proxy_set_header X-Gitlab-Token $http_x_gitlab_token;
proxy_connect_timeout 10s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
}
"
Проверка $http_x_gitlab_token нужна для безопасности: GitLab передает этот секрет в заголовке X-Gitlab-Token, а значение можно задать в интерфейсе webhook. Так proxy route не будет принимать произвольные POST-запросы извне.
Когда serverless-раннеры имеют смысл
Yandex Serverless Containers хорошо подходят для CI/CD с нерегулярной или всплесковой нагрузкой. Если сборки запускаются несколько раз в день, а не непрерывным потоком, serverless-подход снижает стоимость владения и убирает необходимость обслуживать отдельный runner-сервер.
Подход особенно уместен, если:
- раннеры сейчас конкурируют с GitLab за ресурсы;
- нет большого количества запусков пайплайнов;
- нужна параллельность без покупки новых серверов;
- большая часть jobs не зависит от локального Docker cache;
- backend-сборки занимают основную часть pipeline;
- команда готова адаптировать CI под эфемерное окружение.
Но это не универсальная замена классическим раннерам. Если пайплайны идут почти постоянно, frontend-сборки тяжелые, а локальный Docker cache сильно влияет на время релиза, выделенный runner может быть быстрее и экономически оправданнее.
Итог
Yandex Serverless Containers можно использовать как GitLab-раннеры, но переход стоит рассматривать как изменение архитектуры CI/CD, а не как замену одного executor на другой.
Главные последствия перехода:
- Docker-in-Docker нужно заменить на rootless-инструмент, например Kaniko;
- локальный Docker cache исчезает;
- frontend-сборки требуют отдельной оптимизации;
- старые версии self-managed GitLab могут потребовать дополнительных настроек;
- экономику нужно считать по фактической частоте и весу pipeline.
В нашем случае подход оказался рабочим, но потребовал тюнинга. Для части jobs serverless-раннеры дают понятный инфраструктурный выигрыш: меньше постоянных серверов, проще параллельность, оплата за использование. Для тяжелых сборок важно заранее проверить время выполнения и не исключать гибридную модель.
Оптимальная стратегия - переносить не все сразу, а по типам задач. Serverless хорошо подходит для легких, изолированных и нерегулярных jobs. Там, где локальный cache критичен, классический runner может оставаться лучшим инструментом.
Нужны услуги DevOps?
Рассмотрите Spectr в качестве подрядчика. Просто оставьте заявку, мы свяжемся и обсудим ваши задачи
