АйТи Фреш
Главная / Статьи / DevOps и автоматизация
DevOps и автоматизация

Почему обновление control plane до Kubernetes 1.33 ещё не защищает PersistentVolume

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~9 мин чтения
Почему обновление control plane до Kubernetes 1.33 ещё не защищает PersistentVolume

Вы обновили один узел control plane до Kubernetes 1.33, увидели GA у защиты PersistentVolume и решили, что вопрос закрыт. А контейнер, который должен поставить нужный finalizer, остался прежним. Я, Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш», начинаю такую проверку с конкретного PV и его контроллера. Разберу, где заканчивается гарантия Kubernetes, какие тома требуют отдельного внимания и как проверить результат на одноразовых данных.

1. Что именно удаляется раньше времени

Сначала договоримся о терминах. PersistentVolume — объект Kubernetes, описывающий хранилище. Само хранилище живёт отдельно: это диск, LUN или каталог на NFS. Ошибка, которую исправляет HonorPVReclaimPolicy, возникает при определённом порядке удаления: сначала запрашивают удаление PV, потом удаляют связанный PVC. Раньше объект PV мог исчезнуть, а выделенный ресурс оставался в системе хранения, хотя у PV стояла политика Delete. Получался потерянный для Kubernetes том. Именно предотвращение таких утечек стало GA в версии 1.33; beta с включением по умолчанию появилась в 1.31. [Описание исправления Kubernetes](https://kubernetes.io/blog/2025/05/05/kubernetes-v1-33-prevent-persistentvolume-leaks-when-deleting-out-of-order-graduate-to-ga/).

Механизм устроен просто: finalizer удерживает объект PV в API до завершения предусмотренной очистки. Для CSI используется external-provisioner.volume.kubernetes.io/finalizer. Его обслуживает external-provisioner: обращается к CSI-драйверу и снимает finalizer после успешного удаления тома. Это дополнительное условие завершения операции. Уже знакомый kubernetes.io/pv-protection решает другую задачу — удерживает PV, пока тот связан с PVC. Поэтому одинокая строка pv-protection в YAML ещё ничего не доказывает про очистку backend. [Защита используемых PV и PVC](https://v1-33.docs.kubernetes.io/docs/concepts/storage/persistent-volumes/#storage-object-in-use-protection).

Я бы сразу убрал опасное ожидание: эта функция не обещает сохранить ваши данные после команды удаления. При Delete исправный механизм как раз доводит удаление хранилища до конца. Если администратор рассчитывал, что забытый каталог случайно переживёт удаление PV, после исправления такой страховки может больше не оказаться. Для ценных данных я отдельно выбираю политику хранения и проверяю восстановление из резервной копии. А здесь проверяю порядок завершения удаления и отсутствие бесхозных ресурсов.

Terminating после запроса удаления может означать, что защита работает. Исчезнувшая строка в kubectl get pv сама по себе не доказывает успешную очистку хранилища.

2. Почему обновление одного control plane ничего не гарантирует

В этой истории участвуют разные компоненты: kube-apiserver принимает изменения объектов, kube-controller-manager выполняет встроенные контроллеры, а CSI external-provisioner работает отдельным контейнером рядом с контроллером драйвера. Его поставляет манифест, Helm chart или оператор системы хранения. Обновление Kubernetes само по себе этот контейнер не заменяет. Поэтому даже полностью обновлённый control plane может соседствовать со старой реализацией удаления CSI-томов. Зависимость от обновления обоих компонентов прямо разобрана в [KEP-2644](https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/2644-honor-pv-reclaim-policy/README.md#version-skew-strategy).

Для описанного GA-поведения официальный анонс требует external-provisioner 5.0.1 или новее. Это версия sidecar, а не версия CSI-драйвера и не номер Helm chart. Я дополнительно смотрю аргументы реально запущенного контейнера: в релизах, где переключатель доступен, --feature-gates=HonorPVReclaimPolicy=false отключает механизм. Переносить старые флаги между версиями без проверки нельзя. У v5.0.1 даже есть расхождение: release notes указывают Alpha и Off, тогда как исходники тега — Beta и Default: true. Для диагностики я выбираю код релиза и фактическую конфигурацию. [Заметки релиза v5.0.1](https://github.com/kubernetes-csi/external-provisioner/releases/tag/v5.0.1), [исходник feature gates](https://github.com/kubernetes-csi/external-provisioner/blob/v5.0.1/pkg/features/features.go).

С одним обновлённым узлом добавляется вопрос: какой kube-controller-manager сейчас лидер и с каким API он работает? Для in-tree PV это существенно. Но объяснять проблему только смешанными версиями тоже неправильно: в Kubernetes 1.32 функция уже была beta, поэтому сам факт наличия 1.32 рядом с 1.33 не доказывает отсутствие защиты. Порядок обновления выбираю по version skew policy: контроллер не должен быть новее API-серверов, к которым подключается. Строка VERSION в kubectl get nodes показывает версию kubelet, а не состояние всей этой цепочки. [Политика совместимости компонентов](https://v1-33.docs.kubernetes.io/releases/version-skew-policy/).

Проверять нужно образ и аргументы работающего csi-provisioner, владельца конкретного PV и его metadata.finalizers. GA в описании релиза не заменяет эти проверки.
Почему обновление control plane до Kubernetes 1.33 ещё не защищает PersistentVolume — схема

3. Какие PV действительно входят в область защиты

Я делю инвентаризацию по способу обслуживания тома. Отдельно смотрю CSI и in-tree, отдельно — динамическое и статическое создание. Слово «статический» не означает автоматическое исключение: документация относит к механизму и статические CSI-тома. А статические in-tree PV прямо исключены из нового исправления. При CSI migration учитываю уже фактического внешнего обработчика: для мигрированного плагина встроенный finalizer заменяется внешним. [Матрица в документации Kubernetes 1.33](https://v1-33.docs.kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolume-deletion-protection-finalizer).

Однако между заявленной поддержкой типа и наличием finalizer на каждом объекте есть существенная деталь. В проверенной реализации external-provisioner v5.2.0 существующий PV получает новый finalizer при сочетании Delete, состояния Bound и отсутствия deletionTimestamp. Свежий статический CSI PV, который ещё находится в Available и никогда не связывался с PVC, под такую обработку не попадает. Это ограничение конкретной реализации, которое легко пропустить при чтении общей документации. [Условия добавления finalizer в исходнике v5.2.0](https://github.com/kubernetes-csi/external-provisioner/blob/v5.2.0/vendor/sigs.k8s.io/sig-storage-lib-external-provisioner/v11/controller/controller.go).

Ещё я проверяю spec.csi.driver и аннотации pv.kubernetes.io/provisioned-by, pv.kubernetes.io/migrated-to. Скопированная из старого манифеста чужая аннотация может помешать provisioner распознать статический том как свой. Исправлять её вслепую опасно: сначала нужно установить владельца и смысл volumeHandle. Для Retain критерий другой — backend должен сохраняться согласно политике. Требовать от такого PV того же результата, что от Delete, бессмысленно; само наличие или отсутствие внешнего finalizer здесь недостаточно для вывода.

Если deletionTimestamp уже установлен, добавить новый finalizer через API нельзя. Обновление sidecar не защищает задним числом объект, удаление которого уже началось.

4. Разбор стенда: Kubernetes обновили, NFS-комплект остался

Для конкретики беру модельный стенд условного ООО «Вектор», производственной компании на 120 рабочих мест. Это учебная реконструкция с ожидаемыми результатами по коду, а не отчёт о выполненном клиентском внедрении. В модели три виртуальные машины control plane по 2 vCPU и 4 ГиБ RAM, два worker по 4 vCPU и 8 ГиБ, отдельный NFSv4-сервер 10.20.30.40 с экспортом /srv/k8s-lab. Это выбранные ресурсы стенда, не универсальные требования Kubernetes. Приложения компании для проверки не нужны.

Исходная версия — Kubernetes 1.32.4; на первом узле API-сервер переведён на 1.33.0, два других API и контроллеры пока остаются на 1.32.4. Комплект NFS CSI v4.7.0 не трогали. Его официальный манифест действительно содержит csi-provisioner:v4.0.0 без включения HonorPVReclaimPolicy. Вот конкретный разрыв в обновлении: новая версия API не меняет старый контейнер хранения. Если затем закончить обновление control plane, этот разрыв сам не исчезнет. [Манифест NFS CSI v4.7.0](https://github.com/kubernetes-csi/csi-driver-nfs/blob/v4.7.0/deploy/csi-nfs-controller.yaml).

Для проверки выделяю namespace pv-finalizer-lab и отдельный StorageClass lab-nfs-delete. За три последовательных прогона создаю три новых PVC по 1Gi; каждый получает собственный динамический PV и каталог. Эти 1Gi — запрос Kubernetes, а не обещание жёсткой NFS-квоты. В параметрах явно задаю onDelete: delete, чтобы сохранение или архивирование каталога средствами драйвера не исказило вывод. NFS CSI создаёт подкаталоги в существующем экспорте, а параметр onDelete определяет обращение с ними при удалении. [Параметры NFS CSI](https://github.com/kubernetes-csi/csi-driver-nfs/blob/v4.11.0/docs/driver-parameters.md). ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: lab-nfs-delete provisioner: nfs.csi.k8s.io parameters: server: 10.20.30.40 share: /srv/k8s-lab onDelete: delete reclaimPolicy: Delete volumeBindingMode: Immediate ```

Первый прогон относится к частичному обновлению API, второй — к полностью обновлённому control plane при прежнем NFS-комплекте. Третий делаю после отдельного перехода на NFS CSI v4.11.0: его официальный комплект содержит provisioner v5.2.0 и явный --feature-gates=HonorPVReclaimPolicy=true. Я выбираю обновление согласованного комплекта, включая RBAC и сопутствующие манифесты, потому что замена одного image оставляет слишком много непроверенных зависимостей. Эти исторические версии нужны для сравнения поведения, а не как рекомендация нового production-стека. [Манифест NFS CSI v4.11.0](https://github.com/kubernetes-csi/csi-driver-nfs/blob/v4.11.0/deploy/csi-nfs-controller.yaml).

Экспорт /srv/k8s-lab должен содержать только одноразовые данные. Проверка специально запускает удаление хранилища.
Цифры и версии: Разбор стенда: Kubernetes обновили, NFS-комплект остался — схема
Цифры и версии: Разбор стенда: Kubernetes обновили, NFS-комплект остался

5. Как проверить неправильный порядок удаления и результат

В каждом прогоне создаю новый PVC с именем probe и дожидаюсь Bound. Pod здесь намеренно не создаю: нужно проверить удаление PV/PVC, без дополнительного ожидания pvc-protection из-за потребителя. При volumeBindingMode: Immediate динамическое выделение не требует запуска Pod. Namespace и StorageClass должны быть подготовлены заранее; следующий манифест сохраняю как probe.yaml и применяю через kubectl apply -f probe.yaml. ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: probe namespace: pv-finalizer-lab spec: storageClassName: lab-nfs-delete accessModes: - ReadWriteMany resources: requests: storage: 1Gi ```

До удаления сохраняю YAML PV и соответствующий volumeHandle. На NFS-сервере фиксирую именно связанный с ним каталог: произвольная старая директория рядом доказательством не будет. В третьем прогоне обязательно дожидаюсь внешнего finalizer до команды delete. Затем выполняю команды по одной, проверяя результат каждого шага. После первого удаления PV ожидается Terminating, пока существует связанный PVC; --wait=false нужен, чтобы терминал не ждал завершения этой операции перед следующим шагом. ```bash kubectl -n pv-finalizer-lab wait --for=jsonpath='{.status.phase}'=Bound pvc/probe --timeout=120s PV_NAME=$(kubectl -n pv-finalizer-lab get pvc probe -o jsonpath='{.spec.volumeName}') kubectl get pv "$PV_NAME" -o yaml > probe-pv-before.yaml kubectl get pv "$PV_NAME" -o jsonpath='{.metadata.finalizers}' kubectl delete pv "$PV_NAME" --wait=false kubectl get pv "$PV_NAME" -o yaml kubectl -n pv-finalizer-lab delete pvc probe --wait=false kubectl wait --for=delete "pv/$PV_NAME" --timeout=180s ```

Ожидаемый итог первых двух прогонов одинаков: после удаления PVC объект PV исчезает, а его каталог может остаться на NFS из-за старого пути обработки. Обновление оставшихся control plane проблему CSI не устраняет. В третьем прогоне после освобождения тома provisioner вызывает DeleteVolume; при успешной очистке каталога снимается внешний finalizer и завершается удаление PV. Итоговый критерий — согласованное исчезновение объекта и его каталога. Здесь нет измеренного обещания «очистится за 30 секунд»: 180 секунд в команде — только выбранное окно ожидания.

Для усиления проверки я добавляю отдельный сценарий с недоступным тестовым NFS после создания нового защищённого тома. Пока драйвер возвращает ошибку удаления, PV должен сохраняться с внешним finalizer; после восстановления доступа и успешного повтора — удалиться. Именно такой результат показывает пользу механизма при сбое. Модельный разбор заканчивается конкретным решением: обновлять storage-комплект отдельным пунктом и принимать работу по состоянию backend. Для заявления о реальном внедрении к этому нужны сохранённые логи и результаты выполненных прогонов.

Тайм-аут kubectl wait не доказывает поломку finalizer. Сначала проверьте ошибку DeleteVolume и интервалы повторов у вашего provisioner.

6. Что я проверяю в работающем кластере

Начинаю с инвентаризации, а не с массового удаления тестовых объектов. Нужны имя PV, reclaim policy, CSI driver, finalizers и deletionTimestamp. Параллельно читаю список контейнеров в реально работающих Pod. Смотреть только values.yaml недостаточно: обновление могло не завершиться, оператор мог восстановить прежнюю конфигурацию, а в репликах могли остаться разные версии. Для модельного NFS-комплекта достаточно двух команд; если у вас другой драйвер, замените namespace и selector. ```bash kubectl get pv -o custom-columns='NAME:.metadata.name,RECLAIM:.spec.persistentVolumeReclaimPolicy,DRIVER:.spec.csi.driver,FINALIZERS:.metadata.finalizers,DELETING:.metadata.deletionTimestamp' kubectl -n kube-system get pods -l app=csi-nfs-controller -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{range .spec.containers[*]}{.name}{"\t"}{.image}{"\t"}{.args}{"\n"}{end}{end}' ```

Если ожидаемого finalizer нет, проверяю принадлежность PV, его состояние, аргументы provisioner, завершение rollout и ошибки доступа к API. При нескольких репликах выясняю, кто лидер, по Lease и логам. Увидеть новый image в одной резервной реплике недостаточно. Для уже существующих Bound PV даю контроллеру обработать объекты и затем проверяю результат повторно. Фиксированная пауза «подождать минуту» мне не нравится: она ничего не говорит о зависшем контроллере или запрещённом обновлении PV.

Если PV уже в Terminating с внешним finalizer, иду в логи csi-provisioner и самого CSI-драйвера: ищу конкретный volumeHandle, DeleteVolume, отказ авторизации, недоступность NFS и ошибки файловой системы. Снимать finalizers целиком ради зелёной панели я не выбираю. После этого API может потерять объект, по которому контроллер должен завершить уборку. Ручное снятие допускаю только как оформленное восстановительное действие: причина понятна, состояние backend проверено, требуемая очистка выполнена отдельно. API также запрещает добавлять новые finalizers после начала удаления. [Правила работы finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/).

Не добавляйте чужой finalizer вручную «для надёжности». Без контроллера, который распознаёт PV и умеет выполнить очистку, вы получите зависшее удаление.
Обратите внимание: Что я проверяю в работающем кластере — схема
Обратите внимание: Что я проверяю в работающем кластере

7. Что делать первым, а что можно отложить

Мой порядок такой: сначала список ценных томов и их владельцев, затем версии storage-комплектов, после этого проверка finalizers и одноразовый тест удаления. Для критичной базы я предпочитаю заранее выбранный Retain плюс проверенное восстановление. Для временных сред и воспроизводимых данных выбираю Delete с работающей очисткой. Retain требует ручного учёта освобождённых томов; бесконтрольно включать его всему кластеру — значит заменить одну утечку другой. При необходимости политику меняют у самого существующего PV до начала удаления, а не надеются на новую настройку StorageClass. [Изменение reclaim policy](https://kubernetes.io/docs/tasks/administer-cluster/change-pv-reclaim-policy/).

У приложения я оставляю в поставке PVC, а жизненный цикл динамических PV поручаю системе хранения. Отдельно проверяю, не удаляет ли служебный скрипт PV раньше PVC и не очищает ли finalizers при каждом зависании. GA полезен как дополнительная устойчивость к неправильному порядку, но плохой регламент от этого хорошим не становится. Настройку красивых дашбордов и ускорение очистки на несколько секунд можно отложить. А вот список статических in-tree томов и отсутствие ответственного за их удаление — нельзя.

Есть и поправка на дату публикации: по состоянию на 5 сентября 2026 года upstream-поддержка Kubernetes 1.33 уже завершена, дата окончания — 28 июня 2026 года. Поэтому сегодня я использую 1.33 для разбора механизма и старого инцидента, а целевую поддерживаемую ветку выбираю вместе с совместимым CSI-комплектом. После такого перехода всё равно повторяю проверку конкретных PV. Критерий приёмки остаётся проверяемым: контроллер обслуживает нужные тома, а удаление заканчивается так, как задано политикой. [Сроки поддержки Kubernetes](https://kubernetes.io/releases/).

Не нужно останавливать приложения только потому, что у одного PV не найден новый finalizer. Сначала установите тип тома, политику и состояние удаления; риск зависит от этого сочетания.

Частые вопросы

Достаточно обновить весь control plane до 1.33?

Для CSI — нет. Нужны подходящий external-provisioner, действующая конфигурация и фактически установленный finalizer на обслуживаемом PV.

Все статические PV исключены из защиты?

Нет. Статические CSI поддерживаются с условиями конкретной реализации. Статические in-tree PV без CSI migration новым исправлением не покрываются.

Почему PV с finalizer остаётся в Terminating?

Возможно, контроллер ещё не завершил очистку хранилища. Проверяйте, какой finalizer удерживает объект, затем состояние PVC и ошибки CSI-драйвера.

Можно обновить provisioner после команды delete и получить защиту?

На это рассчитывать нельзя: после установки deletionTimestamp API запрещает добавление новых finalizers. Проверять защиту нужно до начала удаления.

Проверим удаление томов в Kubernetes
Я, Семёнов Евгений Сергеевич, помогу проверить жизненный цикл ваших PV и подготовить обновление Kubernetes вместе с CSI-комплектом — обратитесь в «АйТи-Фреш» через itfresh.ru. Если вам также нужны услуги rf-buh, предлагаю обсудить их отдельной задачей.
Бесплатная консультация →
© ООО «АйТи-Фреш» · Москва · Все статьи