Ты собрал образ руками, запушил в реестр, зашёл на ноду по ssh, сделал kubectl set image - и в пятницу вечером половина пользователей получила 500-е. Откатываться не на что, потому что ты не помнишь, какой тег крутился вчера. Знакомо. Ручной деплой в кластер - это не процесс, а лотерея, где приз - инцидент.
Грамотный kubernetes ci cd превращает выкатку в скучную, предсказуемую и повторяемую операцию. Любое изменение проходит один и тот же конвейер: собрали образ, проверили на дыры, запушили в реестр, описали желаемое состояние в Git, а контроллер в кластере сам привёл реальность к этому описанию. Если что-то пошло не так - автоматический откат по метрикам, а не нервный человек в три часа ночи. В этом уроке разберём весь путь от исходника до прогрессивного релиза с canary, и почему gitops ci cd вытеснил классический push из CI.

Сборка образа в кластере без демона: kaniko, buildah, BuildKit
Первая ловушка новичка - запустить docker build прямо в раннере, который сам крутится подом в кластере. Для этого нужен доступ к Docker-демону, а его прокидывают через монтирование /var/run/docker.sock с ноды. Это дыра размером с ворота: под получает фактический root на хосте, может собрать любой контейнер и стартовать его в обход всех политик. В современном кластере на containerd демона Docker вообще нет (вспомни урок про отказ от dockershim), монтировать нечего.
Поэтому образы в CI собирают без демона, в userspace. Исторически тут правил kaniko от Google: один pod-процесс читает Dockerfile, выполняет инструкции в своей файловой системе, пушит результат в реестр по HTTPS и завершается. Важная новость 2026 года - проект kaniko заархивирован и больше не развивается. Он ещё работает, но новых фич и заплаток безопасности не будет, так что для новых пайплайнов его брать не стоит.
Что выбрать сегодня:
- BuildKit (buildkitd) - современный дефолт. Умеет кэш слоёв в реестре, параллельную сборку стадий, mount-секреты на этапе build. Требует демон buildkitd, который запускают отдельным подом (rootless-режим есть). Это та же машинерия, что стоит за docker buildx из курса по Docker.
- Buildah - daemonless-инструмент из мира Podman/CRI-O. Хорош, если ты уже в этой экосистеме, умеет собирать образы скриптами вообще без Dockerfile. Минус - чаще требует привилегий или возни с rootless-настройками.
Код: Выделить всё
build:
image:
name: moby/buildkit:rootless
entrypoint: [""]
variables:
BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
script:
- mkdir -p ~/.docker
- echo "$DOCKER_AUTH_CONFIG" > ~/.docker/config.json
- |
buildctl-daemonless.sh build \
--frontend dockerfile.v0 \
--local context=. \
--local dockerfile=. \
--output type=image,name=registry.example.ru/app:$CI_COMMIT_SHA,push=true \
--export-cache type=registry,ref=registry.example.ru/app:buildcache \
--import-cache type=registry,ref=registry.example.ru/app:buildcache
Пайплайн build -> scan -> push -> deploy
Конвейер не заканчивается на сборке. Минимальный честный пайплайн - четыре стадии:
- build - собрали иммутабельный образ.
- scan - прогнали сканер уязвимостей (Trivy, Grype) по образу и манифестам. Нашли критическую CVE в базовом слое - пайплайн красный, дальше не едем. Сюда же кладут проверку манифестов на политики: kube-linter, conftest, kubeconform для валидации схемы.
- push - запушили в реестр (Yandex Container Registry, GitLab Registry, Harbor). Хорошая практика - подписать образ через cosign, чтобы кластер потом мог проверить происхождение.
- deploy - обновили желаемое состояние.
Деплой из CI или GitOps: почему Argo и Flux победили push
Теперь главный архитектурный выбор - как именно происходит деплой в kubernetes.
Push-модель (деплой из CI). Раннер держит kubeconfig с правами на кластер и сам делает kubectl apply, helm upgrade или kustomize build | kubectl apply. Просто, понятно, работает с первого дня. Проблемы вылезают на масштабе:
- Раннер имеет полный доступ к кластеру. Скомпрометировали CI - скомпрометировали прод. Credentials живут вне кластера.
- Дрейф состояния. Кто-то поправил реплики руками kubectl scale, и реальное состояние разошлось с тем, что в Git. CI об этом не знает.
- Нет единого источника правды о том, что реально крутится в проде прямо сейчас.
Что это даёт на практике:
- Кластер сам тянет изменения - наружу торчащих кредов с правами админа нет.
- Дрейф лечится автоматически: поправил кто-то руками - Argo вернёт как в Git (self-heal). В UI Argo CD ты видишь статус OutOfSync по каждому ресурсу.
- Откат - это git revert. История деплоев = история коммитов.
- Code review на инфраструктуру: меняешь прод через Merge Request, а не через доступ к kubectl.
Код: Выделить всё
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments
namespace: argocd
spec:
project: default
source:
repoURL: https://git.example.ru/infra/payments.git
targetRevision: main
path: deploy/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: payments
syncPolicy:
automated:
prune: true
selfHeal: true
Прогрессивные релизы: canary kubernetes и blue-green через Argo Rollouts
Обычный Deployment умеет только RollingUpdate - тупо заменяет старые поды новыми пачками. Если новая версия багованная, она поедет на 100% трафика, и ты заметишь это по графику ошибок постфактум. Прогрессивная доставка решает это: новую версию выкатывают на маленькую долю трафика, смотрят на метрики и только потом промоутят на всех.
Argo Rollouts - контроллер, который заменяет стандартный Deployment ресурсом Rollout и добавляет стратегии canary и blue-green. Canary kubernetes: подняли новую версию рядом, пустили на неё 10% трафика, подождали, проверили, повысили до 30%, и так до 100%. Blue-green: подняли полную копию (green) рядом с боевой (blue), прогнали тесты на green, потом мгновенно переключили сервис - удобно, когда нужен моментальный cutover и быстрый откат.
Самое ценное - argo rollouts умеет автоматический анализ по метрикам и rollback. Между шагами canary вставляется AnalysisTemplate, который дёргает Prometheus (или Datadog, или webhook) и проверяет, не вырос ли процент ошибок. Не прошло порог - rollout сам откатывается на стабильную версию, не дожидаясь человека.
Манифест canary с анализом (apiVersion argoproj.io/v1alpha1):
Код: Выделить всё
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: api
spec:
replicas: 6
strategy:
canary:
steps:
- setWeight: 10
- pause: { duration: 2m }
- analysis:
templates:
- templateName: success-rate
- setWeight: 30
- pause: { duration: 5m }
- setWeight: 60
- pause: { duration: 5m }
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
containers:
- name: api
image: registry.example.ru/api:abc123
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
metrics:
- name: success-rate
interval: 30s
successCondition: result[0] >= 0.99
failureLimit: 3
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{app="api",code!~"5.."}[2m]))
/
sum(rate(http_requests_total{app="api"}[2m]))
Код: Выделить всё
$ kubectl argo rollouts get rollout api --watch
Name: api
Status: Paused
Strategy: Canary
Step: 2/7
SetWeight: 10
ActualWeight: 10
Images: registry.example.ru/api:abc123 (canary)
registry.example.ru/api:9f0c1d (stable)
Replicas:
Desired: 6
Updated: 1
Ready: 6
Available: 6
Preview-окружения на каждый Pull Request
Мощная практика - поднимать эфемерное окружение на каждый PR. Открыл MR - CI собрал образ с тегом ветки, GitOps создал отдельный namespace preview-mr-128, развернул туда стек, и ревьюер получает живую ссылку потрогать фичу руками. Смержили или закрыли MR - namespace сносится, ресурсы освобождаются.
В Argo CD это делается через ApplicationSet с PR-генератором: он сам плодит Application на каждый открытый Pull Request и удаляет их при закрытии. Так QA тестирует не "на словах", а на работающем стенде, изолированном по namespace, с собственными лимитами через ResourceQuota.
Секреты в пайплайне и раннеры в кластере
Секреты - больное место. Категорически нельзя коммитить креды в Git в открытом виде, даже в приватный репозиторий, даже в base64 (это не шифрование). В GitOps секреты решают через Sealed Secrets (шифруешь публичным ключом контроллера, в Git лежит зашифрованный SealedSecret, расшифровать может только кластер) или External Secrets Operator (в Git лежит только ссылка, а сам секрет тянется из Vault, Yandex Lockbox, AWS Secrets Manager). В CI-переменные клади только то, что нужно самому пайплайну - токен реестра, ключ подписи - и помечай их masked/protected.
Раннеры в кластере. GitLab Runner и GitHub Actions Runner (ARC, Actions Runner Controller) можно запускать подами прямо в Kubernetes. Каждая CI-задача поднимает временный pod, отрабатывает и умирает - чисто и масштабируемо. Но помни про принцип наименьших привилегий: раннеру для GitOps не нужен доступ к проду. Его задача - собрать образ и сделать git push. Деплоит уже Argo изнутри. Если связываешь это с миром Jenkins из соседнего курса - там тот же принцип: Jenkins-агент в кластере собирает и пушит, а выкатку отдаёт GitOps-контроллеру, а не дёргает kubectl сам.
Типичные грабли и антипаттерны
- Тег latest в продакшене - теряешь воспроизводимость и контроль версий. Всегда иммутабельный тег по SHA коммита.
- docker.sock в раннере - root на ноде. Собирай через BuildKit/Buildah без демона.
- kubeconfig админа в CI-переменных - один украденный токен = весь кластер. Переходи на pull-модель GitOps.
- Деплой руками поверх GitOps - self-heal откатит твои ручные правки, и ты будешь долго думать, почему изменения "не применяются". Меняй через Git.
- Canary без анализа метрик - это просто медленный rollout. Без AnalysisTemplate ты всё равно ловишь баги глазами. Подключай Prometheus.
- Слепой analysis-запрос - если в начале канарейки трафика мало, метрика на пустом окне даёт деление на ноль и ложный аборт. Ставь разумный inconclusive-порог и достаточный interval.
- Установи Argo Rollouts в kind/minikube: kubectl create namespace argo-rollouts и kubectl apply -n argo-rollouts -f install.yaml с офиц. релиза.
- Поставь плагин kubectl argo rollouts (через krew).
- Возьми любой образ с двумя тегами (например, разный текст на странице) и опиши Rollout с canary-стратегией из трёх шагов: setWeight 20, pause 1m, setWeight 100.
- Примени, потом обнови image на второй тег: kubectl argo rollouts set image api api=твой/образ:v2.
- Смотри kubectl argo rollouts get rollout api --watch - увидишь, как поднимается canary.
- Откати вручную: kubectl argo rollouts abort api - и убедись, что трафик вернулся на stable.
- Почему монтировать docker.sock в CI-раннер опасно и чем собирать образ вместо этого в 2026 году?
- В чём принципиальная разница push-модели деплоя из CI и pull-модели GitOps по потоку доступа к кластеру?
- Что именно делает AnalysisTemplate в Argo Rollouts и как он приводит к автоматическому откату?
- Зачем preview-namespace на каждый PR и как его жизненный цикл связан с состоянием Merge Request?
Хороший CI/CD в Kubernetes - это конвейер build -> scan -> push -> deploy, где сборка идёт без демона на BuildKit, выкатка - через GitOps (Argo CD/Flux) с Git как единственным источником правды, а релизы прогрессивные: canary с анализом метрик и автоматическим откатом через Argo Rollouts. Ты убираешь человека из критического пути, а вместе с ним - и пятничные инциденты. Дальше будем углубляться в наблюдаемость, чтобы те самые метрики для analysis у тебя были честными.