Kubernetes для самых маленьких. Часть 35: Обновляем кластер
Все части серии «Kubernetes для самых маленьких»Кластер, поставленный в части 4, не может остаться на своей версии навсегда: каждая минорная версия Kubernetes поддерживается чуть больше года, сертификаты kubeadm живут ровно год, а ядро и пакеты узлов получают исправления безопасности каждую неделю. Сегодня — об обслуживании кластера: как устроены версии и какой перекос между компонентами допустим, что проверить перед обновлением, как kubeadm upgrade меняет мастер и воркеры, как продлеваются сертификаты, как обновлять ОС узлов и как заменить узел целиком. Всё — на примере обновления стенда с 1.36 на 1.37. Перед началом нужен свежий снапшот etcd и копия /etc/kubernetes/pki из прошлой части, а drain, cordon и PodDisruptionBudget, без которых не обходится ни одно обслуживание узла, разобраны в части 13.
Версии и сроки поддержки
Версия Kubernetes — 1.MINOR.PATCH. Минорная версия (1.36, 1.37) приносит новые возможности, переводит фичи из alpha в beta и GA, удаляет устаревшее. Патч-версия (1.36.5 → 1.36.6) — только исправления ошибок и уязвимостей, без изменений поведения; патчи выходят примерно раз в месяц.
Минорных версий выходит примерно три в год. Проект поддерживает три последние: для каждой выпускаются патчи примерно 14 месяцев — год обычной поддержки и два месяца, когда исправляются только критичные ошибки и уязвимости. На момент написания:
| Версия | Последний патч | Конец поддержки |
|---|---|---|
| 1.37 | 1.37.2 | 28 октября 2027 |
| 1.36 | 1.36.6 | 28 июня 2027 |
| 1.35 | 1.35.10 | 28 февраля 2027 |
Версия 1.34 перестала получать патчи 27 октября 2026 года. Отсюда ритм: обновлять кластер на следующую минорную версию примерно раз в 4–8 месяцев и ставить патчи текущей по мере выхода. Кластер, пропустивший год, оказывается на неподдерживаемой версии, и обновлять его придётся несколько раз подряд: минорные версии нельзя перепрыгивать. С 1.35 на 1.37 идут через 1.36 — так требует политика проекта даже для однонодового control plane, потому что миграции хранимых объектов и конфигурации компонентов рассчитаны на переход ровно на одну версию.
Перекос версий
Компоненты кластера не обновляются одновременно, и политика version skew говорит, насколько их версии могут расходиться:
| Компонент | Относительно kube-apiserver |
|---|---|
| kube-apiserver в HA | все экземпляры — в пределах одной минорной версии друг от друга |
| kube-controller-manager, kube-scheduler | не новее apiserver, могут отставать на одну минорную версию |
| kubelet | не новее apiserver, может отставать до трёх минорных версий |
| kube-proxy | не новее apiserver, может отставать до трёх минорных версий, и от kubelet на своём узле — не больше чем на три в любую сторону |
| kubectl | на одну минорную версию старше или новее apiserver |
Следствия:
- Порядок всегда один: сначала control plane, потом узлы. kubelet новее apiserver не поддерживается, поэтому воркер нельзя обновить раньше мастера.
- Воркеры можно обновлять не сразу. После перехода мастера на 1.37 kubelet 1.36 и даже 1.34 продолжает работать. Это позволяет растянуть обновление большого кластера на дни. Но долго так держать нельзя: если kubelet отстаёт на три версии, следующую версию control plane поставить уже нельзя, пока не обновлены узлы.
- Минорную версию kubelet меняют только на осушённом узле. Политика перекоса говорит об этом прямо: обновление минорной версии kubelet на месте, с работающими подами, не поддерживается. Новая версия может по-другому обращаться с состоянием в
/var/lib/kubeletи с контейнерами, которые запускала старая, и такой переход проект не тестирует. - kubectl на рабочей станции обновляют вместе с кластером: kubectl 1.36 с apiserver 1.37 работает, kubectl 1.35 — уже нет.
Подготовка
Обновление — это изменение всего кластера разом, и большая часть работы делается до первой команды.
Release notes. Начинают с раздела «Urgent Upgrade Notes» в CHANGELOG новой версии: там перечислено то, что требует действий до обновления. У 1.37 таких пунктов четыре, и наш стенд не задевает ни один: SELinux-монтирование томов (SELinuxMount) стало стабильным и включено по умолчанию — это важно только узлам с SELinux; удалена alpha-версия API scheduling.k8s.io/v1alpha2; eventRecordQPS: 0 в конфигурации kubelet теперь означает «без ограничения»; kubelet пишет в журнал свою итоговую конфигурацию, поэтому стоит проверить, кто может читать журналы узла через API. Дальше — изменения kubeadm: в 1.37 он больше не понимает формат конфигурации kubeadm.k8s.io/v1beta3. Конфигурацию стенда мы с части 4 пишем в v1beta4, а старую переводят командой kubeadm config migrate из kubeadm 1.35 или 1.36 — до обновления.
Полезно помнить и то, что поменялось раньше и легко пропускается при переходе через версию. Начиная с 1.35 kubelet по умолчанию отказывается запускаться на узле с cgroup v1 (failCgroupV1: true). Узлы стенда с части 4 работают на cgroup v2 с SystemdCgroup = true в containerd, но проверить стоит каждый узел:
user@k8s-node-1:~$ stat -fc %T /sys/fs/cgroup
cgroup2fs
Удалённые API. Kubernetes удаляет beta-версии API через несколько релизов после того, как появилась стабильная замена. После удаления манифест с apiVersion: flowcontrol.apiserver.k8s.io/v1beta3, например, просто не применится, а Helm-релиз, где он записан, перестанет обновляться. Как найти такие места:
- метрика apiserver
apiserver_requested_deprecated_apis— по ней видно, к каким устаревшим версиям API кто-то обращался, а меткаremoved_releaseговорит, в каком релизе их удалят. Это ловит клиентов, которые работают сейчас: контроллеры, скрипты, CI; - аудит-лог apiserver (часть 33): запросы к устаревшим API помечены аннотацией
k8s.io/deprecated, по ней находится конкретный клиент; - манифесты и чарты — утилитой pluto (проверяет файлы и Helm-релизы на устаревшие и удалённые
apiVersionдля заданной версии Kubernetes) или kubent (kube-no-trouble, то же по объектам в кластере).
На стенде метрика показывает одно обращение:
$ kubectl get --raw /metrics | grep '^apiserver_requested_deprecated_apis'
apiserver_requested_deprecated_apis{group="",removed_release="",resource="endpoints",subresource="",version="v1"} 1
Это объект Endpoints: с 1.33 он объявлен устаревшим в пользу EndpointSlice, но удалять его не собираются — removed_release пуст. Тревога была бы, если бы в метке стояла версия, на которую мы переходим.
pluto проверяет каталог с манифестами и объекты в кластере. В каталог для примера положен FlowSchema со старой версией API рядом с манифестом hello:
$ pluto detect-files -d ./manifests --target-versions k8s=v1.37.0 -o wide
NAME NAMESPACE KIND VERSION REPLACEMENT DEPRECATED DEPRECATED IN REMOVED REMOVED IN REPL AVAIL REPL AVAIL IN
batch <UNKNOWN> FlowSchema flowcontrol.apiserver.k8s.io/v1beta3 flowcontrol.apiserver.k8s.io/v1 true v1.24.0 true v1.32.0 false
$ pluto detect-all-in-cluster --target-versions k8s=v1.37.0 -o wide
There were no resources found with known deprecated apiVersions.
detect-helm делает то же для манифестов, сохранённых в Helm-релизах. Найденное переписывают на новую версию API — руками или плагином kubectl convert (ставится отдельно от kubectl), который переводит манифест между версиями одного ресурса. Объекты, уже лежащие в etcd, переводить не нужно: apiserver хранит их в своей версии хранения и отдаёт в любой поддерживаемой.
Последние удаления в руководстве по устаревшим API относятся к версии 1.32: оно перечисляет только beta-версии, а удаления alpha-версий, как scheduling.k8s.io/v1alpha2 в 1.37, ищут в CHANGELOG. Наш стенд на этом шаге чист. Но привычку проверять стоит завести: каждое удаление ломает именно те кластеры, где не проверяли.
Совместимость компонентов. Всё, что стоит в кластере поверх Kubernetes, имеет свою матрицу поддерживаемых версий:
- Calico 3.32 тестируется с Kubernetes 1.34–1.36, а Calico 3.33 — с 1.35–1.37. Значит, сначала Calico обновляется до 3.33 на кластере 1.36 (обе версии в его диапазоне), и только потом Kubernetes. Обновление Calico с Tigera operator — два
kubectl applyиз его инструкции: CRD и манифест оператора новой версии, дальше оператор сам обновитcalico-nodeпо узлам:
$ curl -fsSLO https://raw.githubusercontent.com/projectcalico/calico/v3.33.0/manifests/v1_crd_projectcalico_org.yaml
$ curl -fsSLO https://raw.githubusercontent.com/projectcalico/calico/v3.33.0/manifests/tigera-operator.yaml
$ kubectl apply --server-side --force-conflicts -f v1_crd_projectcalico_org.yaml
...
customresourcedefinition.apiextensions.k8s.io/tiers.crd.projectcalico.org serverside-applied
$ kubectl apply --server-side --force-conflicts -f tigera-operator.yaml
...
deployment.apps/tigera-operator serverside-applied
Оператор перезапускает поды Calico по одному узлу. На стенде это заняло пять с половиной минут; поды приложений не перезапускались. Готовность видна по версии в объекте Installation и по tigerastatus:
$ kubectl get installation default -o jsonpath='{.status.calicoVersion}{"\n"}'
v3.33.0
$ kubectl get tigerastatus
NAME AVAILABLE PROGRESSING DEGRADED SINCE MESSAGE
apiserver True False False 85s All objects available
calico True False False 1s All objects available
ippools True False False 12d All objects available
tiers True False False 12d All objects available
- MetalLB, Traefik, cert-manager, csi-driver-nfs и csi-driver-smb, metrics-server, kube-prometheus-stack — у каждого проекта в документации есть таблица поддерживаемых версий Kubernetes. Если компонент новую версию ещё не поддерживает, обновление кластера ждёт его, а не наоборот.
Здоровье и резервная копия. Обновлять стоит только здоровый кластер: все узлы Ready, нет подов в CrashLoopBackOff, etcdctl endpoint health отвечает. И непосредственно перед обновлением — ручной снапшот etcd и копия /etc/kubernetes/pki по части 34: при переходе на 1.37 kubeadm обновляет и сам etcd с 3.6 на 3.7, и снапшот, снятый на старой версии, — самый надёжный путь назад.
Порядок обновления
1. рабочая станция Calico 3.32 → 3.33, снапшот etcd, копия pki
│
▼
2. k8s-master-1 репозиторий v1.37 → kubeadm 1.37.2
kubeadm upgrade plan
kubeadm upgrade apply v1.37.2
static pods: etcd, apiserver,
controller-manager, scheduler
сертификаты, CoreDNS, kube-proxy
drain → kubelet, kubectl 1.37.2 → uncordon
│
▼
3. k8s-node-1 drain → kubeadm 1.37.2 → kubeadm upgrade node
→ kubelet 1.37.2 → uncordon
│
▼
4. k8s-node-2 то же самое
│
▼
5. рабочая станция kubectl 1.37.2, проверка
Узлы обновляются по одному: пока узел осушён, его нагрузку держат остальные, и на двух воркерах стенда это значит, что всё приложение на время живёт на одном узле. В кластере побольше узлы обновляют небольшими группами, но так, чтобы оставшейся ёмкости хватало на все поды.
Мастер
Репозиторий. Пакеты Kubernetes на pkgs.k8s.io разложены по отдельному репозиторию на каждую минорную версию: в репозитории v1.36 версии 1.37 нет вовсе. Это сделано намеренно — apt upgrade не может случайно перескочить на новую минорную версию. Переход на новую — правка одной строки:
user@k8s-master-1:~$ cat /etc/apt/sources.list.d/kubernetes.list
deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.36/deb/ /
user@k8s-master-1:~$ sudo sed -i 's#/v1.36/#/v1.37/#' /etc/apt/sources.list.d/kubernetes.list
user@k8s-master-1:~$ sudo apt-get update
user@k8s-master-1:~$ apt-cache madison kubeadm
kubeadm | 1.37.2-1.1 | https://pkgs.k8s.io/core:/stable:/v1.37/deb Packages
kubeadm | 1.37.1-1.1 | https://pkgs.k8s.io/core:/stable:/v1.37/deb Packages
kubeadm | 1.37.0-1.1 | https://pkgs.k8s.io/core:/stable:/v1.37/deb Packages
kubeadm. Пакеты kubeadm, kubelet и kubectl после установки в части 4 заморожены apt-mark hold, чтобы их не обновил ни apt upgrade, ни автоматические обновления. Для обновления заморозку снимают и ставят снова:
user@k8s-master-1:~$ sudo apt-mark unhold kubeadm && \
sudo apt-get install -y kubeadm='1.37.2-*' && \
sudo apt-mark hold kubeadm
Canceled hold on kubeadm.
...
Setting up kubeadm (1.37.2-1.1)…
...
kubeadm set on hold.
user@k8s-master-1:~$ kubeadm version -o short
v1.37.2
План. kubeadm upgrade plan проверяет, что кластер здоров и обновляем, и показывает, что во что превратится:
user@k8s-master-1:~$ sudo kubeadm upgrade plan
[preflight] Running pre-flight checks.
[upgrade/config] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[upgrade/config] Use 'kubeadm init phase upload-config kubeadm --config your-config-file' to re-upload it.
[upgrade] Running cluster health checks
[upgrade] Fetching available versions to upgrade to
[upgrade/versions] Cluster version: 1.36.5
[upgrade/versions] kubeadm version: v1.37.2
[upgrade/versions] Target version: v1.37.2
[upgrade/versions] Latest version in the v1.36 series: v1.36.6
Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':
COMPONENT NODE CURRENT TARGET
kubelet k8s-master-1 v1.36.5 v1.36.6
kubelet k8s-node-1 v1.36.5 v1.36.6
kubelet k8s-node-2 v1.36.5 v1.36.6
Upgrade to the latest version in the v1.36 series:
COMPONENT NODE CURRENT TARGET
kube-apiserver k8s-master-1 v1.36.5 v1.36.6
kube-controller-manager k8s-master-1 v1.36.5 v1.36.6
kube-scheduler k8s-master-1 v1.36.5 v1.36.6
kube-proxy 1.36.5 v1.36.6
CoreDNS v1.14.2 v1.14.6
etcd k8s-master-1 3.6.8-0 3.7.0-0
You can now apply the upgrade by executing the following command:
kubeadm upgrade apply v1.36.6
_____________________________________________________________________
Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':
COMPONENT NODE CURRENT TARGET
kubelet k8s-master-1 v1.36.5 v1.37.2
kubelet k8s-node-1 v1.36.5 v1.37.2
kubelet k8s-node-2 v1.36.5 v1.37.2
Upgrade to the latest stable version:
COMPONENT NODE CURRENT TARGET
kube-apiserver k8s-master-1 v1.36.5 v1.37.2
kube-controller-manager k8s-master-1 v1.36.5 v1.37.2
kube-scheduler k8s-master-1 v1.36.5 v1.37.2
kube-proxy 1.36.5 v1.37.2
CoreDNS v1.14.2 v1.14.6
etcd k8s-master-1 3.6.8-0 3.7.0-0
You can now apply the upgrade by executing the following command:
kubeadm upgrade apply v1.37.2
_____________________________________________________________________
The table below shows the current state of component configs as understood by this version of kubeadm.
Configs that have a "yes" mark in the "MANUAL UPGRADE REQUIRED" column require manual config upgrade or
resetting to kubeadm defaults before a successful upgrade can be performed. The version to manually
upgrade to is denoted in the "PREFERRED VERSION" column.
API GROUP CURRENT VERSION PREFERRED VERSION MANUAL UPGRADE REQUIRED
kubeproxy.config.k8s.io v1alpha1 v1alpha1 no
kubelet.config.k8s.io v1beta1 v1beta1 no
_____________________________________________________________________
Вариантов два. Для 1.36 вышел патч 1.36.6, и kubeadm предлагает сначала его, а затем — переход на 1.37.2. Перепрыгивать через патчи можно: ограничение «по одной» касается только минорных версий, поэтому мы сразу идём на 1.37.2. Обратите внимание на etcd: в обоих вариантах это 3.7.0 — версию etcd выбирает kubeadm, которым выполняется обновление, а kubeadm 1.37 знает только её. Кто хочет остаться на 1.36 с её etcd, ставит патч kubeadm 1.36.6 из репозитория v1.36.
Последняя таблица — версии конфигураций компонентов (KubeProxyConfiguration, где у нас задан режим nftables, и KubeletConfiguration). «yes» в последней колонке означало бы, что формат конфигурации изменился и её надо перевести руками до обновления.
Применение. kubeadm upgrade apply делает следующее:
- проверяет, что apiserver доступен, все узлы
Ready, control plane здоров, и что целевая версия не нарушает политику перекоса. Для проверки здоровья он создаёт вkube-systemJobupgrade-health-checkс одним контейнеромpauseи ждёт минуту, пока тот завершится, — так проверяется вся цепочка: apiserver, controller-manager, планировщик, kubelet; - скачивает образы новых компонентов заранее, чтобы не ждать их в момент замены;
- обновляет конфигурации компонентов, если их формат требует миграции;
- по одному заменяет static pods: пишет новые манифесты в
/etc/kubernetes/manifests, ждёт, пока kubelet поднимет новый под и тот станет здоровым, и откатывает компонент, если он не поднялся. Старые манифесты и копия данных etcd кладутся в/etc/kubernetes/tmp; - продлевает все сертификаты control plane на этом узле ещё на год (отключается флагом
--certificate-renewal=false, но отключать незачем); - применяет новые манифесты аддонов: Deployment CoreDNS и DaemonSet kube-proxy с образами новой версии, вместе с нужными правилами RBAC. В кластере с несколькими мастерами аддоны обновляются только после того, как обновлён последний мастер; у нас мастер один, и это происходит сразу.
etcd обновляется вместе с control plane, и тут есть тонкость: пока etcd перезапускается, запросы к kube-apiserver повисают. Документация kubeadm предлагает обходной путь — за несколько секунд до обновления мягко остановить apiserver, чтобы текущие запросы успели завершиться:
user@k8s-master-1:~$ sudo killall -s SIGTERM kube-apiserver; sleep 20
user@k8s-master-1:~$ sudo kubeadm upgrade apply v1.37.2 -y
[upgrade] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[upgrade] Use 'kubeadm init phase upload-config kubeadm --config your-config-file' to re-upload it.
[upgrade/preflight] Running preflight checks
[upgrade] Running cluster health checks
[preflight] Some fatal errors occurred:
[ERROR CreateJob]: Job "upgrade-health-check-1793951130060" in the namespace "kube-system" did not complete in 1m0s: no condition of type Complete
[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`
error: error execution phase preflight: preflight checks failed
To see the stack trace of this error execute with --v=5 or higher
На одном мастере этот приём не сработал. kubelet перезапустил остановленный apiserver только через минуту, и controller-manager с планировщиком за это время потеряли аренду лидера. Job для проверки здоровья был создан, но его под назначился на узел только через 42 секунды, и минуты, которую ждёт kubeadm, не хватило. Ничего не сломалось: Job завершился сам чуть позже, кластер остался на 1.36. Вывод для однонодового control plane: либо не останавливать apiserver вовсе, либо после остановки дождаться, пока kubectl get --raw /readyz снова ответит ok, и выждать ещё минуту. Повторный запуск kubeadm upgrade apply на успокоившемся кластере прошёл:
user@k8s-master-1:~$ sudo kubeadm upgrade apply v1.37.2 -y
[upgrade] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[upgrade] Use 'kubeadm init phase upload-config kubeadm --config your-config-file' to re-upload it.
[upgrade/preflight] Running preflight checks
[upgrade] Running cluster health checks
[upgrade/preflight] You have chosen to upgrade the cluster version to "v1.37.2"
[upgrade/versions] Cluster version: v1.36.5
[upgrade/versions] kubeadm version: v1.37.2
[upgrade/preflight] Pulling images required for setting up a Kubernetes cluster
[upgrade/preflight] This might take a minute or two, depending on the speed of your internet connection
[upgrade/preflight] You can also perform this action beforehand using 'kubeadm config images pull'
[upgrade/control-plane] Upgrading your static Pod-hosted control plane to version "v1.37.2" (timeout: 5m0s)...
[upgrade/staticpods] Writing new Static Pod manifests to "/etc/kubernetes/tmp/kubeadm-upgraded-manifests2526878674"
[upgrade/staticpods] Preparing for "etcd" upgrade
[upgrade/staticpods] Renewing etcd-server certificate
[upgrade/staticpods] Renewing etcd-peer certificate
[upgrade/staticpods] Renewing etcd-healthcheck-client certificate
[upgrade/staticpods] Moving new manifest to "/etc/kubernetes/manifests/etcd.yaml" and backing up old manifest to "/etc/kubernetes/tmp/kubeadm-backup-manifests-2026-11-06-07-48-09/etcd.yaml"
[upgrade/staticpods] Waiting for the kubelet to restart the component
[upgrade/staticpods] This can take up to 5m0s
E1106 07:48:44.339960 3287212 request.go:1198] "Unexpected error when reading response body" err="net/http: request canceled (Client.Timeout or context cancellation while reading body)"
[apiclient] Found 1 Pods for label selector component=etcd
[upgrade/staticpods] Component "etcd" upgraded successfully!
[upgrade/etcd] Waiting for etcd to become available
[upgrade/staticpods] Preparing for "kube-apiserver" upgrade
[upgrade/staticpods] Renewing apiserver certificate
[upgrade/staticpods] Renewing apiserver-kubelet-client certificate
[upgrade/staticpods] Renewing front-proxy-client certificate
[upgrade/staticpods] Renewing apiserver-etcd-client certificate
[upgrade/staticpods] Moving new manifest to "/etc/kubernetes/manifests/kube-apiserver.yaml" and backing up old manifest to "/etc/kubernetes/tmp/kubeadm-backup-manifests-2026-11-06-07-48-09/kube-apiserver.yaml"
...
[upgrade/staticpods] Component "kube-apiserver" upgraded successfully!
[upgrade/staticpods] Preparing for "kube-controller-manager" upgrade
[upgrade/staticpods] Renewing controller-manager.conf certificate
...
[upgrade/staticpods] Component "kube-controller-manager" upgraded successfully!
[upgrade/staticpods] Preparing for "kube-scheduler" upgrade
[upgrade/staticpods] Renewing scheduler.conf certificate
...
[upgrade/staticpods] Component "kube-scheduler" upgraded successfully!
[upgrade/control-plane] The control plane instance for this node was successfully upgraded!
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config" in namespace kube-system with the configuration for the kubelets in the cluster
[upgrade/kubeconfig] The kubeconfig files for this node were successfully upgraded!
W1106 07:51:15.059802 3287212 postupgrade.go:105] Using temporary directory /etc/kubernetes/tmp/kubeadm-kubelet-config-2026-11-06-07-51-15 for kubelet config. To override it set the environment variable KUBEADM_UPGRADE_DRYRUN_DIR
[upgrade] Backing up kubelet config file to /etc/kubernetes/tmp/kubeadm-kubelet-config-2026-11-06-07-51-15/config.yaml
[patches] Applied patch of type "application/strategic-merge-patch+json" to target "kubeletconfiguration"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[upgrade/kubelet-config] The kubelet configuration for this node was successfully upgraded!
[upgrade/bootstrap-token] Configuring bootstrap token and cluster-info RBAC rules
...
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy
[upgrade] SUCCESS! A control plane node of your cluster was upgraded to "v1.37.2".
[upgrade] Now please proceed with upgrading the rest of the nodes by following the right order.
Всё обновление control plane заняло четыре с половиной минуты. Строка E1106 … request canceled — та самая повисшая на перезапуске etcd операция: собственный запрос kubeadm к apiserver не дождался ответа, и kubeadm просто повторил его. Флаг -y отвечает «да» на вопрос Are you sure you want to proceed? [y/N], который без него задаётся перед обновлением.
Если команда упала на середине — например, оборвалась сессия, — её запускают ещё раз: kubeadm upgrade идемпотентна и доводит состояние до заданного. Если не поднялся сам компонент и автоматический откат не сработал, старые манифесты лежат в /etc/kubernetes/tmp/kubeadm-backup-manifests-*, а копия данных etcd — в /etc/kubernetes/tmp/kubeadm-backup-etcd-*; их возвращают в /etc/kubernetes/manifests и /var/lib/etcd руками. Сам каталог tmp после обновления не удаляется, его чистят вручную, когда стало ясно, что всё работает.
Манифест etcd kubeadm тоже пишет заново, и каталог данных в нём берётся из etcd.local.dataDir конфигурации kubeadm — у нас /var/lib/etcd. Поэтому в прошлой части снапшот восстанавливался в прежний каталог: правка hostPath в манифесте здесь бы молча пропала.
kubelet и kubectl на мастере. Control plane уже 1.37, а kubelet на мастере всё ещё 1.36. Его обновляют на осушённом узле — даже если осушать почти нечего:
$ kubectl drain k8s-master-1 --ignore-daemonsets
node/k8s-master-1 cordoned
Warning: ignoring DaemonSet-managed Pods: calico-system/calico-node-n6sv7, calico-system/csi-node-driver-r4ltf, kube-system/kube-proxy-wrtc9
node/k8s-master-1 drained
На стенде CoreDNS и поды Calico к этому моменту уже были на воркерах: их пересоздали обновление Calico и замена CoreDNS, и планировщик разложил новые поды по другим узлам. Но на другом кластере на мастере может оказаться что угодно с подходящим toleration, и drain их выселит.
user@k8s-master-1:~$ sudo apt-mark unhold kubelet kubectl && \
sudo apt-get install -y kubelet='1.37.2-*' kubectl='1.37.2-*' && \
sudo apt-mark hold kubelet kubectl
user@k8s-master-1:~$ sudo systemctl daemon-reload
user@k8s-master-1:~$ sudo systemctl restart kubelet
$ kubectl uncordon k8s-master-1
node/k8s-master-1 uncordoned
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-master-1 Ready control-plane 12d v1.37.2
k8s-node-1 Ready <none> 12d v1.36.5
k8s-node-2 Ready <none> 12d v1.36.5
Колонка VERSION — это версия kubelet, а не control plane. Сейчас кластер в допустимом смешанном состоянии: apiserver 1.37, воркеры 1.36.
Воркеры
Порядок для каждого воркера: осушить, обновить kubeadm и через него конфигурацию kubelet, обновить kubelet, вернуть узел в работу.
Осушение. kubectl drain помечает узел неназначаемым (cordon) и выселяет поды через Eviction API, а не удаляет их напрямую. Разница в том, что выселение проверяет PodDisruptionBudget: если после выселения пода у его приложения станет меньше доступных реплик, чем требует PDB, apiserver отвечает 429, и drain повторяет попытку, пока замена не поднимется на другом узле. Так обслуживание не роняет приложение, если реплик достаточно и они разнесены по узлам. Для hello у нас две реплики, разнесённые по узлам (topology spread из части 13), и PDB minAvailable: 1:
$ kubectl drain k8s-node-1 --ignore-daemonsets --delete-emptydir-data --timeout=10m
node/k8s-node-1 cordoned
Warning: ignoring DaemonSet-managed Pods: calico-system/calico-node-gx9mh, calico-system/csi-node-driver-x6xtp, kube-system/kube-proxy-t78zm
evicting pod tigera-operator/tigera-operator-686489b657-5vwv8
evicting pod calico-system/calico-apiserver-78bf74db6b-fhcb8
evicting pod demo/hello-7f74d68455-wlwkw
evicting pod kube-system/coredns-559f6c778d-rw859
evicting pod calico-system/calico-typha-757df7544f-b6h8m
pod/tigera-operator-686489b657-5vwv8 evicted
pod/calico-apiserver-78bf74db6b-fhcb8 evicted
pod/calico-typha-757df7544f-b6h8m evicted
pod/hello-7f74d68455-wlwkw evicted
pod/coredns-559f6c778d-rw859 evicted
node/k8s-node-1 drained
PDB выселение разрешил: вторая реплика hello работает и готова на k8s-node-2, так что после выселения доступна одна из двух, ровно сколько требует minAvailable: 1. Выселенная реплика пересоздаётся контроллером ReplicaSet и, раз k8s-node-1 закрыт для планирования, тоже попадает на k8s-node-2. Свои PDB есть и у Calico (calico-typha, calico-apiserver, по maxUnavailable: 1), и они работают так же.
Отказ выглядит так — его показал следующий узел, k8s-node-2, на котором к этому моменту жили обе реплики hello и обе calico-typha:
$ kubectl drain k8s-node-2 --ignore-daemonsets --delete-emptydir-data --timeout=10m
node/k8s-node-2 cordoned
...
evicting pod demo/hello-7f74d68455-t4kjd
evicting pod demo/hello-7f74d68455-jlc2t
evicting pod kube-system/coredns-559f6c778d-kl4bj
error when evicting pods/"calico-typha-757df7544f-b2vk5" -n "calico-system" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
error when evicting pods/"hello-7f74d68455-jlc2t" -n "demo" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
...
pod/hello-7f74d68455-t4kjd evicted
evicting pod demo/hello-7f74d68455-jlc2t
...
pod/calico-typha-757df7544f-b2vk5 evicted
pod/hello-7f74d68455-jlc2t evicted
node/k8s-node-2 drained
Первую реплику PDB выпустил, вторую — нет: пока замена не стала готова на k8s-node-1, её выселение оставило бы приложение без единой доступной реплики. drain повторял попытку каждые пять секунд и через полминуты закончил.
Флаги:
--ignore-daemonsets— поды DaemonSet не выселяются: контроллер DaemonSet всё равно вернул бы их на узел, и именно они (calico-node,kube-proxy,speakerMetalLB, узловой плагин CSI) нужны узлу, чтобы работать;--delete-emptydir-data— разрешение выселять поды сemptyDir; данные в нём пропадут вместе с подом. Без флагаdrainостановится на таком поде и попросит подтверждения;--timeout— сколько ждать в сумме. Без негоdrain, упёршийся в PDB, будет пытаться вечно.
Два случая, когда drain не пройдёт сам:
- PDB, который нельзя выполнить: одна реплика и
minAvailable: 1(илиmaxUnavailable: 0). Выселить такой под нельзя никогда — это ошибка конфигурации, а не защита. Приложению, которое должно переживать обслуживание узлов, нужны минимум две реплики на разных узлах; - не хватает места: выселенные поды не помещаются на оставшиеся узлы по
requestsи висят вPending, а PDB не даёт выселить следующие. На стенде с двумя воркерами это реальная угроза: на время обслуживания всё должно уместиться на одном узле. Проверить заранее помогаетkubectl describe node k8s-node-2— раздел «Allocated resources» показывает сумму запросов.
kubeadm и конфигурация kubelet. На воркере тоже меняется репозиторий и ставится новый kubeadm, после чего kubeadm upgrade node скачивает ClusterConfiguration из кластера и обновляет конфигурацию kubelet этого узла (/var/lib/kubelet/config.yaml) из ConfigMap kubelet-config:
user@k8s-node-1:~$ sudo sed -i 's#/v1.36/#/v1.37/#' /etc/apt/sources.list.d/kubernetes.list
user@k8s-node-1:~$ sudo apt-get update
user@k8s-node-1:~$ sudo apt-mark unhold kubeadm && \
sudo apt-get install -y kubeadm='1.37.2-*' && \
sudo apt-mark hold kubeadm
user@k8s-node-1:~$ sudo kubeadm upgrade node
[upgrade] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[upgrade] Use 'kubeadm init phase upload-config kubeadm --config your-config-file' to re-upload it.
[upgrade/preflight] Running pre-flight checks
[upgrade/preflight] Skipping prepull. Not a control plane node.
[upgrade/control-plane] Skipping phase. Not a control plane node.
[upgrade/kubeconfig] Skipping phase. Not a control plane node.
W1106 07:55:52.696863 3025021 postupgrade.go:105] Using temporary directory /etc/kubernetes/tmp/kubeadm-kubelet-config-2026-11-06-07-55-52 for kubelet config. To override it set the environment variable KUBEADM_UPGRADE_DRYRUN_DIR
[upgrade] Backing up kubelet config file to /etc/kubernetes/tmp/kubeadm-kubelet-config-2026-11-06-07-55-52/config.yaml
[patches] Applied patch of type "application/strategic-merge-patch+json" to target "kubeletconfiguration"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[upgrade/kubelet-config] The kubelet configuration for this node was successfully upgraded!
[upgrade/addon] Skipping the addon/coredns phase. Not a control plane node.
[upgrade/addon] Skipping the addon/kube-proxy phase. Not a control plane node.
Старая конфигурация kubelet остаётся в /etc/kubernetes/tmp и на воркере — каталог чистят так же, как на мастере.
kubelet — так же, как на мастере:
user@k8s-node-1:~$ sudo apt-mark unhold kubelet kubectl && \
sudo apt-get install -y kubelet='1.37.2-*' kubectl='1.37.2-*' && \
sudo apt-mark hold kubelet kubectl
user@k8s-node-1:~$ sudo systemctl daemon-reload
user@k8s-node-1:~$ sudo systemctl restart kubelet
$ kubectl uncordon k8s-node-1
node/k8s-node-1 uncordoned
После uncordon узел снова принимает новые поды, но уже работающие поды сами обратно не переедут: планировщик не перебалансирует кластер. Поды, выселенные с k8s-node-1, живут на k8s-node-2, и если теперь осушить k8s-node-2, они переедут обратно — так обычно и получается. Если нужно разнести нагрузку сразу, помогает kubectl rollout restart deployment/hello -n demo: новые поды разложатся заново с учётом topology spread. Здесь важен matchLabelKeys: [pod-template-hash] из части 13: при перекатке ограничение считает только поды новой ревизии, а не старые, которые ещё доживают на одном узле.
Затем то же самое для k8s-node-2. И последним — kubectl на рабочей станции той же версии 1.37.
Смотрим руками
Версии всех компонентов:
$ kubectl version -o json | jq -r '.clientVersion.gitVersion, .serverVersion.gitVersion'
v1.37.2
v1.37.2
$ kubectl get nodes -o custom-columns=NAME:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion
NAME KUBELET
k8s-master-1 v1.37.2
k8s-node-1 v1.37.2
k8s-node-2 v1.37.2
Образы static pods и аддонов:
$ kubectl -n kube-system get pods -o custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image | grep -E 'etcd|kube-|coredns'
coredns-559f6c778d-gqkbp registry.k8s.io/coredns/coredns:v1.14.6
coredns-559f6c778d-kjsll registry.k8s.io/coredns/coredns:v1.14.6
etcd-k8s-master-1 registry.k8s.io/etcd:3.7.0-0
kube-apiserver-k8s-master-1 registry.k8s.io/kube-apiserver:v1.37.2
kube-controller-manager-k8s-master-1 registry.k8s.io/kube-controller-manager:v1.37.2
kube-proxy-t78zm registry.k8s.io/kube-proxy:v1.37.2
kube-proxy-t7zlp registry.k8s.io/kube-proxy:v1.37.2
kube-proxy-wrtc9 registry.k8s.io/kube-proxy:v1.37.2
kube-scheduler-k8s-master-1 registry.k8s.io/kube-scheduler:v1.37.2
Версию кластера с точки зрения kubeadm хранит ConfigMap kubeadm-config — по ней kubeadm upgrade node на воркерах узнаёт, что ставить:
$ kubectl -n kube-system get configmap kubeadm-config -o jsonpath='{.data.ClusterConfiguration}' | grep -E 'kubernetesVersion|imageRepository'
imageRepository: registry.k8s.io
kubernetesVersion: v1.37.2
Резервные копии, которые оставил kubeadm upgrade apply:
user@k8s-master-1:~$ sudo ls /etc/kubernetes/tmp/
kubeadm-backup-etcd-2026-11-06-07-48-09
kubeadm-backup-manifests-2026-11-06-07-48-09
kubeadm-kubelet-config-2026-11-06-07-51-15
user@k8s-master-1:~$ sudo ls /etc/kubernetes/tmp/kubeadm-backup-manifests-2026-11-06-07-48-09/
etcd.yaml
kube-apiserver.yaml
kube-controller-manager.yaml
kube-scheduler.yaml
user@k8s-master-1:~$ sudo du -sh /etc/kubernetes/tmp/kubeadm-backup-etcd-2026-11-06-07-48-09/
278M /etc/kubernetes/tmp/kubeadm-backup-etcd-2026-11-06-07-48-09/
Копия данных etcd в tmp — такие же Secret кластера в открытом каталоге на диске мастера, как снапшот из прошлой части, только целиком, со всей историей ревизий: 278 МБ против 35 МБ снапшота. Права на каталог только у root, но держать её дольше, чем нужно для отката, не стоит:
user@k8s-master-1:~$ sudo rm -rf /etc/kubernetes/tmp
Сертификаты kubeadm
kubeadm при init выпустил набор сертификатов в /etc/kubernetes/pki и встроил клиентские сертификаты в kubeconfig-файлы компонентов. Срок клиентских и серверных сертификатов — 1 год, срок трёх CA (кластера, etcd и front-proxy) — 10 лет. Оба задаются в ClusterConfiguration версии v1beta4 полями certificateValidityPeriod и caCertificateValidityPeriod, но удлинять срок обычных сертификатов не стоит: короткий срок ограничивает ущерб от утёкшего ключа, а продление автоматическое.
user@k8s-master-1:~$ sudo kubeadm certs check-expiration
[check-expiration] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[check-expiration] Use 'kubeadm init phase upload-config kubeadm --config your-config-file' to re-upload it.
CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGED
admin.conf Nov 06, 2027 07:47 UTC 364d ca no
apiserver Nov 06, 2027 07:47 UTC 364d ca no
apiserver-etcd-client Nov 06, 2027 07:47 UTC 364d etcd-ca no
apiserver-kubelet-client Nov 06, 2027 07:47 UTC 364d ca no
controller-manager.conf Nov 06, 2027 07:47 UTC 364d ca no
etcd-healthcheck-client Nov 06, 2027 07:47 UTC 364d etcd-ca no
etcd-peer Nov 06, 2027 07:47 UTC 364d etcd-ca no
etcd-server Nov 06, 2027 07:47 UTC 364d etcd-ca no
front-proxy-client Nov 06, 2027 07:47 UTC 364d front-proxy-ca no
scheduler.conf Nov 06, 2027 07:47 UTC 364d ca no
super-admin.conf Nov 06, 2027 07:47 UTC 364d ca no
CERTIFICATE AUTHORITY EXPIRES RESIDUAL TIME EXTERNALLY MANAGED
ca Oct 21, 2036 10:31 UTC 9y no
etcd-ca Oct 21, 2036 10:31 UTC 9y no
front-proxy-ca Oct 21, 2036 10:31 UTC 9y no
Все сертификаты свежие: kubeadm upgrade apply продлил их несколько минут назад. Это главное правило: кластер, который обновляется хотя бы раз в год, никогда не сталкивается с истечением сертификатов. А кластер, который год не трогали, в один день перестаёт отвечать: apiserver не принимает истёкший сертификат controller-manager, kubectl с admin.conf получает отказ.
Что за строки:
| Сертификат | Кто им пользуется |
|---|---|
apiserver |
серверный сертификат API на 6443 с именами k8s-api.example.com, k8s-master-1, k8s-master-1.example.com, kubernetes, kubernetes.default.svc.cluster.local и адресами 10.96.0.1, 192.168.10.17 |
apiserver-kubelet-client |
apiserver, когда идёт к kubelet за логами и exec |
apiserver-etcd-client |
apiserver, когда идёт в etcd |
front-proxy-client |
apiserver, когда проксирует запросы к расширениям API (metrics-server) |
etcd-* |
etcd и его клиенты, см. прошлую часть |
controller-manager.conf, scheduler.conf |
клиентские сертификаты внутри kubeconfig компонентов |
admin.conf |
администратор кластера, группа kubeadm:cluster-admins с правами через RBAC |
super-admin.conf |
аварийный доступ, группа system:masters, которая обходит RBAC целиком |
admin.conf и тем более super-admin.conf — ключи от всего кластера, которыми не делятся и не пользуются каждый день: для работы людям выдают свой доступ по части 29, а эти файлы остаются на мастере для аварий.
Продление вручную — если кластер долго не обновлялся или сертификат нужен срочно:
user@k8s-master-1:~$ sudo kubeadm certs renew all
[renew] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[renew] Use 'kubeadm init phase upload-config kubeadm --config your-config-file' to re-upload it.
certificate embedded in the kubeconfig file for the admin to use and for kubeadm itself renewed
certificate for serving the Kubernetes API renewed
certificate the apiserver uses to access etcd renewed
certificate for the API server to connect to kubelet renewed
certificate embedded in the kubeconfig file for the controller manager to use renewed
certificate for liveness probes to healthcheck etcd renewed
certificate for etcd nodes to communicate with each other renewed
certificate for serving etcd renewed
certificate for the front proxy client renewed
certificate embedded in the kubeconfig file for the scheduler manager to use renewed
certificate embedded in the kubeconfig file for the super-admin renewed
Done renewing certificates. You must restart the kube-apiserver, kube-controller-manager, kube-scheduler and etcd, so that they can use the new certificates.
Последняя строка — главное. Не все компоненты перечитывают сертификаты на лету: apiserver свой серверный сертификат подхватил сам — на стенде openssl s_client к порту 6443 показал новый сертификат ещё до перезапуска, — а клиентские сертификаты внутри kubeconfig controller-manager и планировщика читаются только при старте. Поэтому после renew перезапускают все четыре static pod. systemctl restart kubelet для этого не годится: kubelet не перезапускает static pod, если манифест не изменился. Нужно убрать манифест из каталога, дождаться, пока kubelet остановит под (каталог он проверяет раз в 20 секунд), и вернуть:
user@k8s-master-1:~$ for c in etcd kube-apiserver kube-controller-manager kube-scheduler; do
sudo mv /etc/kubernetes/manifests/$c.yaml /etc/kubernetes/
sleep 25
sudo mv /etc/kubernetes/$c.yaml /etc/kubernetes/manifests/
sleep 25
done
Цикл идёт три с лишним минуты, и пока перезапускаются etcd и apiserver, API недоступен — на однонодовом control plane это минута-другая простоя API, работающие поды её не замечают. После продления admin.conf содержит новый сертификат, и копию в ~/.kube/config на мастере обновляют: sudo cp /etc/kubernetes/admin.conf ~/.kube/config. Старый сертификат при этом не отзывается и работает до своего срока.
Сертификаты kubelet kubeadm не продлевает, и это не нужно: kubelet делает это сам. У него их два, оба в /var/lib/kubelet/pki/, и каждый — символическая ссылка *-current.pem на файл с датой выпуска:
user@k8s-node-1:~$ sudo ls -l /var/lib/kubelet/pki/
-rw------- 1 root root 1118 Oct 24 10:37 kubelet-client-2026-10-24-10-37-18.pem
lrwxrwxrwx 1 root root 59 Oct 24 10:37 kubelet-client-current.pem -> /var/lib/kubelet/pki/kubelet-client-2026-10-24-10-37-18.pem
-rw------- 1 root root 1167 Oct 24 10:37 kubelet-server-2026-10-24-10-37-49.pem
lrwxrwxrwx 1 root root 59 Oct 24 10:37 kubelet-server-current.pem -> /var/lib/kubelet/pki/kubelet-server-2026-10-24-10-37-49.pem
user@k8s-node-1:~$ sudo openssl x509 -noout -subject -enddate -in /var/lib/kubelet/pki/kubelet-client-current.pem
subject=O=system:nodes, CN=system:node:k8s-node-1
notAfter=Oct 24 10:32:18 2027 GMT
user@k8s-node-1:~$ sudo openssl x509 -noout -issuer -enddate -in /var/lib/kubelet/pki/kubelet-server-current.pem
issuer=CN=kubernetes
notAfter=Oct 24 10:32:48 2027 GMT
- Клиентский сертификат — им kubelet ходит в apiserver. Параметр
rotateCertificates: true, который kubeadm ставит вKubeletConfiguration, заставляет kubelet заранее, при прохождении 70–90 % срока, создать новый запрос на сертификат (CertificateSigningRequest). Такие запросы от узлов controller-manager одобряет и подписывает автоматически: kubeadm создал для этого привязки RBAC — их же он заново настраивает в строках[bootstrap-token]выводаkubeadm upgrade apply. kubelet переключает ссылку на новый файл без перезапуска. - Серверный сертификат — на порту 10250, куда ходят apiserver за логами и
execи metrics-server за метриками. В части 4 мы включилиserverTLSBootstrap: true, поэтому он подписан CA кластера (issuer=CN=kubernetes), а не самоподписан, — и именно поэтому metrics-server в части 32 проверяет kubelet без--kubelet-insecure-tls. Его kubelet обновляет так же, но с одной разницей: запросы с подписантомkubernetes.io/kubelet-servingcontroller-manager не одобряет автоматически, потому что не может проверить, что имена и адреса в запросе действительно принадлежат узлу. Через год, при прохождении 70–90 % срока, каждый kubelet пришлёт новый запрос, и его надо одобрить — руками или отдельным контроллером-одобрителем, который такую проверку делает. Иначе серверный сертификат истечёт, иkubectl logs,execи metrics-server перестанут работать с этим узлом. Как выглядит такой запрос и его одобрение — ниже, в разделе о замене узла.
Срок CA — 10 лет, и его продление kubeadm не автоматизирует: это ручная процедура с выпуском нового CA и переводом на него всех компонентов и узлов, описанная в документации kubeadm. Кластер, который живёт десять лет без переустановки, — редкость, но дату истечения CA стоит записать туда же, где хранятся остальные сроки.
Обновление ОС узлов
Ubuntu Server сам ставит исправления безопасности: пакет unattended-upgrades включён по умолчанию и раз в сутки устанавливает обновления из репозитория -security. Для узла Kubernetes это хорошо, с двумя оговорками.
Что обновлять не автоматически. kubelet, kubeadm и kubectl заморожены apt-mark hold, и unattended-upgrades их не трогает — их версия меняется только процедурой выше. containerd стоит заморозить так же (или добавить в Unattended-Upgrade::Package-Blacklist в /etc/apt/apt.conf.d/50unattended-upgrades): перезапуск containerd не убивает работающие контейнеры — их держат процессы-прослойки containerd-shim, — но смена версии runtime на работающем узле без осушения — лишний риск. Обновляют его, как kubelet: drain, обновление, проверка, uncordon.
Перезагрузка. Новое ядро, библиотеки вроде glibc и OpenSSL, сам systemd начинают работать только после перезагрузки. unattended-upgrades по умолчанию узел не перезагружает, а только создаёт файл /var/run/reboot-required со списком причин в /var/run/reboot-required.pkgs. Перезагружать узел кластера без осушения нельзя — его поды умрут без предупреждения, и PDB не сработает. Поэтому автоматическую перезагрузку в unattended-upgrades оставляют выключенной, а перезагружают по одному узлу.
Ждать готовности узла после перезагрузки нужно аккуратно. Сразу после systemctl reboot узел в API ещё Ready: kubelet уже не отвечает, но контроллер узлов замечает это не сразу — на стенде через 47 секунд. Простой kubectl wait --for=condition=Ready в этот момент вернётся мгновенно — на стенде так и случилось, и узел был возвращён в работу посреди перезагрузки. Надёжный признак — смена bootID, идентификатора загрузки, который kubelet сообщает в статусе узла:
user@k8s-node-1:~$ cat /var/run/reboot-required.pkgs
linux-image-7.0.0-42-generic
linux-base
$ boot=$(kubectl get node k8s-node-1 -o jsonpath='{.status.nodeInfo.bootID}')
$ kubectl drain k8s-node-1 --ignore-daemonsets --delete-emptydir-data --timeout=10m
user@k8s-node-1:~$ sudo systemctl reboot
$ until [ "$(kubectl get node k8s-node-1 -o jsonpath='{.status.nodeInfo.bootID}')" != "$boot" ]; do sleep 5; done
$ kubectl wait --for=condition=Ready node/k8s-node-1 --timeout=10m
node/k8s-node-1 condition met
$ kubectl uncordon k8s-node-1
Новый bootID появился через минуту после команды перезагрузки, ещё при NotReady, а через секунду узел стал Ready. После перезагрузки uname -r показывает новое ядро 7.0.0-42-generic, а файла /var/run/reboot-required больше нет.
Мастер перезагружается так же, но на однонодовом control plane это несколько минут недоступности API — на стенде три: работающие поды на воркерах живут дальше, а вот изменения, масштабирование и перезапуск упавших подов ждут. Это делают в окно обслуживания.
Автоматизирует ровно этот цикл kured (Kubernetes Reboot Daemon) — DaemonSet, который на каждом узле следит за /var/run/reboot-required, берёт общую на кластер блокировку, чтобы перезагружался только один узел за раз, осушает узел, перезагружает и возвращает в работу, причём только в заданное окно времени.
Замена узла
Узел выводят из кластера, когда меняют железо, переустанавливают ОС или узел просто сломался.
Вывод. Сначала осушить, затем удалить объект Node из API, затем очистить саму машину:
$ kubectl drain k8s-node-2 --ignore-daemonsets --delete-emptydir-data --timeout=10m
node/k8s-node-2 drained
$ kubectl delete node k8s-node-2
node "k8s-node-2" deleted
Lease узла в kube-node-lease исчезает сразу — его удаляет сборщик мусора, потому что Lease принадлежит объекту Node. Блок адресов подов, который Calico закрепил за узлом (172.20.172.0/26), контроллер Calico освободил сам примерно через две минуты. На самой машине kubeadm reset откатывает то, что сделал kubeadm join: останавливает kubelet, удаляет контейнеры, чистит /etc/kubernetes и /var/lib/kubelet:
user@k8s-node-2:~$ sudo kubeadm reset
W1106 08:06:42.031356 2401991 preflight.go:55] [reset] WARNING: Changes made to this host by 'kubeadm init' or 'kubeadm join' will be reverted.
[reset] Are you sure you want to proceed? [y/N]: y
[preflight] Running pre-flight checks
...
[reset] Stopping the kubelet service
[reset] Unmounting mounted directories in "/var/lib/kubelet"
E1106 08:06:46.513050 2401991 remote_runtime.go:355] "StopPodSandbox from runtime service failed" err="rpc error: code = DeadlineExceeded desc = context deadline exceeded" podSandboxID="81e0dc0d8ab6ec997a5d65c45a31b54fbe115b5a74033aa8a9de7826ed34a77d"
[reset] Deleting contents of directories: [/etc/kubernetes/manifests /var/lib/kubelet /etc/kubernetes/pki]
[reset] Deleting files: [/etc/kubernetes/admin.conf /etc/kubernetes/super-admin.conf /etc/kubernetes/kubelet.conf /etc/kubernetes/bootstrap-kubelet.conf /etc/kubernetes/controller-manager.conf /etc/kubernetes/scheduler.conf]
The reset process does not perform cleanup of CNI plugin configuration,
network filtering rules and kubeconfig files.
For information on how to perform this cleanup manually, please see:
https://k8s.io/docs/reference/setup-tools/kubeadm/kubeadm-reset/
Ошибка StopPodSandbox — сеть пода не успела аккуратно разобраться за отведённое время; reset всё равно удаляет контейнеры, и crictl pods после него пуст. А вот что он честно не трогает: конфигурацию CNI в /etc/cni/net.d (у нас 10-calico.conflist и calico-kubeconfig), таблицы nftables kube-proxy и calico, интерфейс vxlan.calico и каталог /etc/kubernetes/tmp. Если машина уходит из кластера насовсем или возвращается в него заново, надёжнее всего переустановить ОС: так на ней гарантированно не останется ни старых правил, ни остатков данных подов. Если переустанавливать нельзя — удалить конфигурацию CNI и остатки kubeadm и перезагрузить узел: правила nftables и интерфейсы Calico живут только в памяти и исчезают сами.
user@k8s-node-2:~$ sudo rm -rf /etc/cni/net.d/* /etc/kubernetes/tmp
user@k8s-node-2:~$ sudo systemctl reboot
После перезагрузки nft list tables ничего не показывает, интерфейсов cali* и vxlan.calico нет.
Ввод нового. Новая машина — k8s-node-2 с тем же именем и адресом после переустановки — готовится по части 4: ОС, модули ядра и sysctl, containerd с SystemdCgroup = true, пакеты kubeadm, kubelet, kubectl текущей версии кластера, то есть уже 1.37.2 из репозитория v1.37. Команду присоединения выдаёт мастер:
user@k8s-master-1:~$ sudo kubeadm token create --ttl 1h --print-join-command
kubeadm join k8s-api.example.com:6443 --token 7q3k2m.x9w4p8v1n6c5b2z0 --discovery-token-ca-cert-hash sha256:5f1b8c3e9a7d2f4b6c8e0a1d3f5b7c9e2a4d6f8b0c2e4a6d8f0b2c4e6a8d0f2b
--token — bootstrap-токен, которым новый kubelet один раз представится apiserver, чтобы получить свой клиентский сертификат; --ttl 1h ограничивает его жизнь часом вместо суток по умолчанию — токен даёт право присоединить к кластеру любую машину, и лишнее время ему ни к чему. --discovery-token-ca-cert-hash — хеш открытого ключа CA кластера: по нему новый узел проверяет, что разговаривает с настоящим мастером, а не с подставным. Адрес в команде — controlPlaneEndpoint из конфигурации части 4, имя API, а не адрес мастера.
user@k8s-node-2:~$ sudo kubeadm join k8s-api.example.com:6443 --token 7q3k2m.x9w4p8v1n6c5b2z0 \
--discovery-token-ca-cert-hash sha256:5f1b8c3e9a7d2f4b6c8e0a1d3f5b7c9e2a4d6f8b0c2e4a6d8f0b2c4e6a8d0f2b
[preflight] Running pre-flight checks
[preflight] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[preflight] Use 'kubeadm init phase upload-config kubeadm --config your-config-file' to re-upload it.
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/instance-config.yaml"
[patches] Applied patch of type "application/strategic-merge-patch+json" to target "kubeletconfiguration"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-check] Waiting for a healthy kubelet at http://127.0.0.1:10248/healthz. This can take up to 4m0s
[kubelet-check] The kubelet is healthy after 3.000766747s
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
Новый kubelet сразу прислал два запроса на сертификат. Клиентский, от имени bootstrap-токена, controller-manager одобрил сам; серверный ждёт администратора:
$ kubectl get csr
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
csr-fwhcg 19s kubernetes.io/kubelet-serving system:node:k8s-node-2 <none> Pending
csr-ktp7w 22s kubernetes.io/kube-apiserver-client-kubelet system:bootstrap:7q3k2m <none> Approved,Issued
$ kubectl get csr csr-fwhcg -o jsonpath='{.spec.request}' | base64 -d | openssl req -noout -text | grep -A1 'Subject Alternative'
X509v3 Subject Alternative Name:
DNS:k8s-node-2, IP Address:192.168.10.21
$ kubectl certificate approve csr-fwhcg
certificatesigningrequest.certificates.k8s.io/csr-fwhcg approved
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-master-1 Ready control-plane 12d v1.37.2
k8s-node-1 Ready <none> 12d v1.37.2
k8s-node-2 Ready <none> 26s v1.37.2
Имя и адрес в запросе совпадают с узлом, запрос пришёл от его же учётной записи — одобряем. Пока серверный сертификат не выпущен, узел уже Ready и принимает поды, но kubectl logs и exec к его подам не работают. DaemonSet сами разворачивают на новом узле calico-node, kube-proxy, speaker MetalLB и узловые плагины CSI; обычные поды придут на него по мере перезапусков или после rollout restart.
Замена мастера при одном мастере — это восстановление control plane из резервной копии по прошлой части: новая машина с тем же именем и адресом, каталог pki из копии, данные etcd из снапшота и kubeadm init с той же конфигурацией поверх них. В отказоустойчивом control plane мастер заменяется как обычный член: вывести, удалить из etcd через etcdctl member remove, присоединить новый с kubeadm join --control-plane; эта процедура разобрана в серии про Kubernetes на BGP.
Стенд на 1.37.2, Calico на 3.33, сертификаты продлены на год, узлы обновляются и перезагружаются по одному без простоя приложений.
Источники
- Kubernetes: Releases и Patch Releases — версии, патчи и сроки поддержки; Version Skew Policy — допустимый перекос и обновление kubelet только на осушённом узле.
- CHANGELOG 1.37 — Urgent Upgrade Notes и изменения kubeadm.
- Deprecated API Migration Guide — удалённые версии API по релизам.
- Upgrading kubeadm clusters — порядок обновления, остановка apiserver перед заменой etcd, резервные копии в
/etc/kubernetes/tmp; Changing The Kubernetes Package Repository — репозитории pkgs.k8s.io по минорным версиям. - Certificate Management with kubeadm — сроки,
check-expiration,renew, перезапуск static pods, CSRkubelet-serving. - Configure Certificate Rotation for the Kubelet и TLS bootstrapping — сертификаты kubelet и их обновление.
- Safely Drain a Node и Specifying a Disruption Budget —
drain, Eviction API и PDB. - kubeadm reset и kubeadm join — вывод и ввод узла.
- Calico: требования с поддерживаемыми версиями Kubernetes и обновление при установке оператором.
- pluto — поиск устаревших версий API в файлах, Helm-релизах и кластере.
- Ubuntu: Automatic updates — unattended-upgrades; kured — перезагрузка узлов по очереди.
Дальше — часть 36, Windows-узлы в кластере: с неё начинается блок о гибридном кластере из Linux- и Windows-воркеров. Как устроен Windows-узел: почему control plane остаётся только на Linux, как kubelet и containerd работают службами Windows, что делают HCS и HNS, почему при process isolation версия образа должна совпадать с версией хоста, как метка kubernetes.io/os, taint и RuntimeClass разводят поды по своим узлам, что такое HostProcess-контейнеры и чего на Windows нет.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.