Кластер без CNI — это не «почти рабочий» кластер. В нём не поднимется ни CoreDNS, ни сам Flux, а манифест, который поставил бы сетевой плагин, применить будет некому. Разбираю, из чего складывается этот круг, каким порядком действий он разрывается и какая ловушка ждёт на последнем шаге
Проще всего показать на живом примере. Поднимем KinD с одним флагом disableDefaultCNI: true, то есть без сетевого плагина. Команда отработает, пожелает хорошего дня, и кластер формально будет
Кластер есть, а кластера нет
Смотрим ноду:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
bare-demo-control-plane NotReady control-plane 27s v1.37.0
Ready = False · KubeletNotReady
container runtime network not ready: NetworkReady=false
reason:NetworkPluginNotReady
message:Network plugin returns error: cni plugin not initialized
Теперь что в нём запустилось:
$ kubectl get pods -A
kube-system coredns-559f6c778d-27ntk 0/1 Pending
kube-system coredns-559f6c778d-nrt8z 0/1 Pending
kube-system etcd-bare-demo-… 1/1 Running
kube-system kube-apiserver-… 1/1 Running
kube-system kube-controller-manager-… 1/1 Running
kube-system kube-proxy-qd9xt 1/1 Running
kube-system kube-scheduler-… 1/1 Running
local-path-storage local-path-provisioner-… 0/1 Pending
Здесь видно главное. Всё, что Running, сидит в hostNetwork и пользуется адресом самой ноды. У всех пятерых он одинаковый, 172.18.0.3. А всё, чему нужен собственный адрес пода, висит в Pending и не получает ничего. Оба CoreDNS и local-path-provisioner
Это не «почти рабочий кластер». Это кластер, в котором работает ровно то, что обходится без сети подов
Но ведь всё лежит в git
Мысль напрашивается сама: положить Cilium в HelmRelease, и Flux всё сделает. Проверим. Накатим манифесты Flux на этот самый кластер:
$ kubectl apply -f gotk-components.yaml
$ kubectl get pods -n flux-system
helm-controller-7cddb7cf65-n7vfv 0/1 Pending
image-automation-controller-74c87c5f66-6pmnj 0/1 Pending
image-reflector-controller-f66556558-mbr25 0/1 Pending
kustomize-controller-8447db79c5-xvrk2 0/1 Pending
notification-controller-69d97d6f5c-8vhd9 0/1 Pending
source-controller-858998b4db-bhbb4 0/1 Pending
PodScheduled = False · Unschedulable
0/1 nodes are available: 1 node(s) had untolerated taint(s)
Шесть контроллеров из шести не планируются. Нода NotReady носит соответствующий taint, и шедулер отказывается ставить на неё обычные поды. Flux установлен и совершенно бесполезен
И здесь становится видно, в чём дело
Манифест, который поставил бы CNI, применить некому. kustomize-controller не запустился, потому что нет сети подов. Сети подов нет, потому что нет CNI. А CNI ставит ровно тот манифест, который некому применить
Почему об это спотыкаются поздно. Если в вашем кластере есть CNI по умолчанию, проблемы не видно вообще. KinD с kindnet, любой managed-кластер у провайдера: Flux там спокойно ставит и обновляет хоть Cilium, хоть что угодно. Сеть уже есть, круг не замыкается. Вылезает это ровно тогда, когда кластер поднимают с нуля. То есть при восстановлении после аварии, когда разбираться хочется меньше всего
Четыре звена зависимости и стрелка, которая их замыкает. Пока CNI нет, не существует ни одно звено левее него. Применять манифест нечему
Почему об это спотыкаются поздно. Если в вашем кластере есть CNI по умолчанию, проблемы не видно вообще. KinD с kindnet, любой managed-кластер у провайдера: Flux там спокойно ставит и обновляет хоть Cilium, хоть что угодно. Сеть уже есть, круг не замыкается. Вылезает это ровно тогда, когда кластер поднимают с нуля. То есть при восстановлении после аварии, когда разбираться хочется меньше всего
Решение: развести установку и эксплуатацию
Побеждать этот круг бессмысленно. Его надо признать и развести два действия, которые обычно называют одним словом «деплой»
Первичная установка. Разовая, руками, снаружи git. Задача одна: создать условия, в которых GitOps вообще способен работать
Эксплуатация. Версия, values, обновления. Это уже живёт в git и управляется Flux, как всё остальное
Граница проходит по времени, а не по компонентам. Слева то, что должно существовать до Flux. Справа то, чем Flux владеет
Порядок получается такой, и переставить в нём ничего нельзя:
-
Создать кластер без CNI. Нода
NotReady, обычные поды не планируются -
helm install ciliumруками -
Нода становится
Ready, taint снимается, CoreDNS получает адрес -
flux bootstrap. Контроллеры наконец планируются и разворачивают остальное из git -
Flux принимает уже существующий релиз Cilium и дальше ведёт его версию и values
На втором шаге стоит задержаться. Cilium ставится на неготовую ноду не вопреки правилам. Его агент сам работает в hostNetwork, сеть подов ему не нужна, он её и создаёт. Тем же приёмом живут kube-proxy и вся статика control plane
Пятый шаг в репозитории выглядит как обычный HelmRelease. Предупреждение в комментарии стоит написать сразу, чтобы через полгода никто не решил, будто на чистом кластере он отработает:
# ВНИМАНИЕ: этот HelmRelease НЕ устанавливает Cilium на чистом кластере.
# Flux сам работает поверх сети: пока нет CNI, не поднимется ни CoreDNS,
# ни source-controller, ни helm-controller — применять манифест нечему.
# Первичная установка всегда идёт руками:
# helm install cilium cilium/cilium --version 1.20.4 -n kube-system -f values.yaml
releaseName: cilium
interval: 1h
upgrade:
remediation:
retries: 0 # для CNI автоматический rollback опаснее
# неудачного обновления: Flux начнёт тасовать
# датаплейн и может оставить кластер без сети
Ловушка на последнем шаге
Здесь легко ошибиться
Логика кажется верной: релиз уже установлен, кладём в git манифест с теми же values, Flux его подхватит и ничего не сделает. Values можно скопировать прямо из helm get values и сверить диффом до коммита. Совпадение будет побайтным
Первая же реконсиляция всё равно перекатит DaemonSet
Совпадения values для этого мало. helm-controller рендерит чарт своим Helm SDK: вешает на объекты метки helm.toolkit.fluxcd.io/* и иначе нормализует кавычки, name: "cilium" превращается в name: cilium. Helm сравнивает отрендеренные манифесты, видит разницу и честно делает upgrade. В диффе между ревизиями 2733 строки, и почти всё это форматирование
Под агента меняется один раз, в момент приёма. Дальше helm-controller рендерит стабильно, и каждая следующая реконсиляция пустая
Обходится это заметно. На пару минут в CrashLoopBackOff уходят kube-controller-manager, kube-scheduler и cilium-operator, authentik отдаёт 503. Поднимается само, вмешиваться не нужно
Важно другое: платишь за это один раз. Повторная реконсиляция уже даёт generation 2 → 2 и тот же под. Значит подход рабочий, просто окно под один перекат надо закладывать заранее, а не надеяться на бесшовность
Как убедиться, что реконсиляция пустая
Чтобы отличить «ничего не произошло» от «произошло и сломалось», нужна точка отсчёта до изменения. Снимаем три величины
|
Что |
Чем |
Что доказывает |
|---|---|---|
|
Values в git против установленных |
|
расхождений в намерении нет |
|
Поколение DaemonSet |
|
менялся ли spec, а не метки |
|
Имя пода агента |
|
был ли реальный перекат |
Две последние снимаются до и после. Совпали generation и имя пода — реконсиляция действительно пустая, и это уже не вопрос доверия к логам
Где ещё так же
Закономерность шире, чем CNI. В любом GitOps-контуре есть слой, который обязан существовать до того, как контур заработает, и потому не может им управляться. Таких позиций обычно две
Первая — ключ расшифровки секретов. Flux не прочитает зашифрованные манифесты, не имея ключа, а ключ не положишь в репозиторий, который им же шифруется
Вторая — сетевой плагин, про него вся статья
Обе объединяет не техническая природа, а место в порядке загрузки. И обе стоит держать в списке явно. Не «мы забыли это автоматизировать», а «это принципиально не автоматизируется изнутри, вот команда»
Кстати, есть третий случай, который легко с ними спутать. У меня в кластере однажды нашёлся секрет с токеном Telegram, который тоже жил только в кластере и исчез при пересоздании. notification-controller молча падал на каждом событии. Но он-то мог лежать в git и теперь лежит. Это не ограничение архитектуры, а недосмотр, и лечится иначе: не абзацем в документации, а коммитом
Короткий порядок действий
-
Слой-основание ставьте руками и пишите это прямо в манифесте, чтобы потом не гадать
-
Values копируйте из живого релиза, а не по памяти, и сверяйте диффом до коммита
-
Закладывайте один перекат на момент приёма. Совпадения values для этого мало
-
Снимайте generation и имя пода до и после. Иначе «ничего не изменилось» остаётся догадкой
-
Для CNI отключайте автоматический откат. Тасовать сеть туда-сюда опаснее, чем оставить неудачное обновление неудачным
Занимает это минуты. Дешевле, чем потом полчаса смотреть на кластер, который формально поднялся и ничего не делает
Автор: mfn29ifb03
