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

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

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

Контракт для разработчиков модулей

  1. Объявите SystemBucket в namespace своего модуля: d8-* или kube-* с меткой heritage: deckhouse — такой Deckhouse создаёт для каждого модуля. spec.size задаёт квоту бакета, не размер выделенного только ему PVC. Удаление CR уничтожает все объекты: жизненный цикл бакета должен быть осознанной частью удаления модуля.
  2. Дождитесь Ready=True с observedGeneration, равной metadata.generation, и читайте ConfigMap из status.connectionRef. Не фиксируйте endpoint в коде и не рассчитывайте на ConfigMap сразу после создания CR.
  3. Выдайте ServiceAccount потребителя RBAC на systembuckets/read и, для записи, systembuckets/write, с resourceNames только нужных бакетов. STS проверяет TokenReview и SubjectAccessReview; сам факт владения CR не даёт прав потребителю.
  4. Подключите projected token с audience system-s3-sts, оба CA и параметры WebIdentity из ConfigMap. Читайте файл токена заново при обновлении ключа: kubelet заменяет его. Срок projected token (в примере час) независим от срока S3-ключа — не более 30 минут.
  5. Используйте кеширующий credentials provider SDK и фактический Expiration ответа. Не получайте ключ на каждый S3-запрос, не сохраняйте его в Secret и не печатайте токен или ответ STS. Запрос SDK на час ограничивается получасом. Проверяйте обновление ключей одним процессом, работающим дольше 30 минут.
  6. Обрабатывайте недоступность при HA и раннюю недействительность ключей после StorageClass cutover. Одного refresh по Expiration недостаточно: обработку раннего отказа и получение новой пары нужно проверить в вашем SDK. Ключ проверяется в начале запроса: загрузка, начатая до истечения ключа, доходит до конца, а multipart, начатый одним ключом, можно продолжить и завершить следующим, потому что загрузка принадлежит бакету, а не ключу. Multipart, который никто не завершил, требует retry или abort; его части удаляются через 24 часа. 403 также может означать квоту или отсутствие прав: бесконечное получение новых ключей не является обработкой всех ошибок. Если ключ можно будет выдать чуть позже, STS отвечает 503 ServiceUnavailable с заголовком Retry-After (в секундах): во время переключения на новый кластер — для ключа на запись, пока хранилище создаётся, и примерно минуту после того, как пропала реплика отказоустойчивого хранилища; повторите запрос через указанное время.
  7. Задайте таймауты запросов в S3-клиенте и повторяйте запрос при ошибке соединения. Когда перезапускается реплика отказоустойчивого хранилища, остальные замечают её уход только примерно через 75 секунд неудачных пингов (порог зашит в Garage). До этого запрос может ждать эту реплику, а уже идущая длинная загрузка — оборваться сбросом соединения вместо продолжения с другой копии: на нагрузочном тесте две из шести загрузок по 32 МиБ через перезапуск реплики оборвались примерно на 90-й секунде. Повторяйте такой GET с Range от последнего полученного байта, а не с начала; соединение из пула к перезапущенному поду может и просто повиснуть до таймаута клиента, поэтому не оставляйте его бесконечным.

Использование и квоты

kubectl get sb показывает Objects, UsedBytes и TotalBytes; -o wide дополнительно показывает ObjectLimit. TotalBytes — установленная квота этого бакета, не физическая ёмкость Garage. Size — желаемая квота. Пустой бакет сообщает objects: 0 и usedBytes: 0, а не пропускает поля.

kubectl -n d8-monitoring get sb loki -o wide
kubectl -n d8-monitoring get sb loki -o json |
  jq '.status | {objects,usedBytes,quotaBytes,quotaObjects,conditions}'

Статистика обновляется при reconcile, не является счётчиком реального времени и при недоступности backend может отражать последнее успешное чтение. Повышайте spec.size и ожидайте SizeUpToDate текущей generation; уменьшение запрещено. Проверяйте байтовую квоту и число объектов отдельно.

Как запросить бакет

Компонент Deckhouse создаёт SystemBucket в своём namespace:

apiVersion: storage.deckhouse.io/v1alpha1
kind: SystemBucket
metadata:
  name: loki
  namespace: d8-monitoring
spec:
  size: 10Gi

size — сколько бакет просит. Значение по умолчанию (1Gi) подходит компоненту, который хранит немного; если нужно больше, указывайте явно. Из этого же значения складывается размер томов системного хранилища: оно заказывает сумму размеров всех SystemBucket, поэтому увеличение здесь и есть то, от чего растут тома. Запись сверх size бэкенд отклоняет.

size ограничивает и число объектов: size / 64 КиБ (16384 для 1Gi, видно в status.quotaObjects). Каждый объект занимает на диске больше своих байт — около 2 КиБ метаданных и округление до блока файловой системы, — и тома хранилища рассчитаны на этот перерасход до такого числа объектов. Запись сверх него отклоняется с 403 AccessDenied («Object quota is reached»), даже если в бакете лежит меньше size. Компоненту, который хранит много объектов в среднем мельче 64 КиБ, нужно заказывать size под их число, а не только под объём.

size можно увеличивать в любой момент и нельзя уменьшать — API-сервер такую правку отклонит. SystemBucket.SizeUpToDate=True означает, что бэкенд применил квоту этого бакета; QuotaPending — квота ещё не применена. Это не гарантия достаточной физической ёмкости. Общий рост PVC и ошибки CSI отражаются только в Module/system-s3.CapacityReady: Growing — тома растут, CannotGrow — расширение невозможно или завершилось ошибкой. При нехватке физического места запись может отказать до достижения квоты; проверяйте оба объекта. Бакет при расширении PVC может оставаться Ready.

kubectl -n d8-monitoring get sb loki -o jsonpath='{.status.conditions[?(@.type=="SizeUpToDate")]}'

Контроллер создаёт бакет в системном хранилище и кладёт рядом с ресурсом ConfigMap со всем, что нужно, чтобы до бакета добраться:

$ kubectl -n d8-monitoring get sb
NAME   BUCKET               PHASE   CONNECTION           READY   AGE
loki   d8-monitoring-loki   Ready   loki-s3-connection   True    30s
Ключ Значение
S3_ENDPOINT Внутрикластерный адрес системного хранилища, https://.
S3_REGION Регион, который объявляет эндпоинт (garage).
S3_BUCKET Фактическое имя бакета.
S3_CA_CERT CA, которым подписан S3-эндпоинт. Собственный для модуля, как и S3_STS_CA_CERT (сейчас это один и тот же CA), поэтому по умолчанию ему в кластере никто не доверяет.
S3_STS_ENDPOINT Куда обменивать токен ServiceAccount на учётные данные.
S3_STS_AUDIENCE Значение audience (адресат токена), которое нужно запросить для токена ServiceAccount, предъявляемого на S3_STS_ENDPOINT, — всегда system-s3-sts. Указывается в спецификации пода, в projected-томе с токеном (см. ниже): из ConfigMap Kubernetes его не возьмёт. Эндпоинт принимает только токены, выпущенные для этого адресата, поэтому токен пода по умолчанию будет отвергнут. Такой токен не аутентифицирует в API-сервере: утёкший, он позволяет только запросить учётные данные, которые его ServiceAccount и так вправе получить.
S3_STS_CA_CERT CA, которым подписан эндпоинт выдачи. Он собственный для модуля, поэтому по умолчанию ему в кластере никто не доверяет.
AWS_ROLE_ARN, AWS_ENDPOINT_URL_STS, AWS_ENDPOINT_URL_S3, AWS_REGION То же подключение под именами, которые AWS SDK читают сами: бакет в виде arn:deckhouse:system-s3:::systembucket/<namespace>/<name>, эндпоинт выдачи, эндпоинт S3 и регион. См. AWS SDK без кода.

Учётных данных в нём нет — и больше их нет нигде. Долгоживущий ключ модуль не выпускает ни для кого, включая владельца бакета. Учётные данные получают все одинаково: предъявляют токен своего ServiceAccount на эндпоинте выдачи и получают пару ключей, которую бэкенд перестаёт принимать не позднее чем через 30 минут.

Адреса берите через envFrom этого ConfigMap, токен монтируйте, а оба CA смонтируйте в отдельный каталог:

    envFrom:
      - configMapRef:
          name: loki-s3-connection
    env:
      # Go: системные корни плюс CA модуля, для всех TLS-клиентов процесса.
      # /etc/ssl/certs остаётся в списке, потому что SSL_CERT_DIR заменяет
      # каталоги по умолчанию, а не дополняет их.
      - name: SSL_CERT_DIR
        value: /etc/ssl/certs:/etc/system-s3/ca
    volumeMounts:
      - name: s3-token
        mountPath: /var/run/secrets/system-s3
        readOnly: true
      - name: s3-ca
        mountPath: /etc/system-s3/ca
        readOnly: true
    volumes:
      - name: s3-token
        projected:
          sources:
            - serviceAccountToken:
                path: token
                audience: system-s3-sts     # S3_STS_AUDIENCE
                expirationSeconds: 3600
      - name: s3-ca
        configMap:
          name: loki-s3-connection
          items:                            # только CA, PEM-файлами
            - {key: S3_CA_CERT, path: s3-ca.crt}
            - {key: S3_STS_CA_CERT, path: sts-ca.crt}

S3-эндпоинт работает только по TLS, с сертификатом от собственного CA модуля, и клиент должен этому CA доверять — открытого порта, на который можно откатиться, нет.

  • Go (minio-go, aws-sdk-go-v2, всё на crypto/tls): примера выше достаточно. Go на Linux собирает системные корни из файла-бандла и из всех файлов в каталогах SSL_CERT_DIR, поэтому CA модуля добавляются к публичным, и любой клиент процесса, который пользуется системными корнями, доверяет обоим эндпоинтам без единой строки кода. Системные корни Go читает один раз за процесс: новый CA вступает в силу после рестарта. CA долгоживущий; продление серверного сертификата его не меняет.
  • AWS CLI и boto3: AWS_CA_BUNDLE=/etc/system-s3/ca/s3-ca.crt. Переменная заменяет бандл SDK, а не дополняет его, так что клиенту, который ходит ещё и на публичные эндпоинты, нужен бандл с обоими.
  • Инструменты на OpenSSL (curl, ssl в Python): SSL_CERT_DIR не поможет — OpenSSL ищет сертификаты в каталоге по именам-хешам, которых в смонтированном ConfigMap нет. Укажите файл: curl --cacert /etc/system-s3/ca/s3-ca.crt, SSL_CERT_FILE, REQUESTS_CA_BUNDLE.
  • Не через subPath в /etc/ssl/certs. Том с subPath не обновляется при изменении ConfigMap, и под останется со старым CA после его замены модулем.

Как получить учётные данные

Обмен — это AssumeRoleWithWebIdentity, который умеет любая клиентская библиотека S3:

creds, _ := credentials.NewSTSWebIdentity(
    stsEndpoint+"/d8-monitoring/loki",              // S3_STS_ENDPOINT + /<namespace>/<имя>
    func() (*credentials.WebIdentityToken, error) {
        token, err := os.ReadFile("/var/run/secrets/system-s3/token")
        return &credentials.WebIdentityToken{Token: string(token)}, err
    },
)

Эндпоинт возвращает пару ключей, которую Garage перестаёт принимать в момент, указанный в ответе (по умолчанию через 30 минут; DurationSeconds — от 15 до 30 минут, запрос большего срока, в том числе стандартного часа SDK, ограничивается 30 минутами). Session token не возвращается и не нужен: это настоящая пара ключей, запрос подписывается ею обычным образом.

AWS SDK без кода

AWS CLI, boto3, aws-sdk-go-v2 и другие AWS SDK сами выполняют этот обмен: если заданы роль, файл с токеном и эндпоинт STS, они запрашивают ключ перед первым запросом и запрашивают новый до истечения срока. Роль и оба эндпоинта уже лежат в ConfigMap под именами, которые читают SDK, поэтому потребитель добавляет только два пути, зависящих от его пода:

    envFrom:
      - configMapRef:
          name: loki-s3-connection
    env:
      - name: AWS_WEB_IDENTITY_TOKEN_FILE
        value: /var/run/secrets/system-s3/token
      - name: AWS_CA_BUNDLE               # сейчас оба эндпоинта подписаны одним CA
        value: /etc/system-s3/ca/s3-ca.crt

с теми же томами s3-token и s3-ca, что и выше. После этого aws s3 cp работает без --endpoint-url и без ключей в окружении. Четыре ключа AWS_* появляются в ConfigMap, только пока модуль обслуживает эндпоинт выдачи.

Право спросить — это право в RBAC, и компоненту, который создал SystemBucket, оно нужно так же, как всем остальным: владение ресурсом — это право править ресурс, а не читать объекты в его бакете.

# Два субресурса: читать объекты в бакете и править ресурс SystemBucket —
# разные права.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: loki-bucket-access
  namespace: d8-monitoring
rules:
  - apiGroups: ["storage.deckhouse.io"]
    resources: ["systembuckets/read", "systembuckets/write"]
    resourceNames: ["loki"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: loki-bucket-access
  namespace: d8-monitoring
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: loki-bucket-access}
subjects:
  - kind: ServiceAccount
    name: loki
    namespace: d8-monitoring

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

Кто держит доступ к бакету прямо сейчас, видно по метрике system_s3_temporary_key_expiration_seconds: по серии на каждый неистёкший временный ключ с метками bucket, access_key_id, subject (ServiceAccount, которому выдан ключ) и write, значение — время истечения (Unix). Список обновляется раз в минуту; секретной части ключа в нём нет.

Как часто просят учётные данные для каждого бакета, видно по system_s3_sts_requests_total с метками namespace и name объекта SystemBucket и result (Granted или код ошибки, который получил клиент). Запрос учитывается за бакетом, только если токен вызывающего принят и SystemBucket существует; все остальные запросы учитываются с пустыми метками, так что вызывающий не может наплодить серий, называя несуществующие бакеты.

Полученный ключ надо переиспользовать до истечения срока. Каждый запрос выпускает новый ключ, и он доживает свои не более 30 минут независимо от того, нужен ли ещё клиенту, — в метаданных, которые Garage реплицирует на все узлы. Поэтому на бакет разрешено не больше 50 живых временных ключей: клиент, который просит учётные данные на каждый запрос, упрётся в этот предел и получит отказ с объяснением. Правильный клиент держит один ключ и обновляет его ближе к концу срока.

Имена бакетов

spec.bucketName необязателен. Если он не задан, бакет называется <namespace>-<name>: пространство имён бакетов в бэкенде плоское, а SystemBucket — namespaced, поэтому два компонента, попросивших backups, не должны оказаться в одном бакете.

Задавайте имя явно только тогда, когда конкретное имя нужно чему-то за пределами кластера. Два SystemBucket не могут указывать на один бакет. Второй ресурс создаётся, но контроллер его не обслуживает: он показывает BucketReady=False, reason Pending, с сообщением, в котором указано, какому SystemBucket бакет уже принадлежит (bucket "…" already belongs to SystemBucket <namespace>/<name>: refusing to issue credentials for it …). Такому ресурсу нужно задать другой spec.bucketName. Решение принимает контроллер, и только он, поэтому оно соблюдается и при двух одновременных созданиях.

Отказ здесь не перестраховка: SystemBucket владеет своими данными безраздельно, политики reclaim нет, поэтому два владельца одного бакета — это две нагрузки, пишущие в одно хранилище, и удаление любой из них уносит объекты другой.

Каким бы способом имя ни получилось, оно должно быть таким, какое принимает S3: от 3 до 63 символов, строчные латинские буквы, цифры и дефисы, начинается и заканчивается буквой или цифрой. Проблема возникает именно с выведенным именем, потому что его не набирают, а составляют: namespace и имя ресурса длиной до 63 символов каждое дают в сумме до 127. Если пара слишком длинная, задайте spec.bucketName:

$ kubectl -n d8-observability-long-term-metrics get systembucket prometheus-remote-write-backend \
    -o jsonpath='{.status.conditions[?(@.type=="BucketReady")].message}'
the bucket name "d8-observability-long-term-metrics-prometheus-remote-write-backend"
cannot be used: it is 66 characters long, and S3 bucket names stop at 63. With
spec.bucketName unset it is derived as <namespace>-<name>; set spec.bucketName to
something S3 accepts

Такой ресурс создаётся, но контроллер его не обслуживает: до обращения к бэкенду он выставляет BucketReady=False, reason ReconcileFailed, с сообщением выше. Ничего не изменится, пока не задан spec.bucketName; поле неизменяемо, поэтому ресурс нужно удалить и создать заново уже с именем.

Удаление бакета

kubectl -n d8-monitoring delete systembucket loki

Удаляется бакет и все объекты в нём. Ни политики хранения, ни корзины нет — убедитесь, что данные больше не нужны или скопированы в другое место.

Ресурс держит finalizer до тех пор, пока бакет не исчезнет из бэкенда, — то есть недоступность хранилища задержит удаление, а не оставит в нём осиротевшие данные.

Статус и диагностика

В SystemBucket.status.conditions стадии идут по порядку:

Condition Что значит False
BucketReady Бакет не удалось создать или зависимость ещё недоступна (WaitingForDependency; подробности в Module/system-s3).
ConnectionReady Не удалось записать ConfigMap с параметрами подключения.
Ready Агрегат перечисленного выше.

SystemBucket в namespace без лейбла heritage: deckhouse или с именем не вида d8-*/kube-* создаётся, но контроллер его не обслуживает: ничего не выделяется, а ресурс показывает BucketReady=False, reason ReconcileFailed, с сообщением namespace "<namespace>" is not a Deckhouse platform namespace (a d8-* or kube-* name and the label heritage=deckhouse); the system object storage is not available to it.

Общие состояния Garage публикуются только в Module/system-s3.status.conditions, в том числе до создания первого бакета. Условия Deckhouse (IsReady, причины включения и другие) контроллер system-s3 не изменяет; фаза Module.status.phase также остаётся под управлением платформы.

Condition Что отражает
StorageReady Запуск, здоровье Garage, quorum и layout.
PlacementReady Размещение реплик, нехватку подходящих узлов, перемещение и сохранение идентичности узлов.
CapacityReady Общую ёмкость PVC, их рост и ошибки CSI; Exhausted — на томе реплики свободно меньше 5 % (реплика остаётся в строю, но не принимает записи, и запросы, которые она обслуживает, завершаются ошибкой); тогда тома растут по шагу в 5 ГиБ, для этого StorageClass должен позволять расширение.
ReplicationReady Соответствие режима HA настройкам; переходы ResizeFreeze, ResizeStop, ResizePurge, ResizeSync.
MigrationReady Миграцию StorageClass: Provisioning, Copying, Verifying, Cutover, Draining, Decommission.

До первого SystemBucket data plane нет: он создаётся для первого бакета и остаётся, даже если потом удалить все бакеты. В этом состоянии StorageReady равно False с причиной WaitingForFirstBucket (если StorageClass не настроен, сообщение говорит и об этом), остальные четыре условия — True с причиной NoDataPlane, а серий system_s3_store_ready и system_s3_store_capacity_bytes контроллер не публикует, поэтому D8SystemS3StoreNotReady не срабатывает. Первый бакет показывает BucketReady=False с причиной WaitingForDependency, пока запущенный им data plane не начнёт обслуживать запросы. Смена storageClass или режима высокой доступности до первого бакета миграцию не запускает: она меняет только то, что будет создано.

False может означать нормальную операцию в процессе, не только ошибку; подробности находятся в reason и message. Unknown/NotEvaluated означает, что текущая попытка reconcile не дошла до этой проверки. После завершения операции соответствующее условие становится True. Conditions — текущие состояния, не журнал всех событий. При обновлении контроллер убирает старые условия Garage из бакетов; ожидание SystemBucket.StorageReady нужно заменить на Module.StorageReady.

Когда ничего не идёт, сообщения говорят, в каком виде работает хранилище, и состояние читается по одному модулю:

StorageReady=True     Serving    Garage store is healthy; 3 of 3 replicas ready on huawei-storage-stable
PlacementReady=True   Placed     one replica per node: debian-0, ubuntu-0, ubuntu-1
ReplicationReady=True Stable     3 replicas, high availability on; the highly available mode follows the number of control-plane nodes
MigrationReady=True   Converged  the volumes are on StorageClass huawei-storage-stable, the configured one; no migration is pending

Пока реплика не готова, StorageReady называет её и узел (not ready: system-garage-i2-1 (on ubuntu-1)).

Условие квоты переименовано из SizeApplied в SizeUpToDate. Старое имя удаляется из статуса при reconcile, в том числе при недоступности Garage. Обновите JSONPath и проверки готовности в потребителях. Имя Prometheus-метрики system_s3_bucket_size_applied сохранено для совместимости.

Само хранилище описывают метрики контроллера; они обновляются при каждом проходе reconcile (по умолчанию раз в 30 секунд). По ним, а не по подам и StatefulSet в d8-system-s3, консоль рисует страницу хранилища:

Метрика Метки Значение
system_s3_replica_ready plane, replica, node, serving 1, если под Garage в состоянии Ready, иначе 0. По серии на каждый существующий под; node пуст, пока под не назначен на узел.
system_s3_store_info storage_class, high_availability, replicas 1. Форма, в которой обслуживающий кластер работает на самом деле: StorageClass, к которому привязаны его тома, режим по закреплённому фактору репликации и число реплик. Отличается от настроек модуля, пока изменение не применено.
system_s3_plane_info plane, storage_class, high_availability, replicas, serving 1 для каждого существующего кластера Garage.
system_s3_condition type, status, reason 1 для текущих статуса и причины каждого из пяти условий Module/system-s3, включая Unknown/NotEvaluated. При смене условия прежняя серия удаляется.
system_s3_certificate_expiration_seconds secret Время (Unix), когда истекает конечный сертификат в garage-tls (эндпоинт S3) или webhooks-https-certs (эндпоинт учётных данных). Читается раз в 5 минут; у Secret, который не удалось прочитать или разобрать, серии нет.

plane — номер кластера Garage; во время миграции их два, и serving="true" отмечает тот, на который указывает эндпоинт. Серии исчезнувших подов и кластеров удаляются. Публикует их только реплика контроллера, держащая leader lease.

Трафик считает TLS-терминатор в каждом поде Garage: через него проходит каждое подключение клиента, а сам Garage считает запросы, но не байты. У этих серий job="system-s3-tls-terminator", pod, node, store и incarnation:

Метрика Метки Значение
system_s3_proxy_received_bytes_total port Байты от клиентов: 3900 — S3, 3903 — API администрирования.
system_s3_proxy_sent_bytes_total port Байты клиентам.
system_s3_proxy_connections_total port Подключения клиентов, прошедшие TLS-рукопожатие.
system_s3_replica_metadata_bytes — Размер базы метаданных Garage на томе реплики (файлы в meta/, meta/db.lmdb/ и meta/snapshots/*/), измеряется при каждом опросе. Для этого терминатор монтирует том только на чтение.

Байты считаются по открытому HTTP вместе с заголовками и растут по ходу передачи, а не в её конце. Репликации между репликами в них нет.

Запросы S3 считаются ещё и по бакетам. У этих серий, помимо job, pod, node, store и incarnation, есть bucket, namespace и name (а port нет) — бакет и его SystemBucket, как в system_s3_bucket_*:

Метрика Метки Значение
system_s3_proxy_bucket_requests_total method, code Запросы. method — GET, HEAD, PUT, POST, DELETE, OPTIONS или other; code — класс статуса: 2xx, 3xx, 4xx или 5xx, а 403 отдельно: иначе отказ по ключу потерялся бы среди 404 на отсутствующие объекты. Запрос, на который Garage не ответил, — 5xx (клиент получает 502); запрос, клиент которого ушёл раньше ответа, — 4xx.
system_s3_proxy_bucket_received_bytes_total Байты тел запросов от клиентов: загруженные данные объектов.
system_s3_proxy_bucket_sent_bytes_total Байты тел ответов клиентам: скачанные данные объектов.
system_s3_proxy_bucket_request_duration_seconds Гистограмма времени от заголовков запроса до конца ответа, вместе с передачей тела.

Называются только бакеты существующих ресурсов SystemBucket. Запрос к любому другому бакету считается как bucket="_other" с пустыми namespace и name, а запрос без бакета, например ListBuckets, — как bucket="_none", так что клиент не может добавить серий, придумывая имена бакетов. Новый SystemBucket первые минуту-две считается как _other, пока список в подах Garage не обновится.

Посмотреть на само хранилище:

kubectl get module system-s3 -o json | jq '.status.conditions'
kubectl -n d8-system-s3 get pods,sts,pvc
kubectl -n d8-system-s3 logs deploy/controller -c controller

Метрики и алерты Garage

Prometheus собирает /metrics каждого пода Garage всех плоскостей данных с job system-s3-garage. У каждой серии есть метки pod, node, store и incarnation; во время миграции между StorageClass собираются обе плоскости, и различаются они по incarnation. Для сбора используется отдельный токен из Secret garage-metrics-token в d8-system-s3: он открывает только /metrics, токена администратора у Prometheus нет. Контроллер создаёт Secret до первого пода Garage и никогда его не перезаписывает; Garage читает токен при запуске.

Дашборд System S3 Garage показывает S3-запросы, ошибки и задержки p95/p99 по операциям, скорость и длительность чтения и записи блоков, очередь resync (повторной синхронизации блоков), свободное место на каждой реплике и состояние кластера так, как его видят узлы.

Алерт Когда срабатывает
D8SystemS3GarageClusterUnavailable Ни у одного узла плоскости нет кворума по всем партициям в течение минуты: чтение и запись не работают.
D8SystemS3GarageClusterDegraded Узел хранения отключён, но плоскость продолжает обслуживать запросы, в течение 5 минут. Хранилище с одной репликой в это состояние не попадает.
D8SystemS3GarageNodeDisconnected Узла из layout (раскладки данных по узлам) не видно дольше 5 минут.
D8SystemS3GarageServerErrors Больше десяти S3-ответов 5xx за 5 минут (501 — функция, которой нет в Garage, — не считается).
D8SystemS3GarageResyncErrors У реплики 15 минут есть блоки данных, которые не удалось синхронизировать.
D8SystemS3GarageDiskAlmostFull На томе реплики меньше 10% свободного места в течение 10 минут.
D8SystemS3GarageMetricsUnavailable Под Garage не удаётся опросить 15 минут; алерты выше его не видят.
D8SystemS3GarageQueuesStalled Очередь resync, Merkle или GC узла 2 часа не опустела и ни разу не уменьшилась (для GC — 25 часов: Garage откладывает её на сутки).

В описании каждого алерта сказано, что проверить и что делать.

Алерты хранилища, учётных данных и узлов

Алерт Когда срабатывает
D8SystemS3ReplicaNotReady Реплика обслуживающего кластера Garage 15 минут не в состоянии Ready, хотя хранилище обслуживает запросы.
D8SystemS3PlacementBlocked PlacementReady 30 минут в состоянии False с причиной PlacementBlocked: реплике негде работать.
D8SystemS3TooManyTemporaryKeys Больше 500 временных ключей живы одновременно в течение 15 минут: какой-то клиент просит ключ на каждый запрос.
D8SystemS3CertificateExpiring Сертификат в garage-tls или webhooks-https-certs истекает меньше чем через 30 дней (их обновляют за полгода, значит, обновление не сработало).
D8SystemS3DrainBlocked PodDisruptionBudget Garage 30 минут не допускает ни одного прерывания: drain (вывод узла из работы) ждёт отсутствующую реплику.
D8SystemS3ReplicaHogsNode Контейнер Garage 15 минут использует больше половины выделяемых (allocatable) CPU или памяти своего узла.
D8SystemS3NodeDiskSaturated Диск узла занят больше 90% времени, пока Garage на этом узле читает и пишет блоки быстрее 10 МиБ/с, в течение 15 минут.

У серий контроллера нет метки namespace, а серии node-exporter относятся к узлу; эти правила ставят на свои алерты namespace="d8-system-s3", чтобы их пропускал фильтр платформы для алертов уровня кластера. Дашборд System S3 показывает условия модуля, оставшийся срок сертификатов, число живых временных ключей и запросы учётных данных по бакетам; System S3 Garage — очереди таблиц и размер метаданных каждой реплики.

Уровень сервиса (SLO)

Хранилище обещает по каждой операции S3 в окне 5 минут:

Операция Обещание Алерт
GetObject p99 времени до первого байта ≤ 250 мс D8SystemS3SLOReadLatency
PutObject, объекты до 1 МиБ p99 ≤ 500 мс D8SystemS3SLOWriteLatency
HeadObject p99 ≤ 100 мс D8SystemS3SLOHeadLatency
ListObjectsV2 (одна страница) p99 ≤ 1 с D8SystemS3SLOListLatency
все запросы ≥ 99,9 % без серверной ошибки D8SystemS3SLOAvailability

Пороги в 3–5 раз выше того, что показало нагрузочное тестирование хранилища из трёх реплик на сетевом и на привязанном к узлу StorageClass. Алерт срабатывает через 15 минут выше порога и только пока хранилище получает хотя бы один такой запрос раз в 10 секунд. PutObject замеряется до сохранения всего тела, а размера объекта в метриках нет, поэтому потребитель, загружающий большие объекты, может поднять перцентиль записи без всякой проблемы; как это отличить, сказано в описании алерта.

Выключение модуля и снос кластера

Выключение защищено: Deckhouse не примет enabled: false, пока на ModuleConfig нет подтверждения, — каждый компонент с SystemBucket на время выключения остаётся без хранилища.

kubectl annotate moduleconfig system-s3 modules.deckhouse.io/allow-disabling=true
kubectl patch moduleconfig system-s3 --type merge -p '{"spec":{"enabled":false}}'

Что остаётся после выключения, а что нет:

  • namespace d8-system-s3 остаётся (helm.sh/resource-policy: keep), а с ним и данные: StatefulSet’ы Garage, масштабированные в ноль, их PersistentVolumeClaim и Secret’ы с RPC-секретом кластера, admin-токеном и идентичностями узлов, а также CA модуля (system-s3-ca), поэтому CA в ConfigMap подключения потребителей остаётся действительным. Job’ы копировщика незавершённой миграции удаляются; миграция продолжится с сохранённого состояния после включения модуля;
  • уходит то, что отрисовал чарт: контроллер, его сервисы, эндпоинт выдачи ключей;
  • ресурсы SystemBucket остаются — замороженными в последнем статусе, — и их CRD тоже: выключение модуля не удаляет ни то, ни другое. Свои финализаторы модуль при выгрузке снимает, поэтому их можно удалить вручную;
  • конфигурация admission-вебхука для SystemBucket, оставшаяся от прежней версии модуля, удаляется: вебхук, у которого не осталось бэкенда, блокировал бы работу с ресурсами, которые он стерёг;
  • оставшееся перестаёт принадлежать Deckhouse: лейбл heritage: deckhouse снимается с namespace и со всех объектов в нём, у которых он есть (в том числе с CA модуля), а вместо него ставится лейбл system-s3.deckhouse.io/released-heritage: deckhouse. Политика допуска Deckhouse label-objects.deckhouse.io никому, кроме самого Deckhouse, не даёт менять и удалять объекты с heritage: deckhouse; без этого лейбла администратор кластера может удалить namespace с обычными правами. Остальные лейблы namespace остаются.

После повторного включения хранилище такое же, каким было: heritage: deckhouse возвращается ровно на те объекты, у которых есть лейбл released-heritage (объекты, созданные в namespace за это время, не трогаются), контроллер возвращает StatefulSet’ам реплики, кластер поднимается на тех же томах, бакеты и данные на месте.

Чтобы удалить данные, удалите namespace при выключенном модуле:

kubectl delete namespace d8-system-s3

Дальше судьбу томов решает reclaim policy StorageClass: Delete удаляет их вместе со всем содержимым, Retain сохраняет, но модуль, включённый после этого, начинает с пустого хранилища и такие тома не подключает.

Перед сносом кластера выключите модуль и удалите его namespace. dhctl destroy удаляет все PersistentVolumeClaim и ждёт их исчезновения. При выключенном модуле data plane стоит на нуле реплик и claim’ы не пересоздаёт, но удаление namespace заранее — это явное решение, что данные должны уйти.

Пока модуль находится на стадии Experimental, для повторного включения после выключения нужно разрешение платформы: добавьте system-s3 в spec.settings.allowedExperimentalModules в ModuleConfig deckhouse (или включите allowExperimentalModules: true) до enabled: true.