Введение
В Kubernetes-кластере рано или поздно встаёт вопрос: где собирать Docker image приложений? Вариант «на своей машине разработчика» не масштабируется на команду. Вынос сборок на отдельную виртуальную машину решает эту проблему, но создаёт накладные расходы на обслуживание инфраструктуры и лишает ключевых преимуществ k8s: отдельная ВМ не масштабируется горизонтально под нагрузку, параллельные джобы конкурируют за общие CPU, RAM и диск, а накапливающийся кэш требует регулярной очистки.
Классических ответов два — Kaniko и BuildKit. Kubernetes executor с использованием Kaniko или BuildKit лишен этих недостатков: сборка происходит в изолированных подах прямо на нодах кластера, ресурсы динамически масштабируются, а виртуальные машины для Docker-демона больше не требуются.
Kaniko (GoogleContainerTools/kaniko) — инструмент от Google для сборки без privileged-контейнера. С июня 2025 года репозиторий архивирован и проект больше не развивается.
BuildKit (moby/buildkit) — стандартный движок
docker build, работающий в k8s в daemonless и rootless-режиме без привилегий ноды.
В этой статье будет протестировано 5 проектов разных языков и фреймворков собираются обоими инструментами в одних и тех же условиях, с замером времени, потребления CPU/RAM и поведения кэша. В конце — итоговая сводная таблица и разбор преимуществ и недостатков каждого подхода для продакшна.
Кэш. Оба инструмента используют только registry-кэш. Локальный кэш на ноде намеренно не используется — он копится на диске и требует очистки. Registry-кэш чистить не нужно: манифест кэша перезаписывается на каждом прогоне, а мусор подчищает garbage collection реестра. Хранясь вне пода, он переживает пересоздание и смену ноды.
Сравниваемые проекты
Бенчмарк собирает 5 проектов — по одному на характерный «профиль сборки»:
№ | Проект | Язык/Framework | Профиль сборки | Репозиторий |
|---|---|---|---|---|
1 | Next.js | Node/React SSR |
| |
2 | Nuxt 3 | Node/Vue SSR |
| |
3 | Go HTTP-сервис | Go |
| |
4 | Android APK | Java/Kotlin, Gradle |
| |
5 | ML: PyTorch inference | Python |
|
Архитектура стенда

Развёртывание
Перед развертыванием gitlab runner требуется чтобы у вас был создан Kubernetes кластер, S3 бакет и Container Registry.
В S3 бакет заливаем файл весов, например pytorch_model для job ml-pytorch.
Для мониторинга устанавливаем VictoriaMetrics k8s-stack.
1a. Установка GitLab Runner
Устанавливаем GitLab Runner с такой конфигурацией values.yaml:
gitlabUrl: https://gitlab.com/ # Количество параллельно выполняемых джобов. Пара kaniko+buildkit одного # проекта запускается одновременно (2 джоба), поэтому 2 достаточно. concurrent: 2 # RBAC для создания/управления подами джобов. rbac: create: true rules: [] serviceAccount: create: true runners: executor: kubernetes # Тег, по которому джобы в .gitlab-ci.yml выбирают этот раннер. tags: "k8s-benchmark" # Раннер принимает только джобы с указанным тегом. runUntagged: false # Глобальный конфиг executor'а. Задаём лимиты build-контейнера # (те же 4 CPU / 4 GiB, что были у старых K8s-джобов бенчмарка). config: | [[runners]] request_concurrency = 2 [runners.kubernetes] namespace = "{{ .Release.Namespace }}" image = "alpine:3.20" cpu_request = "1" cpu_limit = "4" memory_request = "1Gi" memory_limit = "12Gi" helper_cpu_request = "100m" helper_cpu_limit = "500m" helper_memory_request = "128Mi" helper_memory_limit = "512Mi" # Build-контейнер работает без privileged (условия замеров уравнены # с прежним стендом). BuildKit в rootless-режиме требует ослабленный # securityContext: seccomp/apparmor Unconfined (нужен unshare mount ns). privileged = false allow_privilege_escalation = true [runners.kubernetes.build_container_security_context] [runners.kubernetes.build_container_security_context.seccomp_profile] type = "Unconfined" [runners.kubernetes.build_container_security_context.app_armor_profile] type = "Unconfined" # Имена подов джобов важны для Grafana: GitLab Runner включает в них # GitLab project ID (runner-…-project-<ID>-concurrent-…), по которому # дашборд различает проекты, а инструменты различаются по label `image` # метрик cAdvisor (…/kaniko… vs …/buildkit…). pull_policy = "if-not-present" resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi
2. Настройка переменных GitLab CI
В группе gitlab.com/buildkit-vs-kaniko-benchmark → Settings → CI/CD → Variables задать необходимо задать YCR_REGISTRY_ID.
3. Настройка GitLab Runner для push в Yandex Container Registry
Для авторизации и пуша собранных образов в YCR не используются статические токены, пароли или секреты, сохранённые в репозитории:
Сервисный аккаунт нод кластера (
node_service_account): При развёртывании инфраструктуры через Terraform сервисному аккаунту нод кластера (sa_k8s_node) назначаются ролиcontainer-registry.images.pusherиcontainer-registry.images.pullerна созданный реестр (см.registry.tf). Поды GitLab Runner запускаются на этих нодах и имеют сетевой доступ к сервису метаданных инстанса.Получение короткоживущего IAM-токена из метаданных ноды: В секции
before_scriptкаждого CI-джоба выполняется запрос к сервису метаданных ноды по адресу169.254.169.254(интерфейс метаданных Google Compute Engine):TOKEN=$(wget -q -O - --header="Metadata-Flavor: Google" \ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token" \ | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')Токен генерируется платформой Yandex Cloud на базе привязанного к ноде сервисного аккаунта, действует ~12 часов и обновляется платформой автоматически.
Формирование конфигурации Docker (
config.json): Для аутентификации в реестреcr.yandexформируется заголовок с пользователемiamи полученным токеном в качестве пароля:AUTH=$(printf "iam:%s" "$TOKEN" | base64 | tr -d '\n') # Для Kaniko (/kaniko/.docker/config.json): mkdir -p /kaniko/.docker echo "{\"auths\":{\"$YCR_REGISTRY\":{\"auth\":\"$AUTH\"}}}" > /kaniko/.docker/config.json # Для BuildKit (~/.docker/config.json): mkdir -p ~/.docker echo "{\"auths\":{\"$YCR_REGISTRY\":{\"auth\":\"$AUTH\"}}}" > ~/.docker/config.jsonУтилиты сборки (Kaniko и BuildKit) прозрачно считывают этот конфигурационный файл и аутентифицируются в реестре без необходимости хранить постоянные учетные данные.
4. Перенос проектов в репозитории
Эталонный .gitlab-ci.yml (одинаков для всех 5 проектов; $CI_PROJECT_NAME автоматически подставляет имя репозитория):
variables: # cr.yandex — верный хост Yandex Container Registry # (registry.yandex.cloud не существует в DNS). YCR_REGISTRY: cr.yandex stages: - build before_script: &docker-auth # Короткоживущий IAM-токен из метаданных ноды -> docker config для push/pull. - mkdir -p "$DOCKER_CONFIG" - TOKEN=$(wget -q -O - --header="Metadata-Flavor: Google" "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') - AUTH_B64=$(printf 'iam:%s' "$TOKEN" | base64 | tr -d '\n') - printf '{"auths":{"%s":{"auth":"%s"}}}' "$YCR_REGISTRY" "$AUTH_B64" > "$DOCKER_CONFIG/config.json" kaniko-build: stage: build image: gcr.io/kaniko-project/executor:v1.23.2-debug variables: DOCKER_CONFIG: /kaniko/.docker script: - /kaniko/executor --dockerfile=Dockerfile --context=dir://$CI_PROJECT_DIR --destination="$YCR_REGISTRY/$YCR_REGISTRY_ID/$CI_PROJECT_NAME-kaniko:latest" --cache=true --cache-repo="$YCR_REGISTRY/$YCR_REGISTRY_ID/$CI_PROJECT_NAME-kaniko" buildkit-build: stage: build image: moby/buildkit:v0.32.2-rootless variables: DOCKER_CONFIG: /home/user/.docker XDG_RUNTIME_DIR: /tmp/buildkit BUILDKITD_FLAGS: --oci-worker-no-process-sandbox script: - buildctl-daemonless.sh build --frontend dockerfile.v0 --local "context=$CI_PROJECT_DIR" --local "dockerfile=$CI_PROJECT_DIR" --output "type=image,name=$YCR_REGISTRY/$YCR_REGISTRY_ID/$CI_PROJECT_NAME-buildkit:latest,push=true" --import-cache "type=registry,ref=$YCR_REGISTRY/$YCR_REGISTRY_ID/$CI_PROJECT_NAME-buildkit" --export-cache "type=registry,ref=$YCR_REGISTRY/$YCR_REGISTRY_ID/$CI_PROJECT_NAME-buildkit,mode=max"
Ослабленный securityContext для rootless BuildKit (seccompProfile: Unconfined, appArmorProfile: Unconfined) задаётся на уровне раннера в gitlab-runner/values.yaml (build_container_security_context) — в .gitlab-ci.yml его прописывать не нужно.
5. Дашборд в Grafana
Дашборд: kaniko-vs-buildkit-per-project.json — импортируйте в Grafana вручную (Dashboards → Import → Upload JSON).
Скриншоты дашборда
Скриншоты сняты на тёплом прогоне (с прогретым registry-кэшем) — сравнение Kaniko и BuildKit в одинаковых условиях кэш-хита.
Next.js — потребление CPU (BuildKit слева, Kaniko справа), тёплый кэш.

Next.js — потребление памяти, тёплый кэш.

Nuxt 3 — потребление CPU (BuildKit слева, Kaniko справа), тёплый кэш.

Nuxt 3 — потребление памяти, тёплый кэш.

Go HTTP-сервис — потребление CPU (BuildKit слева, Kaniko справа), тёплый кэш.

Go HTTP-сервис — потребление памяти, тёплый кэш.

Android APK — потребление CPU (BuildKit слева, Kaniko справа), тёплый кэш.

Android APK — потребление памяти, тёплый кэш.

ML: PyTorch inference — потребление CPU (BuildKit слева, Kaniko справа), тёплый кэш.

ML: PyTorch inference — потребление памяти, тёплый кэш.

Итоговая сводная таблица (тёплый кэш)
Проект | Время kaniko (с) | Время buildkit (с) | Выигрыш BuildKit % |
|---|---|---|---|
nextjs | 75 | 13 | 83 |
nuxtjs | 50 | 13 | 74 |
golang | 33 | 13 | 61 |
android | 109 | 14 | 87 |
ml-pytorch | 289 | 19 | 93 |
Проект | CPU kaniko (cores) | CPU buildkit (cores) | RAM kaniko | RAM buildkit |
|---|---|---|---|---|
nextjs | 1.94 | 0.04 | 1.55 GiB | 3.2 MiB |
nuxtjs | 1.52 | 0.04 | 1.18 GiB | 3.5 MiB |
golang | 0.67 | 0.05 | 88 MiB | 3.2 MiB |
android | 1.74 | 0.04 | 1.38 GiB | 3.2 MiB |
ml-pytorch | 1.78 | 0.04 | 10.87 GiB | 3.5 MiB |
Вывод
BuildKit на всех пяти проектах держит ~0.04–0.05 CPU и ~3 MiB RAM (уровень простоя контейнера). Это cache hit: слои берутся из registry, локальной сборки нет. Kaniko на том же кэше всё равно грузит CPU (0.67 на golang, 1.5–1.9 на Node/Android/ML) и держит большой working set: 88 MiB (golang), 1.2–1.6 GiB (nextjs/nuxtjs/android), 10.9 GiB (ml-pytorch).
Следствие по времени: BuildKit укладывается в 13–19 с на любом профиле; Kaniko — от 33 с (golang) до 289 с (ml-pytorch). Разница не в «многопоточности под нагрузкой», а в том, что тёплый кэш BuildKit почти обнуляет работу, а Kaniko продолжает разворачивать слои и жечь CPU/RAM.
Ссылки:
Исходный код: https://github.com/patsevanton/buildkit-vs-kaniko-benchmark

