Стадия жизненного цикла модуля: Общедоступная версия

У модуля есть требования для установки

Что такое 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 контроллер:

  1. Перечисляет все namespaced-виды ресурсов в кластере через discovery API Kubernetes (не по захардкоженному списку) — поэтому захватываются и произвольные кастомные ресурсы.
  2. Листит объекты каждого вида в namespace снимка и отбирает те, что представляют пользовательский desired-state (см. Что попадает в снимок).
  3. Сохраняет отобранные манифесты в надёжный cluster-scoped артефакт (SnapshotContent + внутренний ManifestCheckpoint, с разбиением на чанки для больших объёмов).
  4. Публикует статус: когда захват завершён и сохранён, Snapshot становится Ready=True, а status.boundSnapshotContentName указывает на сохранённый контент.
  5. Удерживает сохранённый контент после удаления 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), standalone Pod (без 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-ногой, а пользовательский CSI VolumeSnapshot поверх исключённого 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: {} выбирает динамический захват namespace my-app. Не добавляйте поле целевого namespace — Snapshot всегда захватывает свой namespace.

Отслеживание статуса

d8 k -n my-app get snap
NAME                    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 CSI VolumeSnapshot. Они несут marker state-snapshotter.deckhouse.io/delete-protected: "true".
  • Что НЕ защищено. Корневой Snapshot marker’а не имеет — его удаление это штатный способ снести всё дерево (контроллеры сами удалят внутренние объекты). Cluster-scoped объекты дерева (SnapshotContent, ManifestCheckpoint и его чанки, ObjectKeeper, managed CSI VolumeSnapshotContent) 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 — отдельная будущая задача.