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

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

Перед включением модуля необходимо разрешить использование экспериментальных модулей в ModuleConfig deckhouse. Для этого установите параметр allowExperimentalModules в true.

Модуль находится в стадии Experimental. API, конфигурация и custom resources могут меняться без предупреждения; не используйте его для продуктивных нагрузок.

Модуль system-s3 предоставляет системное объектное хранилище S3 — то, в котором компоненты Deckhouse держат свои данные. Он поднимает один кластер Garage на узлах, выбранных по роли system-s3, затем system, затем среди доступных узлов. SystemBucket создаёт бакет и параметры подключения; временные учётные данные потребитель получает отдельно через STS по токену ServiceAccount и RBAC.

Это не сервис хранения для приложений: пользовательские нагрузки получают объектное хранилище из модуля sds-object, где живут пользовательские бэкенды. Этот модуль существует, чтобы компонентам платформы было куда писать данные, не дожидаясь того модуля, и намеренно сделан меньше.

Что умеет и чего не умеет

Что модуль умеет:

  • выдавать компоненту платформы бакет по SystemBucket с квотой по месту и по числу объектов, а параметры подключения — в ConfigMap;
  • выдавать короткоживущие ключи через STS по токену ServiceAccount, с правами через обычный RBAC;
  • работать с одной репликой или с тремя в отказоустойчивом режиме и переключаться между ними на тех же данных;
  • переезжать на другой StorageClass со сверкой каждого объекта; запись приостанавливается только на время переключения;
  • растить тома вместе с размерами бакетов и при заполнении реплики;
  • сообщать о своём состоянии в условиях Module/system-s3, метриках, алертах, дашбордах Grafana и на странице в консоли.

Чего модуль не умеет:

  • обслуживать приложения: объектное хранилище для них — в модуле sds-object;
  • выдавать долгоживущие ключи;
  • хранить версии объектов и блокировать объекты (Object Lock): Garage этого не поддерживает;
  • работать с внешним S3: данные всегда лежат в собственном кластере Garage модуля;
  • уменьшать тома и сохранять данные бакета после удаления его SystemBucket;
  • ограничивать CPU, память, диск и сеть хранилища на узлах: вместо этого выделите ему собственные узлы с ролью system-s3 (см. Data plane).

Custom resource

Ресурс Scope Назначение
SystemBucket (sb) Namespaced Запрашивает бакет в системном хранилище и получает в том же namespace ConfigMap с параметрами подключения к S3.

Это весь API, и каждое отсутствующее поле — сознательное решение:

  • Нет ресурса для самого хранилища. Модуль поднимает ровно один data plane, его настройки живут в ModuleConfig. Ссылаться не на что, и знать имя не нужно.
  • Нет отдельного ресурса доступа. Права задаются RBAC на systembuckets/read и systembuckets/write. STS выдаёт временные ключи; долгоживущих ключей и аннотации ручной ротации в этом контракте нет.
  • Нет политики хранения данных (reclaim policy). Удаление SystemBucket удаляет бакет и все объекты в нём: компонент платформы владеет своим бакетом целиком.

Кому доступен

SystemBucket принимается только в namespace, у которого есть и лейбл heritage: deckhouse — его Deckhouse ставит на namespace каждого модуля, — и имя вида d8-* или kube-*, которое может дать namespace только Deckhouse (политика допуска system-ns.deckhouse.io). Одного из условий мало: лейбл может поставить любой, кому разрешено менять лейблы namespace, а алерты модуля видят только namespace с такими именами.

Пользовательских прав на ресурс не выдаётся: видеть и менять его могут только администраторы кластера и хранилища (capability d8:system-capability:system-s3:* в DKP 1.78 и новее, роли d8:manage:permission:module:system-s3:* в более ранних версиях).

Data plane

Data plane создаётся вместе с первым SystemBucket, а не при включении модуля: пока ни один компонент платформы не попросил бакет, модуль запускает только контроллер и эндпоинт выдачи ключей, а Module/system-s3 сообщает StorageReady=False с причиной WaitingForFirstBucket. Созданный data plane остаётся: удаление последнего SystemBucket удаляет этот бакет, а не хранилище; убрать хранилище можно только удалением namespace выключенного модуля (см. Удаление модуля). До первого бакета настройки можно менять свободно: они определяют, что будет создано, и миграцию не запускают.

Один кластер Garage, который разворачивает контроллер:

  • в отказоустойчивом режиме платформы — три реплики с фактором репликации 3 на разных подходящих узлах: обязательная anti-affinity запрещает размещать их вместе. Если подходящих узлов меньше трёх, часть Pod останется Pending;

  • размер не настраивается: том каждой реплики считается из суммы размеров всех SystemBucket (плюс 10 % запаса и 4 ГиБ резерва под метаданные, не меньше 10 ГиБ, с округлением вверх до кратного 5 ГиБ) и растёт вместе с ней, если StorageClass позволяет расширение томов. Тома растут и тогда, когда на томе какой-то реплики свободно меньше 5 % (CapacityReady=Exhausted), — по одному шагу в 5 ГиБ: метаданные Garage могут перерасти любой резерв;

  • хранение — один PersistentVolumeClaim на реплику, заказанный из storageClass (при его отсутствии — global.modules.storageClass, затем StorageClass по умолчанию). Класс обязателен: без него data plane не разворачивается, и каждый SystemBucket об этом сообщает;

  • вне этого режима — одна реплика с фактором репликации 1: треть потребления и никакой избыточности. Такая реплика никуда не переезжает: на локальном классе её переезд означал бы потерю всего хранилища (второй копии нет), поэтому при уходе узла она остаётся Pending с целыми данными на диске этого узла — хранилище недоступно, пока узел не вернётся. На сетевом классе том едет за подом, и перерыв ограничен переносом. Режим берётся из highAvailability, затем из global.highAvailability, а если не задано ни то, ни другое — из того, отказоустойчив ли сам control plane. В последнем случае уменьшение не исполняется: кластер, потерявший мастера, читается как неотказоустойчивый, но хранилище остаётся с тремя репликами, пока highAvailability: false не написано прямо;

  • во что обходится переезд реплики, решает класс. Там, где тома привязаны к узлу (локальный класс), переезд оставляет копию на старом узле, и Garage восстанавливает её с остальных — следующая реплика переезжает, только когда это восстановление закончено, то есть не раньше чем через десять минут после предыдущего переезда; там, где том едет за подом (сетевой класс), том просто подключается в новом месте. Модуль определяет это по самому тому;

  • размещение следует правилу платформы для системных компонентов: data plane работает на узлах с ролью node-role.deckhouse.io/system-s3, если они в кластере есть, иначе на системных узлах (node-role.deckhouse.io/system), а если нет и их — на мастерах; допускается taint dedicated.deckhouse.io со значением system-s3 или system, а на мастерах — их taint node-role.kubernetes.io/control-plane и node-role.kubernetes.io/master. Чтобы выделить хранилищу собственные узлы, назначьте роль system-s3 NodeGroup — с дисками под хранилище, потому что на локальном классе тома заказываются именно там. Обязательная anti-affinity разделяет HA-реплики по kubernetes.io/hostname, поэтому в отказоустойчивом режиме нужны три узла выбранной ступени. Ступень выбирается по тому, есть ли в кластере узлы с ролью, а не по тому, хватает ли их: даже один узел с ролью system-s3 (или system) уводит data plane на эту роль, и на следующую ступень он не переходит, сколько бы реплик ни осталось без места. Когда узлов меньше, чем реплик:

    • на каждый узел встаёт по реплике, остальные Pod остаются Pending, а PlacementReady=False/PlacementBlocked называет, сколько узлов нужно и сколько есть. PVC ради консолидации не пересоздаются;
    • хранилище, которое уже работало на трёх репликах, продолжает работать на двух: Garage держит кворум записи 2 из 3, но избыточности нет. С одной оставшейся репликой кворума нет, и хранилище недоступно;
    • новая установка не запускается вовсе: Garage не принимает раскладку, в которой узлов меньше фактора репликации, и StorageReady=False сообщает об ошибке применения раскладки;
    • без ролей system-s3 и system реплики встают на мастера. Режим по умолчанию следует control plane, а Deckhouse считает его отказоустойчивым начиная с двух мастеров. Поэтому с одним мастером хранилище работает в одной реплике, с тремя — по реплике на мастер, а с двумя мастерами получает три реплики, из которых третьей негде встать. Так же будет при highAvailability: true с одним или двумя мастерами.

    Выход в каждом случае один из трёх: дать роль system-s3 трём узлам, добавить узлы выбранной ступени до трёх или задать highAvailability: false. Во время миграции на тех же узлах бок о бок работают два кластера Garage: каждый разъезжается сам по себе и ни один не вытесняет другой с узла. Сам контроллер остаётся на control-plane узлах, а его HA следует HA control plane независимо от числа реплик Garage;

  • ресурсы: контейнер Garage запрашивает 100m CPU и 256 МиБ, его TLS-терминатор — 20m и 32 МиБ. Если включён модуль vertical-pod-autoscaler, каждому StatefulSet Garage создаётся VerticalPodAutoscaler в режиме Initial: реплика получает рекомендованные requests (для Garage — от 100m/256 МиБ до 2 CPU/4 ГиБ) при создании Pod — при обновлении модуля, смене конфигурации, переезде реплики или drain узла, — а работающую реплику автоскейлер ради изменения ресурсов не вытесняет. Без модуля остаются указанные выше requests;

  • что это значит для StorageClass: он обязан уметь давать тома на этих узлах, и обязательно с volumeBindingMode: WaitForFirstConsumer — при Immediate том выделяется до выбора узла, и реплика оказывается привязана к тому узлу, где он выделился.

Смена режима отказоустойчивости на инсталляции, где уже есть данные, выполняется на том же кластере Garage и на тех же томах. Garage не умеет менять фактор репликации на работающем кластере, поэтому модуль останавливает data plane, удаляет сохранённую раскладку кластера и запускает его снова с другим числом реплик; ничего не копируется. При переходе с одной реплики на три хранилище недоступно от остановки до запуска трёх реплик — длительность зависит от томов и времени запуска, — а новые реплики наполняются в фоне, пока оно работает. При переходе с трёх реплик на одну запись отклоняется (403), пока каждая реплика не получит все данные, а чтение продолжает работать; если это занимает больше 10 минут, запись возвращается, а смена повторяется позже. Затем хранилище недоступно во время рестарта, пока перезапускается на одной реплике, а тома двух ушедших реплик освобождаются. Временные ключи с правом записи, запрошенные, пока запись отклоняется, тоже не выдаются.

Смена StorageClass — одна или вместе со сменой режима — выполняется миграцией между двумя кластерами Garage. Привязанные тома нельзя перезаказать из другого класса, поэтому модуль поднимает рядом второй кластер, копирует в него объекты и переводит на него эндпоинт. В зависимости от объёма данных это занимает от минут до часов; пока идёт копирование, MigrationReady показывает, сколько из исходного кластера уже есть в новом (байты, объекты и процент — по суммам бакетов двух кластеров).

В фазе переключения запись отклоняется с 403; финальный проход копирует дельту и удаляет на назначении объекты, отсутствующие на замороженном источнике. Затем сверяются агрегированные objects и bytes, а не SHA256 всех объектов. Начатая multipart-загрузка может потребовать повтора; незавершённые части старше суток убирает контроллер. Временные ключи, выданные до переключения, не переносятся: потребитель должен получить новую пару даже до прежнего Expiration. Обновление credentials по времени в SDK не гарантирует реакцию на такой 403; обработку ранней недействительности ключа нужно проверить в конкретном приложении. Адрес эндпоинта сохраняется, но бесшовность не гарантируется. Ход миграции пишется в Module/system-s3.MigrationReady, переход HA — в ReplicationReady; после переключения старый кластер остаётся только для чтения, пока каждая реплика нового не получит все блоки, и затем удаляется; вернуться на него после этого нельзя.

Перед финальной копией каждый исходный Pod Garage перезапускается на прежнем PVC, без очистки layout, чтобы завершились все запросы, принятые до заморозки. В режиме высокой доступности Pod перезапускаются по одному — следующий только после того, как предыдущий вернулся, готов и кластер синхронизирован, — поэтому две реплики сохраняют кворум и хранилище продолжает работать. Чтение объекта при этом может зависнуть примерно на минуту, пока отсутствует реплика, к которой Garage обращается за его блоками первой: пропавший узел Garage ждёт, пока тот не пропустит четыре пинга с интервалом 15 секунд. Хранилище из одной реплики для этого приходится остановить, и на время перезапуска чтение недоступно. Копирование начинается только после замены всех старых Pod и синхронизации кластера; таймаут не обходит барьер завершения принудительным удалением. Незавершённые выдачи write-key учитываются в ConfigMap миграции для всех реплик контроллера и сохраняются после рестарта. Если результат RPC неизвестен, барьер сохраняется до подтверждённого Garage истечения ключа; отсутствие записи само по себе не доказывает истечение. Нельзя вручную удалять эти записи или принудительно удалять исходные Pod ради продолжения миграции. Обычно через 30 минут контроллер отступает от переключения, сначала останавливая Pod копировщиков; фактическое восстановление зависит от доступности исходных томов и Pod.

Объект пропускается только при совпадении размера и непустого ETag. Различающиеся multipart ETag требуют повторного копирования независимо от времён: крупные multipart-наборы могут не уложиться в окно переключения. Подтверждённый отказ по квоте назначения при bulk-copy переводит миграцию в ограниченную по времени финальную синхронизацию: сначала удаляются устаревшие ключи назначения, затем копируются замены. Ошибки аутентификации такого перехода не вызывают, квоты назначения не снимаются.

На время миграции оба кластера существуют одновременно, поэтому места требуется до двух раз больше обычного (а при переходе с одной реплики на три — тем более). Если вернуть настройку назад до переключения, миграция отменяется, а недостроенный кластер удаляется.

Единственный путь к бакету

Долгоживущих учётных данных нет, и нет Secret’а, в котором лежал бы ключ от бакета. Потребитель предъявляет токен своего ServiceAccount на эндпоинте выдачи и получает пару ключей, которую бэкенд перестаёт принимать не позднее чем через 30 минут. Эндпоинт говорит на AWS STS AssumeRoleWithWebIdentity, так что любой S3-клиент уже умеет с ним работать; см. USAGE.

Кому и к какому бакету это разрешено, решает RBAC Kubernetes: Role с get на systembuckets/read или systembuckets/write для конкретного бакета. В том числе и компоненту, который создал ресурс: владение SystemBucket — это право править ресурс, а не читать объекты в его бакете.

Именно это делает доступ отзываемым. Сохранённый ключ — учётные данные, за которые никто не отвечает, и забрать их обратно значит ротировать ключ для всех, у кого есть копия. Выданное право — это RoleBinding: его удаление останавливает следующий запрос, а уже выданное истекает по часам бэкенда, потому что ничего долговечного и не выдавалось.

Плата за это — потребитель обязан уметь такой обмен. Компонент, который умеет только прочитать ключ из файла или переменной окружения, этим хранилищем воспользоваться не сможет.

Garage используется как есть, через его стандартный admin API. Он под лицензией AGPL — поэтому он обслуживает только хранилище самой платформы и никогда не предлагается пользователям как сервис.

Удаление модуля

Выключение модуля требует подтверждения. Оно останавливает системное объектное хранилище, но не удаляет его. Уходит то, что отрисовал чарт, — контроллер, его сервисы, эндпоинт выдачи ключей, — а Garage масштабируется в ноль. Namespace модуля остаётся, а с ним StatefulSet’ы, claim’ы их томов и Secret’ы, без которых тома не собрать обратно в кластер. Пока модуль выключен, компоненты платформы не имеют доступа к своим данным; после повторного включения хранилище возвращается с данными и бакетами.

Ресурсы SystemBucket не удаляются. Модуль снимает с них свой финализатор, чтобы их можно было удалить без него, и они остаются вместе с ConfigMap подключения.

Удаление данных — отдельное явное действие: при выключенном модуле удалите namespace (kubectl delete namespace d8-system-s3). При выключении модуль снимает лейбл heritage: deckhouse с namespace и объектов в нём, поэтому администратору кластера для этого не нужны права самого Deckhouse; при включении лейбл возвращается. Дальше судьбу томов решает reclaim policy StorageClass: Delete удаляет их вместе с объектами, Retain сохраняет, но модуль, включённый после этого, такие тома не подключает — он начинает с пустого хранилища.

Перед выключением модуля посмотрите список бакетов: kubectl get sb -A. Каждый компонент из списка хранит данные в системном объектном хранилище и теряет к ним доступ до повторного включения модуля.