GitOps в 2026 — это уже не «продвинутая практика», а базовый способ эксплуатации Kubernetes. Но для маленькой команды он часто выглядит пугающе: кажется, что нужно поднять зоопарк операторов и месяц это настраивать. На деле минимально жизнеспособный GitOps на Flux ставится за вечер. Разберёмся, чем он лучше привычного «kubectl apply из CI» и как собрать конфигурацию, где git — единственный источник истины.
Содержание
Открыть содержание
Что не так с «kubectl apply из CI»
Самый частый способ катить в Kubernetes: CI собирает образ и в конце делает kubectl apply. Работает, но у этого подхода есть встроенные проблемы.
Во-первых, CI имеет ключи от прода. Раннер с правами kubectl apply на кластер — это лакомая мишень: скомпрометировали пайплайн, скомпрометировали кластер. Во-вторых, нет источника истины. Кто-то сделал kubectl edit руками в три часа ночи — и состояние кластера разъехалось с тем, что в репозитории, а вы об этом не знаете. В-третьих, apply — это «выстрелил и забыл»: он применяет манифест один раз и не следит, что будет дальше.
GitOps переворачивает модель: не CI пушит в кластер, а кластер сам тянет желаемое состояние из git и непрерывно приводит себя к нему.
Что такое Flux
Flux — это GitOps-инструмент из экосистемы CNCF (graduated-проект). Важная деталь: это не один большой агент, а набор маленьких контроллеров, каждый из которых делает одну вещь:
- source-controller — следит за источниками: git-репозиториями, Helm-репозиториями, OCI-артефактами. Его задача — скачать и проверить, что появилась новая версия.
- kustomize-controller — применяет манифесты (в том числе через Kustomize) и непрерывно сверяет фактическое состояние с желаемым.
- helm-controller — то же самое для Helm-релизов.
- notification-controller — события и оповещения наружу (в Slack, в git как статусы).
Такое разделение — не академизм. Маленькие контроллеры проще понять, проще чинить и проще масштабировать по отдельности. Вам не обязательно знать их все: для старта хватает source- и kustomize-контроллеров.
Reconcile, а не apply
Ключевое отличие — модель reconcile. kustomize-controller не «применил и ушёл», он крутится в цикле: взять желаемое состояние из git → сравнить с фактическим в кластере → устранить разницу. Это даёт две вещи, которых нет у kubectl apply.
Drift detection. Изменили что-то руками? На следующем цикле reconcile Flux заметит расхождение и вернёт состояние к тому, что записано в git. Кластер физически не может надолго разойтись с репозиторием.
Pruning. Удалили манифест из git? С prune: true Flux удалит соответствующий ресурс и из кластера. Не будет «ресурсов-призраков», которые когда-то заапплаили и забыли. Git становится действительно полной картиной того, что есть в кластере.
Есть и третий, неочевидный плюс — безопасность pull-модели. В схеме «CI делает apply» наружу торчит доступ к кластеру: у раннера есть kubeconfig с правами на прод, и этот доступ нужно где-то хранить и защищать. В GitOps всё наоборот: контроллер живёт внутри кластера и сам ходит за изменениями в git. Кластеру не нужны входящие соединения и внешние ключи к нему — он только читает репозиторий. Поверхность атаки сжимается: компрометация CI больше не означает автоматически компрометацию прода, потому что CI вообще не имеет прямого доступа к кластеру. Его задача — собрать образ и обновить манифест в git, а выкатка — забота Flux.
Сравнение в одной таблице
| kubectl apply из CI | GitOps (Flux) | |
|---|---|---|
| Кто меняет кластер | CI пушит | кластер тянет сам |
| Ключи от прода | у раннера CI | внутри кластера |
| Источник истины | размывается | git, всегда |
| Drift руками | незаметен | откатывается reconcile |
| Удаление ресурсов | вручную | prune: true |
| Откат | пере-деплой | git revert |
Что нужно, чтобы поднять Flux
Понадобятся кластер, flux CLI и git-репозиторий (возьмём GitHub) с токеном. Самый приятный способ старта — flux bootstrap: команда сама ставит контроллеры в кластер и коммитит свои же манифесты в git, так что и сам Flux дальше управляется через GitOps.
export GITHUB_TOKEN=<personal-access-token>
flux bootstrap github \
--owner=acme \
--repository=infra \
--branch=main \
--path=clusters/prod \
--personal
После этого в репозитории acme/infra появится папка clusters/prod с манифестами Flux. Теперь опишем источник и то, что из него синкать. Один GitRepository (откуда брать) и один Kustomization (что применять):
# clusters/prod/apps.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: infra
namespace: flux-system
spec:
interval: 1m # как часто проверять git
url: https://github.com/acme/infra
ref:
branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
interval: 10m # как часто сверять кластер с git
sourceRef:
kind: GitRepository
name: infra
path: ./apps # синкать всё из папки apps/
prune: true # удалять из кластера то, что удалили из git
Это и есть минимально жизнеспособный GitOps: всё, что вы положите в папку apps/ репозитория, Flux применит и будет держать в актуальном состоянии. Хотите добавить сервис — кладёте его манифест в apps/ и делаете git push. Больше ничего.
Порядок и зависимости
Когда ресурсов становится больше, появляется вопрос порядка: например, CRD должны приехать раньше, чем объекты, которые их используют, а база — раньше приложения. В Argo CD для этого были sync waves; во Flux порядок задаётся через dependsOn между Kustomization-ами:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
spec:
dependsOn:
- name: infra-crds # apps поедут только после infra-crds
# ...
Секреты
Главный «а как же секреты» в GitOps решается просто: секреты тоже лежат в git, но зашифрованными. Flux нативно поддерживает SOPS — вы шифруете значения ключом age (или из облачного KMS), коммитите зашифрованный файл, а kustomize-controller расшифровывает его в кластере при применении. Приватный ключ лежит только в кластере, в репозитории — ничего читаемого.
Как масштабировать один root
Пока сервисов мало, хватает одного Kustomization на папку apps/. Когда их становится много, удобен паттерн «root, который порождает остальные»: один корневой Kustomization синкает папку, где лежат описания других Kustomization-ов, а те уже указывают на конкретные приложения. По смыслу это то же, что App-of-Apps в Argo CD, только средствами самого Flux.
Типичная раскладка репозитория выглядит так:
infra/
├── clusters/
│ └── prod/ # сюда bootstrap положил Flux + root-Kustomization
├── infrastructure/ # ingress, cert-manager, мониторинг
│ └── ...
└── apps/ # ваши сервисы
├── web/
└── api/
Корневой Kustomization указывает на infrastructure/ и apps/, между ними — dependsOn, чтобы платформенные вещи приезжали раньше приложений. Добавление нового окружения или кластера сводится к новой папке и новому Kustomization, а не к переписыванию пайплайнов. Вся топология остаётся в git и читается как дерево каталогов.
Автообновление образов
Отдельная приятная вещь: Flux умеет сам обновлять теги образов в git. Контроллеры image-reflector и image-automation сканируют реестр, находят новый подходящий тег по заданной политике (например, semver-диапазон), коммитят изменение манифеста обратно в репозиторий — и дальше срабатывает обычный reconcile. Получается полностью замкнутый цикл: собрали и запушили образ → Flux заметил новый тег → обновил манифест в git → выкатил в кластер. При этом история деплоев остаётся в git как обычные коммиты, а откат — это git revert. Для маленькой команды это убирает последний ручной шаг между сборкой и продом.
Как проверить, что всё работает
Сначала смотрим, что Flux подхватил источник и синканул:
flux get kustomizations
# NAME READY MESSAGE
# apps True Applied revision: main@sha1:...
READY=True и свежая ревизия означают, что кластер приведён к состоянию из git. Теперь — показательный тест на drift. Поменяем что-нибудь руками и подождём цикл reconcile:
kubectl scale deploy/web --replicas=5 # «ручное» вмешательство
# ждём interval reconcile...
kubectl get deploy/web # реплик снова столько, сколько в git
Flux вернёт количество реплик к тому, что записано в репозитории — это и есть доказательство, что git действительно стал источником истины. Принудительно запустить сверку, не дожидаясь интервала, можно через flux reconcile kustomization apps.
Ловушки
- Drift detection — это не «бэкап». Flux вернёт кластер к git, в том числе затрёт ваше срочное ручное исправление. Это by design: чините через коммит, а не через
kubectl edit, иначе reconcile откатит вас обратно. pruneтребует аккуратности. Удобно, но удалив манифест из git, вы удалите ресурс из кластера. Убедитесь, что вpruneне попадают объекты, которыми Flux не должен управлять.- Слишком частый
intervalбьёт по API. Очень маленький интервал reconcile нагружает kube-apiserver и git-провайдера. Для большинства кластеров минуты на источник и десяток минут на сверку — нормально. - Секреты в открытом виде. Самая опасная ошибка — закоммитить незашифрованный Secret «на минутку». Настройте SOPS до того, как первый секрет попадёт в репозиторий.
- kubectl apply мимо Flux. Если параллельно с GitOps кто-то продолжает катать руками, вы получите бесконечную войну reconcile с человеком. GitOps работает, только когда git — единственный путь изменений.
Итог
GitOps — это всего две вещи: git как единственный источник истины и reconciler, который непрерывно приводит к нему кластер. Flux даёт это набором маленьких контроллеров, которые ставятся одной командой flux bootstrap и дальше управляют собой сами. «Слишком сложно» — миф из мира больших энтерпрайзов: для одной команды и одного кластера рабочая конфигурация — это GitRepository плюс Kustomization, собранные за вечер. А когда дорастёте до прогрессивных выкаток, поверх той же модели ляжет Flagger — но это уже тема отдельной статьи.