Kubernetes как ядро Linux, часть 2: kubepkg, универсальный пакетный менеджер а ля Debian
У Linux есть apt, dnf и pacman, и никто уже не помнит, как жили без них. У Kubernetes за десять лет так и не появилось ничего подобного. Есть Helm, который прекрасно ставит один чарт, есть Argo CD и Flux, которые умело синкают кластер с Git, но нет инструмента, который знал бы, что cert-manager нужен половине других компонентов, что KubeVirt нельзя откатить на прошлую версию после обновления и что CRD, поставленные одним релизом, не получится молча снести вместе с ним. Каждый, кто собирает платформу на Kubernetes, пишет эту прослойку сам, и каждый раз заново.
Проект, о котором я бы хотел рассказать сегодня, называется kubepkg, он работает на любом кластере, ставит обычные Helm-чарты и функционирует по модели Debian, с репозиторием пакетов, подписанным индексом и сборкой из исходников апстрима. В первой части своего цикла статей я рассказывал, зачем всё это затеял, почему считаю важным начать относиться к K8s как к ядру Linux и как устроен мой тулкит для построения дистрибутивов Kubernetes. В ней я немного затрагивал концепцию универсального менеджера пакетов для K8s, который легко можно встроить в любую платформу или добавить в кластер с ванильной версией оркестратора. Эта статья о том, как kubepkg устроен изнутри и что он умеет. Все примеры с живого кластера внутри тенанта Cozystack.
Снова отмечу, что это мой личный проект и все ошибки сделаны мною. Инженеры Aenix никогда бы так плохо не сделали:)
Откуда взялся kubepkg
История kubepkg
Как всё началось, я подробно рассказал в первой части, поэтому здесь будет только суть. Мэтт Дагган в ответ на мою давнюю статью написал, что главная беда Kubernetes в отсутствии настоящего пакетного менеджера, перечислил основные его характеристики, а позднее даже придумал для него рабочее название KubePkg. И совпадение с названием моего проекта не случайное.
Сам код я писал не с нуля (да и вообще не то чтобы писал код сам), за основу решил взять cozypkg, тулзу из проекта Cozystack: когда платформа переходила на пакетную архитектуру, понадобилось ставить и обновлять десятки компонентов в правильном порядке и с понятной историей. cozypkg хорошо решал задачу одной платформы и был намертво к ней привязан, так что в начале октября я взял часть кода этого компонента и построил на нём самостоятельный проект, который не зависит ни от какой платформы и может быть использован как в ванильном куб-кластере, так и интегрироваться в любую куб-платформу. Сейчас я буду описывать то, что свойственно версии 1.3, которая уже обладает стабильным API и развивается в рамках GitHub-организации kuberoot-dev, рядом со сборщиком дистрибутивов kuberoot, для которого он и делался.
Чего не хватает Helm
Helm отлично решает задачу шаблонизации и установки одного релиза. Проблемы начинаются, когда релизов становится много и они зависят друг от друга. Helm не разрешает зависимости между релизами: если вашему приложению нужен cert-manager, Helm об этом не узнает, пока под не упадёт с ошибкой про отсутствующий CRD. Он не знает, какой компонент какой API предоставляет, поэтому не может сказать, что в кластере уже установлен Prometheus Operator, просто впихнул его туда кто-то другой. Он не показывает заранее, что сделает обновление. И у него нет какого-то своего мнения о CRD, кроме общего понимания «поставить один раз и больше не трогать», хотя именно CRD в Kubernetes самая опасная вещь: они общие для всего кластера, переживают поставивший их релиз, а удаление CRD удаляет все объекты этого типа.
Каждая платформа закрывает эти дыры по-своему. У Deckhouse есть модули, у OpenShift есть OLM, у Rancher свой каталог, и всё это нельзя взять и использовать в другом месте. kubepkg задуман как пакетный слой именно для тех, кто строит платформы и дистрибутивы. Он не заменяет ни Helm, ни Argo CD, ни Flux, а работает поверх них и вместе с ними.
Почему apt для Kubernetes до сих пор не случился
Идея не нова, и я далеко не первый, кому она пришла в голову. Сам Helm появился в 2015 году под девизом «пакетный менеджер для Kubernetes», и формально он им и является, ведь у него есть пакеты, репозитории и версии. Но apt ценен не форматом архива, а знанием о том, как пакеты живут вместе, и этого слоя Helm так и не получил. За десять лет его много раз пытались доработать или заменить, и почти все попытки закончились одинаково.
CoreOS ещё в 2016 году сделала KPM, свой формат пакетов со своим реестром, из которого потом выросли App Registry и раздел Applications в Quay. После того как CoreOS купил Red Hat, KPM развивать перестали, а в 2022 году quay.io убрал и сами Applications. В 2018 году Microsoft, Docker и ещё несколько компаний предложили CNAB, универсальный формат бандла для любых облачных приложений, но его эталонная реализация Duffle давно лежит в архиве, а из живых осталась одна Porter, о которой мало кто слышал. В D2iQ придумали KUDO, декларативный способ описывать операторы со своим форматом пакетов, и последний его релиз вышел весной 2021 года. Glasskube в 2024 году громко назвал себя пакетным менеджером нового поколения для Kubernetes и собрал несколько тысяч звёзд на GitHub, но команда переключилась на другой продукт, и репозиторий теперь в архиве. Даже Kubeapps, который вообще не придумывал своего формата и был просто удобным каталогом поверх Helm-чартов, в августе 2025 года ушёл в архив, и поддерживает его теперь форк SAP.
Выжили те, у кого за спиной стоит большая платформа. OLM прекрасно работает в OpenShift, а kapp-controller из Carvel, модель пакетов и репозиториев которого очень похожа на то, что хотелось реализовать мне, вырос вместе с Tanzu. Но оба так и остались частью экосистем своих вендоров, и за их пределами ими почти никто не пользуется.
Причина всегда одна, и она не техническая. Новый формат пакета требует, чтобы кто-то упаковал в него софт. Авторы cert-manager, Cilium или KubeVirt не станут поддерживать ещё один формат ради инструмента, которым пока никто не пользуется, а пользователи не станут использовать инструмент, в котором нет нужных им пакетов. Поэтому такой проект живёт ровно столько, сколько его команда успевает сама переупаковывать чужие релизы, а когда команда устаёт или у компании меняются планы, всё заканчивается. Helm-чарт тем временем стал фактическим стандартом поставки: почти каждый серьёзный проект для Kubernetes публикует его сам, в каждом релизе, и на Artifact Hub таких чартов тысячи.
Отсюда главное решение по kubepkg. Пакет это неизменённый Helm-чарт, а всё, чего Helm не знает, лежит рядом с ним в отдельном описании. Я никого не прошу ничего переупаковывать, рецепт берёт релиз апстрима как есть, а если апстрим чарт не публикует, как KubeVirt, рецепт собирает его из релизных манифестов, и на выходе всё равно получается обычный чарт. Собранный пакет лежит в OCI-реестре и ставится без kubepkg, хоть самим Helm, хоть через Flux или Argo CD, так что, даже если kubepkg завтра исчезнет, пакеты останутся рабочими. Цена этого решения в том, что мы ограничены возможностями Helm там, где их не хватает, и всё недостающее, от порядка установки до готовности компонентов и владения CRD, приходится делать снаружи чарта, а не внутри него. Как именно, я покажу дальше.
Что такое пакет
Из чего состоит пакет
Что входит в описание рядом с чартом, видно на схеме, а интереснее посмотреть, как это работает.
Пакет может состоять из нескольких компонентов, каждый из которых это отдельный чарт и отдельный релиз в своём пространстве имён. Хороший пример KubeVirt: сначала ставится оператор, а потом ресурс KubeVirt, который говорит оператору развернуть виртуализацию. Второй компонент не имеет смысла, пока не готов первый, и kubepkg ставит их по очереди.
Зависимости бывают двух видов. Можно потребовать другой пакет с ограничением версии, например cert-manager не ниже 1.20 и не выше второй мажорной. А можно потребовать возможность, например API monitoring.coreos.com. Возможность удовлетворяется либо пакетом, который её предоставляет, либо самим кластером, если такой API в нём уже обслуживается. Это значит, что Prometheus Operator, поставленный вручную три года назад, тоже учитывается, и kubepkg не будет требовать поставить его второй раз.
Готовые пакеты остаются обычными Helm-чартами в OCI-реестре. Если завтра вы решите, что kubepkg вам не нужен, эти чарты поставит сам Helm, Flux, Argo CD или werf (Алексей, Илья, Иван, Дмитрий, вам большой привет!).
Как это выглядит в кластере
Объекты kubepkg в кластере
Отдельной базы пакетов у kubepkg нет, всё его состояние живёт в обычных ресурсах кластера, и их всего четыре.
Repository это подписка на репозиторий пакетов, адрес индекса и ключи, которым кластер доверяет. Package описывает то, что вы хотите: имя пакета, ограничение версии, ваши значения для чартов. PackageSource это конкретная версия пакета, которую оператор выбрал из репозиториев. А PackageRevision это неизменяемая запись о том, что именно было применено, вплоть до хеша каждого чарта и каждого набора значений.
Вы меняете только Package, всё остальное делает оператор. Он выбирает версию, проверяет требования, конфликты и владение CRD, и если желаемое состояние отличается от текущей ревизии, записывает новую ревизию и применяет компоненты в порядке зависимостей. Применять их он может сам, через Helm SDK, а может отдать эту работу Nelm из werf, Flux или Argo CD.
Первое знакомство
Оператор ставится обычным Helm-чартом, а дальше кластер подписывается на репозиторий. Публичный репозиторий проекта собирается из kubepkg-recipes, в нём сейчас 49 пакетов, от cert-manager и Cilium до vLLM и Milvus.
helm install kubepkg oci://ghcr.io/kuberoot-dev/charts/kubepkg \
--namespace kubepkg-system --create-namespace --wait
curl -fsSLO raw.githubusercontent.com
kubepkg repo add main kuberoot-dev.github.io --public-key index.pub
sourcecraft.dev
С этого момента оператор принимает индекс репозитория только тогда, когда он подписан этим ключом.
Прежде чем что-то ставить, стоит посмотреть, что произойдёт. Команда kubepkg plan разрешает пакет и всё, что ему нужно, относительно текущего состояния кластера и ничего не меняет:
kubepkg plan
Здесь virtualization это метапакет. Сам он ничего не ставит, а только требует KubeVirt и CDI в совместимых версиях. План показывает, откуда взялся каждый пакет, какими CRD он будет владеть и можно ли его откатить. В CI ту же команду удобно запускать на каждый пул-реквест, чтобы ревьюер видел последствия изменения, а не только diff в YAML.
Установка показывает тот же план и пишет в кластер по объекту Package на каждый пакет, остальное делает оператор:
kubepkg install
Каждое изменение пакета, будь то новая версия или другие значения, создаёт новую ревизию. Старые никуда не деваются, и к любой из них можно вернуться. Откат тоже создаёт новую ревизию с содержимым старой, история только растёт и никогда не переписывается:
kubepkg rollback
Репозиторий пакетов и сборка из исходников
Как собирается репозиторий
Главная идея, которую я взял у Debian, в том, что пакеты репозитория собирает мейнтейнер этого самого репозитория, а не автор софта. Debian собирает свои deb-файлы из исходников апстрима, и kubepkg делает так же. Неважно, что выпустил апстрим: готовый Helm-чарт, набор манифестов или архив исходников, внутри которого где-то там лежит чарт. Мейнтейнер пишет рецепт, в котором каждый источник закреплён по хешу, а дальше сказано, как превратить его в чарт: какие значения подставить, какие патчи наложить, какие объекты выбросить.
Рецепт для нового пакета не нужно писать с нуля: команда kubepkg init скачивает апстрим, закрепляет его, перечисляет CRD и образы и пишет черновик, который остаётся доработать. Как это выглядит, покажу чуть ниже на живом примере.
Вот часть настоящего рецепта KubeVirt. Он собирается из релизных манифестов, а не из стороннего чарта, и патч убирает жёсткое требование virt-operator к узлам control plane, которых нет в управляемых кластерах:
spec:
version: 1.9.0 # версия апстрима
build: 4 # наша четвёртая сборка этой версии
sources:
operator:
url: github.com/kubevirt/kubevirt/releases/download/v1…
sha256: f11307caafc3c23ffedf9887d8beb5a4419e2694da242fa68f63d1ec820de2e0
charts:
kubevirt-operator:
from: [operator] # обычные манифесты заворачиваются в чарт
exclude: [{kind: Namespace}]
patches:
- kind: Deployment
name: virt-operator
merge: {...}
sourcecraft.dev
Команда kubepkg build превращает рецепт в чарт в реестре, а kubepkg repo index собирает все версии всех пакетов в один индекс и подписывает его. Один и тот же рецепт всегда собирается в одно и то же содержимое, вплоть до байта. Установка в кластер к апстриму больше никогда не ходит: всё, что нужно, лежит в вашем реестре и закреплено по хешу в подписанном индексе. Опубликованная версия не меняется никогда, а исправление публикуется как новая сборка той же версии.
Индекс это один статический файл, его можно положить на GitHub Pages, в S3 или на nginx. Публичный репозиторий так и устроен: каждый мерж в main собирает пакеты, кладёт чарты в GitHub Container Registry и публикует подписанный индекс на Pages.
Пример: от черновика до установки
Посмотрим весь путь на настоящем кластере. Возьмём podinfo, маленькое демонстрационное приложение, у которого есть официальный Helm-чарт, и опубликуем его в собственном репозитории. Реестр и веб-сервер для индекса здесь подняты прямо внутри кластера, но с GitHub Container Registry и Pages всё выглядит точно так же.
Начинается всё с черновика рецепта. kubepkg скачал чарт, закрепил его по хешу и, что важно, сам отрендерил его и выписал все образы, которые он запускает, тоже с хешами:
$ kubepkg init podinfo --chart oci://ghcr.io/stefanprodan/charts/podinfo@6.9.2
wrote podinfo/recipe.yaml; review it, then run: kubepkg validate podinfo
$ cat podinfo/recipe.yaml
apiVersion: kubepkg.dev/v1
kind: Recipe
metadata:
name: podinfo
annotations:
kubepkg.dev/description: "Podinfo Helm chart for Kubernetes"
kubepkg.dev/home: stefanprodan/podinfo
spec:
version: 6.9.2
build: 1
sources:
upstream:
chart:
repository: oci://ghcr.io/stefanprodan/charts
name: podinfo
version: "6.9.2"
digest: sha256:8eacab4365d917867e855bfb4729afdbff28bead3fd9dc01923ab50b26aa4380
charts:
podinfo:
from: [upstream]
values: {} # the package's defaults over the upstream ones
package:
provides: [podinfo]
crds: []
images: # what the charts run, pinned; add images an operator deploys on its own
- docker.io/curlimages/curl:7.69.0@sha256:d845b6dd655da0e9ae0623065a35185f9b4438aa717fd1c9a3b89843e8b4ff26
- docker.io/giantswarm/tiny-tools:latest@sha256:8ccea5962d6e24cf90ac726ff8af6ad25ba8dc6a691ad47cb455e3dfcd3f4c22
- docker.io/stefanprodan/grpc_health_probe:v0.3.0@sha256:0d7bd9c77ef35c3f0432142b5689ae460331a4fecfa469590fe11f3d575669e4
- ghcr.io/stefanprodan/podinfo:6.9.2@sha256:cab90e04829cdae73199a8402882a4e3ccaf251b8a0d50bdefede0ab3f6371e7
rollback:
safe: false # true once rolling back to the previous version is known not to lose data
variants:
- name: default
components:
- name: podinfo
path: podinfo
install: {namespace: podinfo}
sourcecraft.dev
Черновик это только начало, решения в нём принимает мейнтейнер. Здесь я поставил две реплики по умолчанию и объявил откат безопасным, потому что podinfo ничего не хранит и предыдущая сборка всегда сможет запуститься снова. Проверка, сборка и публикация умещаются в три команды:
$ grep -n -E 'values:|safe:' podinfo/recipe.yaml
21: values: {replicaCount: 2}
31: safe: true # stateless: the previous build can always run again
$ kubepkg validate podinfo
podinfo: ok
$ kubepkg build podinfo --registry oci://demo.demo.svc:5000/demo --plain-http -o dist
published oci://demo.demo.svc:5000/demo/podinfo, wrote dist/podinfo-6.9.2-1.yaml
$ kubepkg repo keygen demo
wrote demo.key (private) and demo.pub
$ kubepkg repo index dist --plain-http --sign-key demo.key -o /site/demo/index.yaml
signed /site/demo/index.yaml.sig
wrote /site/demo/index.yaml: 1 packages, 1 versions
$ ls /site/demo
index.yaml
index.yaml.sig
sourcecraft.dev
Теперь в реестре лежит чарт, а рядом индекс с подписью. Осталось подписать на него кластер и поставить пакет:
$ kubepkg repo add demo demo.demo.svc --public-key demo.pub
repository demo added
$ kubepkg search podinfo
NAME LATEST REPOSITORY DESCRIPTION
podinfo 6.9.2 demo Podinfo Helm chart for Kubernetes
$ kubepkg plan podinfo
ACTION PACKAGE FROM TO REPOSITORY WHY
install podinfo - 6.9.2 build 1 demo requested
$ kubepkg install podinfo --yes
ACTION PACKAGE FROM TO REPOSITORY WHY
install podinfo - 6.9.2 build 1 demo requested
1 packages requested; follow them with "list"
$ kubepkg list
NAME VERSION REVISION READY REASON
cert-manager 1.21.2 2 True ReconciliationSucceeded
kube-state-metrics 2.20.0 1 True ReconciliationSucceeded
podinfo 6.9.2 1 True ReconciliationSucceeded
$ kubectl -n podinfo get deploy,pods
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/podinfo 2/2 2 2 10s
NAME READY STATUS RESTARTS AGE
pod/podinfo-c4fdfd856-7sd7c 1/1 Running 0 10s
pod/podinfo-c4fdfd856-f628c 1/1 Running 0 10s
sourcecraft.dev
От ссылки на чужой чарт до работающего пакета в собственном подписанном репозитории ушло десять команд, и ни в одной из них не нужно было писать шаблоны.
Почему откатывается не всё
Почему откат снимками состояния кластера не работает, я писал в первой части: снимок объектов не откатывает данные. Если новая версия KubeVirt уже перевела хранимые объекты на новую версию API, возврат старых манифестов не вернёт старые данные, а скорее сломает то, что ещё работало. Вот как это решено на практике.
Решение об откате
Безопасность отката объявляет автор новой версии пакета, потому что только он знает, что делают её миграции. Если обновление не удалось, а версия, с которой перекатываются, объявила откат безопасным, все уже изменённые компоненты возвращаются к прошлой ревизии в обратном порядке. Если не объявила, kubepkg останавливается и честно сообщает, что обновление не удалось и дальше возможно только исправление вперёд. Откат, который портит данные, хуже неудачного обновления. Для случаев, когда перед обновлением нужно что-то сделать, например снять резервную копию или прогнать миграцию, у пакета есть хуки, которые выполняются до обновления и получают версии «откуда» и «куда».
Как это выглядит вживую
Вернёмся к podinfo, который мы только что опубликовали. Его сборка 1 объявила откат безопасным. Чтобы не ждать долго, разрешим пакету становиться готовым не дольше двух минут, а репозиторий будем перечитывать каждые полминуты. Изменение пакета тоже записывается ревизией, поэтому у podinfo появится ревизия 2:
$ kubectl patch package podinfo --type merge -p '{"spec":{"upgrade":{"timeout":"2m"}}}'
package.kubepkg.dev/podinfo patched
$ kubectl patch repository demo --type merge -p '{"spec":{"interval":"30s"}}'
repository.kubepkg.dev/demo patched
sourcecraft.dev
Теперь представим, что мейнтейнер выпустил неудачную сборку. В ней podinfo запускается, но никогда не становится готовым, то есть ведёт себя как приложение, которое после обновления не может подключиться к базе или не проходит собственные проверки:
$ grep -n -E 'build:|values:' podinfo/recipe.yaml
10: build: 2
21: values: {replicaCount: 2, faults: {unready: true}}
$ kubepkg build podinfo --registry oci://demo.demo.svc:5000/demo --plain-http -o dist
published oci://demo.demo.svc:5000/demo/podinfo, wrote dist/podinfo-6.9.2-2.yaml
$ kubepkg repo index dist --plain-http --merge /site/demo/index.yaml --sign-key demo.key -o /site/demo/index.yaml
signed /site/demo/index.yaml.sig
wrote /site/demo/index.yaml: 1 packages, 2 versions
sourcecraft.dev
Через полминуты оператор видит в индексе новую сборку и начинает обновление. Новые поды стоят неготовыми, а один старый ещё работает и обслуживает запросы:
$ kubepkg list
NAME VERSION REVISION READY REASON
cdi 1.66.1 5 True ReconciliationSucceeded
cert-manager 1.21.2 5 True ReconciliationSucceeded
kubevirt 1.9.0 4 True ReconciliationSucceeded
podinfo 6.9.2 2 False Progressing
virtualization 1.0.0 1 True ReconciliationSucceeded
$ kubectl -n podinfo get pods
NAME READY STATUS RESTARTS AGE
podinfo-84df66bbb8-bwpkg 0/1 Running 0 7s
podinfo-84df66bbb8-vlcwb 0/1 Running 0 7s
podinfo-c4fdfd856-kjts4 1/1 Running 0 116s
sourcecraft.dev
Две минуты спустя срок истекает. Сборка, с которой мы переходим, объявила откат безопасным, поэтому kubepkg возвращает её сам, не дожидаясь человека. Ревизия 3 помечена как неудачная с причиной, а ревизия 4 восстанавливает содержимое ревизии 2:
$ kubepkg history podinfo
REVISION VERSION PHASE CREATED NOTE
1 6.9.2 Superseded 2026-10-18 08:33:16
2 6.9.2 Superseded 2026-10-18 08:34:34
3 6.9.2 Failed 2026-10-18 08:35:04 component podinfo failed: upgrade podinfo/podinfo: resource Deployment/podinfo/podinfo not ready. status: InProgress, message: Pending termination: 1 context deadline exceeded
4 6.9.2 Applied 2026-10-18 08:37:08 restored from 2
$ kubectl get package podinfo -o jsonpath='{range .status.conditions[*]}{.type}={.status} {.reason}: {.message}{"\n"}{end}'
Ready=False UpgradeRolledBack: revision 3 failed (component podinfo failed: upgrade podinfo/podinfo: resource Deployment/podinfo/podinfo not ready. status: InProgress, message: Pending termination: 1
context deadline exceeded), restored revision 2; failed 1 time(s), trying again at 2026-10-18T08:37:35Z, or set kubepkg.dev/retry to try now
$ kubectl get package podinfo -o jsonpath='{.status.retry}{"\n"}'
{"attempts":1,"nextAttempt":"2026-10-18T08:37:35Z"}
$ kubectl -n podinfo get pods
NAME READY STATUS RESTARTS AGE
podinfo-c4fdfd856-kdllf 1/1 Running 0 10s
podinfo-c4fdfd856-kjts4 1/1 Running 0 3m59s
sourcecraft.dev
Пакет остаётся неготовым, и это сделано намеренно. Он работает на прошлой сборке, но желаемое состояние, новейшая сборка из репозитория, не достигнуто, и об этом должен узнать человек, а не только лог оператора. При этом kubepkg не бросает попыток и сам повторяет то же обновление, сначала через 30 секунд, а потом каждый раз с вдвое большей паузой, вплоть до 10 минут, и пишет в status.retry, сколько попыток уже было и когда будет следующая. Так кластер, за которым никто не смотрит, сам выбирается из временного сбоя или подхватывает исправленную сборку 3, как только мейнтейнер её выпустит. Изменение пакета или аннотация kubepkg.dev применяются сразу, а откат, который человек сделал сам через kubepkg rollback, оператор повторять не станет. Вот как это выглядит через пару минут, когда исправленной сборки ещё нет. Оператор дважды попробовал снова, оба раза получил ту же ошибку, оба раза вернул рабочую версию и теперь ждёт минуту до следующей попытки:
$ kubepkg history podinfo
REVISION VERSION PHASE CREATED NOTE
1 6.9.2 Superseded 2026-10-18 08:33:16
2 6.9.2 Superseded 2026-10-18 08:34:34
3 6.9.2 Failed 2026-10-18 08:35:04 component podinfo failed: upgrade podinfo/podinfo: resource Deployment/podinfo/podinfo not ready. status: InProgress, message: Pending termination: 1 context deadline exceeded
4 6.9.2 Superseded 2026-10-18 08:37:08 restored from 2
5 6.9.2 Failed 2026-10-18 08:37:35 component podinfo failed: upgrade podinfo/podinfo: resource Deployment/podinfo/podinfo not ready. status: InProgress, message: Pending termination: 1 context deadline exceeded
6 6.9.2 Applied 2026-10-18 08:39:38 restored from 4
$ kubectl get package podinfo -o jsonpath='{.status.retry}{"\n"}'
{"attempts":2,"nextAttempt":"2026-10-18T08:40:35Z"}
sourcecraft.dev
Отдельная история это готовность. Helm считает релиз готовым, когда готовы созданные им Deployment и StatefulSet. Чарт KubeVirt создаёт один ресурс KubeVirt и сразу возвращается, а оператор потом ещё несколько минут разворачивает настоящую виртуализацию. Пакет может сказать, при каком условии компонент действительно готов, например когда у ресурса KubeVirt появилось условие Available, и пока оно не выполнено, следующий компонент не начинается, а пакеты, которые от него зависят, ждут.
CRD как часть пакета
CRD в Kubernetes устроены неудобно для любого пакетного менеджера. Они общие для всего кластера, не может быть две их версии рядом, они переживают релиз, в рамках которого они были поставлены, а удаление CRD удаляет все объекты этого типа во всех пространствах имён. Helm решает это радикально: CRD из каталога crds ставятся один раз и никогда не обновляются.
В kubepkg владение CRD выведено в отдельную сущность. Пакет объявляет, какими CRD он владеет, и план показывает это до установки. Два пакета не могут владеть одним CRD, а при попытке сделать это kubepkg откажется ставить второй CRD. CRD можно передать от одного пакета другому, например когда компонент переезжает из одного пакета в другой. При удалении пакета его CRD по умолчанию остаются в кластере вместе с объектами, и чтобы удалить их, это нужно явно попросить. И отдельно kubepkg следит, чтобы Helm молча не удалил CRD, которые пропали из новой версии чарта.
Доверие
Цепочка доверия
Каждый репозиторий приходит со своими ключами, и кластер сам решает, кому верить. Неподписанный репозиторий он примет только тогда, когда это явно разрешено.
Простой репозиторий подписывается одним ключом. Для серьёзного репозитория есть корень доверия по схеме TUF, то есть подписанный файл, в котором указано, каким ключам кластер верит и что каждый из них может подписывать. Корневых ключей несколько, они хранятся у разных людей вне сети, и для подписи нужна заданная часть из них, например любые два из трёх, так что один украденный или потерянный ключ ничего не решает. Сам индекс подписывает отдельный ключ, который живёт в CI, и если его скомпрометируют, корневые ключи просто заменят его на новый, не трогая кластеры. У подписи индекса есть срок действия, и это защищает кластеры от того, что кто-то подсунет им старый индекс со старыми уязвимыми версиями. Наконец, в корне доверия можно поручить часть пакетов другой команде, и тогда подпись этой команды будет действительна только для её пакетов. Так устроено доверие у крупных архивов, где разные пакеты поддерживают разные люди.
Корень доверия из двух ключей
Поскольку индекс закрепляет по хешу всё, включая образы, его подпись покрывает всё, что репозиторий ставит в кластер. Это даёт ещё три возможности. Команда kubepkg sbom пишет перечень компонентов (SBOM) в формате CycloneDX, для пакета или для всего кластера. Команда kubepkg scan прогоняет Trivy по каждому образу, который закрепили пакеты, и может завалить CI, если находит критические уязвимости. А политика образов заставляет кластер следить, чтобы в пространствах имён пакетов запускалось ровно то, что закрепил индекс.
Последнее стоит пояснить. Чарты почти всегда ссылаются на образы по тегу, а тег в чужом реестре может в любой момент начать указывать на другой образ. Admission webhook в kubepkg дописывает к тегу закреплённый хеш, так что узлы скачивают ровно тот образ, за который ручается индекс. В строгом режиме образ, который не закрепил ни один пакет, просто не допускается. No pasaran!
Error from server: admission webhook "pods.images.kubepkg.dev" denied the request: no installed package pins docker.io/library/busybox:1.36; namespace cert-manager runs only images packages pin (kubepkg.dev/image-policy=enforce)
sourcecraft.dev
Сертификат вебхука оператор выпускает и обновляет сам. Это сделано намеренно: kubepkg не может зависеть от cert-manager, потому что вполне может оказаться тем, кто cert-manager ставит.
Без интернета
Установка в закрытом контуре
Многие кластеры вообще не знат, что такое интернет: госсектор, банки, промышленные площадки, суверенные облака. Для них kubepkg собирает бандл, один файл, в котором лежат индекс репозитория с подписями, чарты выбранных пакетов и всех их зависимостей и все образы, которые эти пакеты запускают.
Ниже показан настоящий прогон на тестовом кластере. Оператор kubepkg в нём отрезан от внешнего мира сетевой политикой Cilium и видит только сам кластер, а внутри кластера подняты реестр-зеркало и веб-сервер для индекса. Бандл собирается на машине с интернетом из публичного репозитория:
$ kubepkg bundle create cert-manager kube-state-metrics --repo kuberoot-dev.github.io --public-key index.pub -o platform-2026-10.tar
wrote platform-2026-10.tar: 2 packages, 2 charts, 0 package trees, 5 images
$ ls -lh platform-2026-10.tar
-rw-r--r--. 1 root root 377M Oct 12 03:30 platform-2026-10.tar
$ kubepkg bundle inspect platform-2026-10.tar
created 2026-10-12T03:30:25Z
PACKAGE VERSION REPOSITORY
cert-manager 1.21.2 build 3 main
kube-state-metrics 2.20.0 build 3 main
IMAGE PINNED BY PACKAGES
quay.io/jetstack/cert-manager-cainjector:v1.21.2@sha256:c85268c64f2e0e76684bf5fe8906caff34b82523561c6affe0fae3546bd87562 repository cert-manager
quay.io/jetstack/cert-manager-controller:v1.21.2@sha256:70f532fd9cfde0b09d55687200942399d89838bc2d5d5b45152eb799a15912b8 repository cert-manager
quay.io/jetstack/cert-manager-startupapicheck:v1.21.2@sha256:46e75b6866359ffb5d82624f41e3ed1c70b2994982702ced547ce5edb418a8f5 repository cert-manager
quay.io/jetstack/cert-manager-webhook:v1.21.2@sha256:a60e2dac46dbb8a7f3df95c54ce941012f54c2fe022f0ee55aaa1ab40ed957ae repository cert-manager
registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.20.0@sha256:42cfe3723a5f058171c627537fb57a3ea0f26e4380fa18555a95cb1a1b4cfc5b repository kube-state-metrics
2 charts, 0 package trees
sourcecraft.dev
Каждый образ в списке закреплён по хешу, и закрепил его репозиторий, то есть подписанный индекс, а не сам бандл. Это важно для того, что происходит дальше.
Самое интересное начинается внутри контура. Бандлу не доверяют только потому, что он такой красивый сам к вам пришёл. Для начала подсунем сюда бандл, в котором по пути испортили один чарт, дописав в его архив всего один байт:
$ kubepkg bundle import tampered.tar --public-key index.pub --mirror oci://demo.demo.svc:5000/mirror --plain-http --site /site
error: the bundle does not check out: chart kube-state-metrics 2.20.0-3 in the bundle has digest sha256:ff78152558d651ae9564bdb4a2ec705d7e234be872e83753398596ff79fda8df, the package pins sha256:2bf8279f5f6195dfb6332dfb8c1251bdcf5a00754dd97102411a614f685d329e
sourcecraft.dev
Подмена найдена, и в ошибке видны оба хеша, тот, что получился, и тот, что закрепил подписанный пакет. Теперь возьмём подлинный бандл, но проверим его не тем ключом:
$ kubepkg bundle import platform-2026-10.tar --public-key other.pub --mirror oci://demo.demo.svc:5000/mirror --plain-http --site /site
error: the bundle does not check out: repository main: index is not signed by a trusted key
sourcecraft.dev
Ключи, которым вы доверяете, kubepkg берёт только из командной строки и никогда из самого бандла, поэтому их нужно переносить через контур отдельно, например вместе с конфигурацией. С правильным ключом импорт раскладывает всё по местам и заодно пишет настройки для узлов:
$ kubepkg bundle import platform-2026-10.tar --public-key index.pub --mirror oci://demo.demo.svc:5000/mirror --plain-http --site /site --node-config node-config
verified and imported 2 packages: 2 charts, 0 package trees, 5 images into oci://demo.demo.svc:5000/mirror
repository files: /site/main/; serve them over HTTP and point the cluster's repositories there
node settings for quay.io, registry.k8s.io: node-config
operator: helm upgrade kubepkg ... --set mirror=oci://demo.demo.svc:5000/mirror
$ find /site node-config -type f
/site/sbom.cdx.json
/site/main/index.yaml
/site/main/index.yaml.sig
node-config/containerd/certs.d/quay.io/hosts.toml
node-config/containerd/certs.d/registry.k8s.io/hosts.toml
node-config/talos-registries.yaml
$ cat node-config/containerd/certs.d/quay.io/hosts.toml
server = "quay.io"
[host."demo.demo.svc"]
capabilities = ["pull", "resolve"]
override_path = true
sourcecraft.dev
Узлы тянут образы через среду выполнения контейнеров, а не через kubepkg, поэтому вместе с зеркалом нужны и эти настройки: containerd по ним идёт за образами quay.io в зеркало, а для Talos есть готовый патч конфигурации машины. Чарты при этом продолжают ссылаться на quay.io, хеши не меняются, и под получает ровно тот образ, который закрепил индекс. В этом прогоне узлы кластера интернет видели, отрезан был только оператор, так что эту часть я не проверял, а только показываю.
Остаётся переключить оператор на зеркало и на индекс с внутреннего веб-сервера:
helm upgrade kubepkg oci://ghcr.io/kuberoot-dev/charts/kubepkg -n kubepkg-system --reuse-values \
--set mirror=oci://demo.demo.svc:5000/mirror \
--set 'repositories[0].name=main' \
--set 'repositories[0].url=demo.demo.svc' \
--set-file 'repositories[0].publicKeys[0]=index.pub'
sourcecraft.dev
Убедимся, что он действительно никуда не может выйти: публичный репозиторий ему недоступен.
$ kubepkg repo list
NAME PRIORITY PACKAGES READY URL
main 0 49 True demo.demo.svc
$ kubepkg repo add public kuberoot-dev.github.io --public-key index.pub --priority -1
repository public added
$ kubepkg repo list
NAME PRIORITY PACKAGES READY URL
main 0 49 True demo.demo.svc
public -1 0 False (FetchFailed) kuberoot-dev.github.io
$ kubepkg repo remove public
repository public removed
sourcecraft.dev
Репозиторий main теперь читается с внутреннего веб-сервера и принят, а публичный остаётся недоступен. Поставим пакет и посмотрим, откуда он взялся:
$ kubepkg install kube-state-metrics --yes
ACTION PACKAGE FROM TO REPOSITORY WHY
install kube-state-metrics - 2.20.0 build 3 main requested
1 packages requested; follow them with "list"
$ kubepkg list
NAME VERSION REVISION READY REASON
cert-manager 1.21.2 2 True ReconciliationSucceeded
kube-state-metrics 2.20.0 1 True ReconciliationSucceeded
$ python3 -c 'import json,urllib.request; [print(r) for r in json.load(urllib.request.urlopen("demo.demo.svc\_catalog"))["repositories"]]'
mirror/ghcr.io/kuberoot-dev/kubepkg-packages/cert-manager/cert-manager
mirror/ghcr.io/kuberoot-dev/kubepkg-packages/kube-state-metrics/kube-state-metrics
mirror/quay.io/jetstack/cert-manager-cainjector
mirror/quay.io/jetstack/cert-manager-controller
mirror/quay.io/jetstack/cert-manager-startupapicheck
mirror/quay.io/jetstack/cert-manager-webhook
mirror/registry.k8s.io/kube-state-metrics/kube-state-metrics
sourcecraft.dev
cert-manager, который уже стоял в кластере, при этом тоже перешёл на свежую сборку из зеркала, получив ревизию 2. Опубликованные версии в зеркале лежат под путями своих исходных реестров, поэтому описания пакетов не меняются, и включение зеркала само по себе не создаёт новых ревизий.
Взять под управление то, что уже стоит
В реальных кластерах уже что-то стоит. cert-manager, поставленный через helm install два года назад, оператор, применённый через kubectl apply, стек мониторинга, развёрнутый скриптом вендора. Ничего этого не хочется переустанавливать, особенно на проде. Команда kubepkg adopt берёт такие установки под управление на месте. Она находит релиз, показывает, что нашла, и сама ничего не меняет, пока вы не подтвердите:
$ kubepkg adopt cert-manager
adopt cert-manager 1.21.2 build 2 from main
COMPONENT RELEASE FOUND THEN
cert-manager cert-manager/cert-manager cert-manager v1.21.2 (app v1.21.2), revision 2, deployed upgrade in place to the package's chart, keeping its 2 top-level values
sourcecraft.dev
После подтверждения релиз обновляется на месте до чарта пакета с сохранением текущих значений. Deployment остаётся тем же объектом, поды не пересоздаются без нужды, а с этого момента у пакета есть ревизии, откат, зависимости и владение CRD. Объекты, поставленные без Helm, тоже забираются, но только в первой ревизии, чтобы потом случайное совпадение имён не приводило к тихому захвату чужого объекта.
Helm, werf, Flux и Argo CD
Оператор решает, что ставить и в каком порядке, а сам процесс применения манифестов он может отдать другому инструменту. По умолчанию он ставит чарты сам через Helm SDK и не требует в кластере ничего, кроме себя. С werf он работает через Nelm, движок развёртывания werf. С Flux каждый компонент становится HelmRelease, с Argo CD каждый компонент становится Application:
Приложения, которые kubepkg создал в Argo CD
С Nelm история отдельная, и про неё стоит рассказать подробнее. Nelm вырос из частично переписанного кода Helm, понимает те же чарты и хранит релизы в том же формате, но гораздо лучше следит за тем, что происходит с ресурсами во время установки, точнее расставляет их порядок внутри релиза и аккуратнее обращается с CRD. Пока идёт установка, Nelm показывает состояние каждого ресурса вместе с его событиями и логами подов, так что, если обновление застряло, причину видно сразу, а не после раскопок в кластере. Для пакетов вроде KubeVirt или Cilium, обновление которых и без того трудно отслеживать, это заметная разница.
Включается он одной настройкой оператора, backend: werf, и ничего ставить в кластер дополнительно не нужно, потому что бинарник Nelm уже лежит в образе оператора и закреплён там по контрольной сумме. Чарты оператор готовит и проверяет ровно так же, как для Helm, а потом для каждого компонента пакета запускает установку релиза через Nelm. Поскольку история релизов остаётся совместимой с Helm, их видно и в nelm release list, и в helm list, а откат пакета целиком работает так же, как с Helm. Забрать под управление то, что когда-то поставили руками, тоже можно, для этого Nelm умеет принудительно забирать уже существующие ресурсы в релиз.
Может возникнуть вопрос, почему kubepkg не пишет проект werf, а вызывает Nelm отдельно для каждого компонента. Проект werf ставит один релиз в одно пространство имён, а пакет kubepkg часто состоит из нескольких релизов в разных пространствах имён, как та же виртуализация, где KubeVirt и CDI живут по отдельности. Поэтому интеграция сделана на уровне движка, а не проекта. Обратный путь при этом тоже открыт: собранные пакеты kubepkg остаются обычными чартами в реестре, и проект werf может подключить их как зависимости.
Порядок, требования, условия готовности и ревизии работают одинаково с любым бэкендом. Разница в том, кто применяет манифесты и можно ли откатить пакет целиком: Flux и Argo CD не хранят прошлые артефакты, поэтому с ними откат пакета невозможен и kubepkg прямо об этом говорит.
Есть и третий путь, совсем без оператора. Команда kubepkg render разрешает пакеты и их зависимости и пишет результат в формате Flux, Argo CD или helmfile, с правильным порядком через dependsOn, волны синхронизации или needs. Результат коммитится в Git и живёт дальше без kubepkg. При этом теряются ревизии, откат и проверки владения CRD, так что обычно удобнее другой вариант: держать в Git объекты Package, пусть их применяет Argo CD или Flux, а kubepkg решает, как безопасно привести кластер к нужному состоянию.
Много кластеров
В первой части я написал, что управление набором кластеров kubepkg сознательно оставляет Argo CD, Flux, OCM и Karmada. Для большого количества кластеров это по-прежнему так, но платформам оказалось нужно что-то совсем простое: сказать один раз, что во всех продовых кластерах стоит cert-manager 1.21 и виртуализация 1.0, и катить изменения волнами, не поднимая ради этого отдельную систему.
Хаб и кластеры
Для этого в kubepkg есть хаб. Это кластер, который знает своих участников по меткам и пишет в них объекты Repository и Package по описанию PackageSet. На каждом участнике работает свой оператор kubepkg, поэтому ревизии, откаты и готовность остаются локальными и продолжают работать, даже если хаб недоступен. Хаб не трогает объекты, которые создал не он, и честно сообщает о конфликте.
Изменение в наборе можно катить осторожно: сначала на кластеры-канарейки, потом ограниченное число кластеров одновременно, и остановиться, как только какой-нибудь кластер сообщил о неудачном обновлении.
Как это выглядит вживую
Возьмём два настоящих кластера. Хаб регистрирует сам себя как канарейку под именем self, а второй кластер под именем edge:
$ kubepkg cluster add self --kubeconfig hub.kubeconfig --label env=prod --label ring=canary
$ kubepkg cluster add edge --kubeconfig edge.kubeconfig --label env=prod
$ kubepkg cluster list
NAME KUBERNETES READY LABELS STATUS
edge v1.35.6 True env=prod kubepkg is installed
self v1.35.6 True env=prod,ring=canary kubepkg is installed
sourcecraft.dev
Набор говорит, что во всех продовых кластерах должен стоять kube-state-metrics, сначала на канарейках, потом по одному кластеру за раз:
apiVersion: kubepkg.dev/v1
kind: PackageSet
metadata:
name: prod
spec:
clusterSelector: {matchLabels: {env: prod}}
rollout:
canary: {matchLabels: {ring: canary}}
maxInProgress: 1
repositories:
- name: fleet
spec:
url: kuberoot-dev.github.io
publicKeys: [...]
packages:
- name: kube-state-metrics
spec:
version: "2.20.0"
upgrade: {timeout: 90s}
components: {kube-state-metrics: {values: {replicas: 1}}}
sourcecraft.dev
Пока пакет не стал готов на канарейке, edge честно ждёт, и в статусе набора у него так и написано, что он ждёт канареек. Потом пакет появляется на обоих кластерах. Следующее изменение, две реплики вместо одной, ведёт себя так же: какое-то время на канарейке уже две реплики, а на edge ещё одна, и только когда канарейка готова, изменение идёт дальше.
Теперь сломаем набор: зададим образ с тегом, которого не существует. На канарейке обновление не становится готовым за полторы минуты и откатывается, а раскатка останавливается:
$ kubepkg set status prod
prod: 0 of 2 clusters ready, 1 updated
rollout paused: stopped after a failure on self: kube-state-metrics: revision 3 failed (component kube-state-metrics failed: upgrade kube-state-metrics/kube-state-metrics: resource Deployment/kube-state-metrics/kube-state-metrics not ready. status: InProgress, message:…; change the set to go on
CLUSTER UPDATED KUBE-STATE-METRICS
self yes 2.20.0
edge no 2.20.0
sourcecraft.dev
До edge сломанное изменение не дошло вообще, он продолжает работать на прежнем образе. Исправление набора, например правильные значения, это новое изменение, и раскатка продолжается с ним:
$ kubepkg set status prod
prod: 2 of 2 clusters ready
CLUSTER UPDATED KUBE-STATE-METRICS
self yes 2.20.0
edge yes 2.20.0
$ kubepkg set list
SET CLUSTER UPDATED READY STATUS
prod self yes 1/1 -
prod edge yes 1/1 -
sourcecraft.dev
И последнее, что проверялось в этом прогоне. Когда edge переводят из продового набора в тестовый, просто сменив ему метку, пакет не переустанавливается: Deployment остаётся тем же объектом, история ревизий продолжается, меняется только владелец и значения из нового набора.
Наблюдаемость
Оператор отдаёт метрики Prometheus: готовность каждого пакета, версию и ревизию, исходы ревизий, успешность загрузки и возраст индексов. Чарт по желанию ставит ServiceMonitor, алерты и дашборд Grafana:
Дашборд kubepkg в Grafana
Отдельный алерт следит за возрастом индекса. Если у индекса есть срок действия, а конвейер публикации тихо сломался, кластеры через какое-то время начнут индекс отвергать. Алерт срабатывает раньше, пока это ещё не стало проблемой.
Дистрибутив как метапакет
Всё вышеописанное складывается в главный сценарий, ради которого kubepkg делался. Дистрибутив Kubernetes это кластер плюс набор компонентов в версиях, которые проверены вместе. В kubepkg такой набор описывается метапакетом, у которого нет своих чартов, а есть только требования:
apiVersion: kubepkg.dev/v1
kind: Recipe
metadata:
name: mydistro
spec:
version: 2.0.0
package:
variants:
- name: default
requires:
- {package: cert-manager, version: "~1.21"}
- {package: kube-state-metrics, version: "~2.20"}
- {package: virtualization, version: "~1.0"}
- name: minimal
requires:
- {package: cert-manager, version: "~1.21"}
sourcecraft.dev
Установка версии 2.0 дистрибутива ставит все компоненты в нужном порядке, а обновление до 2.1, которая требует cert-manager 1.22, сдвигает cert-manager на нужную версию. Варианты дают дистрибутиву профили вроде полного и минимального.
Именно поэтому в kubepkg нет умного подбора версий, как в apt или npm: точные версии, проверенные вместе, закрепляет метапакет дистрибутива, а ограничения в самих пакетах остаются минимальными.
Настройки, которые принадлежат кластеру, а не пакету, например домен кластера, издатель сертификатов или класс хранилища, кладутся в один секрет, и оператор подкладывает их под значения каждого компонента. Чарты со строгой схемой значений, которые отвергают незнакомые ключи, получают только те платформенные ключи, которые объявила их схема, и ставятся без изменений.
Встраивание в чужую платформу
kubepkg задуман как компонент, который может встраиваться не только в готовый Kubernetes-кластер, но и в любую платфому, будь то дистрибутив Kubernetes, управляемый сервис или IDP. Форк для этого не нужен. На уровне настроек платформа может отдать ресурсы kubepkg под своей API-группой, например packages.example.org, чтобы пользователи видели её собственный API, поставить свои метки на все пространства имён пакетов и выбрать бэкенд. На уровне сборки она добавляет плагины, которые на любом языке подключают её внутреннее хранилище артефактов или перекладывают образы в её реестр. А на уровне кода всё, что делают бинарники kubepkg, лежит в Go-пакетах, из которых платформа собирает свой оператор и свой CLI со своим именем, правилами доверия и бэкендами.
API ресурсов с версии 1.0 стабилен (насколько это возможно для наслопленного за пару недель инструмента). Breaking changes могут появиться только в 2.0, старые версии API продолжают обслуживаться весь цикл 1.x, а оператор сам переводит хранимые объекты на новую версию.
Как kubepkg используется в kuberoot
Первым и самым требовательным пользователем kubepkg стал kuberoot. Там у пакетного менеджера нет администратора, который поставил бы его чартом и потом присматривал бы за ним: узел загружается, и через несколько минут на нём должен работать готовый дистрибутив, без всяких ручных действий.
Поэтому оператор kubepkg в kuberoot работает не подом в кластере, а службой самого узла, рядом с сервером API. Его запускает kinit, первый процесс системы, по профилю дистрибутива, и флаги оператора лежат в том же профиле, что и всё остальное:
- name: kubepkg-operator
after: apiserver
args:
- /usr/bin/kubepkg-operator
- --backend=helm
- --values-secret=kube-system/kuberoot-platform-values
- --registry-secret=kube-system/kuberoot-registry-credentials
- --image-policy=enforce
- --webhook-namespace=kube-system
- --webhook-service=kubepkg-webhook
- '{{if eq .Store.Kind "etcd"}}--leader-elect{{end}}'
sourcecraft.dev
Таким образом, пакетному менеджеру для начала работы не нужен работающий кластер, и он может поставить даже сетевой плагин, без установки которого ни один под не запустится. В отказоустойчивом кластере оператор работает на каждом управляющем узле, а действует один, выбранный через механизм выбора лидера Kubernetes (объект Lease). Вебхук политики образов при этом тоже живёт на узле, и кластер находит его через Service без селектора, у которого единственный адрес это сам узел.
Дальше kinit подписывает кластер на репозиторий. CRD kubepkg лежат в образе системы, туда же при сборке кладётся открытый ключ публичного репозитория, и при первом старте kinit применяет ресурс Repository и первый пакет, CoreDNS, которому нужно знать адрес DNS именно этого кластера:
apiVersion: kubepkg.dev/v1
kind: Repository
metadata:
name: main
s
Аварийное восстановление мультикластерного Kubernetes с Linkerd и HAZL
Высокая доступность приложений помогает выполнять договорные обязательства и соответствовать ожиданиям пользователей. Для этого многие платформенные команды развёртывают кластеры Kubernetes в локальных и облачных окружениях, распределяя их между зонами доступности, регионами и даже провайдерами. Сбой возможен на любом уровне этой инфраструктуры, поэтому нужно заранее проверить, как сервисная сетка поведёт себя при масштабной аварии. Команда VK Tech перевела статью о том, как протестировать аварийное восстановление мультикластерной конфигурации Linkerd и подготовить сервисную сетку к таким сбоям.
Статья адресована организациям, которые уже используют или планируют использовать сервисную сетку в мультикластерной конфигурации. Если вы только знакомитесь с этим подходом, начните сруководства по поддержке мультикластерности в Linkerd, изучитепреимущества и недостатки мультикластерного Kubernetes и разберитесь втрёх типах мультикластерных архитектур, которые поддерживает Linkerd. Другие полезные материалы перечислены в конце статьи.
Авторы моделируют реалистичную мультикластерную конфигурацию с открытой версией Linkerd иBuoyant Enterprise for Linkerd (BEL) и разбирают сценарии аварийного восстановления сервисной сетки. Они показывают, от чего зависит восстановление, как воспроизвести сбои в своих кластерах и какие действия помогут устранить выявленные проблемы. Даже если вы не будете запускать демонстрационную конфигурацию самостоятельно, метрики и результаты экспериментов помогут повысить устойчивость сетки и разобраться в роли Buoyant High Availability Zonal Load Balancing (HAZL) — механизма балансировки нагрузки с учётом зон доступности.
Обо всём по порядку: начнём с основ
Аварийное восстановление (DR) охватывает разные задачи. Здесь мы сосредоточимся на устойчивости сервисов, которые зависят от сервисной сетки: передают трафик через прокси и используют mTLS — взаимную аутентификацию клиента и сервера по TLS-сертификатам. Разберём, как сервисная сетка может снизить доступность во время сбоя и что с этим делать.
Зонами доступности, в зависимости от провайдера, называют изолированные домены отказа (failure domains) внутри региона. Домен отказа — часть инфраструктуры, которую одна неисправность может вывести из строя целиком, не затронув остальные части. Каждая зона включает один или несколько независимых дата-центров, поэтому пожар в одной зоне не должен выводить из строя другие.
Регионы — географически разнесённые территории, обычно удалённые друг от друга на сотни километров и более. Каждый регион включает несколько зон доступности.
Вовведении в мультикластерное взаимодействие Linkerd перечислено несколько причин использовать мультикластерную конфигурацию сервисной сетки. В этой статье мы рассмотрим разделение доменов отказа: если один кластер выходит из строя, остальные продолжают работать.
Кластеры связывают через общую сеть или шлюзы. Если трафик проходит через несколько облачных зон, расходы на передачу данных между ними могут оказаться неожиданно высокими. Ранее авторы разбиралипроблемы topology-aware routing в Kubernetes (TAR) — маршрутизации с учётом топологии, которая по возможности направляет трафик к эндпойнтам в той же зоне, где находится клиент. Они рассматривали стоимость межзонального трафика и то, как такой подход может снижать доступность. В следующей статье авторы представилиBuoyant High Availability Zonal Load Balancing (HAZL), который решает обе проблемы: сохраняет доступность и удерживает расходы на межзональный трафик на низком уровне.
Доступность цепочки сервисов или компонентов A–B–C зависит от связей между ними. Поскольку такие компоненты часто зависят друг от друга, топология приложения и инфраструктуры влияет на общую доступность в той же мере, что и время безотказной работы каждого сервиса. Поэтому нужно учитывать все зависимости сервисов, включая сервисную сетку.
Четыре сценария отказа
Рассмотрим четыре сценария отказа в мультикластерной конфигурации, где три кластера Kubernetes объединены общей сервисной сеткой Linkerd:
Отказ управляющего слоя сервисной сетки.
Отказ кластера.
Отказ зоны доступности.
Отказ региона.
Для каждого сценария разберём, как реагирует сервисная сетка, какие сбои возникают в её работе и какие меры помогут их предотвратить.
Ключевые выводы
Сервисная сетка редко отказывает так, что внезапно перестаёт пропускать трафик. Она отказывает, теряя способность адаптироваться, масштабироваться и оставаться управляемой.
Сервисная сетка добавляет новые точки отказа, которые нужно включить в учения по DR
Вам нужен новый набор алертов и плейбуков под топологию вашей сервисной сетки
Схема эксперимента
В экспериментах используются три кластера: WEST, CENTRAL и EAST. Они запущены в Docker с помощьюk3d и подключены к общей сети Docker. Трафик генерируетk6 — инструмент нагрузочного тестирования с открытым исходным кодом от Grafana Labs, сценарии для которого пишут на JavaScript. Для моделирования сбоев мы останавливаем поды и контейнеры Docker, которые служат узлами кластера, нарушаем работу сети Docker или добавляем задержку с помощьюChaos Mesh — платформы хаос-инжиниринга для Kubernetes.
С помощью Chaos Mesh илиLitmusChaos эти локальные тесты можно воспроизвести воблачных окружениях, моделируя, например, сбои DNS, отказы узлов и нарушения сетевого взаимодействия.
В каждом кластере 3 узла, расположенных в разных зонах доступности. CENTRAL и EAST развёрнуты в одном регионе, WEST — в другом. В WEST работают генератор нагрузки k6 и стек мониторинга Grafana + Prometheus. Сбои мы моделируем только в CENTRAL и EAST, чтобы сохранить сбор метрик.
В кластерах развёрнуто простое приложениеpodinfo: по одной реплике на узел, всего девять реплик. Чтобы сравнить поведение приложения, мы публикуем его во всех трёхмультикластерных режимах Linkerd:
app-federated
app-flat (pod-to-pod)
app-gateway (иерархический)
Сравнение сетевых режимов
Режим federated создаёт один логический сервис и разрешает эндпойнты для каждого кластера-участника.
Поток запросов: режим federated
Режим flat mirror обнаруживает сервисы в удалённых кластерах. Какой из них использовать, выбирает клиент (k6); в нашем случае это EAST.
Поток запросов: режим flat mirror
Режим gateway mirror создаёт в исходном кластере сервис, эндпойнтом которого служит шлюз, развёрнутый в целевом кластере. Трафик идёт через шлюз, а реальные поды разрешает уже целевой кластер.
Поток запросов: gateway mirror
За прогоном мы следим на собственном дашборде Grafana: аннотации на нём отмечают начало и окончание эксперимента, а алерты отслеживают симптомы и ошибки.
Дашборд Grafana, который мы использовали в экспериментах
В каждом эксперименте мы отслеживаем метрики и настраиваем алерты на достижение пороговых значений:
Запросы без mTLS.
Число запросов, обрабатываемых в единицу времени.
Доля запросов с ошибками.
Число эндпойнтов в режиме federated.
Доступность шлюза.
Локальность трафика — доля трафика, направляемого в другую зону.
Код и результаты экспериментов доступны наGitHub. Для каждого прогона естьсводка и еёнаглядное представление. Винструкции описано, как импортироватьдашборд Grafana и визуализировать данные эксперимента. Если что-то не работает или вы хотите предложить другие тесты, создайтеissue на GitHub.
Эксперименты
Базовый трафик во всех экспериментах одинаков: постоянная нагрузка 30 запросов в секунду на каждый режим публикации в каждом кластере, распределённая равномерно. Каждый эксперимент моделирует один сбой и измеряет его во всех трёх мультикластерных режимах.
Эксперимент 1: сбой управляющего слоя
В этом эксперименте мы моделируем сбой управляющего слоя сервисной сетки в кластере CENTRAL и проверяем, как он влияет на работу сетки и трафик между компонентами. Для теста уменьшаем число реплик Deployment приложения в EAST до 0. Назначение каждого сервиса управляющего слоя подробно описано вдокументации по архитектуре Linkerd.
Эксперимент 1a:сервис destination недоступен
Сервис destination отвечает за обнаружение эндпойнтов и работу контроллера политик Linkerd. Если он недоступен, информация об эндпойнтах перестаёт обновляться. Даже спустя 90 секунд CENTRAL продолжал сообщать, что все federated-эндпойнты доступны, хотя подов в EAST уже не было. Сетка использовала устаревшие данные и не учитывала изменения в кластерах.
Эксперимент 1b:сервис identity недоступен
Сервис identity выдаёт подам сертификаты. Если он недоступен, уже запущенные прокси продолжают обслуживать трафик с действующими сертификатами до истечения их срока — по умолчанию до 24 часов после последнего успешного обновления. Новые поды не могут получить сертификаты и присоединиться к сетке, поэтому увеличить число реплик приложений не получится.
Извлечённые уроки
Отслеживайте состояние управляющего слоя Linkerd на дашборде аварийного восстановления, например в Grafana. Настройте алерты на недоступность его сервисов и на поды, которые застряли на этапе инициализации и не могут присоединиться к сетке.
Проверки состояния подов и другие защитные механизмы могут временно скрыть проблемы сетки или защитить приложения от последствий её сбоя.
План аварийного переключения требует увеличения числа реплик, для которого нужен работоспособный управляющий слой. Процедура восстановления зависит от сетки, и во время инцидента она может стать узким местом. На её поведение при восстановлении влияют параметры установки, напримерwebhookFailurePolicy.
Эксперимент 2: кластер недоступен
В этом эксперименте мы останавливаем все три узла EAST. Затем повторяем эксперимент: удаляем сеть Docker, которая соединяет кластеры, и так моделируем полный сбой сети.
Извлечённые уроки
Мультикластерные режимы по-разному реагируют на сбои: в одних отказ заметен сразу, в других его признаки менее очевидны. Разберитесь, как ваша конфигурация обнаруживает эндпойнты и находит бэкенды, а также где это происходит: в локальном кластере (federated или flat-mirror) или в удалённом (gateway). От этого зависит поведение сетки при потере кластера.
Одного алерта на долю ошибок недостаточно. Если механизм обнаружения продолжает возвращать недоступные эндпойнты, запросы всё равно направляются к ним, а соединения зависают до истечения тайм-аута клиента. Без тайм-аутов метрики вродеrequest_total не растут. Тайм-ауты и повторные попытки можно настроить в коде клиента или средствамиLinkerd.
В тестах отдавайте предпочтение внезапным обрывам, а не корректному завершению работы (graceful shutdown), при котором сервис обрабатывает текущие запросы перед остановкой. Корректное завершение замечают и другие механизмы, например проверки состояния, а внезапный обрыв обнаруживается только после истечения тайм-аутов обнаружения эндпойнтов. Такие тесты позволяют точнее оценить время обнаружения сбоя и восстановления.
Эксперимент 3: зона становится медленнее
В этом эксперименте мы добавляем задержку в одну зону кластеров EAST и CENTRAL. Поды при этом всё время оставались работоспособными.
Схема топологии из двух регионов: зона zone-a1 обведена красным, подпись +400ms. В обведённой зоне находятся один под CENTRAL и один под EAST. Все поды по-прежнему показаны работающими.
Извлечённые уроки
Маршрутизация на основе топологии не видит деградировавшую, но работоспособную зону, а HAZL адаптируется и переводит трафик на эндпойнты, которые работают лучше.
В штатном режиме HAZL удерживает трафик в своей зоне, даже когда он переходит из кластера в кластер. В open source-версии Linkerd та же рабочая нагрузка распределяется без учёта зон, и в реальном счёте за трафик это оборачивается межзональным egress-трафиком для большинства запросов.
Примечание: межкластерная зональная локальность HAZL сейчастребует мультикластерного режима pod-to-pod (flat) и в этом эксперименте не применяется к режиму gateway.
Эксперимент 4: регион выходит из строя
Оба кластера в region-a отключены, в работе остаётся только WEST. Уцелевший регион принимает на себя весь трафик.
Извлечённый урок
Режим federated хорошо справляется с потерей региона: поды в WEST уже входят в пул federated-сервиса. Он выдерживает такой сбой, если WEST может принять дополнительную нагрузку и масштабироваться вверх. Если нет, следите, чтобы сервисы принимали ровно столько запросов, сколько могут обработать, и применяйте ключевые методы:ограничение частоты запросов иcircuit breaking10 (Linkerd их поддерживает). Два других режима отказывают, потому что их эндпойнты находились в отказавшем регионе. Так получилось из-за того, как мы спроектировали архитектуру моделирования, а не из-за ограничений Linkerd.
Три шага, которые повысят надёжность вашей сервисной сетки
1. Проверьте свою установку и разберитесь в ней
Этот шаг большинство пропускает. У Buoyant естьчек-лист для промышленной эксплуатации, в котором собраны типичные проблемы и требования к установке.
Мультикластерное взаимодействие в Linkerd можно настроить несколькими способами. Эксперименты показывают, что каждая топология по-своему реагирует на сбои, а возникающие ошибки могут быть явными или скрытыми. Разбор топологий и результаты их хаос-тестирования приведены в статье «Федерация кластеров для Kubernetes без простоев».
Например, установка поруководству Buoyant для промышленной эксплуатации по умолчанию используетпараметры высокой доступности (HA). При установке созначениями Helm-чарта по умолчанию это не так. Без HA для proxy injector — компонента, который автоматически добавляет прокси в создаваемые поды, — задано webhookFailurePolicy=Ignore. Если этот компонент недоступен, поды могут запускаться без прокси и оставаться вне сетки. В режиме HA значение меняется на Fail. В обоих режимах параметр defaultInboundPolicy по умолчанию имеет значение all-unauthenticated, поэтому поды в сетке принимают трафик без mTLS, пока настройку не изменят глобально или для отдельного пространства имён. Во время инцидента эти параметры могут привести к неожиданному поведению сетки.
2. Доработайте алерты
Для многих точек отказа сетки нет стандартных алертов. Алерты на долю запросов с ошибками не выявляют проблемы управляющего слоя напрямую.
Отслеживайте следующие сигналы:
Работоспособность управляющего слоя сетки.
Истечение срока действия сертификатов прокси.
Поды, которые не могут присоединиться к сетке.
Трафик без mTLS между сервисами там, где его быть не должно.
Метрики описаны встатье о работе сервиса destination в Linkerd.
Смоделируйте аварийное переключение и проверьте, сработают ли алерты. Стандартные алерты могут выявить симптомы — ошибки, которые видят пользователи, — но не первопричину, например сбой управляющего слоя.
3. Тестируйте восстановление от начала до конца
Документируйте проблемы, включайте их в учения по аварийному восстановлению и регулярно повторяйте тесты. Готовые дашборды выявляют не все проблемы: поведение сетки зависит от топологии, параметров установки и мультикластерной конфигурации. Надёжно проверить, сможет ли сетка восстановиться, можно только в своём окружении.
Тест должен охватывать весь процесс: внесение сбоя, выявление его признаков и восстановление до исходного рабочего состояния. Например, проблемы с сервисом identity могут проявиться только при увеличении числа реплик приложений. В экспериментах такие проблемы возникали именно во время восстановления, поэтому убедитесь, что тесты охватывают и этот этап.
Выводы
Мы разобрали возможные сбои мультикластерной конфигурации Linkerd и меры, которые помогут доработать план аварийного восстановления сервисной сетки. Эксперименты показали следующее:
При сбое сервисная сетка редко сразу перестаёт пропускать трафик. Чаще она теряет способность адаптироваться к изменениям, масштабироваться и оставаться управляемой.
Сетка добавляет новые точки отказа, которые нужно учитывать в учениях по аварийному восстановлению.
Алерты и процедуры восстановления должны учитывать топологию вашей сетки.
Начните с одного сценария отказа, например роста задержки в регионе или выхода кластера из строя, и протестируйте его от начала до конца:
Зафиксируйте исходное состояние: сервисы работают, нагрузка стабильна.
Опишите ожидаемое поведение во время инцидента. Например, из-за задержки запросы между кластерами завершаются по тайм-ауту.
Внесите сбой и убедитесь, что алерты срабатывают.
Восстановите нормальную работу и проверьте, что алерты закрываются.
Выясните, какие компоненты отказали, устраните проблемы и повторите тест.
Другой вариант — остановить часть компонентов управляющего слоя Linkerd и изменить нагрузку на приложения так, чтобы запустилось масштабирование. Проверьте, выявят ли алерты проблему до того, как запросы начнут завершаться ошибками.
Удачного хаоса!
FAQ
Что происходит с трафиком Linkerd, когда управляющий слой выходит из строя?
Трафик продолжает идти, но сервисная сетка перестаёт адаптироваться к изменениям. Если недоступен сервис destination, информация об эндпойнтах перестаёт обновляться. Если недоступен сервис identity, уже запущенные прокси обслуживают трафик до истечения срока действия сертификатов — по умолчанию до 24 часов. Новые и перезапущенные поды не могут получить сертификаты и перейти в состояние готовности.
Какой мультикластерный режим Linkerd переживает сбой региона?
В наших тестах federated-сервисы пережили потерю целого региона, поскольку логический сервис продолжал обнаруживать эндпойнты в уцелевшем кластере. Режимы pod-to-pod и gateway отказали: их эндпойнты находились в вышедшем из строя регионе.
Как HAZL ведёт себя по сравнению с topology-aware routing, когда зона начинает работать медленно?
Topology-aware routing не распознаёт замедление зоны, пока она остаётся работоспособной, и продолжает направлять в неё трафик. HAZL замечает рост нагрузки из-за задержки и перенаправляет часть трафика на эндпойнты в других зонах. Для сохранения зональной локальности трафика между кластерами HAZL сейчас требует мультикластерного режима flat (pod-to-pod).
Почему алертов на долю ошибок недостаточно для мультикластерного аварийного восстановления?
Когда сервис теряет все эндпойнты, запросы встают в очередь в прокси вместо того, чтобы завершаться ошибкой. Доля ошибок остаётся низкой, а число обрабатываемых запросов резко падает. Настройте тайм-ауты на уровне сетки, чтобы зависшие запросы завершались ошибками. Добавьте алерты на снижение числа обрабатываемых запросов относительно исходного уровня, сбои управляющего слоя, истечение срока действия сертификатов, поды, которые застряли на этапе инициализации прокси или работают без него, и неожиданный трафик без mTLS.
Как протестировать аварийное восстановление сервисной сетки в собственных кластерах?
Выберите один сценарий отказа, например выход кластера из строя, и протестируйте его от начала до конца. Зафиксируйте исходное состояние и ожидаемое поведение, внесите сбой и убедитесь, что алерты срабатывают. Затем восстановите нормальную работу и проверьте, что алерты закрываются. Chaos Mesh или LitmusChaos позволяют сделать такие тесты воспроизводимыми.
Mentions
EKS Hybrid Nodes & Capabilities による BYOI デプロイ管理 はじめに EKS Hybrid Nodes を使うと、EKS の外で自分で用意するマシンを EKS クラスタのノードとして参加させ、AWS 側のコントロールプレーンから管理できます。AWS の文書は、この考え方を bring your own infrastructure と呼んでいます。この記事でも、このように自分で用意するマシンを BYOI (bring your own infrastructure、持ち込みのインフラ) と呼びます。例えばオンプレミスのサーバーを管理下に置けます。EKS には EKS Capabilities という機能もあり、その 1 つとして Argo CD を AWS が運用する形で使えます。この記事では、AWS が運用するこの Argo CD を「Managed Argo CD」と呼びます。Hybrid ノードも EKS クラスタのノードなので、Managed Argo CD から EKS クラスタにデプロイすれば、原理的には Hybrid ノードにも届くはずです。これが実際に問題なく動くのかを、環境を作って確かめました。あわせて、AKS や GKE、自分で建てたクラスタのような、EKS ではない Kubernetes にもデプロイできるかを確かめました。 結論を先に表にします。Managed Argo CD にデプロイ先として登録できるのは EKS クラスタだけで、BYOI には、BYOI が参加した EKS クラスタへのデプロイとして届きます。自分で用意した環境は、マシンを EKS のノードとして参加させる場合と、すでに動いている Kubernetes クラスタにデプロイする場合とで、行が分かれます。 デプロイ先 Managed Argo CD からの直接デプロイ 方法 EKS クラスタ 可 クラスタの ARN (AWS のリソースを一意に指す名前) で登録 BYOI 可 EKS Hybrid Nodes で EKS クラスタのノードとして参加。今回は EC2 で確認 EKS ではない Kubernetes (AKS、GKE、自分で建てたクラスタ) 不可 EKS クラスタに置いた中継用の Argo CD を経由。今回は k3s で確認 この記事は、Argo CD と EKS の基本を知っている読者に向けて、それぞれの方法がなぜ成り立つのかと、確かめた結果を書きます。 Managed Argo CD がデプロイできる相手 まず、Managed Argo CD の部品がどこにあるかを押さえます。 Managed Argo CD の部品は、2 か所に分かれています。 実際にデプロイの仕事をするコントローラーは、利用者の EKS クラスタの中ではなく、AWS が運用する場所で動きます。利用者のノードの上には、コントローラーの Pod はありません。 何をどこにデプロイするかを書いた Application と、デプロイ先を書いた Secret である cluster secret は、今回の構成では、Managed Argo CD を作った EKS クラスタの argocd namespace に、普通の Kubernetes のオブジェクトとして置きます。 利用者は Application を自分のクラスタに kubectl apply するだけです。コントローラーはクラスタの API サーバーを通してそれを読み、Helm chart や Git のリポジトリから中身を取ってきて、デプロイ先の API サーバーにマニフェストを適用します。今回はデプロイ先も同じクラスタなので、同じ API サーバーです。Pod がどのノードに置かれるかは、クラスタのスケジューラが決めます。 次に、デプロイ先をどう指定するかです。 Managed Argo CD がデプロイできるのは EKS クラスタだけで、AWS の比較の文書にもデプロイ先は EKS クラスタの ARN だけと書かれています。理由は、Managed Argo CD がデプロイ先を見つける方法と、デプロイ先に入る方法の両方が EKS の仕組みでできているからです。上の図の上の段がその流れです。 デプロイ先は cluster secret で登録します。Managed Argo CD では、その server に EKS クラスタの ARN を書きます。Managed Argo CD を作った EKS クラスタ自身も、自動ではデプロイ先に登録されないので、同じように ARN で登録します。 AWS 側のコントローラーは、ARN でデプロイ先のクラスタを特定し、Managed Argo CD を作るときに渡した IAM ロールでそのクラスタの API サーバーに入ります。 デプロイ先の EKS クラスタでは、アクセスエントリ (IAM ロールに Kubernetes の権限を結び付ける EKS の設定) が、その IAM ロールに権限を与えます。 権限をもらった Managed Argo CD が、マニフェストを適用します。 図の下の段は、通常の Argo CD と同じように、API サーバーの URL とトークンで登録した場合です。cluster secret はエラーなく作れます。ところが、その cluster secret を使う Application は、condition に次のエラーを出して止まり、相手のクラスタには何も届きませんでした。 Failed to load live state: failed to get cluster info for "10.90.0.48": error getting cluster: error getting cluster RESTConfig: server should be specified as a cluster ARN エラーの文言からは、Managed Argo CD が server に ARN を求め、URL を受け付けずに止まっていると読めます。Managed Argo CD のデプロイ先は EKS クラスタの ARN で指定するので、EKS ではないクラスタはこの流れに乗れません。EKS ではないクラスタにデプロイする方法は、後の節で説明します。 Hybrid Nodes による BYOI の参加 BYOI を EKS クラスタのノードにすれば、Managed Argo CD は、EKS クラスタへのデプロイと同じやり方で、そのマシンにもアプリを届けられます。そのための機能が、はじめにで触れた EKS Hybrid Nodes です。コントロールプレーンは AWS 側にあり、BYOI はノードとして働くだけです。 Managed Argo CD は、AWS 側にある EKS の API サーバーにマニフェストを適用するだけで、BYOI のネットワークに入る必要はありません。Pod をどのノードに置くかは Kubernetes のスケジューラが決めます。Hybrid ノードには既定で eks.amazonaws.com/compute-type: hybrid というラベルが付くので、Pod の nodeSelector にこのラベルを指定すれば、Pod の置き場所を BYOI に限れます。ただし、今回は Hybrid ノードに taint を付けていないので、置き場所を指定しない Pod は BYOI にも置かれることがあります。クラウドで動かしたい Pod には、このラベルを避ける affinity を付けるか、Hybrid ノードに taint を付けます。今回は、後の節で説明する中継用の Argo CD を、この affinity でクラウドのノードに限りました。今回デプロイした podinfo の chart は、Helm の値の nodeSelector を Pod に渡すので、podinfo-onprem という Application の値に次の 2 行を書くだけで済みました。 nodeSelector: eks.amazonaws.com/compute-type: hybrid 次に、BYOI と AWS の間の通信です。 BYOI のマシンは、AWS 側にある EKS のコントロールプレーンと通信できなければ、ノードとして働けません。kubelet が API サーバーに接続してノードとして登録し、API サーバーもログの取得などのために kubelet に接続するからです。そのため、BYOI のネットワークとクラスタの VPC をつなぎ、BYOI のノードと Pod の IP の範囲を EKS に登録して、VPC のルートテーブルと BYOI 側のネットワークに経路を、セキュリティグループに必要なポートの許可を入れます。今回は、BYOI の Pod の IP にも VPC から届く構成にしました。必要なポートと設定の詳細は、AWS の文書にまとまっています。 出典: Amazon EKS ユーザーガイド「ハイブリッドノード用のネットワークを準備する」、CC BY-SA 4.0 クラスタの API エンドポイントは、インターネットに出さず private にしておくのが安全です。今回も API エンドポイントを private だけにし、BYOI との経路もプライベートな接続にした構成で検証し、Managed Argo CD からのデプロイまで動きました。BYOI の置き場所との VPN の代わりには、VPC 同士をプライベートにつなぐ Transit Gateway を使いました。private だけのクラスタでも、Managed Argo CD はデプロイできます。 Hybrid ノードは、Pod のネットワークを作る部品である CNI が動くまで Ready になりません。Hybrid ノードには VPC CNI を使えないので、Cilium などの CNI を自分で入れます。今回は、AWS が ECR Public に置いている Cilium の Helm chart を使いました。対応する CNI と版、OS ごとの制約は CNI のページにあります。 他社クラウドのマシンと Hybrid Nodes 他社のクラウドのマシンを Hybrid Nodes にする構成は、AWS の文書でサポートの対象外とされています。本番で使うなら、AWS のサポートを受けられないことと、障害の切り分けや運用を自分たちで統制する必要があることを受け入れたうえで使うべきです。 Running EKS Hybrid Nodes on cloud infrastructure, including AWS Regions, AWS Local Zones, AWS Outposts, or in other clouds, is not supported. You will be charged the hybrid nodes fee if you run hybrid nodes on Amazon EC2 instances. この線引きは、VM かベアメタルかでは分かれていません。同じページは、Hybrid ノードを物理マシンと仮想マシンのどちらでも動かせるとしています。文書が区別しているのは、オンプレミスか、AWS を含むクラウドかだけです。Azure や Google Cloud が提供するベアメタルのサーバーも「in other clouds」に含まれると読むのが自然で、ベアメタルならサポートされるという記述はありません。 技術的に動くかどうかについては、Hybrid ノードを参加させるツールである nodeadm のソースを見た範囲で、他社のクラウドを見分けて参加を拒否するような処理は見つかりませんでした。今回、EC2 では参加からデプロイまで動いたので、Azure や Google Cloud のマシンも、技術的にはノードとして追加できそうです。ただし、Azure と Google Cloud のマシンでは試していないので、確実に動くとは言えません。 中継用の Argo CD EKS ではない Kubernetes にデプロイする方法の 1 つとして、中継用の Argo CD を経由する構成が考えられます。今回は、この構成を実際に試しました。まず、考え方の全体を示します。 Managed Argo CD とは別に、オープンソースの通常の Argo CD を 1 つ用意し、EKS クラスタの中で動かします。AWS が運用するのではなく、利用者が自分で運用するセルフホストの Argo CD です。この記事では、これを「中継用の Argo CD」と呼びます。デプロイ先を API サーバーの URL とトークンで登録できるので、EKS ではないクラスタにもデプロイできます。 中継用の Argo CD そのものも、Helm chart として配られている普通のアプリです。そのため、中継用の Argo CD を EKS クラスタに入れる作業も、Managed Argo CD のデプロイとして行えます。 その結果、Managed Argo CD が EKS クラスタに中継用の Argo CD を入れ、中継用の Argo CD が EKS ではないクラスタにデプロイする、という 2 段の流れになります。中継用の Argo CD が何をどこにデプロイするかも Managed Argo CD の Application に書けるので、デプロイする内容を変える場所は、トークンを除いて Managed Argo CD の 1 か所にまとまります。 この構成が要るのは、EKS ではないクラスタのノードが、すでにそのクラスタのコントロールプレーンに属しているからです。そのままでは EKS の Hybrid ノードになれません。Managed Argo CD から直接デプロイしたいなら、マシンを元のクラスタから外し、Hybrid ノードとして EKS クラスタに参加させ直すことになります。元のクラスタをそのまま使う場合に、中継用の Argo CD が使えます。 ここからは、中継構成を組むときに大事なネットワークと認証認可の 2 点を説明します。 ネットワークでは、中継用の Argo CD から、EKS ではないクラスタの API サーバーに届く必要があります。中継用の Argo CD は、EKS クラスタの中から相手の API サーバーを呼んでデプロイするからです。相手の API サーバーをインターネットに開くと攻撃される入口が増えるので、BYOI と同じく、VPN などのプライベートな接続でつなぐのが安全です。今回は、k3s に Transit Gateway を通る private なアドレスでつなぎました。 認証認可では、誰がどの認証情報でどこに入るかを 3 つに分けて押さえます。 Managed Argo CD は、IAM ロールとアクセスエントリで EKS クラスタに入ります。中継用の Argo CD の chart は ClusterRole を作るので、Managed Argo CD のアクセスエントリには、ClusterRole を作れる権限が要ります。 中継用の Argo CD は、EKS クラスタの中で、自分の ServiceAccount と ClusterRole で動きます。chart の既定のままでは、この ClusterRole で EKS クラスタ全体を読み書きできます。そこで、cluster.inClusterEnabled: "false" で既定のデプロイ先である自分の EKS クラスタを外し、AppProject で Application が指定できるデプロイ先と取得元を限ります。ただし、これらは ServiceAccount の権限そのものは狭めないので、狭めるなら ClusterRole を絞ります。Managed Argo CD の画面には IAM Identity Center で入りますが、中継用の Argo CD の画面にはローカルの admin で入ります。 中継用の Argo CD は、EKS ではないクラスタに、相手の ServiceAccount のトークンで入ります。トークンは cluster secret に入れ、Git には置かずに中継用の Argo CD の namespace に直接置きます。相手のクラスタで何ができるかは、そのトークンの ServiceAccount の RBAC で決まります。今回は検証を短く済ませるため cluster-admin を与えました。実運用で namespace の中だけに絞るなら、デプロイ先の namespace を先に作り、Role と RoleBinding で権限を与え、Application の CreateNamespace=true も外します。 設定としては、Managed Argo CD に 2 つの Application を置きます。tier2-argocd は Helm chart argo-cd で中継用の Argo CD を入れ、tier2-apps は Helm chart argocd-apps で、中継用の Argo CD の AppProject と Application を作ります。具体的な値は、リポジトリの設定にあります。 版の更新では、中継用の Argo CD のマイナー版を Managed Argo CD にそろえます。2 つの Argo CD は、同じクラスタの CRD を共有するからです。その CRD は Managed Argo CD を作るときに EKS が入れ、版の更新も AWS が行うので、中継用の Argo CD の chart は crds.install: false で CRD を入れません。aws eks describe-capability で Managed Argo CD の版を定期的に確かめ、マイナー版が上がったら、中継用の Argo CD をあとから同じマイナー版に上げます。Helm chart argo-cd の版は Argo CD の版と別なので、chart の appVersion を見て選びます。中継用の Argo CD を先に上げると、古い CRD の上で新しいコントローラーが動くことになります。マイナー版をそろえても互換性が保証されるわけではないので、上げる前に Argo CD のリリースノートで CRD の変更を確かめます。 まとめ EKS Capabilities の Managed Argo CD は EKS クラスタにしかデプロイできませんが、BYOI を Hybrid Nodes で EKS クラスタのノードにすれば、BYOI にも直接デプロイできます。今回はこれを、AWS のサポート対象外である EC2 を BYOI に見立てて確かめました。EKS ではない Kubernetes には、Managed Argo CD が入れた中継用の Argo CD を経由してデプロイできることを k3s で確かめ、デプロイする内容は、トークンを含む cluster secret を除いて、Managed Argo CD の Application だけで変えられました。構築、検証、削除のコードと結果のログは、littlemex/distributed-ai の investigations にあります。
Kubernetes как ядро Linux, часть 2: kubepkg, универсальный пакетный менеджер а ля Debian У Linux есть apt, dnf и pacman, и никто уже не помнит, как жили без них. У Kubernetes за десять лет так и не появилось ничего подобного. Есть Helm, который прекрасно ставит один чарт, есть Argo CD и Flux, которые умело синкают кластер с Git, но нет инструмента, который знал бы, что cert-manager нужен половине других компонентов, что KubeVirt нельзя откатить на прошлую версию после обновления и что CRD, поставленные одним релизом, не получится молча снести вместе с ним. Каждый, кто собирает платформу на Kubernetes, пишет эту прослойку сам, и каждый раз заново. Проект, о котором я бы хотел рассказать сегодня, называется kubepkg, он работает на любом кластере, ставит обычные Helm-чарты и функционирует по модели Debian, с репозиторием пакетов, подписанным индексом и сборкой из исходников апстрима. В первой части своего цикла статей я рассказывал, зачем всё это затеял, почему считаю важным начать относиться к K8s как к ядру Linux и как устроен мой тулкит для построения дистрибутивов Kubernetes. В ней я немного затрагивал концепцию универсального менеджера пакетов для K8s, который легко можно встроить в любую платформу или добавить в кластер с ванильной версией оркестратора. Эта статья о том, как kubepkg устроен изнутри и что он умеет. Все примеры с живого кластера внутри тенанта Cozystack. Снова отмечу, что это мой личный проект и все ошибки сделаны мною. Инженеры Aenix никогда бы так плохо не сделали:) Откуда взялся kubepkg История kubepkg Как всё началось, я подробно рассказал в первой части, поэтому здесь будет только суть. Мэтт Дагган в ответ на мою давнюю статью написал, что главная беда Kubernetes в отсутствии настоящего пакетного менеджера, перечислил основные его характеристики, а позднее даже придумал для него рабочее название KubePkg. И совпадение с названием моего проекта не случайное. Сам код я писал не с нуля (да и вообще не то чтобы писал код сам), за основу решил взять cozypkg, тулзу из проекта Cozystack: когда платформа переходила на пакетную архитектуру, понадобилось ставить и обновлять десятки компонентов в правильном порядке и с понятной историей. cozypkg хорошо решал задачу одной платформы и был намертво к ней привязан, так что в начале октября я взял часть кода этого компонента и построил на нём самостоятельный проект, который не зависит ни от какой платформы и может быть использован как в ванильном куб-кластере, так и интегрироваться в любую куб-платформу. Сейчас я буду описывать то, что свойственно версии 1.3, которая уже обладает стабильным API и развивается в рамках GitHub-организации kuberoot-dev, рядом со сборщиком дистрибутивов kuberoot, для которого он и делался. Чего не хватает Helm Helm отлично решает задачу шаблонизации и установки одного релиза. Проблемы начинаются, когда релизов становится много и они зависят друг от друга. Helm не разрешает зависимости между релизами: если вашему приложению нужен cert-manager, Helm об этом не узнает, пока под не упадёт с ошибкой про отсутствующий CRD. Он не знает, какой компонент какой API предоставляет, поэтому не может сказать, что в кластере уже установлен Prometheus Operator, просто впихнул его туда кто-то другой. Он не показывает заранее, что сделает обновление. И у него нет какого-то своего мнения о CRD, кроме общего понимания «поставить один раз и больше не трогать», хотя именно CRD в Kubernetes самая опасная вещь: они общие для всего кластера, переживают поставивший их релиз, а удаление CRD удаляет все объекты этого типа. Каждая платформа закрывает эти дыры по-своему. У Deckhouse есть модули, у OpenShift есть OLM, у Rancher свой каталог, и всё это нельзя взять и использовать в другом месте. kubepkg задуман как пакетный слой именно для тех, кто строит платформы и дистрибутивы. Он не заменяет ни Helm, ни Argo CD, ни Flux, а работает поверх них и вместе с ними. Почему apt для Kubernetes до сих пор не случился Идея не нова, и я далеко не первый, кому она пришла в голову. Сам Helm появился в 2015 году под девизом «пакетный менеджер для Kubernetes», и формально он им и является, ведь у него есть пакеты, репозитории и версии. Но apt ценен не форматом архива, а знанием о том, как пакеты живут вместе, и этого слоя Helm так и не получил. За десять лет его много раз пытались доработать или заменить, и почти все попытки закончились одинаково. CoreOS ещё в 2016 году сделала KPM, свой формат пакетов со своим реестром, из которого потом выросли App Registry и раздел Applications в Quay. После того как CoreOS купил Red Hat, KPM развивать перестали, а в 2022 году quay.io убрал и сами Applications. В 2018 году Microsoft, Docker и ещё несколько компаний предложили CNAB, универсальный формат бандла для любых облачных приложений, но его эталонная реализация Duffle давно лежит в архиве, а из живых осталась одна Porter, о которой мало кто слышал. В D2iQ придумали KUDO, декларативный способ описывать операторы со своим форматом пакетов, и последний его релиз вышел весной 2021 года. Glasskube в 2024 году громко назвал себя пакетным менеджером нового поколения для Kubernetes и собрал несколько тысяч звёзд на GitHub, но команда переключилась на другой продукт, и репозиторий теперь в архиве. Даже Kubeapps, который вообще не придумывал своего формата и был просто удобным каталогом поверх Helm-чартов, в августе 2025 года ушёл в архив, и поддерживает его теперь форк SAP. Выжили те, у кого за спиной стоит большая платформа. OLM прекрасно работает в OpenShift, а kapp-controller из Carvel, модель пакетов и репозиториев которого очень похожа на то, что хотелось реализовать мне, вырос вместе с Tanzu. Но оба так и остались частью экосистем своих вендоров, и за их пределами ими почти никто не пользуется. Причина всегда одна, и она не техническая. Новый формат пакета требует, чтобы кто-то упаковал в него софт. Авторы cert-manager, Cilium или KubeVirt не станут поддерживать ещё один формат ради инструмента, которым пока никто не пользуется, а пользователи не станут использовать инструмент, в котором нет нужных им пакетов. Поэтому такой проект живёт ровно столько, сколько его команда успевает сама переупаковывать чужие релизы, а когда команда устаёт или у компании меняются планы, всё заканчивается. Helm-чарт тем временем стал фактическим стандартом поставки: почти каждый серьёзный проект для Kubernetes публикует его сам, в каждом релизе, и на Artifact Hub таких чартов тысячи. Отсюда главное решение по kubepkg. Пакет это неизменённый Helm-чарт, а всё, чего Helm не знает, лежит рядом с ним в отдельном описании. Я никого не прошу ничего переупаковывать, рецепт берёт релиз апстрима как есть, а если апстрим чарт не публикует, как KubeVirt, рецепт собирает его из релизных манифестов, и на выходе всё равно получается обычный чарт. Собранный пакет лежит в OCI-реестре и ставится без kubepkg, хоть самим Helm, хоть через Flux или Argo CD, так что, даже если kubepkg завтра исчезнет, пакеты останутся рабочими. Цена этого решения в том, что мы ограничены возможностями Helm там, где их не хватает, и всё недостающее, от порядка установки до готовности компонентов и владения CRD, приходится делать снаружи чарта, а не внутри него. Как именно, я покажу дальше. Что такое пакет Из чего состоит пакет Что входит в описание рядом с чартом, видно на схеме, а интереснее посмотреть, как это работает. Пакет может состоять из нескольких компонентов, каждый из которых это отдельный чарт и отдельный релиз в своём пространстве имён. Хороший пример KubeVirt: сначала ставится оператор, а потом ресурс KubeVirt, который говорит оператору развернуть виртуализацию. Второй компонент не имеет смысла, пока не готов первый, и kubepkg ставит их по очереди. Зависимости бывают двух видов. Можно потребовать другой пакет с ограничением версии, например cert-manager не ниже 1.20 и не выше второй мажорной. А можно потребовать возможность, например API monitoring.coreos.com. Возможность удовлетворяется либо пакетом, который её предоставляет, либо самим кластером, если такой API в нём уже обслуживается. Это значит, что Prometheus Operator, поставленный вручную три года назад, тоже учитывается, и kubepkg не будет требовать поставить его второй раз. Готовые пакеты остаются обычными Helm-чартами в OCI-реестре. Если завтра вы решите, что kubepkg вам не нужен, эти чарты поставит сам Helm, Flux, Argo CD или werf (Алексей, Илья, Иван, Дмитрий, вам большой привет!). Как это выглядит в кластере Объекты kubepkg в кластере Отдельной базы пакетов у kubepkg нет, всё его состояние живёт в обычных ресурсах кластера, и их всего четыре. Repository это подписка на репозиторий пакетов, адрес индекса и ключи, которым кластер доверяет. Package описывает то, что вы хотите: имя пакета, ограничение версии, ваши значения для чартов. PackageSource это конкретная версия пакета, которую оператор выбрал из репозиториев. А PackageRevision это неизменяемая запись о том, что именно было применено, вплоть до хеша каждого чарта и каждого набора значений. Вы меняете только Package, всё остальное делает оператор. Он выбирает версию, проверяет требования, конфликты и владение CRD, и если желаемое состояние отличается от текущей ревизии, записывает новую ревизию и применяет компоненты в порядке зависимостей. Применять их он может сам, через Helm SDK, а может отдать эту работу Nelm из werf, Flux или Argo CD. Первое знакомство Оператор ставится обычным Helm-чартом, а дальше кластер подписывается на репозиторий. Публичный репозиторий проекта собирается из kubepkg-recipes, в нём сейчас 49 пакетов, от cert-manager и Cilium до vLLM и Milvus. helm install kubepkg oci://ghcr.io/kuberoot-dev/charts/kubepkg \ --namespace kubepkg-system --create-namespace --wait curl -fsSLO raw.githubusercontent.com kubepkg repo add main kuberoot-dev.github.io --public-key index.pub sourcecraft.dev С этого момента оператор принимает индекс репозитория только тогда, когда он подписан этим ключом. Прежде чем что-то ставить, стоит посмотреть, что произойдёт. Команда kubepkg plan разрешает пакет и всё, что ему нужно, относительно текущего состояния кластера и ничего не меняет: kubepkg plan Здесь virtualization это метапакет. Сам он ничего не ставит, а только требует KubeVirt и CDI в совместимых версиях. План показывает, откуда взялся каждый пакет, какими CRD он будет владеть и можно ли его откатить. В CI ту же команду удобно запускать на каждый пул-реквест, чтобы ревьюер видел последствия изменения, а не только diff в YAML. Установка показывает тот же план и пишет в кластер по объекту Package на каждый пакет, остальное делает оператор: kubepkg install Каждое изменение пакета, будь то новая версия или другие значения, создаёт новую ревизию. Старые никуда не деваются, и к любой из них можно вернуться. Откат тоже создаёт новую ревизию с содержимым старой, история только растёт и никогда не переписывается: kubepkg rollback Репозиторий пакетов и сборка из исходников Как собирается репозиторий Главная идея, которую я взял у Debian, в том, что пакеты репозитория собирает мейнтейнер этого самого репозитория, а не автор софта. Debian собирает свои deb-файлы из исходников апстрима, и kubepkg делает так же. Неважно, что выпустил апстрим: готовый Helm-чарт, набор манифестов или архив исходников, внутри которого где-то там лежит чарт. Мейнтейнер пишет рецепт, в котором каждый источник закреплён по хешу, а дальше сказано, как превратить его в чарт: какие значения подставить, какие патчи наложить, какие объекты выбросить. Рецепт для нового пакета не нужно писать с нуля: команда kubepkg init скачивает апстрим, закрепляет его, перечисляет CRD и образы и пишет черновик, который остаётся доработать. Как это выглядит, покажу чуть ниже на живом примере. Вот часть настоящего рецепта KubeVirt. Он собирается из релизных манифестов, а не из стороннего чарта, и патч убирает жёсткое требование virt-operator к узлам control plane, которых нет в управляемых кластерах: spec: version: 1.9.0 # версия апстрима build: 4 # наша четвёртая сборка этой версии sources: operator: url: github.com/kubevirt/kubevirt/releases/download/v1… sha256: f11307caafc3c23ffedf9887d8beb5a4419e2694da242fa68f63d1ec820de2e0 charts: kubevirt-operator: from: [operator] # обычные манифесты заворачиваются в чарт exclude: [{kind: Namespace}] patches: - kind: Deployment name: virt-operator merge: {...} sourcecraft.dev Команда kubepkg build превращает рецепт в чарт в реестре, а kubepkg repo index собирает все версии всех пакетов в один индекс и подписывает его. Один и тот же рецепт всегда собирается в одно и то же содержимое, вплоть до байта. Установка в кластер к апстриму больше никогда не ходит: всё, что нужно, лежит в вашем реестре и закреплено по хешу в подписанном индексе. Опубликованная версия не меняется никогда, а исправление публикуется как новая сборка той же версии. Индекс это один статический файл, его можно положить на GitHub Pages, в S3 или на nginx. Публичный репозиторий так и устроен: каждый мерж в main собирает пакеты, кладёт чарты в GitHub Container Registry и публикует подписанный индекс на Pages. Пример: от черновика до установки Посмотрим весь путь на настоящем кластере. Возьмём podinfo, маленькое демонстрационное приложение, у которого есть официальный Helm-чарт, и опубликуем его в собственном репозитории. Реестр и веб-сервер для индекса здесь подняты прямо внутри кластера, но с GitHub Container Registry и Pages всё выглядит точно так же. Начинается всё с черновика рецепта. kubepkg скачал чарт, закрепил его по хешу и, что важно, сам отрендерил его и выписал все образы, которые он запускает, тоже с хешами: $ kubepkg init podinfo --chart oci://ghcr.io/stefanprodan/charts/podinfo@6.9.2 wrote podinfo/recipe.yaml; review it, then run: kubepkg validate podinfo $ cat podinfo/recipe.yaml apiVersion: kubepkg.dev/v1 kind: Recipe metadata: name: podinfo annotations: kubepkg.dev/description: "Podinfo Helm chart for Kubernetes" kubepkg.dev/home: stefanprodan/podinfo spec: version: 6.9.2 build: 1 sources: upstream: chart: repository: oci://ghcr.io/stefanprodan/charts name: podinfo version: "6.9.2" digest: sha256:8eacab4365d917867e855bfb4729afdbff28bead3fd9dc01923ab50b26aa4380 charts: podinfo: from: [upstream] values: {} # the package's defaults over the upstream ones package: provides: [podinfo] crds: [] images: # what the charts run, pinned; add images an operator deploys on its own - docker.io/curlimages/curl:7.69.0@sha256:d845b6dd655da0e9ae0623065a35185f9b4438aa717fd1c9a3b89843e8b4ff26 - docker.io/giantswarm/tiny-tools:latest@sha256:8ccea5962d6e24cf90ac726ff8af6ad25ba8dc6a691ad47cb455e3dfcd3f4c22 - docker.io/stefanprodan/grpc_health_probe:v0.3.0@sha256:0d7bd9c77ef35c3f0432142b5689ae460331a4fecfa469590fe11f3d575669e4 - ghcr.io/stefanprodan/podinfo:6.9.2@sha256:cab90e04829cdae73199a8402882a4e3ccaf251b8a0d50bdefede0ab3f6371e7 rollback: safe: false # true once rolling back to the previous version is known not to lose data variants: - name: default components: - name: podinfo path: podinfo install: {namespace: podinfo} sourcecraft.dev Черновик это только начало, решения в нём принимает мейнтейнер. Здесь я поставил две реплики по умолчанию и объявил откат безопасным, потому что podinfo ничего не хранит и предыдущая сборка всегда сможет запуститься снова. Проверка, сборка и публикация умещаются в три команды: $ grep -n -E 'values:|safe:' podinfo/recipe.yaml 21: values: {replicaCount: 2} 31: safe: true # stateless: the previous build can always run again $ kubepkg validate podinfo podinfo: ok $ kubepkg build podinfo --registry oci://demo.demo.svc:5000/demo --plain-http -o dist published oci://demo.demo.svc:5000/demo/podinfo, wrote dist/podinfo-6.9.2-1.yaml $ kubepkg repo keygen demo wrote demo.key (private) and demo.pub $ kubepkg repo index dist --plain-http --sign-key demo.key -o /site/demo/index.yaml signed /site/demo/index.yaml.sig wrote /site/demo/index.yaml: 1 packages, 1 versions $ ls /site/demo index.yaml index.yaml.sig sourcecraft.dev Теперь в реестре лежит чарт, а рядом индекс с подписью. Осталось подписать на него кластер и поставить пакет: $ kubepkg repo add demo demo.demo.svc --public-key demo.pub repository demo added $ kubepkg search podinfo NAME LATEST REPOSITORY DESCRIPTION podinfo 6.9.2 demo Podinfo Helm chart for Kubernetes $ kubepkg plan podinfo ACTION PACKAGE FROM TO REPOSITORY WHY install podinfo - 6.9.2 build 1 demo requested $ kubepkg install podinfo --yes ACTION PACKAGE FROM TO REPOSITORY WHY install podinfo - 6.9.2 build 1 demo requested 1 packages requested; follow them with "list" $ kubepkg list NAME VERSION REVISION READY REASON cert-manager 1.21.2 2 True ReconciliationSucceeded kube-state-metrics 2.20.0 1 True ReconciliationSucceeded podinfo 6.9.2 1 True ReconciliationSucceeded $ kubectl -n podinfo get deploy,pods NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/podinfo 2/2 2 2 10s NAME READY STATUS RESTARTS AGE pod/podinfo-c4fdfd856-7sd7c 1/1 Running 0 10s pod/podinfo-c4fdfd856-f628c 1/1 Running 0 10s sourcecraft.dev От ссылки на чужой чарт до работающего пакета в собственном подписанном репозитории ушло десять команд, и ни в одной из них не нужно было писать шаблоны. Почему откатывается не всё Почему откат снимками состояния кластера не работает, я писал в первой части: снимок объектов не откатывает данные. Если новая версия KubeVirt уже перевела хранимые объекты на новую версию API, возврат старых манифестов не вернёт старые данные, а скорее сломает то, что ещё работало. Вот как это решено на практике. Решение об откате Безопасность отката объявляет автор новой версии пакета, потому что только он знает, что делают её миграции. Если обновление не удалось, а версия, с которой перекатываются, объявила откат безопасным, все уже изменённые компоненты возвращаются к прошлой ревизии в обратном порядке. Если не объявила, kubepkg останавливается и честно сообщает, что обновление не удалось и дальше возможно только исправление вперёд. Откат, который портит данные, хуже неудачного обновления. Для случаев, когда перед обновлением нужно что-то сделать, например снять резервную копию или прогнать миграцию, у пакета есть хуки, которые выполняются до обновления и получают версии «откуда» и «куда». Как это выглядит вживую Вернёмся к podinfo, который мы только что опубликовали. Его сборка 1 объявила откат безопасным. Чтобы не ждать долго, разрешим пакету становиться готовым не дольше двух минут, а репозиторий будем перечитывать каждые полминуты. Изменение пакета тоже записывается ревизией, поэтому у podinfo появится ревизия 2: $ kubectl patch package podinfo --type merge -p '{"spec":{"upgrade":{"timeout":"2m"}}}' package.kubepkg.dev/podinfo patched $ kubectl patch repository demo --type merge -p '{"spec":{"interval":"30s"}}' repository.kubepkg.dev/demo patched sourcecraft.dev Теперь представим, что мейнтейнер выпустил неудачную сборку. В ней podinfo запускается, но никогда не становится готовым, то есть ведёт себя как приложение, которое после обновления не может подключиться к базе или не проходит собственные проверки: $ grep -n -E 'build:|values:' podinfo/recipe.yaml 10: build: 2 21: values: {replicaCount: 2, faults: {unready: true}} $ kubepkg build podinfo --registry oci://demo.demo.svc:5000/demo --plain-http -o dist published oci://demo.demo.svc:5000/demo/podinfo, wrote dist/podinfo-6.9.2-2.yaml $ kubepkg repo index dist --plain-http --merge /site/demo/index.yaml --sign-key demo.key -o /site/demo/index.yaml signed /site/demo/index.yaml.sig wrote /site/demo/index.yaml: 1 packages, 2 versions sourcecraft.dev Через полминуты оператор видит в индексе новую сборку и начинает обновление. Новые поды стоят неготовыми, а один старый ещё работает и обслуживает запросы: $ kubepkg list NAME VERSION REVISION READY REASON cdi 1.66.1 5 True ReconciliationSucceeded cert-manager 1.21.2 5 True ReconciliationSucceeded kubevirt 1.9.0 4 True ReconciliationSucceeded podinfo 6.9.2 2 False Progressing virtualization 1.0.0 1 True ReconciliationSucceeded $ kubectl -n podinfo get pods NAME READY STATUS RESTARTS AGE podinfo-84df66bbb8-bwpkg 0/1 Running 0 7s podinfo-84df66bbb8-vlcwb 0/1 Running 0 7s podinfo-c4fdfd856-kjts4 1/1 Running 0 116s sourcecraft.dev Две минуты спустя срок истекает. Сборка, с которой мы переходим, объявила откат безопасным, поэтому kubepkg возвращает её сам, не дожидаясь человека. Ревизия 3 помечена как неудачная с причиной, а ревизия 4 восстанавливает содержимое ревизии 2: $ kubepkg history podinfo REVISION VERSION PHASE CREATED NOTE 1 6.9.2 Superseded 2026-10-18 08:33:16 2 6.9.2 Superseded 2026-10-18 08:34:34 3 6.9.2 Failed 2026-10-18 08:35:04 component podinfo failed: upgrade podinfo/podinfo: resource Deployment/podinfo/podinfo not ready. status: InProgress, message: Pending termination: 1 context deadline exceeded 4 6.9.2 Applied 2026-10-18 08:37:08 restored from 2 $ kubectl get package podinfo -o jsonpath='{range .status.conditions[*]}{.type}={.status} {.reason}: {.message}{"\n"}{end}' Ready=False UpgradeRolledBack: revision 3 failed (component podinfo failed: upgrade podinfo/podinfo: resource Deployment/podinfo/podinfo not ready. status: InProgress, message: Pending termination: 1 context deadline exceeded), restored revision 2; failed 1 time(s), trying again at 2026-10-18T08:37:35Z, or set kubepkg.dev/retry to try now $ kubectl get package podinfo -o jsonpath='{.status.retry}{"\n"}' {"attempts":1,"nextAttempt":"2026-10-18T08:37:35Z"} $ kubectl -n podinfo get pods NAME READY STATUS RESTARTS AGE podinfo-c4fdfd856-kdllf 1/1 Running 0 10s podinfo-c4fdfd856-kjts4 1/1 Running 0 3m59s sourcecraft.dev Пакет остаётся неготовым, и это сделано намеренно. Он работает на прошлой сборке, но желаемое состояние, новейшая сборка из репозитория, не достигнуто, и об этом должен узнать человек, а не только лог оператора. При этом kubepkg не бросает попыток и сам повторяет то же обновление, сначала через 30 секунд, а потом каждый раз с вдвое большей паузой, вплоть до 10 минут, и пишет в status.retry, сколько попыток уже было и когда будет следующая. Так кластер, за которым никто не смотрит, сам выбирается из временного сбоя или подхватывает исправленную сборку 3, как только мейнтейнер её выпустит. Изменение пакета или аннотация kubepkg.dev применяются сразу, а откат, который человек сделал сам через kubepkg rollback, оператор повторять не станет. Вот как это выглядит через пару минут, когда исправленной сборки ещё нет. Оператор дважды попробовал снова, оба раза получил ту же ошибку, оба раза вернул рабочую версию и теперь ждёт минуту до следующей попытки: $ kubepkg history podinfo REVISION VERSION PHASE CREATED NOTE 1 6.9.2 Superseded 2026-10-18 08:33:16 2 6.9.2 Superseded 2026-10-18 08:34:34 3 6.9.2 Failed 2026-10-18 08:35:04 component podinfo failed: upgrade podinfo/podinfo: resource Deployment/podinfo/podinfo not ready. status: InProgress, message: Pending termination: 1 context deadline exceeded 4 6.9.2 Superseded 2026-10-18 08:37:08 restored from 2 5 6.9.2 Failed 2026-10-18 08:37:35 component podinfo failed: upgrade podinfo/podinfo: resource Deployment/podinfo/podinfo not ready. status: InProgress, message: Pending termination: 1 context deadline exceeded 6 6.9.2 Applied 2026-10-18 08:39:38 restored from 4 $ kubectl get package podinfo -o jsonpath='{.status.retry}{"\n"}' {"attempts":2,"nextAttempt":"2026-10-18T08:40:35Z"} sourcecraft.dev Отдельная история это готовность. Helm считает релиз готовым, когда готовы созданные им Deployment и StatefulSet. Чарт KubeVirt создаёт один ресурс KubeVirt и сразу возвращается, а оператор потом ещё несколько минут разворачивает настоящую виртуализацию. Пакет может сказать, при каком условии компонент действительно готов, например когда у ресурса KubeVirt появилось условие Available, и пока оно не выполнено, следующий компонент не начинается, а пакеты, которые от него зависят, ждут. CRD как часть пакета CRD в Kubernetes устроены неудобно для любого пакетного менеджера. Они общие для всего кластера, не может быть две их версии рядом, они переживают релиз, в рамках которого они были поставлены, а удаление CRD удаляет все объекты этого типа во всех пространствах имён. Helm решает это радикально: CRD из каталога crds ставятся один раз и никогда не обновляются. В kubepkg владение CRD выведено в отдельную сущность. Пакет объявляет, какими CRD он владеет, и план показывает это до установки. Два пакета не могут владеть одним CRD, а при попытке сделать это kubepkg откажется ставить второй CRD. CRD можно передать от одного пакета другому, например когда компонент переезжает из одного пакета в другой. При удалении пакета его CRD по умолчанию остаются в кластере вместе с объектами, и чтобы удалить их, это нужно явно попросить. И отдельно kubepkg следит, чтобы Helm молча не удалил CRD, которые пропали из новой версии чарта. Доверие Цепочка доверия Каждый репозиторий приходит со своими ключами, и кластер сам решает, кому верить. Неподписанный репозиторий он примет только тогда, когда это явно разрешено. Простой репозиторий подписывается одним ключом. Для серьёзного репозитория есть корень доверия по схеме TUF, то есть подписанный файл, в котором указано, каким ключам кластер верит и что каждый из них может подписывать. Корневых ключей несколько, они хранятся у разных людей вне сети, и для подписи нужна заданная часть из них, например любые два из трёх, так что один украденный или потерянный ключ ничего не решает. Сам индекс подписывает отдельный ключ, который живёт в CI, и если его скомпрометируют, корневые ключи просто заменят его на новый, не трогая кластеры. У подписи индекса есть срок действия, и это защищает кластеры от того, что кто-то подсунет им старый индекс со старыми уязвимыми версиями. Наконец, в корне доверия можно поручить часть пакетов другой команде, и тогда подпись этой команды будет действительна только для её пакетов. Так устроено доверие у крупных архивов, где разные пакеты поддерживают разные люди. Корень доверия из двух ключей Поскольку индекс закрепляет по хешу всё, включая образы, его подпись покрывает всё, что репозиторий ставит в кластер. Это даёт ещё три возможности. Команда kubepkg sbom пишет перечень компонентов (SBOM) в формате CycloneDX, для пакета или для всего кластера. Команда kubepkg scan прогоняет Trivy по каждому образу, который закрепили пакеты, и может завалить CI, если находит критические уязвимости. А политика образов заставляет кластер следить, чтобы в пространствах имён пакетов запускалось ровно то, что закрепил индекс. Последнее стоит пояснить. Чарты почти всегда ссылаются на образы по тегу, а тег в чужом реестре может в любой момент начать указывать на другой образ. Admission webhook в kubepkg дописывает к тегу закреплённый хеш, так что узлы скачивают ровно тот образ, за который ручается индекс. В строгом режиме образ, который не закрепил ни один пакет, просто не допускается. No pasaran! Error from server: admission webhook "pods.images.kubepkg.dev" denied the request: no installed package pins docker.io/library/busybox:1.36; namespace cert-manager runs only images packages pin (kubepkg.dev/image-policy=enforce) sourcecraft.dev Сертификат вебхука оператор выпускает и обновляет сам. Это сделано намеренно: kubepkg не может зависеть от cert-manager, потому что вполне может оказаться тем, кто cert-manager ставит. Без интернета Установка в закрытом контуре Многие кластеры вообще не знат, что такое интернет: госсектор, банки, промышленные площадки, суверенные облака. Для них kubepkg собирает бандл, один файл, в котором лежат индекс репозитория с подписями, чарты выбранных пакетов и всех их зависимостей и все образы, которые эти пакеты запускают. Ниже показан настоящий прогон на тестовом кластере. Оператор kubepkg в нём отрезан от внешнего мира сетевой политикой Cilium и видит только сам кластер, а внутри кластера подняты реестр-зеркало и веб-сервер для индекса. Бандл собирается на машине с интернетом из публичного репозитория: $ kubepkg bundle create cert-manager kube-state-metrics --repo kuberoot-dev.github.io --public-key index.pub -o platform-2026-10.tar wrote platform-2026-10.tar: 2 packages, 2 charts, 0 package trees, 5 images $ ls -lh platform-2026-10.tar -rw-r--r--. 1 root root 377M Oct 12 03:30 platform-2026-10.tar $ kubepkg bundle inspect platform-2026-10.tar created 2026-10-12T03:30:25Z PACKAGE VERSION REPOSITORY cert-manager 1.21.2 build 3 main kube-state-metrics 2.20.0 build 3 main IMAGE PINNED BY PACKAGES quay.io/jetstack/cert-manager-cainjector:v1.21.2@sha256:c85268c64f2e0e76684bf5fe8906caff34b82523561c6affe0fae3546bd87562 repository cert-manager quay.io/jetstack/cert-manager-controller:v1.21.2@sha256:70f532fd9cfde0b09d55687200942399d89838bc2d5d5b45152eb799a15912b8 repository cert-manager quay.io/jetstack/cert-manager-startupapicheck:v1.21.2@sha256:46e75b6866359ffb5d82624f41e3ed1c70b2994982702ced547ce5edb418a8f5 repository cert-manager quay.io/jetstack/cert-manager-webhook:v1.21.2@sha256:a60e2dac46dbb8a7f3df95c54ce941012f54c2fe022f0ee55aaa1ab40ed957ae repository cert-manager registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.20.0@sha256:42cfe3723a5f058171c627537fb57a3ea0f26e4380fa18555a95cb1a1b4cfc5b repository kube-state-metrics 2 charts, 0 package trees sourcecraft.dev Каждый образ в списке закреплён по хешу, и закрепил его репозиторий, то есть подписанный индекс, а не сам бандл. Это важно для того, что происходит дальше. Самое интересное начинается внутри контура. Бандлу не доверяют только потому, что он такой красивый сам к вам пришёл. Для начала подсунем сюда бандл, в котором по пути испортили один чарт, дописав в его архив всего один байт: $ kubepkg bundle import tampered.tar --public-key index.pub --mirror oci://demo.demo.svc:5000/mirror --plain-http --site /site error: the bundle does not check out: chart kube-state-metrics 2.20.0-3 in the bundle has digest sha256:ff78152558d651ae9564bdb4a2ec705d7e234be872e83753398596ff79fda8df, the package pins sha256:2bf8279f5f6195dfb6332dfb8c1251bdcf5a00754dd97102411a614f685d329e sourcecraft.dev Подмена найдена, и в ошибке видны оба хеша, тот, что получился, и тот, что закрепил подписанный пакет. Теперь возьмём подлинный бандл, но проверим его не тем ключом: $ kubepkg bundle import platform-2026-10.tar --public-key other.pub --mirror oci://demo.demo.svc:5000/mirror --plain-http --site /site error: the bundle does not check out: repository main: index is not signed by a trusted key sourcecraft.dev Ключи, которым вы доверяете, kubepkg берёт только из командной строки и никогда из самого бандла, поэтому их нужно переносить через контур отдельно, например вместе с конфигурацией. С правильным ключом импорт раскладывает всё по местам и заодно пишет настройки для узлов: $ kubepkg bundle import platform-2026-10.tar --public-key index.pub --mirror oci://demo.demo.svc:5000/mirror --plain-http --site /site --node-config node-config verified and imported 2 packages: 2 charts, 0 package trees, 5 images into oci://demo.demo.svc:5000/mirror repository files: /site/main/; serve them over HTTP and point the cluster's repositories there node settings for quay.io, registry.k8s.io: node-config operator: helm upgrade kubepkg ... --set mirror=oci://demo.demo.svc:5000/mirror $ find /site node-config -type f /site/sbom.cdx.json /site/main/index.yaml /site/main/index.yaml.sig node-config/containerd/certs.d/quay.io/hosts.toml node-config/containerd/certs.d/registry.k8s.io/hosts.toml node-config/talos-registries.yaml $ cat node-config/containerd/certs.d/quay.io/hosts.toml server = "quay.io" [host."demo.demo.svc"] capabilities = ["pull", "resolve"] override_path = true sourcecraft.dev Узлы тянут образы через среду выполнения контейнеров, а не через kubepkg, поэтому вместе с зеркалом нужны и эти настройки: containerd по ним идёт за образами quay.io в зеркало, а для Talos есть готовый патч конфигурации машины. Чарты при этом продолжают ссылаться на quay.io, хеши не меняются, и под получает ровно тот образ, который закрепил индекс. В этом прогоне узлы кластера интернет видели, отрезан был только оператор, так что эту часть я не проверял, а только показываю. Остаётся переключить оператор на зеркало и на индекс с внутреннего веб-сервера: helm upgrade kubepkg oci://ghcr.io/kuberoot-dev/charts/kubepkg -n kubepkg-system --reuse-values \ --set mirror=oci://demo.demo.svc:5000/mirror \ --set 'repositories[0].name=main' \ --set 'repositories[0].url=demo.demo.svc' \ --set-file 'repositories[0].publicKeys[0]=index.pub' sourcecraft.dev Убедимся, что он действительно никуда не может выйти: публичный репозиторий ему недоступен. $ kubepkg repo list NAME PRIORITY PACKAGES READY URL main 0 49 True demo.demo.svc $ kubepkg repo add public kuberoot-dev.github.io --public-key index.pub --priority -1 repository public added $ kubepkg repo list NAME PRIORITY PACKAGES READY URL main 0 49 True demo.demo.svc public -1 0 False (FetchFailed) kuberoot-dev.github.io $ kubepkg repo remove public repository public removed sourcecraft.dev Репозиторий main теперь читается с внутреннего веб-сервера и принят, а публичный остаётся недоступен. Поставим пакет и посмотрим, откуда он взялся: $ kubepkg install kube-state-metrics --yes ACTION PACKAGE FROM TO REPOSITORY WHY install kube-state-metrics - 2.20.0 build 3 main requested 1 packages requested; follow them with "list" $ kubepkg list NAME VERSION REVISION READY REASON cert-manager 1.21.2 2 True ReconciliationSucceeded kube-state-metrics 2.20.0 1 True ReconciliationSucceeded $ python3 -c 'import json,urllib.request; [print(r) for r in json.load(urllib.request.urlopen("demo.demo.svc\_catalog"))["repositories"]]' mirror/ghcr.io/kuberoot-dev/kubepkg-packages/cert-manager/cert-manager mirror/ghcr.io/kuberoot-dev/kubepkg-packages/kube-state-metrics/kube-state-metrics mirror/quay.io/jetstack/cert-manager-cainjector mirror/quay.io/jetstack/cert-manager-controller mirror/quay.io/jetstack/cert-manager-startupapicheck mirror/quay.io/jetstack/cert-manager-webhook mirror/registry.k8s.io/kube-state-metrics/kube-state-metrics sourcecraft.dev cert-manager, который уже стоял в кластере, при этом тоже перешёл на свежую сборку из зеркала, получив ревизию 2. Опубликованные версии в зеркале лежат под путями своих исходных реестров, поэтому описания пакетов не меняются, и включение зеркала само по себе не создаёт новых ревизий. Взять под управление то, что уже стоит В реальных кластерах уже что-то стоит. cert-manager, поставленный через helm install два года назад, оператор, применённый через kubectl apply, стек мониторинга, развёрнутый скриптом вендора. Ничего этого не хочется переустанавливать, особенно на проде. Команда kubepkg adopt берёт такие установки под управление на месте. Она находит релиз, показывает, что нашла, и сама ничего не меняет, пока вы не подтвердите: $ kubepkg adopt cert-manager adopt cert-manager 1.21.2 build 2 from main COMPONENT RELEASE FOUND THEN cert-manager cert-manager/cert-manager cert-manager v1.21.2 (app v1.21.2), revision 2, deployed upgrade in place to the package's chart, keeping its 2 top-level values sourcecraft.dev После подтверждения релиз обновляется на месте до чарта пакета с сохранением текущих значений. Deployment остаётся тем же объектом, поды не пересоздаются без нужды, а с этого момента у пакета есть ревизии, откат, зависимости и владение CRD. Объекты, поставленные без Helm, тоже забираются, но только в первой ревизии, чтобы потом случайное совпадение имён не приводило к тихому захвату чужого объекта. Helm, werf, Flux и Argo CD Оператор решает, что ставить и в каком порядке, а сам процесс применения манифестов он может отдать другому инструменту. По умолчанию он ставит чарты сам через Helm SDK и не требует в кластере ничего, кроме себя. С werf он работает через Nelm, движок развёртывания werf. С Flux каждый компонент становится HelmRelease, с Argo CD каждый компонент становится Application: Приложения, которые kubepkg создал в Argo CD С Nelm история отдельная, и про неё стоит рассказать подробнее. Nelm вырос из частично переписанного кода Helm, понимает те же чарты и хранит релизы в том же формате, но гораздо лучше следит за тем, что происходит с ресурсами во время установки, точнее расставляет их порядок внутри релиза и аккуратнее обращается с CRD. Пока идёт установка, Nelm показывает состояние каждого ресурса вместе с его событиями и логами подов, так что, если обновление застряло, причину видно сразу, а не после раскопок в кластере. Для пакетов вроде KubeVirt или Cilium, обновление которых и без того трудно отслеживать, это заметная разница. Включается он одной настройкой оператора, backend: werf, и ничего ставить в кластер дополнительно не нужно, потому что бинарник Nelm уже лежит в образе оператора и закреплён там по контрольной сумме. Чарты оператор готовит и проверяет ровно так же, как для Helm, а потом для каждого компонента пакета запускает установку релиза через Nelm. Поскольку история релизов остаётся совместимой с Helm, их видно и в nelm release list, и в helm list, а откат пакета целиком работает так же, как с Helm. Забрать под управление то, что когда-то поставили руками, тоже можно, для этого Nelm умеет принудительно забирать уже существующие ресурсы в релиз. Может возникнуть вопрос, почему kubepkg не пишет проект werf, а вызывает Nelm отдельно для каждого компонента. Проект werf ставит один релиз в одно пространство имён, а пакет kubepkg часто состоит из нескольких релизов в разных пространствах имён, как та же виртуализация, где KubeVirt и CDI живут по отдельности. Поэтому интеграция сделана на уровне движка, а не проекта. Обратный путь при этом тоже открыт: собранные пакеты kubepkg остаются обычными чартами в реестре, и проект werf может подключить их как зависимости. Порядок, требования, условия готовности и ревизии работают одинаково с любым бэкендом. Разница в том, кто применяет манифесты и можно ли откатить пакет целиком: Flux и Argo CD не хранят прошлые артефакты, поэтому с ними откат пакета невозможен и kubepkg прямо об этом говорит. Есть и третий путь, совсем без оператора. Команда kubepkg render разрешает пакеты и их зависимости и пишет результат в формате Flux, Argo CD или helmfile, с правильным порядком через dependsOn, волны синхронизации или needs. Результат коммитится в Git и живёт дальше без kubepkg. При этом теряются ревизии, откат и проверки владения CRD, так что обычно удобнее другой вариант: держать в Git объекты Package, пусть их применяет Argo CD или Flux, а kubepkg решает, как безопасно привести кластер к нужному состоянию. Много кластеров В первой части я написал, что управление набором кластеров kubepkg сознательно оставляет Argo CD, Flux, OCM и Karmada. Для большого количества кластеров это по-прежнему так, но платформам оказалось нужно что-то совсем простое: сказать один раз, что во всех продовых кластерах стоит cert-manager 1.21 и виртуализация 1.0, и катить изменения волнами, не поднимая ради этого отдельную систему. Хаб и кластеры Для этого в kubepkg есть хаб. Это кластер, который знает своих участников по меткам и пишет в них объекты Repository и Package по описанию PackageSet. На каждом участнике работает свой оператор kubepkg, поэтому ревизии, откаты и готовность остаются локальными и продолжают работать, даже если хаб недоступен. Хаб не трогает объекты, которые создал не он, и честно сообщает о конфликте. Изменение в наборе можно катить осторожно: сначала на кластеры-канарейки, потом ограниченное число кластеров одновременно, и остановиться, как только какой-нибудь кластер сообщил о неудачном обновлении. Как это выглядит вживую Возьмём два настоящих кластера. Хаб регистрирует сам себя как канарейку под именем self, а второй кластер под именем edge: $ kubepkg cluster add self --kubeconfig hub.kubeconfig --label env=prod --label ring=canary $ kubepkg cluster add edge --kubeconfig edge.kubeconfig --label env=prod $ kubepkg cluster list NAME KUBERNETES READY LABELS STATUS edge v1.35.6 True env=prod kubepkg is installed self v1.35.6 True env=prod,ring=canary kubepkg is installed sourcecraft.dev Набор говорит, что во всех продовых кластерах должен стоять kube-state-metrics, сначала на канарейках, потом по одному кластеру за раз: apiVersion: kubepkg.dev/v1 kind: PackageSet metadata: name: prod spec: clusterSelector: {matchLabels: {env: prod}} rollout: canary: {matchLabels: {ring: canary}} maxInProgress: 1 repositories: - name: fleet spec: url: kuberoot-dev.github.io publicKeys: [...] packages: - name: kube-state-metrics spec: version: "2.20.0" upgrade: {timeout: 90s} components: {kube-state-metrics: {values: {replicas: 1}}} sourcecraft.dev Пока пакет не стал готов на канарейке, edge честно ждёт, и в статусе набора у него так и написано, что он ждёт канареек. Потом пакет появляется на обоих кластерах. Следующее изменение, две реплики вместо одной, ведёт себя так же: какое-то время на канарейке уже две реплики, а на edge ещё одна, и только когда канарейка готова, изменение идёт дальше. Теперь сломаем набор: зададим образ с тегом, которого не существует. На канарейке обновление не становится готовым за полторы минуты и откатывается, а раскатка останавливается: $ kubepkg set status prod prod: 0 of 2 clusters ready, 1 updated rollout paused: stopped after a failure on self: kube-state-metrics: revision 3 failed (component kube-state-metrics failed: upgrade kube-state-metrics/kube-state-metrics: resource Deployment/kube-state-metrics/kube-state-metrics not ready. status: InProgress, message:…; change the set to go on CLUSTER UPDATED KUBE-STATE-METRICS self yes 2.20.0 edge no 2.20.0 sourcecraft.dev До edge сломанное изменение не дошло вообще, он продолжает работать на прежнем образе. Исправление набора, например правильные значения, это новое изменение, и раскатка продолжается с ним: $ kubepkg set status prod prod: 2 of 2 clusters ready CLUSTER UPDATED KUBE-STATE-METRICS self yes 2.20.0 edge yes 2.20.0 $ kubepkg set list SET CLUSTER UPDATED READY STATUS prod self yes 1/1 - prod edge yes 1/1 - sourcecraft.dev И последнее, что проверялось в этом прогоне. Когда edge переводят из продового набора в тестовый, просто сменив ему метку, пакет не переустанавливается: Deployment остаётся тем же объектом, история ревизий продолжается, меняется только владелец и значения из нового набора. Наблюдаемость Оператор отдаёт метрики Prometheus: готовность каждого пакета, версию и ревизию, исходы ревизий, успешность загрузки и возраст индексов. Чарт по желанию ставит ServiceMonitor, алерты и дашборд Grafana: Дашборд kubepkg в Grafana Отдельный алерт следит за возрастом индекса. Если у индекса есть срок действия, а конвейер публикации тихо сломался, кластеры через какое-то время начнут индекс отвергать. Алерт срабатывает раньше, пока это ещё не стало проблемой. Дистрибутив как метапакет Всё вышеописанное складывается в главный сценарий, ради которого kubepkg делался. Дистрибутив Kubernetes это кластер плюс набор компонентов в версиях, которые проверены вместе. В kubepkg такой набор описывается метапакетом, у которого нет своих чартов, а есть только требования: apiVersion: kubepkg.dev/v1 kind: Recipe metadata: name: mydistro spec: version: 2.0.0 package: variants: - name: default requires: - {package: cert-manager, version: "~1.21"} - {package: kube-state-metrics, version: "~2.20"} - {package: virtualization, version: "~1.0"} - name: minimal requires: - {package: cert-manager, version: "~1.21"} sourcecraft.dev Установка версии 2.0 дистрибутива ставит все компоненты в нужном порядке, а обновление до 2.1, которая требует cert-manager 1.22, сдвигает cert-manager на нужную версию. Варианты дают дистрибутиву профили вроде полного и минимального. Именно поэтому в kubepkg нет умного подбора версий, как в apt или npm: точные версии, проверенные вместе, закрепляет метапакет дистрибутива, а ограничения в самих пакетах остаются минимальными. Настройки, которые принадлежат кластеру, а не пакету, например домен кластера, издатель сертификатов или класс хранилища, кладутся в один секрет, и оператор подкладывает их под значения каждого компонента. Чарты со строгой схемой значений, которые отвергают незнакомые ключи, получают только те платформенные ключи, которые объявила их схема, и ставятся без изменений. Встраивание в чужую платформу kubepkg задуман как компонент, который может встраиваться не только в готовый Kubernetes-кластер, но и в любую платфому, будь то дистрибутив Kubernetes, управляемый сервис или IDP. Форк для этого не нужен. На уровне настроек платформа может отдать ресурсы kubepkg под своей API-группой, например packages.example.org, чтобы пользователи видели её собственный API, поставить свои метки на все пространства имён пакетов и выбрать бэкенд. На уровне сборки она добавляет плагины, которые на любом языке подключают её внутреннее хранилище артефактов или перекладывают образы в её реестр. А на уровне кода всё, что делают бинарники kubepkg, лежит в Go-пакетах, из которых платформа собирает свой оператор и свой CLI со своим именем, правилами доверия и бэкендами. API ресурсов с версии 1.0 стабилен (насколько это возможно для наслопленного за пару недель инструмента). Breaking changes могут появиться только в 2.0, старые версии API продолжают обслуживаться весь цикл 1.x, а оператор сам переводит хранимые объекты на новую версию. Как kubepkg используется в kuberoot Первым и самым требовательным пользователем kubepkg стал kuberoot. Там у пакетного менеджера нет администратора, который поставил бы его чартом и потом присматривал бы за ним: узел загружается, и через несколько минут на нём должен работать готовый дистрибутив, без всяких ручных действий. Поэтому оператор kubepkg в kuberoot работает не подом в кластере, а службой самого узла, рядом с сервером API. Его запускает kinit, первый процесс системы, по профилю дистрибутива, и флаги оператора лежат в том же профиле, что и всё остальное: - name: kubepkg-operator after: apiserver args: - /usr/bin/kubepkg-operator - --backend=helm - --values-secret=kube-system/kuberoot-platform-values - --registry-secret=kube-system/kuberoot-registry-credentials - --image-policy=enforce - --webhook-namespace=kube-system - --webhook-service=kubepkg-webhook - '{{if eq .Store.Kind "etcd"}}--leader-elect{{end}}' sourcecraft.dev Таким образом, пакетному менеджеру для начала работы не нужен работающий кластер, и он может поставить даже сетевой плагин, без установки которого ни один под не запустится. В отказоустойчивом кластере оператор работает на каждом управляющем узле, а действует один, выбранный через механизм выбора лидера Kubernetes (объект Lease). Вебхук политики образов при этом тоже живёт на узле, и кластер находит его через Service без селектора, у которого единственный адрес это сам узел. Дальше kinit подписывает кластер на репозиторий. CRD kubepkg лежат в образе системы, туда же при сборке кладётся открытый ключ публичного репозитория, и при первом старте kinit применяет ресурс Repository и первый пакет, CoreDNS, которому нужно знать адрес DNS именно этого кластера: apiVersion: kubepkg.dev/v1 kind: Repository metadata: name: main s
Аварийное восстановление мультикластерного Kubernetes с Linkerd и HAZL Высокая доступность приложений помогает выполнять договорные обязательства и соответствовать ожиданиям пользователей. Для этого многие платформенные команды развёртывают кластеры Kubernetes в локальных и облачных окружениях, распределяя их между зонами доступности, регионами и даже провайдерами. Сбой возможен на любом уровне этой инфраструктуры, поэтому нужно заранее проверить, как сервисная сетка поведёт себя при масштабной аварии. Команда VK Tech перевела статью о том, как протестировать аварийное восстановление мультикластерной конфигурации Linkerd и подготовить сервисную сетку к таким сбоям. Статья адресована организациям, которые уже используют или планируют использовать сервисную сетку в мультикластерной конфигурации. Если вы только знакомитесь с этим подходом, начните сруководства по поддержке мультикластерности в Linkerd, изучитепреимущества и недостатки мультикластерного Kubernetes и разберитесь втрёх типах мультикластерных архитектур, которые поддерживает Linkerd. Другие полезные материалы перечислены в конце статьи. Авторы моделируют реалистичную мультикластерную конфигурацию с открытой версией Linkerd иBuoyant Enterprise for Linkerd (BEL) и разбирают сценарии аварийного восстановления сервисной сетки. Они показывают, от чего зависит восстановление, как воспроизвести сбои в своих кластерах и какие действия помогут устранить выявленные проблемы. Даже если вы не будете запускать демонстрационную конфигурацию самостоятельно, метрики и результаты экспериментов помогут повысить устойчивость сетки и разобраться в роли Buoyant High Availability Zonal Load Balancing (HAZL) — механизма балансировки нагрузки с учётом зон доступности. Обо всём по порядку: начнём с основ Аварийное восстановление (DR) охватывает разные задачи. Здесь мы сосредоточимся на устойчивости сервисов, которые зависят от сервисной сетки: передают трафик через прокси и используют mTLS — взаимную аутентификацию клиента и сервера по TLS-сертификатам. Разберём, как сервисная сетка может снизить доступность во время сбоя и что с этим делать. Зонами доступности, в зависимости от провайдера, называют изолированные домены отказа (failure domains) внутри региона. Домен отказа — часть инфраструктуры, которую одна неисправность может вывести из строя целиком, не затронув остальные части. Каждая зона включает один или несколько независимых дата-центров, поэтому пожар в одной зоне не должен выводить из строя другие. Регионы — географически разнесённые территории, обычно удалённые друг от друга на сотни километров и более. Каждый регион включает несколько зон доступности. Вовведении в мультикластерное взаимодействие Linkerd перечислено несколько причин использовать мультикластерную конфигурацию сервисной сетки. В этой статье мы рассмотрим разделение доменов отказа: если один кластер выходит из строя, остальные продолжают работать. Кластеры связывают через общую сеть или шлюзы. Если трафик проходит через несколько облачных зон, расходы на передачу данных между ними могут оказаться неожиданно высокими. Ранее авторы разбиралипроблемы topology-aware routing в Kubernetes (TAR) — маршрутизации с учётом топологии, которая по возможности направляет трафик к эндпойнтам в той же зоне, где находится клиент. Они рассматривали стоимость межзонального трафика и то, как такой подход может снижать доступность. В следующей статье авторы представилиBuoyant High Availability Zonal Load Balancing (HAZL), который решает обе проблемы: сохраняет доступность и удерживает расходы на межзональный трафик на низком уровне. Доступность цепочки сервисов или компонентов A–B–C зависит от связей между ними. Поскольку такие компоненты часто зависят друг от друга, топология приложения и инфраструктуры влияет на общую доступность в той же мере, что и время безотказной работы каждого сервиса. Поэтому нужно учитывать все зависимости сервисов, включая сервисную сетку. Четыре сценария отказа Рассмотрим четыре сценария отказа в мультикластерной конфигурации, где три кластера Kubernetes объединены общей сервисной сеткой Linkerd: Отказ управляющего слоя сервисной сетки. Отказ кластера. Отказ зоны доступности. Отказ региона. Для каждого сценария разберём, как реагирует сервисная сетка, какие сбои возникают в её работе и какие меры помогут их предотвратить. Ключевые выводы Сервисная сетка редко отказывает так, что внезапно перестаёт пропускать трафик. Она отказывает, теряя способность адаптироваться, масштабироваться и оставаться управляемой. Сервисная сетка добавляет новые точки отказа, которые нужно включить в учения по DR Вам нужен новый набор алертов и плейбуков под топологию вашей сервисной сетки Схема эксперимента В экспериментах используются три кластера: WEST, CENTRAL и EAST. Они запущены в Docker с помощьюk3d и подключены к общей сети Docker. Трафик генерируетk6 — инструмент нагрузочного тестирования с открытым исходным кодом от Grafana Labs, сценарии для которого пишут на JavaScript. Для моделирования сбоев мы останавливаем поды и контейнеры Docker, которые служат узлами кластера, нарушаем работу сети Docker или добавляем задержку с помощьюChaos Mesh — платформы хаос-инжиниринга для Kubernetes. С помощью Chaos Mesh илиLitmusChaos эти локальные тесты можно воспроизвести воблачных окружениях, моделируя, например, сбои DNS, отказы узлов и нарушения сетевого взаимодействия. В каждом кластере 3 узла, расположенных в разных зонах доступности. CENTRAL и EAST развёрнуты в одном регионе, WEST — в другом. В WEST работают генератор нагрузки k6 и стек мониторинга Grafana + Prometheus. Сбои мы моделируем только в CENTRAL и EAST, чтобы сохранить сбор метрик. В кластерах развёрнуто простое приложениеpodinfo: по одной реплике на узел, всего девять реплик. Чтобы сравнить поведение приложения, мы публикуем его во всех трёхмультикластерных режимах Linkerd: app-federated app-flat (pod-to-pod) app-gateway (иерархический) Сравнение сетевых режимов Режим federated создаёт один логический сервис и разрешает эндпойнты для каждого кластера-участника. Поток запросов: режим federated Режим flat mirror обнаруживает сервисы в удалённых кластерах. Какой из них использовать, выбирает клиент (k6); в нашем случае это EAST. Поток запросов: режим flat mirror Режим gateway mirror создаёт в исходном кластере сервис, эндпойнтом которого служит шлюз, развёрнутый в целевом кластере. Трафик идёт через шлюз, а реальные поды разрешает уже целевой кластер. Поток запросов: gateway mirror За прогоном мы следим на собственном дашборде Grafana: аннотации на нём отмечают начало и окончание эксперимента, а алерты отслеживают симптомы и ошибки. Дашборд Grafana, который мы использовали в экспериментах В каждом эксперименте мы отслеживаем метрики и настраиваем алерты на достижение пороговых значений: Запросы без mTLS. Число запросов, обрабатываемых в единицу времени. Доля запросов с ошибками. Число эндпойнтов в режиме federated. Доступность шлюза. Локальность трафика — доля трафика, направляемого в другую зону. Код и результаты экспериментов доступны наGitHub. Для каждого прогона естьсводка и еёнаглядное представление. Винструкции описано, как импортироватьдашборд Grafana и визуализировать данные эксперимента. Если что-то не работает или вы хотите предложить другие тесты, создайтеissue на GitHub. Эксперименты Базовый трафик во всех экспериментах одинаков: постоянная нагрузка 30 запросов в секунду на каждый режим публикации в каждом кластере, распределённая равномерно. Каждый эксперимент моделирует один сбой и измеряет его во всех трёх мультикластерных режимах. Эксперимент 1: сбой управляющего слоя В этом эксперименте мы моделируем сбой управляющего слоя сервисной сетки в кластере CENTRAL и проверяем, как он влияет на работу сетки и трафик между компонентами. Для теста уменьшаем число реплик Deployment приложения в EAST до 0. Назначение каждого сервиса управляющего слоя подробно описано вдокументации по архитектуре Linkerd. Эксперимент 1a:сервис destination недоступен Сервис destination отвечает за обнаружение эндпойнтов и работу контроллера политик Linkerd. Если он недоступен, информация об эндпойнтах перестаёт обновляться. Даже спустя 90 секунд CENTRAL продолжал сообщать, что все federated-эндпойнты доступны, хотя подов в EAST уже не было. Сетка использовала устаревшие данные и не учитывала изменения в кластерах. Эксперимент 1b:сервис identity недоступен Сервис identity выдаёт подам сертификаты. Если он недоступен, уже запущенные прокси продолжают обслуживать трафик с действующими сертификатами до истечения их срока — по умолчанию до 24 часов после последнего успешного обновления. Новые поды не могут получить сертификаты и присоединиться к сетке, поэтому увеличить число реплик приложений не получится. Извлечённые уроки Отслеживайте состояние управляющего слоя Linkerd на дашборде аварийного восстановления, например в Grafana. Настройте алерты на недоступность его сервисов и на поды, которые застряли на этапе инициализации и не могут присоединиться к сетке. Проверки состояния подов и другие защитные механизмы могут временно скрыть проблемы сетки или защитить приложения от последствий её сбоя. План аварийного переключения требует увеличения числа реплик, для которого нужен работоспособный управляющий слой. Процедура восстановления зависит от сетки, и во время инцидента она может стать узким местом. На её поведение при восстановлении влияют параметры установки, напримерwebhookFailurePolicy. Эксперимент 2: кластер недоступен В этом эксперименте мы останавливаем все три узла EAST. Затем повторяем эксперимент: удаляем сеть Docker, которая соединяет кластеры, и так моделируем полный сбой сети. Извлечённые уроки Мультикластерные режимы по-разному реагируют на сбои: в одних отказ заметен сразу, в других его признаки менее очевидны. Разберитесь, как ваша конфигурация обнаруживает эндпойнты и находит бэкенды, а также где это происходит: в локальном кластере (federated или flat-mirror) или в удалённом (gateway). От этого зависит поведение сетки при потере кластера. Одного алерта на долю ошибок недостаточно. Если механизм обнаружения продолжает возвращать недоступные эндпойнты, запросы всё равно направляются к ним, а соединения зависают до истечения тайм-аута клиента. Без тайм-аутов метрики вродеrequest_total не растут. Тайм-ауты и повторные попытки можно настроить в коде клиента или средствамиLinkerd. В тестах отдавайте предпочтение внезапным обрывам, а не корректному завершению работы (graceful shutdown), при котором сервис обрабатывает текущие запросы перед остановкой. Корректное завершение замечают и другие механизмы, например проверки состояния, а внезапный обрыв обнаруживается только после истечения тайм-аутов обнаружения эндпойнтов. Такие тесты позволяют точнее оценить время обнаружения сбоя и восстановления. Эксперимент 3: зона становится медленнее В этом эксперименте мы добавляем задержку в одну зону кластеров EAST и CENTRAL. Поды при этом всё время оставались работоспособными. Схема топологии из двух регионов: зона zone-a1 обведена красным, подпись +400ms. В обведённой зоне находятся один под CENTRAL и один под EAST. Все поды по-прежнему показаны работающими. Извлечённые уроки Маршрутизация на основе топологии не видит деградировавшую, но работоспособную зону, а HAZL адаптируется и переводит трафик на эндпойнты, которые работают лучше. В штатном режиме HAZL удерживает трафик в своей зоне, даже когда он переходит из кластера в кластер. В open source-версии Linkerd та же рабочая нагрузка распределяется без учёта зон, и в реальном счёте за трафик это оборачивается межзональным egress-трафиком для большинства запросов. Примечание: межкластерная зональная локальность HAZL сейчастребует мультикластерного режима pod-to-pod (flat) и в этом эксперименте не применяется к режиму gateway. Эксперимент 4: регион выходит из строя Оба кластера в region-a отключены, в работе остаётся только WEST. Уцелевший регион принимает на себя весь трафик. Извлечённый урок Режим federated хорошо справляется с потерей региона: поды в WEST уже входят в пул federated-сервиса. Он выдерживает такой сбой, если WEST может принять дополнительную нагрузку и масштабироваться вверх. Если нет, следите, чтобы сервисы принимали ровно столько запросов, сколько могут обработать, и применяйте ключевые методы:ограничение частоты запросов иcircuit breaking10 (Linkerd их поддерживает). Два других режима отказывают, потому что их эндпойнты находились в отказавшем регионе. Так получилось из-за того, как мы спроектировали архитектуру моделирования, а не из-за ограничений Linkerd. Три шага, которые повысят надёжность вашей сервисной сетки 1. Проверьте свою установку и разберитесь в ней Этот шаг большинство пропускает. У Buoyant естьчек-лист для промышленной эксплуатации, в котором собраны типичные проблемы и требования к установке. Мультикластерное взаимодействие в Linkerd можно настроить несколькими способами. Эксперименты показывают, что каждая топология по-своему реагирует на сбои, а возникающие ошибки могут быть явными или скрытыми. Разбор топологий и результаты их хаос-тестирования приведены в статье «Федерация кластеров для Kubernetes без простоев». Например, установка поруководству Buoyant для промышленной эксплуатации по умолчанию используетпараметры высокой доступности (HA). При установке созначениями Helm-чарта по умолчанию это не так. Без HA для proxy injector — компонента, который автоматически добавляет прокси в создаваемые поды, — задано webhookFailurePolicy=Ignore. Если этот компонент недоступен, поды могут запускаться без прокси и оставаться вне сетки. В режиме HA значение меняется на Fail. В обоих режимах параметр defaultInboundPolicy по умолчанию имеет значение all-unauthenticated, поэтому поды в сетке принимают трафик без mTLS, пока настройку не изменят глобально или для отдельного пространства имён. Во время инцидента эти параметры могут привести к неожиданному поведению сетки. 2. Доработайте алерты Для многих точек отказа сетки нет стандартных алертов. Алерты на долю запросов с ошибками не выявляют проблемы управляющего слоя напрямую. Отслеживайте следующие сигналы: Работоспособность управляющего слоя сетки. Истечение срока действия сертификатов прокси. Поды, которые не могут присоединиться к сетке. Трафик без mTLS между сервисами там, где его быть не должно. Метрики описаны встатье о работе сервиса destination в Linkerd. Смоделируйте аварийное переключение и проверьте, сработают ли алерты. Стандартные алерты могут выявить симптомы — ошибки, которые видят пользователи, — но не первопричину, например сбой управляющего слоя. 3. Тестируйте восстановление от начала до конца Документируйте проблемы, включайте их в учения по аварийному восстановлению и регулярно повторяйте тесты. Готовые дашборды выявляют не все проблемы: поведение сетки зависит от топологии, параметров установки и мультикластерной конфигурации. Надёжно проверить, сможет ли сетка восстановиться, можно только в своём окружении. Тест должен охватывать весь процесс: внесение сбоя, выявление его признаков и восстановление до исходного рабочего состояния. Например, проблемы с сервисом identity могут проявиться только при увеличении числа реплик приложений. В экспериментах такие проблемы возникали именно во время восстановления, поэтому убедитесь, что тесты охватывают и этот этап. Выводы Мы разобрали возможные сбои мультикластерной конфигурации Linkerd и меры, которые помогут доработать план аварийного восстановления сервисной сетки. Эксперименты показали следующее: При сбое сервисная сетка редко сразу перестаёт пропускать трафик. Чаще она теряет способность адаптироваться к изменениям, масштабироваться и оставаться управляемой. Сетка добавляет новые точки отказа, которые нужно учитывать в учениях по аварийному восстановлению. Алерты и процедуры восстановления должны учитывать топологию вашей сетки. Начните с одного сценария отказа, например роста задержки в регионе или выхода кластера из строя, и протестируйте его от начала до конца: Зафиксируйте исходное состояние: сервисы работают, нагрузка стабильна. Опишите ожидаемое поведение во время инцидента. Например, из-за задержки запросы между кластерами завершаются по тайм-ауту. Внесите сбой и убедитесь, что алерты срабатывают. Восстановите нормальную работу и проверьте, что алерты закрываются. Выясните, какие компоненты отказали, устраните проблемы и повторите тест. Другой вариант — остановить часть компонентов управляющего слоя Linkerd и изменить нагрузку на приложения так, чтобы запустилось масштабирование. Проверьте, выявят ли алерты проблему до того, как запросы начнут завершаться ошибками. Удачного хаоса! FAQ Что происходит с трафиком Linkerd, когда управляющий слой выходит из строя? Трафик продолжает идти, но сервисная сетка перестаёт адаптироваться к изменениям. Если недоступен сервис destination, информация об эндпойнтах перестаёт обновляться. Если недоступен сервис identity, уже запущенные прокси обслуживают трафик до истечения срока действия сертификатов — по умолчанию до 24 часов. Новые и перезапущенные поды не могут получить сертификаты и перейти в состояние готовности. Какой мультикластерный режим Linkerd переживает сбой региона? В наших тестах federated-сервисы пережили потерю целого региона, поскольку логический сервис продолжал обнаруживать эндпойнты в уцелевшем кластере. Режимы pod-to-pod и gateway отказали: их эндпойнты находились в вышедшем из строя регионе. Как HAZL ведёт себя по сравнению с topology-aware routing, когда зона начинает работать медленно? Topology-aware routing не распознаёт замедление зоны, пока она остаётся работоспособной, и продолжает направлять в неё трафик. HAZL замечает рост нагрузки из-за задержки и перенаправляет часть трафика на эндпойнты в других зонах. Для сохранения зональной локальности трафика между кластерами HAZL сейчас требует мультикластерного режима flat (pod-to-pod). Почему алертов на долю ошибок недостаточно для мультикластерного аварийного восстановления? Когда сервис теряет все эндпойнты, запросы встают в очередь в прокси вместо того, чтобы завершаться ошибкой. Доля ошибок остаётся низкой, а число обрабатываемых запросов резко падает. Настройте тайм-ауты на уровне сетки, чтобы зависшие запросы завершались ошибками. Добавьте алерты на снижение числа обрабатываемых запросов относительно исходного уровня, сбои управляющего слоя, истечение срока действия сертификатов, поды, которые застряли на этапе инициализации прокси или работают без него, и неожиданный трафик без mTLS. Как протестировать аварийное восстановление сервисной сетки в собственных кластерах? Выберите один сценарий отказа, например выход кластера из строя, и протестируйте его от начала до конца. Зафиксируйте исходное состояние и ожидаемое поведение, внесите сбой и убедитесь, что алерты срабатывают. Затем восстановите нормальную работу и проверьте, что алерты закрываются. Chaos Mesh или LitmusChaos позволяют сделать такие тесты воспроизводимыми.