← Все статьи

Kubernetes с 1.32 до 1.37 за день, пять хопов, без kubespray, которым кластер собирали

Bare-metal кластер на EOL-версии, потерянный инвентарь kubespray и бинарники вне пакетного менеджера. Как прошли пять хопов kubeadm за один день и грабли, которых не было ни в одном плане.

Bare-metal кластер в Optimacros был на Kubernetes 1.32.8 — ветке, уже вышедшей из поддержки. План — довести его до 1.37: пять minor-хопов, потому что kubeadm никогда не перепрыгивает minor:

1.32.8 → 1.33.13 → 1.34.12 → 1.35.9 → 1.36.5 → 1.37.1

С чем мы работали

Кластер собирали kubespray’ем, а инвентарь потерялся. Бинарники лежали в /usr/local/bin, не принадлежали ни одному пакету, apt-репозитория Kubernetes не было — так что apt install kubeadm=… не вариант: каждый хоп означал подмену бинарников, скачанных с официального сайта релизов. Три мастера, четыре крупных воркера, внешний etcd под systemd, Calico из манифестов kubespray, kube-proxy в режиме ipvs.

На воркерах — 124 тома local-path под тестовые стенды без бэкапа. Drain их никуда бы не перенёс, поэтому воркеры обновлялись на месте, без drain. А алертинг доставлялся в receiver null, так что вся операция шла с глазами на дашбордах, а не на пейджере, — решение, записанное до старта, а не обнаруженное потом.

Готовиться так, чтобы менялось одно за раз

Накануне кластер получил:

  • аудит устаревших API через pluto на 1.37 — ноль находок в живых объектах и во всех 17 Helm-релизах;
  • дефрагментацию etcd: файл базы весил 743 МБ при 73 МБ данных — 90% фрагментации, дефрагментации не было с установки. По одному участнику, лидер последним, меньше секунды каждый: 82 МБ;
  • снапшот etcd с проверкой на копии, бэкапы конфигов со всех семи нод и выгрузку объектов кластера;
  • релиз-апгрейд ОС отдельным окном, чтобы хопы Kubernetes были единственной переменной.

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

Хопы

Каждый хоп шёл по одному сценарию: новые kubeadm и kubelet со сверкой контрольных сумм, kubeadm upgrade apply на первом мастере, upgrade node на двух других, потом воркеры по одному — cordon, upgrade, kubelet, Ready, uncordon, около десяти секунд на каждый. Приёмка после каждого хопа: ноды, etcd, kube-system, хранилище, Argo CD, оператор базы, маршруты Gateway, смоук движка политик и сетевой тест «свежий под ↔ свежий под» с таймером.

Первый и второй хопы заняли примерно по десять минут. Все пять закончились в тот же день.

Грабли, которых не было ни в одном плане

  • Calico от kubespray требовал апстримных CRD и ClusterRole’ов, а не только новых образов: RBAC у kubespray — строгое подмножество. А новая версия зациклилась на XDP-программе, которую не могла снять, и свежие поды были недоступны до 42 секунд, пока XDP не выключили.
  • --apiserver-count удалён в kube-apiserver 1.35. kubespray кладёт его в kubeadm-конфиг, так что его пришлось убрать до upgrade apply. Поймала это сверка флагов с --help нового образа перед каждым хопом.
  • Longhorn 1.11 требует Kubernetes 1.34 и выше, а 1.10 поддерживает только до 1.33. Значит, Longhorn нужно было поднимать сразу после хопа на 1.34 и строго по одному minor.
  • CoreDNS вне kubeadm в кластерах kubespray — фаза аддона отключена, — так что он остаётся на старой версии, пока не обновишь руками.
  • kubelet читает собственный конфиг kubespray, а не тот, что пишет kubeadm upgrade node. clusterDNS после каждого хопа надо проверять в правильном файле.
  • Argo CD кэширует версию кластера. После каждого хопа его application controller продолжал рендерить Helm со старым --kube-version, и чарт, требующий 1.34+, не рендерился. Рестарт контроллера после каждого хопа это лечил.
  • etcd 3.6 — дверь в одну сторону: минимальная стартовая версия — 3.5.32, проверка v2-store должна пройти на каждом участнике, а когда последний участник на 3.6, назад — только через снапшот.
  • На соседнем фронте переезд ingress на Envoy Gateway вскрыл дефолт kube-proxy strictARP, из-за которого на ARP за LoadBalancer IP отвечали все ноды.

Гейт, который мы сняли осознанно

В плане был жёсткий гейт перед 1.37: Longhorn и движок политик должны были указать 1.37 в своих матрицах поддержки. В тот день — не указывали. Мы сняли гейт сознательно и записали почему: каждый компонент вне матрицы использовал только стабильные API и прошёл функциональный смоук на 1.37 — запись в PVC, webhook и generate-правила, маршруты, сеть свежих подов. Прыжок на свежайший релиз Calico x.0, перестраивающий DaemonSet, выглядел большим риском, чем предыдущая версия чуть за пределами матрицы.

Выводы

  • Без исходного установщика kubeadm руками — это нормально, но процедуру надо написать один раз и выполнять одинаково на каждом хопе.
  • Поднимай компоненты до хопа, который их ломает, и каждый раз перечитывай матрицы.
  • Дефрагментируй и снапшоть etcd до старта: это дёшево, а фрагментация была 90%.
  • Сверяй удалённые флаги с новым бинарником до upgrade apply.
  • Перезапускай всё, что кэширует версию кластера, — Argo CD в первую очередь.
  • Гейт можно снять, но только с письменным обоснованием и функциональным тестом, никогда молча.