В статье «Как CI/CD и Flux делят ответственность в DocOps-проекте» я разобрал путь изменения от Markdown-файла до работающей версии сайта. Пайплайн собирает и публикует образ контейнера, затем меняет его версию в отдельном GitOps-репозитории, а Flux приводит Kubernetes к записанному там состоянию.

При разборе этой цепочки обнаружилась отдельная проблема. В GitOps-репозитории был настроен собственный CI, но пайплайн документации отправлял изменение напрямую в основную ветку, которую отслеживает Flux.

Получалось, что проверка существовала, но не стояла перед развёртыванием.

Это не разбор произошедшего инцидента. Риск обнаружился при изучении порядка событий. Чтобы увидеть его, не потребовалось дожидаться некорректного коммита или сбоя в Kubernetes.

Один коммит запускал два независимых процесса

После выпуска новой версии документации пайплайн менял тег образа в GitOps-репозитории и отправлял коммит напрямую в основную ветку.

Один push запускал два процесса:

коммит в основной ветке GitOps-репозитория
├── запускает GitOps CI
└── становится доступен Flux

GitLab начинал выполнять проверки. Flux независимо от него получал новую ревизию Git и запускал согласование состояния.

В модели Flux ресурс GitRepository получает указанную ветку и формирует артефакт из её текущей ревизии. Kustomization реагирует на изменение этого артефакта, строит манифесты и применяет их в Kubernetes.

Статус пайплайна GitLab в этой последовательности не участвует. Между CI и Flux не было причинной зависимости: ни один из них не ждал результата другого.

Gate — это зависимость, а не задержка

На первый взгляд риск мог казаться небольшим. CI обычно работает быстро, а Flux получает изменения с заданным интервалом. Пайплайн мог завершаться раньше почти при каждом обычном развёртывании.

Но deployment gate не может основываться на слове «обычно».

Увеличение интервала Flux тоже не решает проблему. Пайплайн иногда выполняется дольше, согласование можно запустить вручную, а webhook способен передать изменение Flux почти сразу.

Пока между проверкой и публикацией коммита нет явной зависимости, порядок остаётся случайным.

Настоящая граница допуска должна обеспечивать следующую последовательность:

изменение конфигурации
→ обязательная проверка
→ успешный результат
→ публикация в отслеживаемую ветку
→ согласование состояния Flux

В прежней схеме порядок был другим:

публикация в отслеживаемую ветку
→ параллельно проверка и согласование состояния

CI проверял состояние, которое уже было опубликовано для Flux.

Красный пайплайн ничего не возвращал назад

Если автоматический коммит содержал ошибку, GitOps CI мог обнаружить её и завершиться неуспешно.

Красный статус не удалял коммит из основной ветки. Он не возвращал предыдущий тег образа, не отменял уже начатое согласование состояния и не сообщал Flux, что эту ревизию применять нельзя.

К этому моменту Flux уже мог получить коммит и начать обработку манифестов. Если изменение отклонял сам Flux или Kubernetes API, это был другой защитный слой. GitOps CI по-прежнему ничего не блокировал.

Такая проверка оставалась полезной. Она могла обнаружить ошибку и сообщить о ней инженеру. Но в этой схеме CI работал как детектор после публикации, а не как предварительный допуск.

Как я перенёс проверку перед Flux

Пайплайн документации больше не отправляет изменение напрямую в основную ветку GitOps-репозитория.

После выпуска новой версии он запускает отдельный скрипт, который:

  1. Создаёт временную ветку в GitOps-репозитории
  2. Меняет тег образа документации
  3. Отправляет временную ветку в GitLab
  4. Создаёт Merge Request в основную ветку
  5. Включает автоматическое слияние после успешного пайплайна

Имя ветки содержит CI_PIPELINE_ID:

automation/docops/update-image-${CI_PIPELINE_ID}

Поэтому разные пайплайны DocOps не используют одну общую ветку. При повторном запуске того же пайплайна имя остаётся прежним, а существующие ветка и Merge Request используются повторно.

Текущая цепочка выглядит так:

DocOps выпускает новую версию
→ создаёт временную ветку в GitOps-репозитории
→ меняет тег образа
→ открывает Merge Request
→ запускается GitOps MR pipeline
→ включается auto-merge
→ GitLab ожидает успешного pipeline
→ Merge Request вливается в основную ветку
→ Flux получает новый коммит

Теперь изменение становится доступно Flux только после merge.

Auto-merge связывает CI и публикацию состояния

DocOps создаёт Merge Request через GitLab API и включает merge_when_pipeline_succeeds.

При этом пайплайн DocOps не ждёт фактического слияния. Его задача заканчивается после того, как временная ветка отправлена, Merge Request создан и auto-merge успешно включён.

Дальше порядок контролирует GitLab:

GitOps CI завершился успешно
→ GitLab выполняет merge
→ коммит появляется в основной ветке
→ Flux получает новую ревизию

В наблюдавшемся запуске auto-merge был включён, пока проверки ещё выполнялись. Пайплайн DocOps уже завершился, GitOps CI продолжил работу, а Merge Request автоматически влился после его успешного завершения.

Так появилась причинная зависимость, которой не было при прямом push:

успешный GitOps CI
→ merge
→ публикация состояния для Flux

Ошибка оставляет изменение вне основной ветки

Если GitOps CI завершается неуспешно, автоматического слияния не происходит. Изменение остаётся во временной ветке и открытом Merge Request.

Flux продолжает читать прежнее состояние основной ветки.

При конфликте с основной веткой Merge Request также не сливается автоматически. Скрипт не пытается разрешать конфликт или переписывать историю. Для продолжения требуется вмешательство инженера.

Это важное свойство процесса. Ошибка автоматизации не должна маскироваться автоматическим исправлением Git-истории.

Merge Request нужен не ради ручного согласования

В этой цепочке Merge Request не означает, что человек должен вручную подтверждать каждое обновление образа.

Изменение стандартное и воспроизводимое: пайплайн меняет версию уже собранного образа. Поэтому после успешных проверок оно может сливаться автоматически.

Merge Request здесь нужен как техническая граница:

временная ветка
→ проверка
→ основная ветка

Главное изменение заключается не в появлении новой сущности в GitLab. Изменился момент публикации желаемого состояния.

Раньше:

push в основную ветку
→ CI и Flux

Теперь:

ветка
→ CI
→ merge в основную ветку
→ Flux

Основная ветка начинает хранить состояние, прошедшее штатный процесс проверки.

Не нужно учить Flux ждать GitLab

Другой вариант — заставить Flux перед согласованием состояния запрашивать статус пайплайна GitLab.

Такое решение связывает механизм доставки с конкретной CI-системой и добавляет новую точку отказа. Потребуется определить, кто получает статус, где хранятся учётные данные, какой пайплайн считается обязательным и что делать при недоступности GitLab.

Появится и новая гонка: статус одной ревизии может быть получен уже после появления следующей.

Проще изменить точку публикации. Отслеживаемая ветка должна быть источником состояния, уже прошедшего предусмотренный процесс.

Тогда Flux продолжает выполнять свою обычную задачу: читает Git и приводит Kubernetes к записанному там состоянию.

Переход команды требует времени

Новая схема закрывает прямой push для штатной автоматизации DocOps. Однако она пока не запрещает все возможные обходные пути на уровне GitLab-проекта.

Пользователи с привилегированными ролями ещё могут напрямую изменить основную ветку или вручную выполнить merge. Это известное ограничение.

Полностью перейти от прямых push к обязательным Merge Request за одно изменение нельзя. Нужно согласовать процесс команды, определить исключения, настроить права и убедиться, что аварийные и эксплуатационные сценарии не заблокированы неожиданно.

Поэтому переход выполняется поэтапно:

сначала автоматизация начинает использовать MR
→ процесс проверяется на реальных выпусках
→ команда согласует правила работы
→ закрываются оставшиеся обходные пути

Deployment gate уже работает в штатной цепочке DocOps. Распространение этой границы на ручную работу команды — следующий этап.

Что именно подтверждает этот gate

Эта статья рассматривает порядок событий: когда изменение проверяется и когда оно становится доступно Flux.

Она не оценивает полноту конкретного набора проверок GitOps CI. Состав и качество проверок — отдельный вопрос.

Даже идеальная проверка не является gate, если запускается после прямого push в отслеживаемую ветку. И наоборот, правильный порядок не гарантирует, что набор проверок покрывает все возможные ошибки.

Это две разные задачи:

положение проверки в процессе
полнота самой проверки

В этой работе исправлялась первая.

Gate не подтверждает результат развёртывания

Правильно поставленная граница допуска решает только одну задачу: не позволяет изменению из штатного процесса попасть в отслеживаемую ветку до успешного завершения GitOps CI.

Она не подтверждает, что Kubernetes скачал образ, завершил обновление Deployment и запустил работоспособное приложение.

Подтверждение результата развёртывания требует отдельного наблюдения за Flux, Kubernetes и самим приложением. Это следующая граница процесса, а не обязанность GitOps CI.

Как проверить собственную GitOps-схему

Для проверки процесса достаточно ответить на четыре вопроса:

  • Какую ветку, тег или другую Git-ссылку отслеживает Flux
  • Кто и каким способом может изменить эту ссылку
  • Какие проверки должны завершиться до её изменения
  • Может ли штатная автоматизация обойти эти проверки

Дополнительно стоит проверить, существуют ли ручные обходные пути и являются ли они временной частью перехода или осознанным эксплуатационным исключением.

Если успешный результат CI не определяет, попадёт ли изменение в отслеживаемую ссылку, этот CI не является deployment gate.

Вывод

В моём DocOps-проекте наличие отдельного CI в GitOps-репозитории создавало впечатление предварительной проверки. Но порядок событий показывал другое: сначала коммит публиковался в основной ветке, а затем CI и Flux независимо начинали с ним работать.

Я заменил прямой push штатной автоматизации на временную ветку, Merge Request и auto-merge после успешного GitOps pipeline.

Теперь для автоматизированного выпуска порядок выглядит так:

изменение
→ GitOps CI
→ merge
→ Flux

Красный пайплайн или конфликт оставляет изменение вне основной ветки. При этом переход всей команды на обязательную работу через Merge Request продолжается отдельно и требует постепенного изменения правил и прав доступа.

Deployment gate определяется не наличием CI и не скоростью пайплайна. Он определяется порядком событий: сначала проверка, потом публикация состояния для Flux.