Как устроена миграция на модуль registry?

Управлять путём загрузки образов в Deckhouse Platform (DP) можно двумя способами:

  • предыдущая реализация — секция registry в ModuleConfig deckhouse с режимами Unmanaged, Direct, Proxy и Local;
  • текущая реализация — этот модуль, настраиваемый через ModuleConfig registry.

Миграция — это передача пути загрузки от предыдущей реализации модулю. Её не нужно запускать вручную, и отдельной команды для неё нет: модуль принимает управление автоматически после обновления кластера на релиз, в котором он появился. Единственное условие — к этому моменту предыдущая реализация должна освободить путь загрузки. Обе реализации настраивают на каждом узле одно и то же — из какого хранилища образов контейнеров container runtime загружает образы и с какими учётными данными, — поэтому они никогда не управляют кластером одновременно.

Что нужно сделать для передачи управления и когда — зависит от режима, в котором работает предыдущая реализация. Режим указан в параметре settings.registry.mode ModuleConfig deckhouse:

d8 k get mc deckhouse -o jsonpath='{.spec.settings.registry.mode}'

Пустой вывод означает режим Unmanaged: настройки хранилища образов контейнеров в этом кластере никогда не задавали. Такому кластеру — как и любому другому в Unmanaged — для миграции ничего делать не нужно: передача управления произойдёт сама.

Режим Что сделать Когда
Unmanaged Ничего — передача произойдёт сама —
Direct Настроить модуль: mode: Managed с primary.upstream До обновления
Proxy Перевести кластер в Unmanaged До обновления
Local Выполнить процедуру для изолированных кластеров До обновления

Подготовьте кластер до обновления: обновление на релиз с модулем заблокировано, пока кластер работает в режиме Proxy или Local, а также в режиме Direct без настроенного ModuleConfig registry. В новом релизе нет ни компонентов предыдущей реализации, ни кода, который переключает её режимы, поэтому вся подготовка выполняется на текущем релизе.

Режиму Direct переключение не требуется. Настройки модуля намеренно принимаются на релиз раньше: записанные до обновления, они хранятся без эффекта и срабатывают на первой итерации согласования модуля после обновления.

Кластер записывает, на какой реализации он фактически работает (как это проверить). Пока передача управления невозможна, обновление на релиз с модулем блокируется, и в сообщении об ошибке сказано, что сделать.

На какой реализации работает мой кластер?

Когда модуль принимает управление путём загрузки, он записывает это в секрет registry-v2-switch. Проверьте, существует ли секрет:

d8 k -n d8-system get secret registry-v2-switch >/dev/null 2>&1 \
  && echo "текущая реализация" || echo "предыдущая реализация"

Если кластер всё ещё на предыдущей реализации, модуль сообщает причину на каждой итерации согласования, а в кластере срабатывает алерт D8RegistryMigrationPending. Посмотреть, чего он ждёт:

d8 k -n d8-system get secret registry-state -o jsonpath='{.data.state}' | base64 -d | head

Как мигрировать из режима Unmanaged?

В режиме Unmanaged предыдущая реализация не управляет путём загрузки, поэтому готовить нечего: передача управления произойдёт сама.

  1. Если кластер переводится в Unmanaged из другого режима, дождитесь завершения перехода. В статусе должно быть mode: Unmanaged без ожидающего целевого режима:

    d8 k -n d8-system get secret registry-state -o jsonpath='{.data.state}' | base64 -d | head
    
  2. Обновите кластер на релиз с текущей реализацией модуля (или, если он уже обновлён, просто дождитесь следующей итерации согласования). Модуль примет управление автоматически, и поведение не изменится: режим модуля по умолчанию — тоже Unmanaged, поэтому кластер продолжит загружать образы из того же хранилища образов контейнеров, что и раньше.

  3. Чтобы модуль начал управлять путём загрузки, задайте mode: Managed в ModuleConfig registry и укажите хранилище образов контейнеров, из которого загружать образы. Готовая конфигурация для вашего кластера публикуется в секрете registry-suggested-config; как её применить, показано в примере «Включение модуля».

Как мигрировать из режима Direct?

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

  1. Настройте модуль до обновления — без этой конфигурации обновление заблокировано. Возьмите значения из секции registry.direct в ModuleConfig deckhouse: imagesRepo разделяется на host и path, учётные данные — тот же license (или username/password), плюс ca, если хранилище образов контейнеров его требует:

    apiVersion: deckhouse.io/v1alpha1
    kind: ModuleConfig
    metadata:
      name: registry
    spec:
      enabled: true
      version: 1
      settings:
        mode: Managed
        primary:
          upstream:
            scheme: HTTPS
            host: registry.deckhouse.io
            path: /deckhouse/ee
            auth:
              license: <LICENSE_KEY>
    

    Предыдущий релиз сохраняет эти настройки, но не действует по ним, поэтому до обновления в кластере ничего не меняется.

  2. Обновите кластер. Передача произойдёт на следующей итерации согласования модуля, и загрузка образов всё это время работает: Service и прокси предыдущей реализации продолжают обслуживать внутрикластерный адрес, пока агент модуля не примет его на каждом узле, и только после этого контроллер их удаляет.

  3. Следите за ходом передачи:

    d8 k -n d8-system get secret registry-v2-switch >/dev/null 2>&1 && echo "управление передано"
    d8 k get registrynode -o custom-columns='NODE:.metadata.name,READY:.status.reconciled,SERVING:.status.proxyListening'
    

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

Как мигрировать из режима Proxy?

Режим Proxy держит на каждом узле собственный прокси со своими сертификатами — состояние, которое модуль перенять не может. Поэтому кластер сначала нужно перевести в Unmanaged, и сделать это можно только до обновления.

  1. В ModuleConfig deckhouse задайте registry.mode: Unmanaged, сохранив тот же адрес хранилища образов контейнеров и учётные данные. Готовые манифесты приведены в примерах переключения режимов. Все узлы будут перенастроены на загрузку напрямую из внешнего хранилища образов контейнеров, поэтому кеширование, которое давал Proxy, пропадёт до шага 4.

  2. Дождитесь завершения перехода — mode: Unmanaged без ожидающего целевого режима. Кластер, застигнутый посреди перехода, мигрировать нельзя; в статусе видно, в какой режим он ещё переключается:

    d8 k -n d8-system get secret registry-state -o jsonpath='{.data.state}' | base64 -d | head
    
  3. Обновите кластер. Передача произойдёт на следующей итерации согласования модуля и не изменит поведения: в режиме Unmanaged модуль тоже не управляет путём загрузки.

  4. Чтобы вернуть кеширование внутри кластера, задайте mode: Managed с storage.cache: true и тем же внешним хранилищем образов контейнеров. Учтите, что кеш модуля устроен иначе, чем Proxy: одно хранилище с репликами на master-узлах вместо прокси на каждом узле. Прежде чем включать его на кластере, где на master-узлах мало свободного места, прочитайте, как кеш наполняется и очищается.

Как мигрировать изолированный кластер из режима Local?

В режиме Local у кластера нет внешнего хранилища образов контейнеров — его роль выполняет хранилище внутри самого кластера. Поэтому стандартная процедура миграции здесь не работает: она проходит через режим Unmanaged, в котором каждый узел загружает образы напрямую из внешнего хранилища образов контейнеров, а такому кластеру загружать их неоткуда.

Выход в том, чтобы подключить к кластеру внешнее хранилище образов контейнеров на время миграции. Оно запускается в том же кластере, но вне неймспейсов DP. Кластер переключается на него и обновляется, а когда хранилище модуля наполнится образами, временное хранилище образов контейнеров удаляется.

Требования к диску

Перед началом убедитесь, что на master-узлах свободно место под четыре набора образов: на пике миграции одновременно существуют три копии набора — хранилище Local, временное хранилище образов контейнеров и наполняющееся хранилище модуля, — а место под четвёртый набор нужно как запас, который узел не должен исчерпать.

Масштаб можно оценить на примере набора образов в 13 ГБ (платформа с модулями): он занимает около 21 ГБ во временном хранилище образов контейнеров (там хранятся два релиза — тот, на котором кластер работает, и тот, на который обновляется) и увеличивает хранилище с 13 до 21 ГБ, не считая кеша образов самого узла и системы. На master-узле со 100 ГБ диска пик составляет около 38 ГБ, а на узле с 50 ГБ та же миграция упирается в вытеснение подов.

Запас нужен не для перестраховки. Когда свободное место на master-узле заканчивается, kubelet начинает вытеснять поды и удалять образы, которые считает неиспользуемыми, — а в кластере, чьё хранилище образов контейнеров работает внутри него самого, удалённым может оказаться образ самого хранилища образов контейнеров. Тогда хранилище остаётся без процесса, который его обслуживает, а узел — без места, откуда этот образ можно загрузить. При тестировании эта взаимная блокировка продержалась 99 минут, и разрешить её удалось только ручной загрузкой образов на узел. Поэтому рассчитайте место заранее и следите за диском, пока идёт миграция.

Часть места экономится за счёт того, что образы, уже хранящиеся на дисках master-узлов, хранилище модуля проверяет и принимает на месте, а не скачивает заново (шаг 6). Но превратить два разных релиза в один набор такая проверка не может: в хранилище оказываются и релиз, на котором работал старый кластер, и релиз, на котором работает новый, — отсюда и размер пика.

Порядок действий

Шаги 1–4 выполняются до обновления, на релизе, в котором ещё есть предыдущая реализация.

  1. Запустите временный OCI-registry в собственном неймспейсе. Реализация хранилища образов контейнеров может быть любой, но оно должно отдавать TLS с сертификатом, который кластер сможет проверить, и DP не должна им управлять: что бы ни происходило с объектами модуля, временное хранилище образов контейнеров должно продолжать работать.

    Кроме того, хранилище образов контейнеров должно быть доступно по одному и тому же адресу из двух мест: на шаге 3 из него загружают образы узлы, а на шаге 6 его читает syncer модуля, работающий в поде. Сертификат должен покрывать выбранный адрес. Варианты, проверенные на одном кластере:

    Адрес С узла Из пода
    порт hostNetwork на IP самого узла (<NODE_IP>:5000) работает работает
    имя Service (<SERVICE>.<NAMESPACE>.svc:<PORT>) не разрешается работает
    NodePort на IP узла работает не работает: operation not permitted

    Используйте первый вариант: запустите хранилище образов контейнеров с hostNetwork: true на одном узле, обращайтесь к нему по IP этого узла и добавьте этот IP в SAN сертификата — тогда вся миграция пройдёт на одном адресе. Имя Service выглядит аккуратнее, но перестаёт работать на шаге 3, потому что container runtime узла не разрешает имена через кластерный DNS.

  2. Загрузите набор образов во временное хранилище образов контейнеров: сначала скачайте его командой d8 mirror pull на машине, у которой есть доступ к хранилищу образов контейнеров DP, а затем отправьте во временное хранилище образов контейнеров командой d8 mirror push. Именно эта копия набора учтена выше в расчёте места на диске.

  3. Направьте предыдущую реализацию модуля на временное хранилище образов контейнеров и переведите её в Unmanaged: в ModuleConfig deckhouse задайте registry.mode: Unmanaged вместе с адресом, сертификатом CA и учётными данными временного хранилища образов контейнеров. После этого узлы начнут загружать образы из него, а хранилище Local уйдёт с пути загрузки — но его данные останутся на месте, в каталоге /opt/deckhouse/registry на master-узлах.

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

    d8 k -n d8-system get secret registry-state -o jsonpath='{.data.state}' | base64 -d | head
    
  5. Обновите кластер на релиз с модулем. Передача управления произойдёт на следующей итерации согласования, и всё это время кластер продолжит загружать образы из временного хранилища образов контейнеров.

  6. Включите модуль: задайте mode: Managed с storage.cache: true, укажите временное хранилище образов контейнеров в качестве primary.upstream — и добавьте storage.source в том же изменении. Хранилище модуля запустится на том же пути на хосте, который использовал Local, поэтому уже имеющиеся на дисках образы не будут скачиваться заново: наполнение проверит их и дозагрузит только недостающее.

    Откладывать указание storage.source нельзя: без него не получится убрать внешнее хранилище образов контейнеров на следующем шаге, потому что конфигурация Managed без primary.upstream принимается только при заданном storage.source, — следующий шаг будет отклонён с ошибкой 'storage.source' is required when 'primary.upstream' is not set.

    В storage.source поле bundleRef — это произвольное имя набора образов, а expectedDigests — число различных дайджестов в нём. Посчитать их можно по bundle с шага 2:

    for tar in <BUNDLE_DIR>/*.tar; do tar -xOf "$tar" --wildcards '*index.json'; done |
      jq -r '.manifests[]?.digest' | sort -u | wc -l
    

    Изменение этих настроек перезапускает процесс хранилища образов контейнеров, поэтому ресурс RegistryStorage примерно на минуту перейдёт в Failed с ошибкой чтения собственного хранилища. Это ожидаемо — дождитесь возврата в Ready.

  7. Дождитесь, пока хранилище сообщит, что держит весь набор (phase: Ready и safeToDropUpstream: true), и уберите primary.upstream из ModuleConfig registry. После этого кластер снова изолирован — теперь уже на модуле:

    d8 k get registrystorage registry -o jsonpath='{.status.phase} {.status.safeToDropUpstream}{"\n"}'
    
  8. Удалите временное хранилище образов контейнеров и освободите занятый им диск.

Что означают алерты модуля registry?

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

D8RegistryMigrationPending
Кластер всё ещё работает на предыдущей реализации модуля. Ничего не сломано — миграция не завершена. Дальнейшие действия описаны в разделе «Как устроена миграция на модуль registry».
D8RegistryConfigInvalid
Конфигурация отклонена; кластер продолжает работать с прежней. Причина — в .status.conditions ресурса registryconfig/registry.
D8RegistryNodeNotConverged
Агент на части узлов не применил выданную ему конфигурацию. Эти узлы продолжают загружать образы по старой конфигурации, и следующие изменения до них тоже не дойдут.
D8RegistryNodeRunningFromDisk
Часть узлов не может подключиться к API-серверу и маршрутизирует загрузки по копии конфигурации на диске. Этот резервный механизм работает как задуман: загрузки на таких узлах проходят успешно, поэтому больше ничто о проблеме не сообщит, — а конфигурация узлов тем временем может сколь угодно сильно отстать от кластерной.
D8RegistryStorageIncomplete
Часть реплик кеша не держит весь ожидаемый набор образов. Пока настроено внешнее хранилище образов контейнеров, на загрузки это не влияет, но его потерю кластер бы не пережил — и именно это блокирует переход в air-gap.
D8RegistryAirGapTransitionHeld
Вы убрали внешнее хранилище образов контейнеров из конфигурации, но модуль продолжает им пользоваться, потому что кеш пока не может обслуживать кластер сам. Это безопасный исход: отключение внешнего хранилища образов контейнеров при неполном кеше оставило бы узлы без источника образов. Алерт не разрешится сам, если кеш перестал наполняться.
D8RegistryUpstreamProbeFailing
Изменение основного внешнего хранилища образов контейнеров отклонено, и кластер продолжает пользоваться последним работавшим. Метка outcome различает три проблемы: unreachable — сеть или само хранилище образов контейнеров; auth — обычно истёкший лицензионный ключ; sentinel — хранилище образов контейнеров ответило и приняло учётные данные, но не содержит образов DP (обычно неверный путь репозитория).
D8RegistryUpstreamRejected
Ресурс RegistryUpstream не принят, поэтому загрузки для указанного в нём хранилища образов контейнеров не перехватываются. Метка reason говорит, конфликтует он с основным хранилищем образов контейнеров или с другим ресурсом, претендующим на то же имя.
D8RegistryStorageNotReclaimed
Ни одна реплика не выполняла сборку мусора неделю. Сборка — единственный механизм, который удаляет данные из хранилища, поэтому, если она остановилась, диск рано или поздно заполнится. Подробнее о сборке — в разделе «Кеш растёт. Что его чистит».
D8RegistryStaleCacheData
На узле лежат данные кеша, которые никто не использует. Как освободить место, описано в разделе «Как удалить с узла оставшиеся данные кеша».
D8RegistryStoreWritesRefused
Кеш на master-узле перестал принимать запись: хранилище заняло storage.size (BudgetExhausted) или дошло до места, оставленного для узла (ReserveExhausted). Узлу ничего не грозит, кеш продолжает отдавать то, что в нём есть; останавливается только его наполнение. Подробнее — в разделе «Что происходит, когда хранилище кеша заполнено».
D8RegistryStoreNearlyFull
Хранилище на master-узле заняло больше 85% от storage.size. Следующий релиз может не поместиться.
D8RegistryStoreUnbounded
Кеш включён, а storage.size не задан, поэтому единственный предел — место, оставленное для узла.

Как удалить с узла оставшиеся данные кеша?

При выключении кеша данные в /opt/deckhouse/registry намеренно сохраняются: если включить кеш снова, он наполнится из того, что уже хранится на диске, а не будет скачивать всё заново — на медленном канале это экономит часы. Автоматическое удаление сделало бы выключение кеша необратимым, поэтому решение модуль оставляет вам: агент измеряет оставшиеся данные и поднимает алерт D8RegistryStaleCacheData.

Чтобы узнать, сколько места занимают данные, используйте команду:

d8 k get registrynodes -o custom-columns=\
NODE:.metadata.name,STALE:.status.staleStorageDataBytes

Если включать кеш обратно вы не собираетесь, удалите каталог на узле:

ssh <NODE> 'du -sh /opt/deckhouse/registry && sudo rm -rf /opt/deckhouse/registry'

Кеш растёт. Что его чистит?

Кеш очищает сборка мусора, которую по расписанию выполняют сами реплики хранилища.

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

Сборка удаляет образы релизов, которые кластер уже прошёл. Сохраняются:

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

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

Чтобы посмотреть состояние и расписание сборки, используйте команды:

Посмотреть время последней сборки на каждой реплике и ошибки, если они были:

d8 k get registrystorage registry -o jsonpath='{.status.replicas}' | jq \
  'map({node, collectedAt, collectionError})'

Посмотреть текущее расписание сборки:

d8 k get registrystorage registry -o jsonpath='{.spec.garbageCollection}' | jq

Когда сборка запускается и почему реплика переходит в режим read-only

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

Все свои образы реплика при этом продолжает отдавать. Чего она не может во время сборки:

  • сохранить результат промаха кеша — агент на узле обращается к внешнему хранилищу образов контейнеров, то есть загрузка становится медленнее, но не завершается ошибкой;
  • принять d8 mirror push — он завершается с явной ошибкой, и его можно повторить.

Одновременно сборку выполняет только одна реплика. Остальные работают как обычно.

По умолчанию сборка назначается на ночной час, а если у NodeGroup master задано окно обслуживания — на его начало: этот час уже объявлен допустимым для перерывов. Чтобы задать своё расписание, используйте параметр storage.garbageCollection:

spec:
  settings:
    storage:
      garbageCollection:
        schedule: "0 2 * * Sun"

Нечитаемое cron-выражение отклоняется, а не угадывается, так как сборка в неожиданный час хуже, чем её отсутствие.

Как выключить

Сборка мусора выключается в настройках модуля (параметр storage.garbageCollection):

spec:
  settings:
    storage:
      garbageCollection:
        enabled: false

Это имеет смысл только с диском, размера которого хватит при неограниченном росте хранилища. Обратите внимание: алерт D8RegistryStorageNotReclaimed всё равно сработает через неделю после последней сборки — снаружи «выключено» и «молча перестало работать» выглядят одинаково.

Что происходит, когда хранилище кеша заполнено?

У хранилища на каждом master-узле два предела, и registry проверяет оба при каждой записи:

  • бюджет — storage.size в ModuleConfig registry, сколько хранилище может занимать;
  • резерв — сколько места на файловой системе хранилища должно оставаться свободным для узла: мягкий порог выселения kubelet (10% файловой системы, не больше 40 ГиБ) плюс запас (10%, не больше 20 ГиБ). Резерв действует при любом storage.size, в том числе незаданном.

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

  • наполнение и репликация останавливаются, причину реплика пишет в .status.replicas[].error;
  • d8 mirror push получает 507 Insufficient Storage с причиной и способом исправить;
  • образ, который хранилище не может сохранить, отдаётся потоком из upstream, пока он настроен, и узлы этого не замечают; в изолированном кластере релиз, которого нет в хранилище, установить нельзя.

Ради места ничего не удаляется. Место освобождает сборка мусора, либо его добавляют, увеличив storage.size или диск под хранилищем.

Посмотреть состояние каждой реплики:

d8 k get registrystorage registry -o json | jq '.status.replicas[] | {node, store}'

writable: false с reason: BudgetExhausted значит, что достигнут storage.size: увеличьте его, если на диске есть место, или дождитесь, пока сборка мусора освободит то, что не нужно развёрнутым релизам. reason: ReserveExhausted значит, что закончился диск: освободите место на узле или перенесите хранилище на отдельный диск.

На узле не загружается образ. Куда смотреть?

Начните с агента: он стоит на пути каждой загрузки на узле. Агент разворачивается в виде статического пода, поэтому доступен, даже когда кластер — нет.

Чтобы посмотреть логи агента, используйте команду:

d8 k -n kube-system logs -l component=registry-agent --tail=100

Чтобы посмотреть, какую конфигурацию агент получил и согласен ли он с кластером, используйте команду:

d8 k get registrynode <NODE> -o jsonpath='{.status}' | jq

Метрики агента — его собственный взгляд на проходящие через него загрузки. Они читаются напрямую с узла, а не через Prometheus: агент работает статическим подом именно потому, что должен работать при недоступном API-сервере, а kube-rbac-proxy рядом с ним аутентифицировался бы в том самом API-сервере:

ssh <NODE> 'curl -s http://127.0.0.1:4286/metrics | grep d8_registry_agent'

Конфигурация, выданная container runtime. Это один файл независимо от того, сколько registry настроено:

ssh <NODE> 'cat /etc/containerd/registry.d/_default/hosts.toml'

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

Модули из других ModuleSource: что хранит кеш и что сделать перед air-gap

Кеш хранит все модули, которые запущены в кластере, из какого бы ModuleSource они ни были установлены, и учитывает их в полноте: кластер не переходит в air-gap, пока в хранилище не хватает хотя бы одного из них.

  • Модуль, чей ModuleSource находится в реестре upstream, заливается из upstream, как и сама платформа.
  • Модуль, чей ModuleSource находится в отдельном реестре, заливается из этого реестра с учётными данными и центром сертификации, указанными в ModuleSource.

Собственные модули платформы (ModuleSource deckhouse) хранятся в system/deckhouse/modules. У каждого другого ModuleSource во внутрикластерном реестре свой путь, system/deckhouse/module-sources/<ModuleSource>: модули — в <путь>/<модуль>, список модулей — тегами самого <путь>. Путь отдельный, потому что путь платформы, пока настроен upstream, отражает upstream, и модуль другого источника с тем же именем, что у модуля платформы, был бы там заменён сборкой из upstream.

Только из этого пути узел сможет загрузить модули источника, когда upstream отключён. Но поды модуля продолжают ссылаться на реестр своего ModuleSource, пока источник не изменён. Поэтому перед отключением upstream направьте каждый такой ModuleSource на его путь, с настройками реестра собственного источника платформы:

NAME=<ModuleSource>
REGISTRY=$(d8 k get modulesource deckhouse -o json | jq -c --arg name "$NAME" \
  '.spec.registry | .repo = "registry.d8-system.svc:5001/system/deckhouse/module-sources/" + $name')
d8 k patch modulesource "$NAME" --type merge -p "{\"spec\":{\"registry\":$REGISTRY}}"

После этого образы модуля рендерятся с внутрикластерным адресом и отдаются из кеша.

Алерты доступности образов от extended-monitoring для внутрикластерного реестра

Когда реестром управляет модуль, все ссылки на образы в кластере указывают на registry.d8-system.svc:5001. Этот адрес обслуживается только на узлах: containerd передаёт pull агенту узла на loopback-адресе. Из пода адрес не резолвится, поэтому экспортер доступности образов модуля extended-monitoring, который проверяет образы из пода, сообщает о каждом таком образе алертом …ImageAvailabilityUnknownError (no such host, позже x509: certificate signed by unknown authority).

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

apiVersion: deckhouse.io/v1alpha1
kind: ModuleConfig
metadata:
  name: extended-monitoring
spec:
  version: 2
  settings:
    imageAvailability:
      ignoredImages:
      - 'registry\.d8-system\.svc:5001/.*'

Каждый элемент — регулярное выражение. Образы из других реестров по-прежнему проверяются.

Как посмотреть состояние внутрикластерного кеша?

Состояние кеша публикуется в статусе ресурса RegistryStorage. Для его просмотра используйте команду:

d8 k get registrystorage registry -o jsonpath='{.status}' | jq

Описание полей ответа:

  • replicas — единственное место, где сообщается информация о полноте кеша; каждая запись — отчёт реплики о самой себе. Реплика с full: true рядом с error полной не считается: full говорит о том, что она держит, а ошибка — о том, завершился ли её последний проход.
  • leader — реплика, которая наполняется из внешнего хранилища образов контейнеров и служит источником репликации для остальных. Выборы намеренно несимметричны: когда у какой-то реплики появляется полный набор, лидер, который не может собрать его сам, уступает ей. Лидер не может собрать набор сам, если нет внешнего хранилища образов, если его последний проход завершился ошибкой или если его хранилище отказывает в записи. Именно это не даёт изолированному кластеру застрять в ситуации, когда лидер пуст, а полный набор есть только у другой реплики. Лидер, который ещё собирает набор из внешнего хранилища, остаётся лидером: последователь копирует данные с него и может закончить раньше, и передача лидерства в этот момент ничего не даёт.

Предыдущая реализация

Информация ниже относится к кластеру, который всё ещё работает на реализации модуля, настраиваемой через ModuleConfig deckhouse.

Как мигрировать на модуль registry?

Во время миграции, для containerd v1 будет выполнен переход на новую схему конфигурации хранилища образов контейнеров. containerd v2 использует новую схему по умолчанию. Подробнее можно ознакомиться в разделе с описанием способов конфигурации

Для containerd v2

  1. Выполните переключение на использование модуля registry. Для этого, укажите в ModuleConfig deckhouse параметры режима Unmanaged. Если используется хранилище образов контейнеров, отличное от registry.deckhouse.ru, ознакомьтесь с конфигурацией модуля deckhouse для корректной настройки.

    Посмотреть текущие настройки хранилища образов контейнеров можно с помощью команды:

    d8 k -n d8-system exec -it svc/deckhouse-leader -c deckhouse -- deckhouse-controller global values | yq e '.modulesImages.registry' -
    

    Данные настройки укажите при конфигурации Unmanaged режима:

    apiVersion: deckhouse.io/v1alpha1
    kind: ModuleConfig
    metadata:
      name: deckhouse
    spec:
      version: 1
      enabled: true
      settings:
        registry:
          mode: Unmanaged
          unmanaged:
            imagesRepo: registry.deckhouse.ru/deckhouse/ee
            scheme: HTTPS
            license: <LICENSE_KEY> # Замените на ваш лицензионный ключ
    
  2. Дождитесь завершения переключения. Пример статуса переключения:

    conditions:
    # ...
      - lastTransitionTime: "..."
        message: ""
        reason: ""
        status: "True"
        type: Ready
    hash: ..
    mode: Unmanaged
    target_mode: Unmanaged
    

Для containerd v1

  • Во время переключения containerd v1 сервис будет перезапущен.
  • Во время переключения containerd v1 будет переведен на новую схему конфигурации хранилища образов контейнеров.
  • Во время переключения, пользовательские конфигурации registry для containerd v1 будут временно недоступны.
  1. Убедитесь, что на узлах с containerd v1 отсутствуют пользовательские конфигурации хранилища образов контейнеров, расположенные в директории /etc/containerd/conf.d.

  2. Если конфигурации присутствуют, необходимо выполнить миграцию на новый формат конфигурации хранилища образов контейнеров в containerd. Для этого, необходимо добавить новые конфигурации в директорию /etc/containerd/registry.d. Данные конфигурации вступят в силу после переключения на модуль registry. Для добавления конфигураций подготовьте NodeGroupConfiguration, подробнее в разделе с описанием способов конфигурации. Пример:

    apiVersion: deckhouse.io/v1alpha1
    kind: NodeGroupConfiguration
    metadata:
      name: containerd-additional-config-auth.sh
    spec:
      # Шаг может быть любой, так как не требуется перезапуск сервиса containerd.
      weight: 0
      bundles:
        - '*'
      nodeGroups:
        - "*"
      content: |
        # Copyright 2023 Flant JSC
        #
        # Licensed under the Apache License, Version 2.0 (the "License");
        # you may not use this file except in compliance with the License.
        # You may obtain a copy of the License at
        #
        #     http://www.apache.org/licenses/LICENSE-2.0
        #
        # Unless required by applicable law or agreed to in writing, software
        # distributed under the License is distributed on an "AS IS" BASIS,
        # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        # See the License for the specific language governing permissions and
        # limitations under the License.
           
        REGISTRY_URL=private.registry.example
    
        mkdir -p "/etc/containerd/registry.d/${REGISTRY_URL}"
        bb-sync-file "/etc/containerd/registry.d/${REGISTRY_URL}/hosts.toml" - << EOF
        [host]
          [host."https://${REGISTRY_URL}"]
            capabilities = ["pull", "resolve"]
            [host."https://${REGISTRY_URL}".auth]
              username = "username"
              password = "password"
        EOF
    
  3. Примените NodeGroupConfiguration. Дождитесь появления конфигурационных файлов в директории /etc/containerd/registry.d на всех узлах.

  4. Проверьте корректность работы конфигураций. Для этого воспользуйтесь командой:

    # Для https:
    ctr -n k8s.io images pull --hosts-dir=/etc/containerd/registry.d/ private.registry.example/registry/path:tag
    
    # Для http:
    ctr -n k8s.io images pull --hosts-dir=/etc/containerd/registry.d/ --plain-http private.registry.example/registry/path:tag
    
  5. Выполните переключение на использование модуля registry. Для этого, укажите в ModuleConfig deckhouse параметры режима Unmanaged. Если используется хранилище образов контейнеров, отличное от registry.deckhouse.ru, ознакомьтесь с конфигурацией модуля deckhouse для корректной настройки.

    Посмотреть текущие настройки хранилища образов контейнеров можно с помощью команды:

    d8 k -n d8-system exec -it svc/deckhouse-leader -c deckhouse -- deckhouse-controller global values | yq e '.modulesImages.registry' -
    

    Данные настройки укажите при конфигурации Unmanaged режима:

    apiVersion: deckhouse.io/v1alpha1
    kind: ModuleConfig
    metadata:
      name: deckhouse
    spec:
      version: 1
      enabled: true
      settings:
        registry:
          mode: Unmanaged
          unmanaged:
            imagesRepo: registry.deckhouse.ru/deckhouse/ee
            scheme: HTTPS
            license: <LICENSE_KEY> # Замените на ваш лицензионный ключ
    
  6. После применения, дождитесь в статусе переключения сообщение:

    Пример вывода:

    conditions:
    # ...
    - lastTransitionTime: "2025-08-13T15:22:34Z"
      message: |
        Check current nodes configuration
        2/2 node(s) Unready:
        - master-0: has custom toml merge containerd configuration
        - worker-5e389be0-578df-s5sm5: has custom toml merge containerd configuration
      reason: Processing
      status: "False"
      type: ContainerdConfigPreflightReady
    

    Данное сообщение означает, что на узлах имеются старые конфигурации хранилища образов контейнеров, расположенные в директории /etc/containerd/conf.d. И в данный момент переключение на новую конфигурацию containerd заблокировано. Для того чтобы разрешить переключение, необходимо удалить старые конфигурационные файлы.

  7. Удалите старые конфигурационные файлы, чтобы разрешить переключение на модуль registry. Для этого создайте NodeGroupConfiguration. Пример манифеста NodeGroupConfiguration:

    apiVersion: deckhouse.io/v1alpha1
    kind: NodeGroupConfiguration
    metadata:
      name: containerd-additional-config-auth-delete.sh
    spec:
      # Шаг должен выполниться до '032_configure_containerd.sh'
      weight: 0
      bundles:
        - '*'
      nodeGroups:
        - "*"
      content: |
        # Copyright 2023 Flant JSC
        #
        # Licensed under the Apache License, Version 2.0 (the "License");
        # you may not use this file except in compliance with the License.
        # You may obtain a copy of the License at
        #
        #     http://www.apache.org/licenses/LICENSE-2.0
        #
        # Unless required by applicable law or agreed to in writing, software
        # distributed under the License is distributed on an "AS IS" BASIS,
        # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        # See the License for the specific language governing permissions and
        # limitations under the License.
    
        file="/etc/containerd/conf.d/old-config.toml"
    
        [ -f "$file" ] && rm -f "$file"
    
  8. После удаления старых конфигураций, убедитесь, что переключение продолжило выполняться. Пример статуса переключения:

    conditions:
    # ...
    - lastTransitionTime: "2025-08-13T16:42:09Z"
      message: ""
      reason: ""
      status: "True"
      type: ContainerdConfigPreflightReady
    
  9. Дождитесь завершения переключения. Пример статуса переключения:

    conditions:
    # ...
      - lastTransitionTime: "..."
        message: ""
        reason: ""
        status: "True"
        type: Ready
    hash: ..
    mode: Unmanaged
    target_mode: Unmanaged
    
  10. Удалите NodeGroupConfiguration, созданный на шаге удаления старых конфигурационных файлов:

    d8 k delete nodegroupconfiguration containerd-additional-config-auth-delete.sh
    

    Чтобы убедиться, что NodeGroupConfiguration удалён, используйте команду:

    d8 k get nodegroupconfiguration
    

    В списке не должно быть NodeGroupConfiguration, подлежащего удалению (в этом примере — containerd-additional-config-auth-delete.sh).

Как мигрировать обратно с модуля registry?

  • Это устаревший (deprecated) формат управления хранилищем образов контейнеров.
  • Во время переключения containerd v1 будет перезапущен.
  • Во время переключения containerd v1 будет переведен на старую схему конфигурации хранилища образов контейнеров.
  • Во время переключения, пользовательские конфигурации registry для containerd v1 будут временно недоступны.
  1. Переведите хранилище образов контейнеров в режим Unmanaged. Если используется хранилище образов контейнеров, отличное от registry.deckhouse.ru, ознакомьтесь с конфигурацией модуля deckhouse для корректной настройки.

    Пример конфигурации:

    apiVersion: deckhouse.io/v1alpha1
    kind: ModuleConfig
    metadata:
      name: deckhouse
    spec:
      version: 1
      enabled: true
      settings:
        registry:
          mode: Unmanaged
          unmanaged:
            imagesRepo: registry.deckhouse.ru/deckhouse/ee
            scheme: HTTPS
            license: <LICENSE_KEY> # Замените на ваш лицензионный ключ
    
  2. Проверьте статус переключения, используя инструкцию. Пример вывода:

    conditions:
    # ...
    - lastTransitionTime: "..."
      message: ""
      reason: ""
      status: "True"
      type: Ready
    hash: ..
    mode: Unmanaged
    target_mode: Unmanaged
    
  3. Переведите хранилище образов контейнеров в неконфигурируемый режим Unmanaged. Пример конфигурации:

    apiVersion: deckhouse.io/v1alpha1
    kind: ModuleConfig
    metadata:
      name: deckhouse
    spec:
      version: 1
      enabled: true
      settings:
        registry:
          mode: Unmanaged
    
  4. Проверьте статус переключения, используя инструкцию. Пример вывода:

    conditions:
    # ...
    - lastTransitionTime: "..."
      message: ""
      reason: ""
      status: "True"
      type: Ready
    hash: ..
    mode: Unmanaged
    target_mode: Unmanaged
    
  5. Если используется containerd v1, и в кластере применены пользовательские конфигурации registry, их необходимо заменить на старый формат. Для этого, подготовьте конфигурации хранилища образов контейнеров старого формата. Данные конфигурации на данном этапе применять не нужно. Пример конфигурации:

    apiVersion: deckhouse.io/v1alpha1
    kind: NodeGroupConfiguration
    metadata:
      name: containerd-additional-config-auth.sh
    spec:
      # Для добавления файла перед шагом '032_configure_containerd.sh'
      weight: 31
      bundles:
        - '*'
      nodeGroups:
        - "*"
      content: |
        # Copyright 2023 Flant JSC
        #
        # Licensed under the Apache License, Version 2.0 (the "License");
        # you may not use this file except in compliance with the License.
        # You may obtain a copy of the License at
        #
        #     http://www.apache.org/licenses/LICENSE-2.0
        #
        # Unless required by applicable law or agreed to in writing, software
        # distributed under the License is distributed on an "AS IS" BASIS,
        # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        # See the License for the specific language governing permissions and
        # limitations under the License.
    
        REGISTRY_URL=private.registry.example
    
        mkdir -p /etc/containerd/conf.d
        bb-sync-file /etc/containerd/conf.d/additional_registry.toml - << EOF
        [plugins]
          [plugins."io.containerd.grpc.v1.cri"]
            [plugins."io.containerd.grpc.v1.cri".registry]
              [plugins."io.containerd.grpc.v1.cri".registry.mirrors]
                [plugins."io.containerd.grpc.v1.cri".registry.mirrors."${REGISTRY_URL}"]
                  endpoint = ["https://${REGISTRY_URL}"]
              [plugins."io.containerd.grpc.v1.cri".registry.configs]
                [plugins."io.containerd.grpc.v1.cri".registry.configs."${REGISTRY_URL}".auth]
                  username = "username"
                  password = "password"
                  # OR
                  auth = "dXNlcm5hbWU6cGFzc3dvcmQ="
        EOF
    
  6. Удалите секрет registry-bashible-config. Во время удаления, containerd v1 переключится на старый формат конфигурации containerd:

    d8 k -n d8-system delete secret registry-bashible-config
    
  7. После удаления дождитесь завершения переключения. Для отслеживания используйте инструкцию. Пример вывода:

    conditions:
    # ...
    - lastTransitionTime: "..."
      message: ""
      reason: ""
      status: "True"
      type: Ready
    hash: ..
    mode: Unmanaged
    target_mode: Unmanaged
    
  8. Если используется containerd v1, примените заготовленные этапом ранее NodeGroupConfiguration с пользовательскими конфигурациями хранилища образов контейнеров.

  9. Отключите модуль registry. Пример:

    apiVersion: deckhouse.io/v1alpha1
    kind: ModuleConfig
    metadata:
      name: registry
    spec:
      enabled: false
      settings: {}
      version: 1
    

Как посмотреть статус переключения режима registry?

Статус переключения режима хранилища образов контейнеров можно получить с помощью следующей команды:

d8 k -n d8-system -o yaml get secret registry-state | yq -C -P '.data | del .state | map_values(@base64d) | .conditions = (.conditions | from_yaml)'

Пример вывода:

conditions:
  - lastTransitionTime: "2025-07-15T12:52:46Z"
    message: 'registry.deckhouse.ru: all 157 items are checked'
    reason: Ready
    status: "True"
    type: RegistryContainsRequiredImages
  - lastTransitionTime: "2025-07-11T11:59:03Z"
    message: ""
    reason: ""
    status: "True"
    type: ContainerdConfigPreflightReady
  - lastTransitionTime: "2025-07-15T12:47:47Z"
    message: ""
    reason: ""
    status: "True"
    type: TransitionContainerdConfigReady
  - lastTransitionTime: "2025-07-15T12:52:48Z"
    message: ""
    reason: ""
    status: "True"
    type: InClusterProxyReady
  - lastTransitionTime: "2025-07-15T12:54:53Z"
    message: ""
    reason: ""
    status: "True"
    type: DeckhouseRegistrySwitchReady
  - lastTransitionTime: "2025-07-15T12:55:48Z"
    message: ""
    reason: ""
    status: "True"
    type: FinalContainerdConfigReady
  - lastTransitionTime: "2025-07-15T12:55:48Z"
    message: ""
    reason: ""
    status: "True"
    type: Ready
mode: Direct
target_mode: Direct

Вывод отображает состояние процесса переключения. Каждое условие может находиться в статусе True или False, а также содержать поле message с пояснением.

Описание условий:

Условие Описание
ContainerdConfigPreflightReady Состояние проверки конфигурации containerd. Проверяется, что на узлах отсутствуют пользовательские auth конфигурации containerd.
TransitionContainerdConfigReady Состояние подготовки конфигурации containerd в новый режим. Проверяется, что конфигурация containerd успешно подготовлена и содержит одновременно конфигурации нового и старого режима.
FinalContainerdConfigReady Состояние завершения переключения containerd в новый режим. Проверяется, что конфигурация containerd успешно применена и содержит конфигурацию нового режима.
DeckhouseRegistrySwitchReady Состояние переключения Deckhouse и его компонентов на использование нового хранилища образов контейнеров. Значение True указывает, что Deckhouse успешно переключился на сконфигурированное хранилище образов контейнеров и готов к работе.
InClusterProxyReady Состояние готовности In-Cluster Proxy. Проверяется, что In-Cluster Proxy успешно запущен и работает.
CleanupInClusterProxy Состояние очистки In-Cluster Proxy, если прокси не нужен для работы желаемого режима. Проверяется, что все ресурсы, связанные с In-Cluster Proxy, успешно удалены.
NodeServicesReady Состояние готовности Node Services Manager и Static-Pod хранилища образов контейнеров. Проверяется, что Node Services Manager успешно запущен и работает, и что Static-Pod хранилища образов контейнеров был успешно развёрнут с помощью Node Services Manager.
CleanupNodeServices Состояние очистки Node Services Manager и Static-Pod хранилища образов контейнеров, если компоненты не нужны для работы желаемого режима. Проверяется, что все ресурсы, связанные с Node Services Manager и Static-Pod хранилища образов контейнеров, успешно удалены.
RegistryContainsRequiredImages Состояние проверки хранилища образов контейнеров на наличие необходимых образов.
Ready Общее состояние готовности хранилища образов контейнеров к работе в указанном режиме. Проверяется, что все предыдущие условия выполнены и модуль готов к работе.