GitLab Runner с Kubernetes Executor: масштабируемая CI-ферма
Привет! Меня зовут Семенов Евгений Сергеевич, я директор компании АйТи Фреш. Скажите честно: вам знакома ситуация, когда пары раннеров на виртуалках катастрофически не хватает, а пайплайны бесконечно стоят в очереди? Это верный знак: пора переводить GitLab CI в Kubernetes! По нашей практике, это занимает совсем немного времени — буквально 1-2 рабочих дня, если кластер уже готов. Сегодня я детально разберу каждый аспект: от helm-чарта и DinD до кэша, автоскейлинга и важнейших вопросов безопасности.
Плюсы и минусы k8s-executor
Плюсы:
- Каждый Pod для новой job создаётся полностью изолированным. Мы устанавливаем чёткие лимиты и таймауты. Это обеспечивает безопасность и предсказуемость.
- Автоскейлинг работает безупречно: cluster-autoscaler сам определяет нагрузку. Он мгновенно увеличивает количество нод, когда это нужно, и так же оперативно их гасит, когда наступает простой. Максимальная эффективность!
- Мы добились единой точки управления. Все настройки — через GitOps и helm values. Это значительно упрощает контроль и делает процесс предсказуемым.
- Теперь у вас могут быть множественные раннеры, каждый со своими тегами, и все они управляются одним менеджером. Это дает огромную гибкость!
Минусы:
- Да, это чуть сложнее, чем обычный shell-раннер. Здесь нужно хорошо разбираться в pod spec, PVC и, конечно, в механизмах кэширования. Но полученные возможности того стоят.
- Работая с DinD (Docker-in-Docker), учтите: он требует режима privileged или же специальной настройки ядра. Это важный нюанс.
- Overhead при старте pod-а 5–15 секунд.
Подготовка кластера
Требования:
- Kubernetes 1.27+ (минимум 1.24).
- Для временных PVC (если вы их используете) понадобится отдельный Storage class. Это важно предусмотреть.
- Node pool под CI — отдельный с тайнтами
ci=true:NoSchedule. - Ноды кластера должны иметь доступ к GitLab API. Это может быть как http, так и https. Проверьте этот момент.
- Namespace
gitlab-runner.
kubectl create namespace gitlab-runner
kubectl label ns gitlab-runner pod-security.kubernetes.io/enforce=privileged
Регистрационный токен
В GitLab → Admin Area → CI/CD → Runners → New instance runner. Копируем токен вида glrt-xxxxx.
Helm values
# values.yaml
image:
registry: registry.gitlab.com
image: gitlab-org/gitlab-runner
gitlabUrl: https://gitlab.company.ru/
runnerToken: "glrt-XXXXXXXXXXXXXXXXXXXX"
concurrent: 30
checkInterval: 10
rbac:
create: true
clusterWideAccess: false
runners:
config: |
[[runners]]
name = "k8s-runner"
executor = "kubernetes"
[runners.kubernetes]
namespace = "gitlab-runner"
image = "ubuntu:22.04"
privileged = true
poll_timeout = 600
cpu_request = "200m"
memory_request = "256Mi"
cpu_limit = "2"
memory_limit = "4Gi"
service_cpu_request = "100m"
service_memory_request = "128Mi"
helper_cpu_request = "50m"
helper_memory_request = "64Mi"
[runners.kubernetes.node_selector]
"role" = "ci"
[[runners.kubernetes.volumes.empty_dir]]
name = "docker-certs"
mount_path = "/certs/client"
medium = "Memory"
[runners.cache]
Type = "s3"
Shared = true
[runners.cache.s3]
ServerAddress = "s3.company.ru"
AccessKey = "$S3_ACCESS"
SecretKey = "$S3_SECRET"
BucketName = "gitlab-cache"
Insecure = false
helm repo add gitlab https://charts.gitlab.io
helm upgrade --install gitlab-runner gitlab/gitlab-runner \
-n gitlab-runner -f values.yaml
DinD для сборки образов
Пример .gitlab-ci.yml:
build-image:
stage: build
image: docker:27
services:
- name: docker:27-dind
alias: docker
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_DRIVER: overlay2
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
Альтернатива: BuildKit rootless
Если не хотите privileged:
build-image:
image: moby/buildkit:rootless
variables:
BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
script:
- |
buildctl-daemonless.sh build \
--frontend dockerfile.v0 \
--local context=. --local dockerfile=. \
--output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA,push=true
Кэш зависимостей
| Стек | Cache key | Paths |
|---|---|---|
| Node.js | $CI_COMMIT_REF_SLUG-node | node_modules, .npm |
| Python | $CI_COMMIT_REF_SLUG-pip | .venv, .cache/pip |
| Go | $CI_COMMIT_REF_SLUG-go | .cache/go-build, pkg/mod |
| Maven | $CI_COMMIT_REF_SLUG-m2 | .m2/repository |
Автоскейлинг нод
Чтобы CI-пул всегда был готов к работе, мы включаем cluster-autoscaler. Его настраиваем на высокую скорость scale-up и агрессивный scale-down.
extraArgs:
- --scale-down-unneeded-time=5m
- --scale-down-delay-after-add=3m
- --scale-down-utilization-threshold=0.4
На CI-нодах главное — это стоимость, а не доступность. Поэтому мы считаем: оптимальный вариант — это настроить scale-down всего через 5 минут простоя. Зачем платить за неиспользуемые ресурсы?
Реальный кейс: ускорение CI в 4 раза
Однажды в 2024 году мы работали с клиентом — финтех-стартап, 22 разработчика. У них был один shell-раннер на VM, 40 пайплайнов в очереди каждое утро. Среднее время запуска нового pipeline — 14 минут в очереди + 22 минуты сборка. Миграция на Kubernetes executor заняла 2 рабочих дня: поднятие менеджера в кластере, перенос .gitlab-ci.yml, настройка S3-кэша на MinIO на нашем Dell Xeon Platinum 8280 в дата-центре МТС Москва.
И какой же результат мы получили? Просто отличный! Конкурентность выросла до 25 параллельных job. Про очереди можно забыть — их просто нет. А среднее время выполнения пайплайна? Всего 8 минут! И это, на секундочку, уже с кэшем на S3 и параллельными этапами. За нашу работу клиент заплатил 68 000 руб. Но самое интересное — клиент сам посчитал: благодаря времени, которое сэкономили разработчики, все затраты окупились всего за 3 недели.
Безопасность
- Разделите менеджеры: один для protected-веток с секретами, другой для feature-веток без них.
- Network policies: job-pod имеет доступ только к нужным сервисам (registry, artifact store), не ко всему кластеру.
- Resource quotas в namespace, чтобы бешеный pipeline не укатил кластер.
- Сервис-аккаунт менеджера — минимум прав: создавать/удалять pods, читать secrets.
- Подписывайте образы через cosign после сборки — доверие в pull.
Грабли
- Забыли privileged на DinD — билд падает на
Cannot connect to the Docker daemon. - Часто возникает проблема: если для docker-certs использовать PVC вместо empty_dir, то PVC просто не успевает attach'иться. Итог — тайм-аут. Мы рекомендуем избегать такого подхода.
- Если вы размещаете кэш на PVC, привязанном к одной зоне, то столкнетесь с проблемой. При scheduling на ноду в другой зоне доступ к этому кэшу просто пропадает. Важно помнить про этот нюанс!
- Запустили `Concurrent = 100` процессов? Отлично! Но если забыли про `resource quotas`, ждите беды. Что случится? Ваши `ноды` просто уйдут в `OOM`. Вся память мигом закончится. Без чётких лимитов ресурсов, система захлёбывается. Наши инженеры знают: это реальная проблема, когда узлы начинают падать из-за нехватки памяти.
- Наш `helper_image` устарел. Это проблема! Теперь некоторые `git-команды` работают с `предупреждениями`. Приятного мало, правда? Образ не обновлялся, поэтому он не понимает современные команды. В ITFresh мы сталкивались с этим: старые версии создают хаос. Приходится регулярно его обновлять, чтобы все `git-команды` работали чисто, без лишних сообщений и ошибок.
Поднимем CI-ферму в Kubernetes
Мы, кстати, уже десятки раз разворачивали GitLab Runner в кластерах — это для нас привычное дело. У нас за плечами более 15 лет опыта в системном администрировании. В нашем распоряжении 8 мощных серверов Dell Xeon Platinum 8280 с 40G Mellanox, которые находятся прямо в дата-центре МТС Москва. Они готовы стать основой для ваших собственных K8s-кластеров. Поможем со всем: от настройки и миграции пайплайнов до их полной оптимизации.
Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш
FAQ — GitLab Runner в Kubernetes
- Зачем запускать раннеры в Kubernetes?
- Автоскейлинг: сколько pipeline-ов — столько pod-ов, ноль простоя ресурсов. Изоляция: каждая job в своём pod. Легко обновлять раннер, откатывать, добавлять несколько параллельных менеджеров. Для команд от 10 разработчиков — почти обязательная архитектура.
- DinD или BuildKit для сборки образов?
- DinD (docker-in-docker) — классика, но требует privileged. BuildKit с rootless — безопаснее и быстрее за счёт кэша слоёв. На свежих кластерах советую BuildKit или Kaniko, на старых совместимостей ради остаётся DinD.
- Где хранить кэш пайплайнов?
- S3-совместимый сторадж (MinIO, AWS S3, Ceph RGW). Настройка в helm values: runners.cache.s3.*. Кэш расшаривается между pod-ами, работает быстрее PVC и не привязан к ноде.
- Сколько параллельных job может тянуть один раннер-менеджер?
- По умолчанию 10 (concurrent). Практический потолок — ресурсы кластера и бандвидт на образы. У нас типично 20–50 concurrent jobs на менеджер, после этого поднимаем второй.
- Как изолировать секреты между job-ами?
- Используйте protected-переменные в GitLab — они доступны только в protected-ветках. Для продакшн-секретов — External Secrets + Vault, раннер получает короткоживущий токен и читает секрет в job.
