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 требует privileged или специальной настройки ядра.
- Overhead при старте pod-а 5–15 секунд.
Подготовка кластера
Требования:
- Kubernetes 1.27+ (минимум 1.24).
- Storage class для временных PVC (если используется).
- 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-нодах нас не волнует availability, важнее стоимость. 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, очередь исчезла, среднее время pipeline — 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. - PVC вместо empty_dir для docker-certs — PVC не успевает attach'иться, тайм-аут.
- Кэш на PVC в одной зоне — при scheduling на ноду другой зоны нет доступа.
- Concurrent = 100 без resource quotas — OOM на нодах.
- helper_image устарел — некоторые 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.