Перезаписал тег со статикой, а image volume показывает старое: как устроен его жизненный цикл в Kubernetes 1.36
CI собрал новый бандл фронта, запушил его в реестр под тем же тегом, вы сделали rollout restart — и часть пользователей всё равно получает прошлую версию. Белый экран, 404 на main.<hash>.js, «у меня всё работает» у половины команды. Разбираю на своём стенде, как на самом деле устроен жизненный цикл image volume в Kubernetes 1.36, где именно тег вас обманывает, почему запись в том не спасёт и что я ставлю в pod template, чтобы это не повторялось.
Том разрешается один раз — на старте пода
Начну с главного, потому что почти вся путаница растёт отсюда. Image volume — это не сетевой каталог, не PVC и не bind-mount живой директории с узла. Это OCI-объект (образ или артефакт), который kubelet вместе с рантаймом разворачивает на хосте и подкладывает в контейнер как каталог. В документации формулировка прямая: том разрешается при запуске Pod, монтируется только для чтения и «gets re-resolved if the Pod gets deleted and recreated» — переразрешается, когда под удалён и создан заново.
Отсюда следствие первое, безобидное: пока под жив, содержимое тома не изменится. Никогда. Вы можете сколько угодно раз перезаписать тег в реестре — под, который уже стартовал, продолжит отдавать ровно те слои, что он смонтировал при старте. Даже если контейнер внутри пода упал по liveness-пробе и был перезапущен рантаймом: под-то не пересоздавался, sandbox тот же, том тот же. Это ровно то поведение, которого люди не ждут, когда мысленно приравнивают image volume к «каталогу».
Следствие второе, коварное: пересоздание пода — условие необходимое, но не достаточное. Пересоздали под — kubelet заново разрешает ссылку. А вот что значит «разрешить», решает pullPolicy. Если политика говорит «бери из локального кэша, если он есть», то узел, на котором нужный тег уже лежит, никуда не пойдёт и смонтирует старые слои. Тег в реестре давно указывает на другой дайджест — узлу всё равно, он про это не спрашивал.
Собственно, весь жизненный цикл укладывается в четыре пункта, и я советую держать их перед глазами при разборе любого «а почему тут старая версия»:
- Под создаётся → kubelet разрешает image.reference согласно pullPolicy → рантайм монтирует слои в mountPath только для чтения.
- Под живёт → содержимое тома заморожено; перезапуск контейнера внутри пода ничего не меняет.
- Под удалён и создан заново (rollout, эвикшн, ребут узла) → ссылка разрешается повторно, снова по pullPolicy.
- Образ, на который ссылается живой под, не будет вычищен image GC кублета; как только ссылок не осталось — станет кандидатом на удаление, и на разных узлах это происходит в разное время.
Разбор со стенда: шесть реплик, четыре из них с прошлым релизом
Возьму реальный случай, компанию назову условно — туристический магазин «РюкзакЛавка»: 30 рабочих мест в офисе и двух точках продаж, плюс интернет-магазин снаряжения. Сам магазин крутится не у них, а у подрядчика-интегратора в небольшом кластере: три рабочих узла, Kubernetes 1.36, containerd 2.1, приватный Harbor внутри периметра подрядчика. Нас позвали как вторую пару глаз, когда после очередного релиза посыпались жалобы покупателей. Фронт подрядчик вынес из основного образа именно так, как это принято рекомендовать: nginx-контейнер отдельно, статика отдельно, статика приезжает как image volume. Тем же способом в соседний Deployment раздаются справочники (размерные сетки, каталог брендов) и небольшая модель рекомендаций «с этим товаром берут». Идея хорошая — базовый nginx перестал пересобираться на каждый релиз фронта, слой со статикой весит 40 МБ вместо полутора гигабайт вместе с nginx-обвязкой.
Deployment web, шесть реплик (перед сезоном походов подрядчик поднял их с трёх), контейнер nginx:1.29-alpine, том — образ registry.example.com/shop/static:prod, смонтированный в /usr/share/nginx/html. pullPolicy в манифесте не указан вообще. Релизный пайплайн простой: собрать бандл, запушить в тот же тег prod, дальше kubectl rollout restart deployment/web. Работало полгода. Точнее — работало ровно до того момента, когда менеджер магазина сама не попала на белый экран при оформлении заказа.
Симптом: после релиза примерно 60 % запросов к /assets/main.<hash>.js уходили в 404, у части пользователей белый экран. Не у всех и не всегда — балансировка размазывала. Смотрим поды: четыре из шести отдают index.html прошлой сборки, со ссылками на бандл, которого в новом релизе уже нет. Все четыре — на двух конкретных узлах. На третьем узле обе реплики свежие.
Разгадка ровно та, что описана выше. Тег prod — не latest, значит, дефолтом включился IfNotPresent. На двух узлах образ shop/static:prod уже лежал в кэше с прошлого раза — kubelet его и взял, в реестр не ходил. На третьем узле образ незадолго до этого выкинул image GC (диск подпёр верхний порог), поэтому там пул произошёл честно и приехал свежий дайджест. Никакой мистики: сколько узлов, столько и версий статики. Причём rollout restart отработал штатно, все шесть подов действительно пересоздались — просто пересоздание само по себе ничего не гарантирует.
- Kubernetes 1.36, containerd 2.1, 3 воркера, Harbor в сети подрядчика.
- Deployment web: 6 реплик, nginx:1.29-alpine + image volume registry.example.com/shop/static:prod → /usr/share/nginx/html.
- pullPolicy не задан → по умолчанию IfNotPresent (тег не latest).
- После релиза: 4 пода на 2 узлах с прошлым бандлом, ~60 % 404 на статику и брошенные корзины, длилось до следующего вытеснения образа с узла.
- Диагноз подтвердился за две минуты: sha256sum index.html внутри каждого пода дал ровно два разных значения.
pullPolicy: три значения и дефолт, который вас подставит
Семантика pullPolicy у image volume ровно та же, что у обычного контейнерного образа, и это отдельная ловушка: люди «знают» её по контейнерам, но не переносят знание на тома. Формулировки из документации 1.36 стоит прочитать буквально. Always — kubelet всегда пытается стянуть ссылку, и если пул не удался, под уходит в Failed. Never — kubelet никогда не тянет и использует только локальный образ или артефакт; под уходит в Failed, если хоть каких-то слоёв нет локально или если манифест этого образа не закэширован. IfNotPresent — kubelet тянет, только если ссылки ещё нет на диске; под уходит в Failed, если ссылки нет и пул не удался.
Дефолт: если pullPolicy не задан, для тега :latest подставляется Always, во всех остальных случаях — IfNotPresent. То есть «нормальные» релизные теги вроде prod, stable, v2 или staging по умолчанию получают самую кэширующую политику. Именно на этом и погорел подрядчик «РюкзакЛавки». Обратите внимание: правило про :latest — про суффикс тега, а не про «свежесть»; тег prod ничем не привилегирован.
Соблазн очевиден: поставить pullPolicy: Always и забыть. Я так делаю только как временную заплатку на время разбора инцидента, и вот почему это не решение. Во-первых, реестр становится жёсткой зависимостью старта пода: реестр недоступен — под в Failed, и это касается не только выкатки, но и любого перепланирования в три часа ночи. Во-вторых, вы платите обращением к реестру за каждый старт каждого пода: у «РюкзакЛавки» при шести репликах и Always выкатка растянулась с 10 до примерно 30 секунд. В-третьих, Always не даёт воспроизводимости: два пода, стартовавшие с разницей в минуту вокруг момента пуша, всё равно получат разные дайджесты. Гонку вы сузили, но не убрали.
Про Never скажу коротко: он честно полезен ровно в одном сценарии — изолированный контур, где образы раскладываются по узлам заранее (пре-пул через DaemonSet или образ в кэше ноды). Во всех остальных случаях это способ получить Failed на пустом месте после того, как image GC подчистил диск.
- Always — пул при каждом старте пода; ошибка пула = Failed.
- Never — только локальные слои и закэшированный манифест; ничего нет = Failed.
- IfNotPresent — пул только при отсутствии ссылки на диске.
- Дефолт: Always для :latest, IfNotPresent для всех прочих тегов.
Как я это чиню: дайджест в pod template
Правильный ответ на «какая версия статики смонтирована» должен быть виден в самом манифесте, а не зависеть от того, что успел закэшировать конкретный узел. Поэтому в проде я ссылаюсь на образ по дайджесту, а не по тегу. Поле reference ведёт себя ровно как pod.spec.containers[*].image, значит форму registry/repo@sha256:... оно принимает без всяких оговорок.
Дальше приятная механика: reference — часть pod template, значит его изменение меняет хэш шаблона, Deployment создаёт новую ReplicaSet и честно выкатывает новые поды. Никаких rollout restart и никаких «а вдруг не пересоздалось». А pullPolicy при дайджесте можно смело оставить IfNotPresent: дайджест уникален, кэш узла либо содержит ровно эти слои, либо не содержит вовсе — подмены не бывает по построению. У «РюкзакЛавки» после перехода на дайджест выкатка вернулась к 12 секундам и расхождения между узлами исчезли полностью.
Сам фрагмент pod template после правки — ссылка по дайджесту и явная политика:
volumes:
- name: static
image:
reference: registry.example.com/shop/static@sha256:<дайджест из реестра>
pullPolicy: IfNotPresentУ справочников и модели рекомендаций сделали то же самое: каждый их релиз теперь виден в истории Deployment как отдельная ревизия, и откат на прошлый каталог размеров — это kubectl rollout undo, а не поиск нужного слоя в Harbor.
Единственная неприятность — kubectl set image умеет только контейнеры и про тома ничего не знает. Подстановку дайджеста в пайплайне я делаю yq или kustomize; дайджест беру у реестра через crane digest (можно skopeo inspect). Тег при этом никуда не девается — он остаётся человекочитаемым указателем «что сейчас в проде», просто в кластер едет не он. Шаг пайплайна у подрядчика после нашей правки выглядит так:
DIGEST=$(crane digest registry.example.com/shop/static:prod)
yq -i '(.spec.template.spec.volumes[] | select(.name == "static") | .image.reference) = "registry.example.com/shop/static@'"$DIGEST"'"' deploy/web.yaml
kubectl apply -f deploy/web.yaml
kubectl rollout status deployment/webИ один кластерный ремень безопасности, про который часто забывают: admission-плагин AlwaysPullImages работает и для image volume — он принудительно выставляет Always всем подам. Это не замена дайджестам, а страховка от чужих манифестов с IfNotPresent, и заодно защита от того, что под чужого namespace смонтирует приватный образ, уже лежащий на узле, без предъявления pull-секретов. Если у вас мультитенантный кластер — включайте.
- reference по дайджесту → воспроизводимость и корректный откат.
- Смена reference сама по себе запускает выкатку: rollout restart не нужен.
- pullPolicy при дайджесте — IfNotPresent, реестр перестаёт быть зависимостью каждого старта.
- AlwaysPullImages — кластерная страховка, действует и на этот тип тома.
Read-only: «сейчас просто поправлю файл в томе» не сработает
Второй по частоте сценарий после «отдаёт старое» — попытка починить расхождение руками: зайти в под и перезаписать index.html или подложить недостающий бандл. Не выйдет. OCI-объект монтируется в единственный каталог (mountPath) и только для чтения. Любая запись вернёт EROFS, «Read-only file system». Это не настройка, которую можно выключить: readOnly здесь свойство самого типа тома, а не флаг монтирования, который вы забыли снять.
Заодно закрою пару вопросов по мелким свойствам, вокруг которых ходят устаревшие статьи. Требование монтировать файлы как noexec было в альфе 1.31 и ещё действовало в бете 1.33 (об этом прямо пишет блог релиза), но в июне 2025 года, когда KEP-4639 перенацелили на бету в 1.34, его из KEP убрали — в актуальных версиях noexec больше не является обязательным свойством тома, и если исполняемые файлы вам там не нужны, запрет запуска стоит обеспечивать политиками допуска и securityContext. Поле spec.securityContext.fsGroupChangePolicy на этот тип тома не влияет никак, так что менять владельца файлов через fsGroup бессмысленно: подгоняйте UID/GID прямо при сборке образа. subPath и subPathExpr поддерживаются, но только начиная с 1.33 — если у вас в парке остались более старые кластеры, манифест туда не переедет.
Если писать всё-таки нужно — а нужно это чаще, чем кажется: кто-то генерирует sitemap, кто-то подставляет runtime-конфиг в index.html — берите нормальный паттерн. Image volume монтируется в init-контейнер, init-контейнер копирует содержимое в emptyDir, рабочий контейнер монтирует emptyDir и пишет туда. Стоит это одного лишнего копирования на старте пода и полной ясности с правами.
И про доступ к приватному реестру: pull-секреты для тома собираются точно так же, как для контейнерного образа — из учётных данных узла, imagePullSecrets сервис-аккаунта и imagePullSecrets в спеке пода. Отдельного места для секретов у image volume нет, и это хорошая новость: ничего нового настраивать не надо.
- Запись в том невозможна: read-only на уровне типа тома, EROFS.
- noexec-требование убрано из KEP при перенацеливании беты на 1.34 (в 1.33 ещё действовало).
- fsGroupChangePolicy не действует; права задавайте в Dockerfile.
- subPath/subPathExpr — с версии 1.33.
- Нужна запись — init-контейнер копирует image volume в emptyDir.
Диагностика: за пять минут понять, что реально смонтировано
Самый быстрый и универсальный способ, работающий на любой версии и без всяких feature gate, — сравнить контрольные суммы внутри подов и заодно посмотреть, на каких узлах они живут. Если сумм получилось больше одной, дальше можно не гадать: у вас разошёлся кэш образов между узлами, и виноват тег плюс IfNotPresent. Я делаю это так:
kubectl get pod -l app=web -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName
for p in $(kubectl get pod -l app=web -o name); do
echo "$p $(kubectl exec "$p" -c nginx -- sha256sum /usr/share/nginx/html/index.html)"
doneБолее цивилизованный способ появился в 1.35 в виде feature gate ImageVolumeWithDigest: при его включении kubelet записывает дайджест смонтированного образа прямо в статус пода — в containerStatuses[*].volumeMounts[*].imageRef. Штука ровно для нашей задачи, но честно предупрежу: гейт по состоянию на 1.36 остаётся альфой и по умолчанию выключен. На управляемом кластере вы его, скорее всего, включить не сможете, на своём — включайте на свой страх и риск и не стройте на нём мониторинг. Если гейт включён, дайджест читается одной командой:
kubectl get pod -l app=web -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[*].volumeMounts[*].imageRef}{"\n"}{end}'Если есть доступ на узел, картину даёт crictl: посмотреть, какой дайджест соответствует тегу в локальном кэше конкретной ноды, и сравнить с тем, что отдаёт реестр:
crictl images --digests | grep shop/static
crane digest registry.example.com/shop/static:prodИменно так подтверждается гипотеза «на двух нодах старый слой». И, наконец, у kubelet есть три счётчика по этой фиче: kubelet_image_volume_requested_total, kubelet_image_volume_mounted_succeed_total и kubelet_image_volume_mounted_errors_total. Их я советую завести в дашборд сразу: рост errors_total — это ваши поды, которые не стартуют из-за недоступного реестра или опечатки в reference, и увидеть это лучше на графике, чем в тикете от поддержки.
Про сами ошибки: сбой разрешения или пула образа на старте блокирует запуск контейнеров, повторяется по обычному volume backoff и отражается в reason и message пода. То есть под будет висеть не в CrashLoopBackOff, а именно в состоянии, где контейнеры ещё не стартовали — смотрите kubectl describe pod, а не логи приложения, логов там пока просто нет.
- kubectl get pod -l app=web -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName — увидеть распределение по узлам.
- sha256sum смонтированного файла в каждом поде — самый быстрый детектор расхождения.
- ImageVolumeWithDigest (alpha, с 1.35) → .status.containerStatuses[*].volumeMounts[*].imageRef.
- crictl images на узле — сверить дайджест локального кэша с реестром.
- kubelet_image_volume_requested_total / _mounted_succeed_total / _mounted_errors_total — на дашборд.
Когда image volume вообще стоит брать, а когда не стоит
Фича в 1.36 доведена до stable, гейт ImageVolume включён по умолчанию и специально ничего включать не надо — путь был долгий: альфа в 1.31, бета с выключенным по умолчанию гейтом с 1.33–1.34, бета по умолчанию включённая в 1.35, stable в 1.36. Единственное реальное требование — рантайм на узлах должен уметь монтировать OCI-объекты. CRI-O умеет с 1.31, containerd — с 2.1.0 (в release notes: «Add OCI/Image Volume Source support»), так что узлы на containerd 1.7 или 2.0 не подойдут; если у вас зоопарк узлов с разными рантаймами, проверьте это до того, как переписывать манифесты, иначе поды на «неумеющих» узлах просто не поднимутся.
Где image volume действительно хорош. Первое — ровно наш кейс: отделить статику фронта от веб-сервера, чтобы не пересобирать полуторагигабайтный образ ради изменения одного JS-файла. Второе — большие бинарные наборы данных: веса моделей, базы сигнатур антивируса, справочники — как каталог размеров и модель рекомендаций у «РюкзакЛавки», которые не хочется и часто нельзя запекать в образ приложения. Третье — конфиги и наборы файлов, которые надо шарить между несколькими контейнерами одного пода, не раздувая основной образ.
Где не стоит. Если файл маленький и текстовый — это ConfigMap или Secret, они проще, версионируются вместе с остальными манифестами и, в отличие от image volume, умеют обновляться в живом поде без пересоздания (с задержкой на синхронизацию kubelet). Помните только про потолок: объект в etcd ограничен примерно мегабайтом, и утрамбовывать туда бандл фронта — плохая идея, как раз ради таких случаев image volume и появился. Если данные должны меняться в рантайме и переживать перезапуск — вам нужен PVC, а не read-only-каталог. А если статики немного и релизный цикл у неё общий с приложением — не усложняйте, запекайте в образ приложения и живите спокойно.
Короткий чек-лист, который я прогоняю перед выкаткой любого манифеста с image volume:
- reference указан по дайджесту sha256, а не по тегу.
- pullPolicy задан явно, а не оставлен на дефолт.
- Приложение не пытается писать в mountPath — иначе init-контейнер и emptyDir.
- Рантайм на всех узлах умеет монтировать OCI-объекты: CRI-O ≥ 1.31, containerd ≥ 2.1.
- В пайплайне дайджест подставляется автоматически (crane/skopeo + yq или kustomize).
- Счётчики kubelet_image_volume_* заведены в мониторинг.
- На мультитенантном кластере включён AlwaysPullImages.
Частые вопросы
Нужно ли в Kubernetes 1.36 включать feature gate ImageVolume?
Нет. С 1.36 фича в статусе stable, гейт включён по умолчанию и на kube-apiserver, и на kubelet. Единственное, что стоит проверить, — поддержку монтирования OCI-объектов контейнерным рантаймом на всех узлах: CRI-O умеет это с версии 1.31, containerd — с 2.1.0. Если в кластере есть узлы со старым рантаймом, поды с image volume на них не поднимутся.
Я сделал kubectl rollout restart, поды пересоздались — почему статика всё равно старая?
Потому что пересоздание пода только запускает повторное разрешение тома, а решение «идти в реестр или взять из кэша» принимает pullPolicy. Если тег не :latest и политика не указана явно, действует IfNotPresent: узел, на котором образ с этим тегом уже лежит, в реестр не пойдёт и смонтирует старые слои. Ссылайтесь на образ по дайджесту — тогда rollout restart вообще не понадобится, смена reference сама создаст новую ReplicaSet.
Можно ли записать файл в image volume или смонтировать его на запись?
Нельзя. OCI-объект монтируется в один каталог строго read-only, любая запись вернёт EROFS «Read-only file system», и отключить это нельзя — так устроен сам тип тома. Если запись нужна, смонтируйте image volume в init-контейнер, скопируйте содержимое в emptyDir и работайте с ним из основного контейнера.
Как узнать, какой именно дайджест смонтирован в конкретном поде?
Универсальный способ без включения чего-либо — снять контрольную сумму файла внутри пода через kubectl exec и сравнить между репликами; расхождение сразу укажет на кэш узлов. Более аккуратный вариант — feature gate ImageVolumeWithDigest, который с 1.35 пишет дайджест в .status.containerStatuses[*].volumeMounts[*].imageRef, но он по-прежнему в альфе и по умолчанию выключен. На самом узле дайджест видно через crictl images.
Что произойдёт, если реестр недоступен в момент старта пода?
При pullPolicy: Always kubelet попытается стянуть ссылку, и при неудаче под уйдёт в Failed. При IfNotPresent под стартует, если слои уже есть на узле, и упадёт, если их нет и пул не удался. Ошибка при разрешении образа блокирует запуск контейнеров, повторяется по обычному volume backoff и отражается в reason и message пода — смотрите kubectl describe pod, логов приложения там ещё не будет. Именно поэтому в проде я предпочитаю дайджест плюс IfNotPresent: реестр перестаёт быть зависимостью каждого старта.
Image volume заменяет ConfigMap?
Нет, у них разные ниши. ConfigMap проще, версионируется вместе с манифестами и умеет обновляться в живом поде без его пересоздания, но объект в etcd ограничен примерно мегабайтом. Image volume, наоборот, рассчитан на десятки и сотни мегабайт — бандлы фронта, веса моделей, справочники, — но обновляется только через пересоздание пода. Маленький текстовый конфиг — ConfigMap, большой неизменяемый набор файлов — image volume, изменяемые данные — PVC.
Подходит ли image volume для раздачи справочников и небольших ML-моделей, если магазин живёт у подрядчика?
Да, это одна из заявленных целей фичи: веса модели или выгрузка справочника упаковываются в отдельный OCI-образ и монтируются в под только для чтения, не раздувая образ приложения. Условия те же, что и для статики: ссылка по дайджесту, явный pullPolicy и рантайм с поддержкой на всех узлах. Если кластер ведёт подрядчик, попросите у него в договоре или регламенте фиксацию дайджестов в манифестах и доступ на чтение к истории Deployment — тогда вы сами видите, какая версия каталога сейчас в проде.
Источники
- Kubernetes Documentation — Volumes, раздел image (v1.36) — Concepts / Storage / Volumes, подраздел «image»: разрешение тома при запуске Pod, семантика Always/Never/IfNotPresent, дефолт Always для :latest и IfNotPresent в остальных случаях, монтирование read-only, поля reference и pullPolicy, отсутствие эффекта fsGroupChangePolicy, subPath с v1.33, работа AlwaysPullImages. https://v1-36.docs.kubernetes.io/docs/concepts/storage/volumes/#image
- Kubernetes Feature Gates — ImageVolume и ImageVolumeWithDigest — Файл стадий гейта ImageVolume: alpha 1.31–1.32, beta 1.33–1.34 (default false), beta default true в 1.35, stable с 1.36. Гейт ImageVolumeWithDigest: alpha с 1.35, добавляет дайджест смонтированного образа в статус пода. https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/
- KEP-4639 «OCI images as VolumeSource» (SIG Node) — kep.yaml: stage stable, latest-milestone v1.36, milestone alpha v1.31 / beta v1.34 / stable v1.36; журнал изменений с записью от 17.06.2025 «dropped noexec requirement»; список метрик kubelet и требования к контейнерным рантаймам. https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/4639-oci-volume-source
- Kubernetes Blog — «Kubernetes 1.31: Read Only Volumes Based On OCI Artifacts (alpha)» — Первичное описание фичи: поведение pullPolicy, переразрешение тома при пересоздании пода, сборка pull-секретов из node credentials / service account / pod spec, поддержка в CRI-O ≥ v1.31. https://kubernetes.io/blog/2024/08/16/kubernetes-1-31-image-volume-source/
- Kubernetes Documentation — Use an Image Volume With a Pod — Практическая задача с примерами манифестов pods/image-volumes.yaml и pods/image-volumes-subpath.yaml, требования к рантайму, минимальная версия сервера v1.31, статус stable в v1.36. https://kubernetes.io/docs/tasks/configure-pod-container/image-volumes/
- Sysdig Blog — Kubernetes 1.36: New security features, OCI VolumeSource — Обзор перевода OCI VolumeSource в stable в 1.36 и рекомендации по безопасности: ограничивать недоверенные реестры, рассматривать блокировку подов с image-томами политиками допуска. https://www.sysdig.com/blog/kubernetes-1-36-new-security-features
- Kubernetes Blog — «Kubernetes v1.33: Image Volumes graduate to beta!» — Бета 1.33 с выключенным по умолчанию гейтом, поддержка subPath/subPathExpr, метрики kubelet_image_volume_*, статус поддержки в CRI-O и containerd v2.1.0. https://kubernetes.io/blog/2025/04/29/kubernetes-v1-33-image-volume-beta/
- containerd v2.1.0 — release notes — Раздел CRI: «Add OCI/Image Volume Source support» (PR #10579). https://github.com/containerd/containerd/releases/tag/v2.1.0
- kubernetes/kubernetes — pkg/kubelet/metrics/metrics.go — Константы image_volume_requested_total, image_volume_mounted_succeed_total, image_volume_mounted_errors_total в подсистеме kubelet. https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/metrics/metrics.go
