Стадия жизненного цикла модуля: Общедоступная версия
У модуля есть требования для установки
Что такое Snapshot?
Snapshot (state-snapshotter.deckhouse.io/v1alpha1, короткое имя snap) — это namespaced-ресурс, снимок desired-state namespace на определённый момент времени. Вы создаёте Snapshot в namespace, и модуль захватывает манифесты пользовательских объектов этого namespace в неизменяемый, надёжно сохранённый артефакт, который позже можно прочитать или восстановить в другой namespace.
Snapshot — одноразовый и неизменяемый: спека замораживается при создании, namespace захватывается ровно один раз, повторного захвата нет. Чтобы сделать новый снимок, создайте новый Snapshot.
| Свойство | Значение |
|---|---|
| API-группа / версия | state-snapshotter.deckhouse.io/v1alpha1 |
| Kind | Snapshot (короткое имя snap) |
| Scope | Namespaced (захватывает свой namespace) |
| Спека | Неизменяема после создания |
Как это работает
При создании Snapshot контроллер:
- Перечисляет все namespaced-виды ресурсов в кластере через discovery API Kubernetes (не по захардкоженному списку) — поэтому захватываются и произвольные кастомные ресурсы.
- Листит объекты каждого вида в namespace снимка и отбирает те, что представляют пользовательский desired-state (см. Что попадает в снимок).
- Сохраняет отобранные манифесты в надёжный cluster-scoped артефакт (
SnapshotContent+ внутреннийManifestCheckpoint, с разбиением на чанки для больших объёмов). - Публикует статус: когда захват завершён и сохранён,
SnapshotстановитсяReady=True, аstatus.boundSnapshotContentNameуказывает на сохранённый контент. - Удерживает сохранённый контент после удаления
Snapshotв течение настраиваемого TTL — чтобы захваченное состояние пережило удаление объекта-запроса.
Захват выполняется fail-closed: если какой-то вид ресурса нельзя прочитать (например, RBAC ещё не распространился или сломан агрегированный API), контроллер не создаёт частичный снимок — Snapshot просто не доходит до Ready=True, и попытка повторяется, пока вид ресурса не станет читаемым. Диагностика публикуется в двух местах: status.captureState.domainSpecificController.message перечисляет виды ресурсов, которые не удалось прочитать, а на Snapshot выпускается событие типа Warning с reason NamespacePlanUnreadable.
Что попадает в снимок
Модель — default-include: в снимок попадает любой namespaced-объект целевого namespace, кроме явно исключённого. Отдельного allowlist «снимать только это» нет.
Попадает (примеры):
- пользовательские объекты конфигурации и данных:
ConfigMap,Secret(любого типа, кроме service-account-token),Service,PersistentVolumeClaim,ServiceAccount(кромеdefault), standalonePod(без controller-владельца); - workload-объекты верхнего уровня, которые создаёте вы:
Deployment,StatefulSet,DaemonSet,Job,CronJobи т. п. Их производные (ReplicaSet/Pod) исключаются — владелец пересоздаёт их при restore; - любые кастомные ресурсы в namespace, в т. ч. агрегированные API с реальным хранилищем;
- сеть и RBAC:
Ingress,NetworkPolicy,Role,RoleBindingи др.
Не попадает:
| Категория | Примеры | Почему |
|---|---|---|
| Производные (controller-owned) | объекты с ownerReference.controller: true (ReplicaSet/Pod от Deployment) |
владелец пересоздаёт их при restore |
| Control-plane noise | Event, Endpoints, Lease, CiliumEndpoint, ConfigMap/kube-root-ca.crt, ServiceAccount/default, service-account-token Secret |
регенерируется контрол-плейном / CNI, не пользовательский desired-state |
| Виртуальные / вычисляемые | metrics.k8s.io (PodMetrics/NodeMetrics), custom/external.metrics.k8s.io |
не хранятся (нет verb watch), невосстановимы |
| Снапшотная машинерия | CSI VolumeSnapshot, snapshot/content-виды, создаваемые самим модулем |
self-referential |
| Машинерия модуля | вся группа state-snapshotter.deckhouse.io и request/transfer-виды state-snapshotter.deckhouse.io (VolumeCaptureRequest, VolumeRestoreRequest, DataExport, DataImport) |
внутренние execution-объекты |
Специального правила «исключать объекты, управляемые Deckhouse» нет. Deckhouse-managed объекты отсекаются теми же общими сигналами (controller-owned, control-plane noise или машинерия модуля). Всё остальное в namespace — в том числе ресурсы, которые вы лишь настроили поверх модулей, — считается desired-state и попадает в снимок.
Исключение объектов из снимка
Snapshot захватывает namespace целиком. Чтобы объект не попал в снимок, повесьте на него лейбл state-snapshotter.deckhouse.io/exclude до создания Snapshot — захват одноразовый, и лейбл, поставленный позже, уже созданный снимок не меняет:
d8 k -n myns label configmap scratch-cache state-snapshotter.deckhouse.io/exclude=""Объект исключает само наличие лейбла; значение игнорируется ("", true — что угодно).
Исключение работает на любом уровне дерева снимка:
- Обычный namespaced-объект (
ConfigMap,Secret, …) выпадает из захваченных манифестов namespace. - Доменный ресурс (например,
VirtualMachine) вообще не разворачивается в дочерний снимок — его поддерево не захватывается целиком. - Объект внутри доменного поддерева (например, один
VirtualDiskзахваченной ВМ или сопутствующийSecret) выпадает один; узел-владелец захватывается как обычно. PersistentVolumeClaimне захватывается data-ногой, а пользовательский CSIVolumeSnapshotповерх исключённого PVC остаётся обычным CSI-снапшотом — модуль его не адоптирует.
Исключение не наследуется: исключение доменного ресурса убирает его снимочное поддерево, но объекты, которые остальные ноги захвата видят сами по себе, — его PersistentVolumeClaim, сопутствующие объекты — продолжают захватываться, пока лейбл не стоит на них самих.
Исключённый объект фиксируется, а не пропадает молча: исключивший узел перечисляет его в status.captureState.domainSpecificController.excludedRefs (точные apiVersion/kind/name), а привязанный SnapshotContent агрегирует исключения всего дерева в status.excludedRefs. По этим полям можно убедиться, что исключение сработало.
Как и встроенные исключения выше, лейбл может только сузить захват. Include-списка на снимок нет: захват — это всегда «namespace минус исключения». Чтобы захватывать разные подмножества, используйте разные namespace.
Создание Snapshot
Создайте Snapshot в том namespace, который хотите захватить. Режим по умолчанию (spec.mode: Capture) выполняет динамический захват namespace:
d8 k apply -f - <<EOF
apiVersion: state-snapshotter.deckhouse.io/v1alpha1
kind: Snapshot
metadata:
name: my-namespace-snapshot
namespace: my-app
spec: {}
EOFСпека неизменяема.
spec: {}выбирает динамический захват namespacemy-app. Не добавляйте поле целевого namespace —Snapshotвсегда захватывает свой namespace.
Отслеживание статуса
d8 k -n my-app get snapNAME READY REASON CONTENT AGE
my-namespace-snapshot True Completed nss-content-1f4b0c9d2a3e5f76 30s
Подробности — в conditions:
d8 k -n my-app get snap my-namespace-snapshot -o jsonpath='{.status.conditions}' | jqКлючевые поля статуса:
status.conditions[type=Ready]— общая готовность.Ready=Trueозначает, что захват завершён и сохранён.status.boundSnapshotContentName— надёжный cluster-scoped контент с захваченным состоянием.status.childrenSnapshotRefs— дочерние снимки, когда доменные контроллеры формируют дерево снимков (например, виртуальные машины/диски).
Чтение захваченного состояния
Захваченные манифесты читаются через контролируемый агрегированный subresource (сырые payload’ы во внутренних чанках напрямую не доступны):
d8 k get --raw \
"/apis/subresources.state-snapshotter.deckhouse.io/v1alpha1/namespaces/my-app/snapshots/my-namespace-snapshot/manifests-download" \
| jq 'length'manifests-download возвращает манифесты только своего узла — по дереву снимка он не ходит — и возвращает их как захвачено, вместе со status и другими рантайм-полями. Остальное дерево читается по запросу на узел: status.childrenSnapshotRefs перечисляет прямых детей только что прочитанного узла, каждый ребёнок читается так же через агрегированную группу своего вида, а его собственный status.childrenSnapshotRefs продолжает обход.
Группа и версия, по которым адресуется ребёнок, определяются видом самого ребёнка, а не группой, по которой вы читали его родителя. Ребёнок вида CSI VolumeSnapshot — узел, в который захватываются данные обычного PVC, — обслуживается модулем, которому принадлежит этот вид: группа subresources.storage-foundation.deckhouse.io, версия v1, ресурс volumesnapshots ({name} ниже — имя ребёнка из status.childrenSnapshotRefs родителя):
d8 k get --raw \
"/apis/subresources.storage-foundation.deckhouse.io/v1/namespaces/my-app/volumesnapshots/{name}/manifests-download" \
| jq 'length'Имена endpoint’ов в обеих группах одинаковые, поэтому между узлом Snapshot и узлом тома меняются только группа, версия и ресурс. Следите за версией: у группы узлов Snapshot это v1alpha1, у группы узлов тома — v1; обращение к одной с версией другой ни во что не откатывается — это просто путь, который API-сервер не обслуживает. Узел, который добавил доменный модуль — скажем, виртуальная машина и её диски, — обслуживается собственной endpoint-группой этого модуля: она не совпадает с группой узла тома и из неё не выводится. Полная таблица групп, версий и endpoint’ов — в Интеграции с системами резервного копирования.
Восстановление
Захваченный namespace восстанавливается через контролируемый read-path модуля — внутренние объекты-запросы (например, VolumeRestoreRequest) вы не создаёте вручную: это машинерия модуля, она не доступна пользователям.
-
Манифесты — прочитать захваченные объекты узел за узлом через endpoint
manifests-downloadвыше и применить их в целевой namespace. -
Состояние с данными — чтобы восстановить объекты вместе с их персистентными данными, читайте снимок через data-restoration-путь
/apis/subresources.state-snapshotter.deckhouse.io/v1alpha1/namespaces/{namespace}/snapshots/{name}/manifests-with-data-restoration(GET). В отличие отmanifests-downloadон рекурсивен — адресованный узел и его дети приходят одним ответом — и манифесты в нём подготовлены к применению: рантайм-метаданные (uid,resourceVersion,ownerReferences,managedFieldsи т. п.) иstatusвычищены, а сами объекты адресованы в namespace того снимка, который вы запросили.Границу модулей рекурсия проходит за вас: узел тома внутри дерева компилирует модуль, которому принадлежит вид
VolumeSnapshot, и его объекты приходят в том же ответе — этот endpoint вы вызываете один раз, на узлеSnapshot, и дерево не собираете сами. Он fail-closed: если какой-нибудь узел адресованного поддерева не готов, запрос завершается ошибкой, а не отдаёт namespace частично.Параметра, которым выбирают, куда попадут объекты, нет: они всегда возвращаются адресованными в namespace того
Snapshot, который вы запросили, а запрос, пытающийся адресовать их в любой другой namespace, отклоняется с400, а не выполняется молча в namespace самого снимка. Чтобы восстановить содержимое namespace в другом месте, снимок надо сначала туда перенести — выгрузить и загрузить в целевой namespace — и уже там читать. Заявку с данными переадресацией и не перенести: она добирается до данных черезPersistentVolumeClaim.spec.dataSourceRef, это поле не несёт namespace и читается внутри namespace самой заявки, аVolumeSnapshot, который в нём назван, остаётся вместе со снимком.Тело ответа — обычный JSON-массив объектов, не объект
Listи не поток YAML: клиент обходит массив и применяет элементы сам, а не отправляет тело вapply -f -. Материализацию данных в этот namespace модуль выполняет внутри себя; результат вы получаете через агрегированный API, а не создавая объекты переноса данных самостоятельно.
Режимы снимка
Способ получения содержимого выбирается полем spec.mode; оно неизменяемо, как и вся спека:
| Режим | spec |
Поведение |
|---|---|---|
Capture (по умолчанию) |
spec: {} или явное spec.mode: Capture |
Контроллер захватывает живой namespace. |
Import |
spec.mode: Import |
Snapshot материализуется из загруженного payload вместо живого namespace — используется инструментами миграции/восстановления между кластерами. Живой namespace при этом не захватывается никогда. |
Пишите имя поля точно. API-сервер вырезает неизвестные поля до валидации, поэтому опечатка или устаревшее имя не отвергаются: объект принимается, поле выбрасывается, и снимок молча остаётся в режиме
Capture— то есть захватывает живой namespace там, где ожидался импорт. Прочитайте режим обратно у созданного объекта, чтобы убедиться, что сохранилось именно то, что вы просили:d8 k -n my-app get snap my-namespace-snapshot -o jsonpath='{.spec.mode}'
Жизненный цикл и удержание
- Одноразовый / неизменяемый. Спека замораживается при создании;
metadata.generationне растёт; повторного захвата нет. - Удержание после удаления. Сохранённый
SnapshotContentудерживается в течение TTL (настраивается на контроллере) после удаления объектаSnapshot, через якорьObjectKeeper. Это позволяет захваченному состоянию пережить объект-запрос. После истечения TTL контент и его checkpoints собираются сборщиком мусора. - Нет фоновой нагрузки. Модуль работает только в ответ на явные запросы; непрерывного фонового захвата нет.
Защита от удаления
Внутренние объекты дерева снимка защищены от случайного прямого удаления admission-политикой (delete-guard).
Это закрывает класс инцидентов, когда удаление дочернего объекта (например, managed CSI VolumeSnapshot)
из UI тихо деградирует корневой Snapshot, хотя долговечные данные ещё существуют.
- Что защищено. Namespaced-узлы внутри дерева: дочерние
Snapshotи managed CSIVolumeSnapshot. Они несут markerstate-snapshotter.deckhouse.io/delete-protected: "true". - Что НЕ защищено. Корневой
Snapshotmarker’а не имеет — его удаление это штатный способ снести всё дерево (контроллеры сами удалят внутренние объекты). Cluster-scoped объекты дерева (SnapshotContent,ManifestCheckpointи его чанки,ObjectKeeper, managed CSIVolumeSnapshotContent) marker’а тоже не несут: из namespace они недостижимы, а повреждение там проявляется деградацией дерева, а не отказом admission. Объекты вне дерева снимка защита не затрагивает. - Принудительное удаление (break-glass). Чтобы удалить защищённый объект напрямую, поставьте на него
аннотацию
deckhouse.io/allow-delete: "true"и удаляйте. Сам marker обычный пользователь снять/изменить не может; аннотация — единственный поддерживаемый обход, обратима до момента удаления. - Защита включена всегда. Настройки модуля для отключения или ослабления guard нет. Для исключительного прямого удаления используйте break-glass-аннотацию выше.
Замечания и ограничения
Snapshotзахватывает только namespaced-ресурсы своего namespace; сам cluster-scoped объектNamespaceне захватывается.- Манифесты захватываются как есть (включая
status); очистка полей (status,resourceVersion,uidи т. п.) выполняется на read-path восстановления, а не при захвате. - Объекты
Secretзахватываются дословно (данные сохраняются как есть), кроме service-account-token-секретов, которые исключаются правилом включения. Шифрование хранилища снимков at-rest — отдельная будущая задача.