Kubernetes 1.31 GA: AppArmor в stable и финальный аудит in-tree volume перед обновлением Deckhouse
Kubernetes 1.31 вышел GA: AppArmor stable, in-tree volume плагины удалены окончательно. Описываем процедуру проверки AppArmor-профилей и миграции томов на CSI в Deckhouse-кластерах.
Kubernetes 1.31 GA - AppArmor перешёл в stable, удалены устаревшие in-tree volume плагины
Kubernetes 1.31 перешёл в GA, и теперь это не бета, которую можно пока игнорировать. Deckhouse традиционно подтягивает мажорные версии с лагом в несколько недель после upstream-релиза, так что на ряде кластеров обновление уже доступно, а на остальных придёт в ближайшее время. Мы готовились к этому переходу ещё с июля - когда в бета-релизе 1.31 всё стало понятно по in-tree плагинам, - так что сейчас у нас уже есть конкретный опыт самого прохода обновления.
Что изменилось по сравнению с бетой
С бетой ситуация была понятна: in-tree volume плагины убраны, AppArmor получил нативный API. В GA ничего принципиально нового не добавилось, но важно, что теперь это финальное состояние: аннотации container.apparmor.security.beta.kubernetes.io/<container_name> в 1.31 ещё обрабатываются для обратной совместимости, но Kubernetes уже конвертирует их во внутреннее поле appArmorProfile автоматически. Если вы обновитесь, не проверив профили, и потом сделаете kubectl get pod -o yaml - увидите, что аннотация исчезла, а поле securityContext.appArmorProfile появилось. Само по себе не страшно, но ломает IaC, который генерирует манифесты с аннотациями и затем сверяет их с тем, что в кластере.
Процедура проверки AppArmor-профилей
На Astra Linux, которая стоит у части наших клиентов из КИИ на хостах, AppArmor включён по умолчанию. Перед обновлением мы прошлись по всем кластерам с таким профилем хостов.
Первый шаг - найти всё, что использует AppArmor через старые аннотации:
kubectl get pods -A -o json | jq -r '
.items[] |
select(.metadata.annotations |
to_entries[]? | .key |
startswith("container.apparmor.security.beta.kubernetes.io")
) |
[.metadata.namespace, .metadata.name] | @tsv
'
Второй шаг - для каждого такого пода проверить, что профиль, на который ссылается аннотация, реально загружен на нодах:
# На каждой ноде
ssh node-01 "sudo aa-status --json | jq '.profiles | keys'"
Если профиль в аннотации называется localhost/custom-nginx, а aa-status его не показывает - после обновления под не запустится: Kubernetes 1.31 попытается применить профиль через новый API и упадёт с ошибкой. Это нас и интересовало.
В нашем случае нашлись два пода с профилем localhost/legacy-profile, который был загружен вручную когда-то давно и не был упакован в DaemonSet. На новых нодах его не было. Подготовили ConfigMap с профилем и DaemonSet, который его устанавливает через apparmor_parser - классика.
Третий шаг - обновить манифесты. Новый формат для тех, кто хочет явную запись вместо аннотации:
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: custom-nginx
Для RuntimeDefault ещё проще: type: RuntimeDefault, без localhostProfile. Обновлять манифесты до обновления кластера не обязательно - старые аннотации 1.31 понимает. Но лучше сделать сейчас, чтобы потом не разбираться с расхождениями.
Миграция in-tree volume: что осталось к GA
В июле мы уже провели инвентаризацию PV по всем кластерам - и нашли несколько исторических томов, которые требовали внимания. К моменту GA основная часть была уже убрана, но один кластер с длинной историей преподнёс сюрприз: пара PV с storageClassName: manual и volumeMode: Filesystem - казалось бы, нейтральные. Но spec содержал nfs: блок, а не csi:. Это старый in-tree NFS.
In-tree NFS плагин в 1.31 формально не удалён (в отличие от облачных плагинов AWS/GCP/Azure), но мы всё равно мигрировали: nfs-subdir-external-provisioner давно стоит в этих кластерах, и жить на двух разных механизмах NFS-монтирования без веской причины незачем.
Миграция PV с данными без простоя - это всегда немного нервно. Схема стандартная: создаём новый PV через CSI с той же точкой монтирования, переключаем PVC через claimRef, не трогая данные на NFS-сервере. Если StorageClass с reclaimPolicy: Retain - старый PV остаётся в Released, его потом чистим руками после проверки.
# Проверить что осталось от in-tree (не CSI)
kubectl get pv -o json | jq -r '
.items[] |
select(.spec | has("csi") | not) |
[.metadata.name, (.spec | keys | join(","))] | @tsv
'
Вывод покажет все PV, где в spec нет блока csi. hostPath и local - легитимные исключения, всё остальное - повод разбираться.
Само обновление в Deckhouse
Deckhouse обновляет control plane и ноды последовательно, с drain перед каждой нодой. Для кластеров с PDB (PodDisruptionBudget) это иногда зависает: drain ждёт, пока можно будет вытеснить поды, но PDB не пускает. Если у вас maxUnavailable: 0 на всех деплойментах и одна реплика - drain не пройдёт никогда.
Перед обновлением стоит проверить:
- PDB без запаса.
kubectl get pdb -A- смотрим наALLOWED DISRUPTIONS. Ноль - значит drain заблокируется. - Ноды с unschedulable-тейнтами. Если нода уже в cordon по другой причине - Deckhouse может запутаться в порядке обновления.
- Версии компонентов хранилища. Rook-ceph 1.14+ для Kubernetes 1.31; более старые версии rook-ceph имеют ограничения совместимости.
Обновление через Deckhouse на managed-сопровождении в нашем случае прошло без инцидентов - примерно полтора часа на кластер с тремя мастерами и шестью воркерами. Один кластер потребовал ручного вмешательства из-за зависшего drain на ноде с maxUnavailable: 0 - временно выставили maxUnavailable: 1 на период обновления, потом вернули.
Что в итоге
AppArmor в stable - это скорее наведение порядка в API, чем новая функциональность. Реальная работа была в аудите томов и проверке профилей. Если вы ещё не провели инвентаризацию PV перед 1.31 - самое время: Deckhouse уже выкатывает это обновление, и лучше знать заранее, где у вас in-tree NFS или исторический RBD без CSI-замены.