The Jerusalem PostUnwanted endorsement: Political ad causes uproar at Jerusalem's Lev Academic Center - opinionPunchTope Alabi threatens legal action over Toluwanisings’ death claimsCNN TürkSavaşta sona doğru mu? İkinci perdede Trump'tan kritik adım!Daily MaverickResponse to sardine mortality must be grounded in science and evidenceESPNUpsets and chaos reign supreme in college football's Week 8한겨레잠수교, 50년만에 보행전용교로…26일 0시부터 차량 통행 금지ZDF heuteAktuelle Pressemitteilungen des ZDFCumhuriyetRAMS Park'a gidecek mi? Aziz Yıldırım, derbi için kararını verdiRapplerWhat schools will do amid intense heat during El NiñoTRT HaberTürkçülük fikrinin mimarı: Ziya GökalpCNN بالعربيةمن يقطينتين إلى 400.. كيف حوّل هذا الرجل منزله إلى مزار للهالوين؟Lenta.ruБывший военный предрек поражение Зеленскому
The Daily Newsstand · Free, Always
Sunday, October 25, 2026

Kubernetes как ядро Linux, часть 2: kubepkg, универсальный пакетный менеджер а ля Debian

Translate

У 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

История 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/v1. Возможность удовлетворяется либо пакетом, который её предоставляет, либо самим кластером, если такой API в нём уже обслуживается. Это значит, что Prometheus Operator, поставленный вручную три года назад, тоже учитывается, и kubepkg не будет требовать поставить его второй раз.

Готовые пакеты остаются обычными Helm-чартами в OCI-реестре. Если завтра вы решите, что kubepkg вам не нужен, эти чарты поставит сам Helm, Flux, Argo CD или werf (Алексей, Илья, Иван, Дмитрий, вам большой привет!).

Как это выглядит в кластере

Объекты kubepkg в кластере

Объекты 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 https://raw.githubusercontent.com/kuberoot-dev/kubepkg-recipes/main/keys/index.pub
kubepkg repo add main https://kuberoot-dev.github.io/kubepkg-recipes/index.yaml --public-key index.pub

С этого момента оператор принимает индекс репозитория только тогда, когда он подписан этим ключом.

Прежде чем что-то ставить, стоит посмотреть, что произойдёт. Команда kubepkg plan разрешает пакет и всё, что ему нужно, относительно текущего состояния кластера и ничего не меняет:

kubepkg plan

kubepkg plan

Здесь virtualization это метапакет. Сам он ничего не ставит, а только требует KubeVirt и CDI в совместимых версиях. План показывает, откуда взялся каждый пакет, какими CRD он будет владеть и можно ли его откатить. В CI ту же команду удобно запускать на каждый пул-реквест, чтобы ревьюер видел последствия изменения, а не только diff в YAML.

Установка показывает тот же план и пишет в кластер по объекту Package на каждый пакет, остальное делает оператор:

kubepkg install

kubepkg install

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

kubepkg rollback

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: https://github.com/kubevirt/kubevirt/releases/download/v1.9.0/kubevirt-operator.yaml
      sha256: f11307caafc3c23ffedf9887d8beb5a4419e2694da242fa68f63d1ec820de2e0
  charts:
    kubevirt-operator:
      from: [operator]    # обычные манифесты заворачиваются в чарт
      exclude: [{kind: Namespace}]
      patches:
        - kind: Deployment
          name: virt-operator
          merge: {...}

Команда 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: https://github.com/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}

Черновик это только начало, решения в нём принимает мейнтейнер. Здесь я поставил две реплики по умолчанию и объявил откат безопасным, потому что 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

Теперь в реестре лежит чарт, а рядом индекс с подписью. Осталось подписать на него кластер и поставить пакет:

$ kubepkg repo add demo http://demo.demo.svc:8000/demo/index.yaml --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

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

Почему откатывается не всё

Почему откат снимками состояния кластера не работает, я писал в первой части: снимок объектов не откатывает данные. Если новая версия 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

Теперь представим, что мейнтейнер выпустил неудачную сборку. В ней 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

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

$ 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

Две минуты спустя срок истекает. Сборка, с которой мы переходим, объявила откат безопасным, поэтому 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

Пакет остаётся неготовым, и это сделано намеренно. Он работает на прошлой сборке, но желаемое состояние, новейшая сборка из репозитория, не достигнуто, и об этом должен узнать человек, а не только лог оператора. При этом kubepkg не бросает попыток и сам повторяет то же обновление, сначала через 30 секунд, а потом каждый раз с вдвое большей паузой, вплоть до 10 минут, и пишет в status.retry, сколько попыток уже было и когда будет следующая. Так кластер, за которым никто не смотрит, сам выбирается из временного сбоя или подхватывает исправленную сборку 3, как только мейнтейнер её выпустит. Изменение пакета или аннотация kubepkg.dev/retry применяются сразу, а откат, который человек сделал сам через 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"}

Отдельная история это готовность. 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)

Сертификат вебхука оператор выпускает и обновляет сам. Это сделано намеренно: kubepkg не может зависеть от cert-manager, потому что вполне может оказаться тем, кто cert-manager ставит.

Без интернета

Установка в закрытом контуре

Установка в закрытом контуре

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

Ниже показан настоящий прогон на тестовом кластере. Оператор kubepkg в нём отрезан от внешнего мира сетевой политикой Cilium и видит только сам кластер, а внутри кластера подняты реестр-зеркало и веб-сервер для индекса. Бандл собирается на машине с интернетом из публичного репозитория:

$ kubepkg bundle create cert-manager kube-state-metrics --repo https://kuberoot-dev.github.io/kubepkg-recipes/index.yaml --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

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

Самое интересное начинается внутри контура. Бандлу не доверяют только потому, что он такой красивый сам к вам пришёл. Для начала подсунем сюда бандл, в котором по пути испортили один чарт, дописав в его архив всего один байт:

$ 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

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

$ 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

Ключи, которым вы доверяете, 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 = "https://quay.io"

[host."http://demo.demo.svc:5000/v2/mirror/quay.io"]
  capabilities = ["pull", "resolve"]
  override_path = true

Узлы тянут образы через среду выполнения контейнеров, а не через 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=http://demo.demo.svc:8000/main/index.yaml' \
  --set-file 'repositories[0].publicKeys[0]=index.pub'

Убедимся, что он действительно никуда не может выйти: публичный репозиторий ему недоступен.

$ kubepkg repo list
NAME  PRIORITY  PACKAGES  READY  URL
main  0         49        True   http://demo.demo.svc:8000/main/index.yaml
$ kubepkg repo add public https://kuberoot-dev.github.io/kubepkg-recipes/index.yaml --public-key index.pub --priority -1
repository public added
$ kubepkg repo list
NAME    PRIORITY  PACKAGES  READY                URL
main    0         49        True                 http://demo.demo.svc:8000/main/index.yaml
public  -1        0         False (FetchFailed)  https://kuberoot-dev.github.io/kubepkg-recipes/index.yaml
$ kubepkg repo remove public
repository public removed

Репозиторий 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("http://demo.demo.svc:5000/v2/\\_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

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

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

Helm, werf, Flux и Argo CD

Оператор решает, что ставить и в каком порядке, а сам процесс применения манифестов он может отдать другому инструменту. По умолчанию он ставит чарты сам через Helm SDK и не требует в кластере ничего, кроме себя. С werf он работает через Nelm, движок развёртывания werf. С Flux каждый компонент становится HelmRelease, с Argo CD каждый компонент становится Application:

Приложения, которые kubepkg создал в Argo CD

Приложения, которые 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

Набор говорит, что во всех продовых кластерах должен стоять 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: https://kuberoot-dev.github.io/kubepkg-recipes/index.yaml
        publicKeys: [...]
  packages:
    - name: kube-state-metrics
      spec:
        version: "2.20.0"
        upgrade: {timeout: 90s}
        components: {kube-state-metrics: {values: {replicas: 1}}}

Пока пакет не стал готов на канарейке, 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

До 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    -

И последнее, что проверялось в этом прогоне. Когда edge переводят из продового набора в тестовый, просто сменив ему метку, пакет не переустанавливается: Deployment остаётся тем же объектом, история ревизий продолжается, меняется только владелец и значения из нового набора.

Наблюдаемость

Оператор отдаёт метрики Prometheus: готовность каждого пакета, версию и ревизию, исходы ревизий, успешность загрузки и возраст индексов. Чарт по желанию ставит ServiceMonitor, алерты и дашборд Grafana:

Дашборд kubepkg в 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"}

Установка версии 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}}'

Таким образом, пакетному менеджеру для начала работы не нужен работающий кластер, и он может поставить даже сетевой плагин, без установки которого ни один под не запустится. В отказоустойчивом кластере оператор работает на каждом управляющем узле, а действует один, выбранный через механизм выбора лидера Kubernetes (объект Lease). Вебхук политики образов при этом тоже живёт на узле, и кластер находит его через Service без селектора, у которого единственный адрес это сам узел.

Дальше kinit подписывает кластер на репозиторий. CRD kubepkg лежат в образе системы, туда же при сборке кладётся открытый ключ публичного репозитория, и при первом старте kinit применяет ресурс Repository и первый пакет, CoreDNS, которому нужно знать адрес DNS именно этого кластера:

apiVersion: kubepkg.dev/v1
kind: Repository
metadata:
  name: main
spec:
  url: https://kuberoot-dev.github.io/kubepkg-recipes/index.yaml
  publicKeys:
    - |
      -----BEGIN PUBLIC KEY-----
      ...
---
apiVersion: kubepkg.dev/v1
kind: Package
metadata:
  name: coredns
spec:
  version: "~1.14"
  components:
    coredns:
      values:
        service:
          clusterIP: "10.96.0.10"

Адрес репозитория и ключ можно подменить параметрами ядра, если у компании свой репозиторий пакетов. После этого kinit выполняет kubepkg install kuberoot-<дистрибутив> --yes и повторяет команду, пока оператор её не примет. Всё остальное делает оператор.

Дистрибутивы kuberoot в репозитории

Каждый дистрибутив kuberoot описан в репозитории метапакетом, и состав дистрибутива читается прямо из рецепта. Базовый edge требует только CoreDNS, metrics-server и локальное хранилище. Дистрибутив наблюдаемости собран поверх edge:

apiVersion: kubepkg.dev/v1
kind: Recipe
metadata:
  name: kuberoot-observability
spec:
  package:
    variants:
      - name: default
        requires:
          - {package: kuberoot-edge, version: "~0.2"}
          - {package: victoria-metrics, version: "~1.153"}
          - {package: victoria-logs, version: "~1.53"}
          - {package: perses-operator, version: "~0.5"}

AI-дистрибутив тоже наследует edge, а поддержку видеокарт, инференс, обучение и векторные базы ставит отдельными метапакетами ai-gpu, ai-inference, ai-training и ai-rag, потому что одному кластеру нужен только инференс, а другому весь набор. Полная платформа с хранилищем на DRBD, политиками и резервным копированием тоже метапакет, kuberoot-platform-full, и ставится одной командой на любой дистрибутив, где есть поды.

Что пакет должен знать об узле

Узел kuberoot не похож на обычный Linux: на нём нет systemd, udev и D-Bus, модули ядра загружаются при старте, а потом их загрузка запрещается. Чарт Piraeus по умолчанию рассчитывает на всё перечисленное, и без подсказки его сателлиты LINSTOR на узлах просто не поднимутся. Подсказка лежит в платформенных значениях, одном секрете, который kinit кладёт в кластер:

linstorNode:
  drbdLoaded: true
  systemd: false
  hostNetwork: true

Оператор подкладывает эти значения под значения каждого пакета. Пакет Piraeus в публичном репозитории один на все кластеры: на обычном кластере он работает со значениями по умолчанию, а на kuberoot видит платформенные значения и сам собирает конфигурацию сателлитов под узлы без systemd, не загружая DRBD второй раз.

Что kuberoot нашёл в kubepkg

Самое полезное, что дал kuberoot, это три раунда замечаний, и почти все они нашлись не на тестах kubepkg, а на настоящей работе дистрибутивов.

Оператор на узле не может прочитать файл с учётными данными реестра на машине администратора, поэтому учётные данные теперь берутся из секрета кластера. А раз дистрибутив не может создать этот секрет заранее, не затерев чужие данные при следующей загрузке, отсутствующий секрет означает «без учётных данных», а не ошибку. Политика образов в режиме предупреждения возвращала предупреждения тому, кто создаёт под, то есть контроллеру ReplicaSet, и их никто не видел, так что теперь отказ становится событием на владельце пода. Cilium, поставленный обычным helm install до появления пакета, не переходил под управление kubepkg и оставлял релиз в состоянии failed. Отключение питания управляющего узла посреди установки оставляло релиз в pending-install навсегда, а теперь он доустанавливается сам через несколько минут после загрузки. Зависимые пакеты долго не видели, что требование уже готово, потому что оператор обрабатывал пакеты в один поток.

Самая интересная находка случилась на тестах на соответствие Kubernetes. Оператор помечал политикой образов каждое пространство имён, куда ставил пакеты, а CoreDNS и metrics-server ставятся в kube-system. Тест, который проверяет вытеснение подов критическим подом, упал так:

admission webhook "pods.images.kubepkg.dev" denied the request: no installed package pins
registry.k8s.io/pause:3.10.2; namespace kube-system runs only images packages pin

С тех пор системные пространства имён политика не трогает, а выключенную администратором политику оператор больше не включает обратно. Из той же серии вышла фильтрация платформенных значений по схеме чарта: строгие схемы mlflow и JupyterHub отвергали ключ linstorNode, который им ни к чему, и в рецептах пришлось держать патчи, а теперь их нет.

Последние два замечания вошли в версию 1.3. Дистрибутиву наблюдаемости не нужна Grafana: дашборды там на Perses, а Grafana была бы самым тяжёлым подом на узле. Раньше метапакет мог только потребовать VictoriaMetrics, но не настроить её, и выключить Grafana можно было лишь вторым вариантом рецепта. Теперь требование несёт значения для зависимости:

requires:
  - package: victoria-metrics
    version: "~1.153"
    components:
      victoria-metrics:
        values:
          grafana: {enabled: false}

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

Что дальше

kubepkg это MVP, и я прекрасно понимаю, что от первой версии до инструмента, которому доверят прод крупного банка, путь неблизкий. Публичный репозиторий пока поддерживаю я сам, а ценность репозитория, как я писал выше, в людях, которые его курируют. Модель доверия не проходила внешний аудит. Часть сценариев проверена на тестовых кластерах, но не годами эксплуатации.

При этом мне кажется, что главное получилось. Пакетный слой для Kubernetes можно построить, не выдумывая новый формат и не ломая существующие инструменты, и он может быть общим для разных платформ, а не каждый раз заново написанным. Всё открыто под лицензией Apache 2.0: пакетный менеджер в kuberoot-dev/kubepkg, рецепты публичного репозитория в kuberoot-dev/kubepkg-recipes, документация на kuberoot-dev.github.io/kubepkg. Если вы собираете свою платформу и узнаёте в этой статье свои проблемы, приходите с замечаниями, пакетами, контрибьюшеном или с тухлыми помидорами. Как показала история переписки с Дагганом, спор является лучшим способом поднять свою (в данном случае мою) задницу и сделать уже что-нибудь.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.