Курица, яйцо и CNI

в 18:52, , рубрики: cilium, devops, fluxcd, gitops, helm, kubernetes, linux

Кластер без CNI — это не «почти рабочий» кластер. В нём не поднимется ни CoreDNS, ни сам Flux, а манифест, который поставил бы сетевой плагин, применить будет некому. Разбираю, из чего складывается этот круг, каким порядком действий он разрывается и какая ловушка ждёт на последнем шаге

Flux управляет Helm, Helm ставит Cilium, Cilium даёт сеть обратно Flux — и эта связь разорвана

Flux управляет Helm, Helm ставит Cilium, Cilium даёт сеть обратно 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 нет, не существует ни одно звено левее него. Применять манифест нечему

Четыре звена зависимости и стрелка, которая их замыкает. Пока CNI нет, не существует ни одно звено левее него. Применять манифест нечему

Почему об это спотыкаются поздно. Если в вашем кластере есть CNI по умолчанию, проблемы не видно вообще. KinD с kindnet, любой managed-кластер у провайдера: Flux там спокойно ставит и обновляет хоть Cilium, хоть что угодно. Сеть уже есть, круг не замыкается. Вылезает это ровно тогда, когда кластер поднимают с нуля. То есть при восстановлении после аварии, когда разбираться хочется меньше всего

Четыре звена зависимости и стрелка, которая их замыкает. Пока CNI нет, не существует ни одно звено левее него. Применять манифест нечему

Почему об это спотыкаются поздно. Если в вашем кластере есть CNI по умолчанию, проблемы не видно вообще. KinD с kindnet, любой managed-кластер у провайдера: Flux там спокойно ставит и обновляет хоть Cilium, хоть что угодно. Сеть уже есть, круг не замыкается. Вылезает это ровно тогда, когда кластер поднимают с нуля. То есть при восстановлении после аварии, когда разбираться хочется меньше всего

Решение: развести установку и эксплуатацию

Побеждать этот круг бессмысленно. Его надо признать и развести два действия, которые обычно называют одним словом «деплой»

Первичная установка. Разовая, руками, снаружи git. Задача одна: создать условия, в которых GitOps вообще способен работать

Эксплуатация. Версия, values, обновления. Это уже живёт в git и управляется Flux, как всё остальное

Граница проходит по времени, а не по компонентам. Слева то, что должно существовать до Flux. Справа то, чем Flux владеет

Граница проходит по времени, а не по компонентам. Слева то, что должно существовать до Flux. Справа то, чем Flux владеет

Граница проходит по времени, а не по компонентам. Слева то, что должно существовать до Flux. Справа то, чем Flux владеет

Порядок получается такой, и переставить в нём ничего нельзя:

  1. Создать кластер без CNI. Нода NotReady, обычные поды не планируются

  2. helm install cilium руками

  3. Нода становится Ready, taint снимается, CoreDNS получает адрес

  4. flux bootstrap. Контроллеры наконец планируются и разворачивают остальное из git

  5. 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 рендерит стабильно, и каждая следующая реконсиляция пустая

Под агента меняется один раз, в момент приёма. Дальше helm-controller рендерит стабильно, и каждая следующая реконсиляция пустая

Под агента меняется один раз, в момент приёма. Дальше helm-controller рендерит стабильно, и каждая следующая реконсиляция пустая

Обходится это заметно. На пару минут в CrashLoopBackOff уходят kube-controller-manager, kube-scheduler и cilium-operator, authentik отдаёт 503. Поднимается само, вмешиваться не нужно

Важно другое: платишь за это один раз. Повторная реконсиляция уже даёт generation 2 → 2 и тот же под. Значит подход рабочий, просто окно под один перекат надо закладывать заранее, а не надеяться на бесшовность

Как убедиться, что реконсиляция пустая

Чтобы отличить «ничего не произошло» от «произошло и сломалось», нужна точка отсчёта до изменения. Снимаем три величины

Что

Чем

Что доказывает

Values в git против установленных

helm get values и дифф по нормализованным ключам

расхождений в намерении нет

Поколение DaemonSet

.metadata.generation

менялся ли spec, а не метки

Имя пода агента

get po -l k8s-app=cilium

был ли реальный перекат

Две последние снимаются до и после. Совпали generation и имя пода — реконсиляция действительно пустая, и это уже не вопрос доверия к логам

Где ещё так же

Закономерность шире, чем CNI. В любом GitOps-контуре есть слой, который обязан существовать до того, как контур заработает, и потому не может им управляться. Таких позиций обычно две

Первая — ключ расшифровки секретов. Flux не прочитает зашифрованные манифесты, не имея ключа, а ключ не положишь в репозиторий, который им же шифруется

Вторая — сетевой плагин, про него вся статья

Обе объединяет не техническая природа, а место в порядке загрузки. И обе стоит держать в списке явно. Не «мы забыли это автоматизировать», а «это принципиально не автоматизируется изнутри, вот команда»

Кстати, есть третий случай, который легко с ними спутать. У меня в кластере однажды нашёлся секрет с токеном Telegram, который тоже жил только в кластере и исчез при пересоздании. notification-controller молча падал на каждом событии. Но он-то мог лежать в git и теперь лежит. Это не ограничение архитектуры, а недосмотр, и лечится иначе: не абзацем в документации, а коммитом

Короткий порядок действий

  1. Слой-основание ставьте руками и пишите это прямо в манифесте, чтобы потом не гадать

  2. Values копируйте из живого релиза, а не по памяти, и сверяйте диффом до коммита

  3. Закладывайте один перекат на момент приёма. Совпадения values для этого мало

  4. Снимайте generation и имя пода до и после. Иначе «ничего не изменилось» остаётся догадкой

  5. Для CNI отключайте автоматический откат. Тасовать сеть туда-сюда опаснее, чем оставить неудачное обновление неудачным

Занимает это минуты. Дешевле, чем потом полчаса смотреть на кластер, который формально поднялся и ничего не делает

Автор: mfn29ifb03

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js