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

Этот цикл статей стал результатом попытки посмотреть на Kubernetes как на ядро Linux: что, если K8s будет аналогом Linux Kernel и инженеры начнут работать не с ванильным Kubernetes, а с готовыми дистрибутивами, не отвлекаясь на настройку и подгонку компонентов. Именно вокруг этой мысли и возникли эти материалы.
Важно: возможно, в приложенных к статье примерах и репозиториях не так много инженерной ценности (если она вообще есть) и вряд ли кто-то пойдет тестировать это на проде, однако я считаю важным обсуждать и всесторонне рассматривать подобные идеи и концепции. По моему убеждению, именно такие дискуссии способствуют развитию всей индустрии и позволяют по-новому или под необычным углом взглянуть на ставшие привычными вещи.

Однако, несмотря на то, что описанные в этом цикле технические решения еще слабоваты, я был бы очень рад, если бы энтузиасты, которым понравится сама идея, попробовали присоединиться к проекту и довести его до более серьезного с технической точки зрения состояния.
И ещё один момент: пока это мой личный проект и все ошибки только мои и не имеют отношения к инженерам Aenix (если увидите проблемы, помните: они бы так не налажали, это всё я виноват). И да, будет длинно и нудно.
Зачем я это делаю
kuberoot не первый мой эксперимент на тему того, каким может быть Kubernetes, и, наверное, стоит объяснить, откуда он вырос, потому что со стороны предыдущие эксперименты выглядели скорее шутками. Кроме того, часть идей я почерпнул из первого выпуска [Kubernetes Community Podcast](https://youtu.be/UgbClcTdYK0) (спасибо технически подкованным ведущим).
Сначала был Sheeternetes, оркестратор контейнеров, у которого вместо etcd вкладка Google Таблицы, а вместо привычного контроллера цикл согласования в Apps Script. Потом мы провели ему нагрузочное тестирование, сделали оператор, реестр контейнеров, живущий в ячейках, федерацию двух кластеров через таблицу и в конце концов перенесли kube-scheduler целиком в формулу электронной таблицы. Потом был Tatarnetes, форк Kubernetes, Talos и дашборда, говорящий по-татарски. Потом Kyvernetria, дистрибутив, поведение которого следует усреднённым различиям между полами, с мозаичным управляющим слоем из двух релизов Kubernetes, и два сатирических инструмента в пару к нему, mansplainctl и misogynectl, высмеивающие стереотипы о том, как ведут себя мужчины и женщины. Потом палеокомпьютинг, где мы запустили Kubernetes на Обероне поверх процессора Вирта.
Всё это выглядело как шутка, но для меня было способом нащупать, где у Kubernetes заканчивается договор и начинается реализация, то есть что в нём является сутью, а что случайностью конкретного исполнения. Если оркестратор продолжает работать, когда его хранилище заменено таблицей, а планировщик формулой, значит, суть Kubernetes не в etcd и не в конкретном коде планировщика, а в модели ресурсов и цикле согласования. kuberoot вырос из этих экспериментов, только на этот раз проверялось не то, насколько далеко можно уйти от привычного Kubernetes, а то, что можно построить, если всерьёз относиться к нему как к ядру.
Кроме того, хочу выразить благодарность крутым инженерам из Aenix и не менее крутым инженерам Фланта, разговоры и наблюдение за работой которых и стали основой для формирования описанных в материале идей.
С чего всё началось
Осенью 2023 года на KubeCon в Чикаго Тим Хокин, один из тех, кто стоял у истоков Kubernetes, говорил со сцены о том, что Kubernetes становится сложнее с каждым релизом, и эта сложность не бесплатна, потому что каждая новая фича, нужная лишь кому-то одному, ложится грузом на всех остальных; разработчики проекта всё хуже удерживают в голове всё целиком, а инженерам, которые его эксплуатируют, всё труднее разобраться во всех его настройках. Хокин предложил относиться к сложности как к бюджету, который у проекта есть и который можно потратить, но только один раз, а значит, надо научиться отказываться от того, что этот бюджет съест без достаточной пользы.
Меня эта мысль зацепила, потому что мне показалось, что у неё есть продолжение, о котором со сцены сказано не было. Если сложность, нужная машинному обучению, сетевым устройствам или промышленным контроллерам, не помещается в бюджет ядра проекта, то ей всё равно нужно какое-то место, где она будет жить, никого не обременяя. В мире Linux такое место давно нашлось. Ядро там остаётся ядром, которое знают и трогают руками немногие, а вся специфика уходит в дистрибутивы, и никто не ждёт от Торвальдса, что он лично позаботится о роутерах, медицинских томографах и игровых консолях. В декабре 2024 года я написал об этом статью «Неизбежное будущее Kubernetes» (английская версия), в которой утверждал, что Kubernetes рано или поздно пойдёт по пути ядра Linux и вокруг него вырастут специализированные дистрибутивы со своими системами сборки, настройками по умолчанию и пакетными менеджерами.
К моему удивлению, на статью ответили, причем люди, мнение которых для меня много значит. Мэтт Дагган написал пост Kubernetes as a Distro, где согласился с тем, что проблема есть, но не согласился с моим диагнозом. По его мнению, корень всех бед не в отсутствии дистрибутивов, а в том, что у Kubernetes нет настоящего пакетного менеджера. Helm делает, что может, операторы слишком сложны, чтобы большинство команд могло их писать, и пространство между ними ничем не заполнено. Он перечислил восемь свойств, которыми должен обладать такой менеджер, и к этому списку я ещё вернусь. А Тим Хокин, чей доклад и стал поводом для статьи, пришёл в обсуждение на Reddit и написал ответ, который я с тех пор перечитывал неоднократно.
Я давно и настойчиво выступаю за то, чтобы относиться к Kubernetes как к ядру операционной системы, но быть в этом вопросе слишком догматичным трудно по многим причинам. Люди ждут, что в K8s будут готовые решения для множества задач, отчасти потому, что они там были и раньше, а отчасти потому, что мир Kubernetes сильно отличается от мира Linux. Linux изначально был рассчитан на энтузиастов, и это породило множество дистрибутивов. Kubernetes же гораздо сильнее ориентирован на бизнес. Сколько дистрибутивов Linux по-настоящему рассчитаны на бизнес? Два или три. [тут бы я всё же не согласился, особенно учитывая количество специлизированных линуксов для совершеннно различных устройств - прим. авт.]
Мы, возможно ошибочно, кладём в одну корзину готовые решения для сети (kube-proxy), обнаружения сервисов (DNS) и жизненного цикла кластера (kubeadm). Даже планировщик скорее относится к пространству пользователя, чем к ядру.
Мы, опять же ошибочно, выпускаем готовые бинарные сборки. Ядро Linux собирают сборщики дистрибутивов.
Не уверен, что аналогия с ядром работает, когда речь заходит об опциях компиляции. Для ядра это имеет гораздо больше смысла, чем для Kubernetes. Возможно, аналогом здесь будет скорее «используйте планировщик попроще», чем флаги компиляции.
Так или иначе, я продолжу настаивать на том, чтобы новые вещи строились ПОВЕРХ Kubernetes, а не ВНУТРИ него.
Оригинал ответа Тима Хокина
I am a vocal proponent of treating Kubernetes as the “kernel” of an OS, but it’s hard to be dogmatic for a number of reasons.
People expect it to have in-the-box solutions to many problems, partly because it has had those in the past and partly because the world of kube is vastly different than that of Linux. The latter was originally aimed at hobbyists, which gave rise to many distributions. Kube is much more business focused. How many Linux distributions really cater to businesses? 2 or 3.
We, perhaps mistakenly, ship in-the-box solutions for things like networking (kube-proxy) and service discovery (DNS) and cluster lifecycle (kubeadm). Even the scheduler is more user-space than kernel.
We, again mistakenly, ship binary builds. The Linux kernel is built by distribution builders.
I don’t know that the kernel analogy works when it comes to things like compile time options. It makes sense for the kernel much more than for kube, I think. Perhaps the analog there is more like “use a simpler scheduler” than compile-flags.
Anyway, I will keep pressing for building new things ON kube rather than IN kube.
К этому ответу я ещё вернусь, потому что почти каждый его тезис в итоге превратился в одно из нижеописанных решений.
Летом 2025 года Дагган снова вернулся к этой теме в нвом своём посте What would a Kubernetes 2.0 look like и пошёл ещё дальше, предложив встроенный пакетный менеджер под рабочим названием KubePkg, отказ от YAML в пользу типизированного языка конфигурации, возможность заменить etcd на что-то полегче для небольших кластеров и IPv6 по умолчанию.
Самое интересное в этой дискуссии, как мне теперь кажется, это то, что мы оба были правы, просто смотрели на одну и ту же вещь с разных сторон. Дистрибутив без пакетного менеджера превращается в очередной монолит, который нельзя обновлять по частям, а пакетный менеджер без дистрибутива остаётся инструментом, которому нечего собирать. Debian это одновременно apt, архив пакетов и решения о том, что в этот архив входит, и одно без другого не живёт. Рассуждать об этом можно было бы ещё долго, но мне было неинтересно спорить в комментариях, и я решил собрать то, о чём мы говорили, и посмотреть, что из этого выйдет, когда теория выльется в код.
Что получилось
Получился проект, который называется kuberoot. Это набор для сборки дистрибутивов Kubernetes, устроенный примерно так же, как Buildroot для встраиваемого Linux. Вы описываете, какой дистрибутив хотите получить, а набор собирает из этого описания готовый загрузочный образ со своим ядром Linux, своей системой инициализации и своим Kubernetes, который ставится прямо на железо или в виртуальную машину. Рядом с ним вырос пакетный менеджер kubepkg (наследник cozypkg), который отвечает на вопросы Даггана, и репозиторий пакетов к нему, и вот этот треугольник из сборщика, пакетного менеджера и архива пакетов я и считаю главным результатом, а всё остальное скорее его следствием.
Чтобы проверить, выдерживает ли идея столкновение с реальностью, я собрал на этом наборе восемь дистрибутивов и нарочно сделал их как можно более непохожими друг на друга. Среди них есть обычный кластер общего назначения, сетевой маршрутизатор, в котором нет ни одного контейнера, гипервизор, переносящий виртуальные машины между узлами на лету, платформу для языковых моделей с драйвером видеокарт, узел реального времени для промышленных задач, шлюз для промышленных датчиков, внешний кластер мониторинга и рабочиую станцию, которая живёт в кластере. Если бы идея была ошибочной, это стало бы понятно на втором или третьем дистрибутиве, потому что каждый следующий потребовал переписывания основы. Этого не случилось, и после первых двух каждый следующий дистрибутив собирался примерно за день, потому что всякий раз писать приходилось только то, что действительно было в нём новым.
Ниже я подробно расскажу, как всё это устроено, какие решения мы приняли и почему, чем это отличается от того, что есть в индустрии сегодня, где я вижу выгоды и где риски. Статья получилась длинной, потому что задумана как карта, по которой будет удобно ориентироваться в остальных статьях серии, где каждая крупная часть будет разобрана отдельно и с живыми демонстрациями.

Почему Kubernetes нужно своё ядро
Обычно Kubernetes появляется на машине так. Сначала ставится операционная система общего назначения, потом на неё ставится Kubernetes, и с этого момента две системы живут параллельными жизнями. Операционная система обновляется своим менеджером пакетов и настраивается по SSH или через систему управления конфигурацией, а Kubernetes ведёт себя так, будто под ним ничего нет и не было никогда. Пока всё работает, это даже удобно, но на стыке двух систем постоянно что-то случается. То модуль ядра для хранилища не собрался под новое ядро, которое приехало с обычным обновлением безопасности, то драйвер видеокарты разошёлся по версии с библиотеками в контейнере, то параметр ядра, нужный одной программе, мешает другой, то обновление ОС перезагрузило узел ровно в тот момент, когда на нём заканчивалась миграция базы данных. И в такие моменты выясняется, что за этот стык не отвечает никто, потому что администратор ОС считает его проблемой Kubernetes, а Kubernetes о нём попросту не знает.

Talos Linux уже сделал очень важный шаг в правильную сторону и показал, что операционная система для Kubernetes может быть неизменяемой, без shell и управляемой через API. Мы во многом шли по его следам и многому у него научились. Однако у Talos свой API и своя утилита для управления узлами, и граница между узлом и кластером там по-прежнему проходит, просто она стала аккуратнее и безопаснее. Мне хотелось проверить, что будет, если эту границу убрать совсем и сделать так, чтобы узел управлялся тем же самым API, что и кластер, тем же kubectl, теми же правами доступа и теми же контроллерами.
Есть и вторая причина, которая для меня даже важнее первой. Как только ядро, системные службы и Kubernetes собираются вместе, в одной сборке и по одному описанию, появляется возможность делать вещи, которые на обычном Linux либо невозможны, либо требуют героических усилий. Можно собрать Kubernetes, в котором вообще нет контейнеров, а его API служит только пультом управления машиной, как в нашем маршрутизаторе. Можно собрать ядро для задач реального времени и сделать так, чтобы kubelet знал, какие процессорные ядра освобождены от системного таймера, и раздавал их подам целиком. Можно положить драйвер видеокарты в саму операционную систему, подписав его ключом ядра, вместо того чтобы устанавливать его привилегированным контейнером, который компилирует модуль ядра прямо на узле. Именно ради таких вещей и затевался весь проект, а прозрачное управление узлом стало скорее приятным побочным эффектом.
Как устроен узел
Образ и загрузка
Каждый дистрибутив kuberoot собирается в один загрузочный образ. В нём лежит ядро Linux, собранное из исходников, маленькая программа инициализации kinit, которая работает первым процессом системы вместо привычного systemd, и неизменяемая корневая файловая система в формате squashfs, в которой есть Kubernetes, containerd, хранилище состояния кластера, наши программы и тот минимум системных утилит, который этим программам нужен, но нет ни shell, ни пакетного менеджера, ни SSH. Ядро собрано в единый загрузочный файл вместе с initramfs и командной строкой, а грузится через UEFI, так что на диске не нужен отдельный загрузчик с его собственными настройками.
Когда узел загружается, kinit монтирует системные файловые системы, читает параметры из командной строки ядра, поднимает сеть по этим параметрам или по DHCP, выставляет параметры ядра, нужные Kubernetes и конкретному дистрибутиву, и загружает модули ядра из профиля. После этого он пишет единицу в /proc/sys/kernel/modules_disabled, и до следующей перезагрузки ни один процесс на узле, даже с правами суперпользователя, не может загрузить в ядро ничего нового. А поскольку ядро собрано с обязательной проверкой подписи модулей и принимает только модули, подписанные ключом, который был сгенерирован при сборке этого самого ядра, подсунуть ему чужой код не получится и до этого момента. Ключ живёт ровно одну сборку, и даже мы сами не можем подписать модуль для уже выпущенного ядра.
Затем kinit определяет роль узла. Если узел поднимает свой кластер, он генерирует удостоверяющий центр кластера, сертификаты всех компонентов, ключи сервисных аккаунтов, конфигурации kubelet и container runtime, а если вступает в чужой кластер, берёт всё нужное из токена присоединения, о котором подробнее чуть ниже. После этого запускаются службы, перечисленные в профиле дистрибутива, каждая в своей контрольной группе, с зависимостями вроде «запускать kubelet только после того, как ответил сервер API», с отслеживанием состояния и перезапуском упавших.

API узла
Одна из служб, kuberoot-node, обслуживает небольшой API узла, и вот здесь начинается то, ради чего мне хотелось всё это собрать. Этот API написан на той же библиотеке, на которой написан сам сервер API Kubernetes, и встраивается в API кластера через механизм агрегации, тот самый, которым подключаются metrics-server и другие расширения. С точки зрения администратора узел становится набором обычных ресурсов Kubernetes в группе node.kuberoot.dev, и их можно читать, менять, следить за ними через watch и защищать правами доступа так же, как поды или сервисы.
Ресурс | Что он делает |
|---|---|
osconfigs | настройки операционной системы узла и то, что узел сообщает о себе |
nodeservices | службы, которые запускает kinit, их состояние, перезапуски и логи |
disks | блочные устройства |
installations | установка системы на диск с загрузочного носителя |
bootentries | два загрузочных слота и какой из них загружен |
upgrades | запись подписанного обновления в неактивный слот |
memberships, jointickets | вступление узлов в кластер |
statebackups | резервные копии состояния управляющего узла |
Права проверяются тем же механизмом SubjectAccessReview, которым пользуются все aggregation API, то есть, когда запрос приходит через сервер API кластера, API узла спрашивает у кластера, разрешено ли этому пользователю это действие. Пока кластера ещё нет или он лежит, узел обслуживает свой API самостоятельно, со своим удостоверяющим центром, так что администратор может починить узел, у которого умер кластер. На управляющем узле API собирает ресурсы со всех узлов кластера и показывает их под именами вида узел.ресурс, находя узлы по их адресам в объектах Node, поэтому службу kubelet на третьем узле можно увидеть одной командой, не заходя на него и даже не зная его адреса.

$ kubectl get nodeservices
NAME STATE PID RESTARTS MEMORY CPU AGE
containerd Running 202 0 104Mi 0s 2m7s
kine Running 201 0 82Mi 2s 2m7s
kube-apiserver Running 198 0 309Mi 7s 2m7s
kube-controller-manager Running 282 0 135Mi 2s 2m2s
kubelet Running 280 0 112Mi 1s 2m2s
kuberoot-node Running 204 0 127Mi 1s 2m7s
Дистрибутивы добавляют в этот API свои ресурсы. У гипервизора это тома и виртуальные машины, у AI-дистрибутива серверы моделей, у дистрибутива реального времени замеры задержек, и об этом я расскажу ниже.
Обновления и откат
Обновление устроено так же, как в современных телефонах и автомобилях. При установке на диск создаются раздел для загрузчика, два раздела с системой по гигабайту и раздел состояния, в котором живёт всё изменяемое, то есть сертификаты, база данных кластера, данные kubelet и containerd и настройки узла. Новая версия, подписанная ключом проекта, приходит как ресурс Upgrade со ссылкой и хешем, проверяется и записывается в неактивный слот, после чего узел перезагружается в неё на испытательный срок с ограниченным числом попыток. Если после перезагрузки система поднялась и её службы работают, слот помечается хорошим и становится основным, а если нет, узел возвращается в предыдущую версию сам, без участия человека, причём только в том случае, если предыдущая версия сама когда-то была признана хорошей.

$ kubectl get bootentries
NAME RELEASE STATE TRIES LEFT BOOTED DEFAULT
slot-a 0.5.0-rt.1 Good 0 false true
slot-b 0.5.0-rt.2 Bad 0 true false
Этот вывод снят в тот момент, когда новая версия дистрибутива реального времени не смогла запустить kubelet, потому что его менеджер памяти помнил объём памяти, оставленный прежним ядром, а новое ядро оставило ему немного другой объём, и kubelet требовал, чтобы его файл состояния удалили руками. Узел признал слот плохим и откатился сам, а мне оставалось только понять причину и научить kinit забывать эти записи при загрузке, когда никаких контейнеров ещё нет и они ничего не описывают.
Вступление в кластер, резервные копии и сеть
Подключение нового узла происходит через тот же API. На управляющем узле создаётся токен присоединения, join token, привычный по kubeadm, k3s или Docker Swarm. Только у нас это не одна строка, а скорее пропуск, ближе по духу к машинному конфигу Talos: ресурс JoinTicket, выписанный на имя конкретного узла и живущий не дольше суток, в котором лежат адрес кластера, его удостоверяющий центр, одноразовый токен для kubelet и сертификат, по которому кластер будет доверять API нового узла. Этот пропуск передаётся новому узлу как ресурс Membership его собственного API. Узел перестраивает свою роль, получает сертификаты, kubelet регистрируется в кластере, а запросы на серверные сертификаты kubelet одобряются автоматически, но только для тех адресов, которые не пересекаются с сетями подов и сервисов, чтобы узел не мог выдать себя за чужой адрес.

Резервные копии состояния управляющего узла снимаются по расписанию или по запросу ресурсом StateBackup. В копию входит ровно то, что переносит установщик: удостоверяющие центры и сертификаты, идентичность узла, сертификаты kubelet и согласованный снимок базы данных кластера. Копия шифруется ключом age и уходит в объектное хранилище, а при установке системы на новую машину можно указать копию, из которой её восстановить, и кластер поднимется с прежними сертификатами и прежними данными, так что рабочим узлам даже не придётся заново вступать.
Сеть подов по умолчанию строится поверх VXLAN самой службой узла, без внешних компонентов, а для плоской сети второго уровня есть режим простой маршрутизации. Если нужно что-то серьёзнее, встроенную сеть можно отключить и поставить Cilium пакетом, и для хранилища точно так же пакетом ставится Piraeus поверх DRBD, модуль которого уже лежит в образе подписанным.
Безопасность выше узла
Пакетный менеджер может требовать, чтобы в пространствах имён пакетов запускались только образы, закреплённые пакетами по хешу, и по умолчанию в дистрибутивах так и есть. А для приложений есть типизированные намерения: пользователь описывает приложение ресурсом высокого уровня, а контроллер разворачивает его в Deployment, Service и остальные примитивы в запечатанном пространстве имён, где сами примитивы менять может только этот контроллер, даже не администратор кластера. Это наш ответ на предложение Даггана заменить YAML типизированным языком: мы не стали менять язык, а сузили то, что человек вообще пишет руками.
Хранилище и тесты на соответствие
Состояние кластера по умолчанию хранится не в etcd, а в kine поверх SQLite, как Дагган и предлагал для небольших кластеров. Для одного управляющего узла это заметно легче и проще в эксплуатации, а для отказоустойчивого управляющего слоя мы используем etcd на трёх узлах. Выбирается это один раз, при создании кластера. Второй и третий управляющие узлы вступают через тот же токен присоединения, только с ролью управляющего узла: узел входит в etcd учеником без права голоса, догоняет остальных и лишь потом начинает голосовать, а вместе с токеном получает общие ключи кластера, в том числе ключ шифрования секретов, без которого, как выяснилось на стенде, новый сервер API не мог прочитать ни одного Secret. У etcd свой удостоверяющий центр, отдельный от кластерного. Рабочие узлы ходят к серверам API через балансировщик, встроенный в сам узел, без виртуального адреса и внешнего балансировщика, поэтому такая схема работает в любой сети. На стенде я выключил по питанию узел, который в тот момент был лидером etcd. За четыре минуты наблюдения сервер API и сервис на рабочем узле не отказали ни разу, лидерство перешло к соседу, а выключенный узел через сорок секунд после включения вернулся в строй сам. Потерянный узел можно и заменить: его удаляют из списка участников etcd через API узла, и он присоединяется заново как новый. Резервная копия на таком кластере это снимок etcd. Про kine мы по дороге узнали одну неочевидную вещь: сервер API Kubernetes просит хранилище сжимать историю примерно раз в пять минут, а kine по умолчанию никогда не трогает последнюю тысячу изменений, и на маленьком тихом кластере тысяча изменений набирается за часы. Старые версии объектов жили часами, и один из тестов на соответствие, который ждёт, пока устареет токен продолжения постраничного списка, падал по таймауту. Решение уместилось в один параметр, а поиск причины занял полдня и несколько ложных гипотез.

Наконец, базовый дистрибутив проходит официальный набор тестов на соответствие Kubernetes. Это тот самый набор, по которому программа сертификации CNCF проверяет, что дистрибутив действительно является Kubernetes, а не чем-то похожим на него. В него входят тесты из основного репозитория проекта, помеченные как Conformance, которые запускаются против живого кластера и проверяют поведение API, планировщика, сети, DNS, хранилища и всего остального, на что вправе рассчитывать пользователи. Результаты сертифицированных дистрибутивов публикуются в репозитории k8s-conformance, а запускаются тесты инструментами вроде Sonobuoy или hydrophone, которым пользовались и мы. На свежем кластере из трёх узлов на Kubernetes 1.37 дистрибутив прошёл все 462 теста, и это не базовый edge, а AI-платформа со всем, что на ней стоит, с моделями, агентом и драйвером видеокарт в образе. Отказоустойчивый кластер из трёх управляющих узлов на etcd тоже прошёл все 462. Для меня это было принципиально с самого начала, потому что всё остальное имеет смысл только в том случае, если под капотом настоящий Kubernetes, а не его имитация.
Как устроен узел, как работает загрузка, обновления и резервные копии и почему у узла нет shell, будет подробно разобрано в отдельной статье.
Как из этого собирается дистрибутив
Описание дистрибутива
Теперь о том, ради чего всё затевалось. Дистрибутив в kuberoot описывается одним файлом профиля и небольшим каталогом рядом с ним, и мне хочется рассказать об этом описании подробно, потому что именно оно отвечает на вопрос, можно ли собрать свой Kubernetes под свою задачу, не переписывая всё с нуля и не поддерживая форк операционной системы.
В профиле перечислено, какие модули ядра нужно загрузить при старте и с какими параметрами, какие параметры ядра выставить, какие службы запускать на управляющих и на рабочих узлах, с какими аргументами и в каком порядке, что добавить к настройкам kubelet, какие ресурсы применить к кластеру при первом старте и нужно ли ставить пакеты. Аргументы служб пишутся шаблонами, в которые kinit подставляет адрес узла, пути к сертификатам и параметры сети, так что один и тот же профиль работает на любом узле. Вот, например, как в профиле AI-дистрибутива описаны модули драйвера NVIDIA и служба, которая управляет моделями кластера.
apiVersion: kuberoot.dev/v1alpha1
kind: Distribution
metadata:
name: ai
spec:
modules:
- name: drbd
params: usermode_helper=disabled
- name: nvidia
- name: nvidia_uvm
roles:
controlPlane:
generators: [node, cluster, containers, kubelet]
addons: true
packages: true
services:
- name: kuberoot-aictl
after: apiserver
args:
- /usr/bin/kuberoot-aictl
- --kubeconfig={{kube "admin.kubeconfig"}}
- --listen=:8000
Настройки kubelet, которые дистрибутив добавляет к базовым, тоже описываются в профиле, причём kinit не позволит профилю переопределить то, без чего узел не будет работать, вроде аутентификации и сертификатов. Дистрибутив реального времени, например, именно здесь включает статический менеджер процессоров и памяти и резервирует первые два ядра под систему.
Рядом с профилем может лежать фрагмент конфигурации ядра, если дистрибутиву нужно своё ядро, список дополнительных системных программ, которые нужно положить в образ, сценарий, который докладывает в образ то, чего нет в пакетах, вроде драйвера NVIDIA или гипервизора, и манифесты, которые надо применить к кластеру при старте, например описания собственных ресурсов дистрибутива и правила доступа к ним.
Сборка
Сборка проходит несколько шагов, и все они общие для любого дистрибутива. Сначала собирается ядро из базовой конфигурации, к которой добавлен фрагмент дистрибутива, если он есть, и вариант ядра собирается в своём каталоге, чтобы дистрибутив реального времени с полностью вытесняемым ядром не мешал остальным. Затем под это ядро собираются сторонние модули, такие как DRBD для реплицируемых дисков или открытые модули драйвера NVIDIA, и подписываются ключом, сгенерированным при сборке ядра. Потом собираются наши программы, и из них, из Kubernetes, containerd, хранилища, минимального набора системных утилит и добавок дистрибутива складывается корневая файловая система, а к ней initramfs. Готовая система упаковывается в подписанный пакет обновления, который узлы проверяют перед установкой, и в загрузочный носитель для первой установки.
Версии всего, что берётся извне, от Kubernetes и containerd до драйвера NVIDIA и движка языковых моделей, закреплены в одном файле, а всё, что собирается из исходников, собирается из закреплённых тегов, так что две сборки одного коммита дают одинаковую систему. Тяжёлые сборки, вроде ядра или движка для видеокарт, идут на сборочной машине, а не на ноутбуке разработчика.

Всё остальное дистрибутив получает даром. Установка на диск, обновления с испытательным сроком и откатом, резервные копии и восстановление, вступление узлов в кластер, управление узлом через kubectl, пакетный менеджер и тесты на соответствие одни и те же для всех дистрибутивов, и автору нового дистрибутива не нужно о них думать.
Своя сущность в двух местах
Если дистрибутиву нужна своя сущность, которой в Kubernetes нет, например виртуальная машина, языковая модель, промышленный датчик или сетевой интерфейс, она добавляется в двух местах. На узле в API узла появляется новый ресурс, за которым стоит обычный процесс, умеющий делать с этой сущностью то, что нужно, будь то запуск гипервизора, работа сервера модели, опрос датчика или настройка таблиц маршрутизации. На управляющем узле появляется контроллер со своим описанием ресурса для всего кластера, который решает, на каком узле что должно работать, раздаёт узлам их части через их API и собирает их состояние обратно.
Это ровно тот же приём, которым Kubernetes расширяют уже много лет, только опущенный на уровень ниже, до самой машины, и именно поэтому новые дистрибутивы собирались так быстро. Со временем у дистрибутивов стали появляться общие части. Механизм испытания изменений с автоматическим откатом, который появился в маршрутизаторе, потом был вынесен в общий пакет и теперь используется и шлюзом для датчиков, и кластером мониторинга, а цикл, который применяет ресурсы узла по одному, не давая долгой операции с одним объектом задержать остальные, общий для виртуальных машин и серверов моделей.
Подробный разбор сборщика со сквозным примером, где мы по шагам соберём небольшой дистрибутив с нуля, будет отдельной статьёй.
Пакетный менеджер и ответ Даггану
Откуда он взялся
Пакетный менеджер kubepkg вырос из cozypkg, пакетного инструмента, который мы сделали для платформы Cozystack, когда переводили её на пакетную архитектуру. В Cozystack он решал вполне конкретную задачу этой платформы, а для kuberoot его пришлось переработать в самостоятельный проект, который работает на любом кластере Kubernetes, не только на нашем, и встраивается в платформу через конфигурацию, без форков. По дороге он получил разрешение зависимостей по возможностям, план изменений перед установкой, подписи и доверие на уровне репозиториев, работу без доступа в интернет, умение взять под управление уже установленные компоненты, политику допуска образов, перечень компонентов и проверку уязвимостей и многое другое, о чём я расскажу в отдельной статье.
Что такое пакет
Пакет в kubepkg это обычный Helm-чарт, неизменённый, и небольшой файл описания рядом с ним, в котором сказано, что пакет даёт и что ему нужно, с чем он конфликтует, какими CRD владеет, какие права ему нужны в кластере, безопасно ли откатываться на предыдущую версию и как понять, что каждая его часть здорова. Такое решение было принято сознательно, потому что история знает немало попыток сделать для Kubernetes свой формат пакетов, и все они разбивались о то, что производители уже публикуют Helm-чарты и не станут переупаковывать их ради нового стандарта. Готовые пакеты остаются обычными чартами в OCI-реестрах, поэтому их можно поставить и без kubepkg, через Flux, Argo CD или сам Helm, а kubepkg умеет как ставить их сам, так и передавать Flux или Argo CD.

Дистрибутив в этой модели устроен так же, как дистрибутив Linux: база плюс метапакет, который закрепляет версии проверенного набора и тянет его целиком. Когда узел с дистрибутивом поднимает кластер, kinit сам ставит метапакет своего дистрибутива, а дальше пользователь доставляет то, что нужно ему. Для AI-дистрибутива, например, в архиве есть готовые конфигурации для инференса, для поиска по документам и для обучения, каждая из которых тоже является метапакетом, и всего в архиве сейчас больше сорока пакетов, от сети и хранилища до очередей задач и векторных баз данных.
Восемь пунктов Даггана
С Дагганом мы во многом сошлись, а в чём-то нет, и мне кажется полезным пройтись по его восьми пунктам, потому что каждое расхождение здесь означает принятое и обдуманное решение.
Централизованное хранилище состояния, как база пакетов в Debian. Мы решили, что такое хранилище у Kubernetes уже есть, это его собственный сервер API, а вторая база рядом с ним неизбежно разойдётся с живыми объектами, как это уже случается у Helm с его секретами релизов. Поэтому kubepkg держит своё состояние в собственных ресурсах кластера, а всё остальное выводит из живых объектов.
Продвинутое разрешение зависимостей. В apt, pip или npm есть механизм, который по ограничениям вроде «этому пакету нужна библиотека не старше такой-то версии» сам подбирает сочетание версий, устраивающее все пакеты сразу. Такой подбор находит формально допустимое сочетание, но вместе его никто никогда не проверял, а дистрибутивы работают не благодаря автоматическому подбору, а благодаря тому, что поставляют наборы, проверенные вместе. Поэтому kubepkg разрешает зависимости по возможностям, то есть по API, CRD и именованным возможностям вроде входящего трафика, ограничения на версии держит минимальными, а точные версии закрепляет дистрибутив в своём метапакете.
Управление жизненным циклом ресурсов. Здесь мы согласны полностью, но с одной оговоркой. Порядок установки помогает только тогда, когда каждая часть пакета сама определяет, что для неё значит быть здоровой, поэтому проверки здоровья входят в пакет, а не добавляются потом.
Безопасная упаковка и подписи. Подписи да, единый центр доверия нет. Доверие в kubepkg устроено по репозиториям, как у архивов Debian, каждый со своими ключами, причём артефакты подписываются средствами Sigstore, а метаданные репозитория по схеме TUF. Сверх этого кластер может требовать, чтобы в пространствах имён пакетов запускались только образы, закреплённые пакетами по хешу.
Управление многими кластерами. Это мы сознательно оставили за рамками. Управлением флотами кластеров уже занимаются Argo CD, Flux, OCM и Karmada, и kubepkg старается хорошо делать одно дело в одном кластере и легко управляться из упомянутых инструментов.
Откат. Снимок объектов кластера не откатывает данные, то есть тома, схемы баз данных после миграции и CRD после удаления их версии. Даже apt не обещает понижения версий. Поэтому безопасность отката в kubepkg объявляется свойством каждой версии пакета, автоматически откатывается только то, что объявлено безопасным, а для остального перед обновлением выполняется резервная копия.
Декларативность и неизменяемость. Полностью согласны, и план изменений можно посчитать в CI из тех же ресурсов, что лежат в Git, ещё до того, как что-то изменится в кластере.
Интеграция с API Kubernetes. Согласны, но с важным добавлением, о котором в списке не сказано. Самая сложная часть здесь это CRD. Они общие для всего кластера, переживают релиз, который их поставил, и удаление CRD удаляет все объекты этого типа, поэтому владение CRD в kubepkg сделано отдельным понятием пакета.
Что касается Kubernetes 2.0 в понимании Даггана, то и там мы прошли часть пути, хотя и не везде так, как предлагал Дагган. Хранилище в дистрибутивах по умолчанию лёгкое, как он и предлагал. Вместо замены YAML на другой язык мы сузили то, что пишется руками, с помощью типизированных намерений. Пакетный менеджер есть, хоть и не встроенный в Kubernetes, а живущий рядом с ним. А вот IPv6 по умолчанию у нас пока нет.
Что из этого ответ Тиму Хокину
Хокин сомневался в аналогии с ядром Linux, и мне кажется правильным пройтись по его сомнениям так же, как по пунктам Даггана, потому что работа над kuberoot во многом была проверкой именно их.
Kubernetes ориентирован на бизнес, а бизнесу нужно мало дистрибутивов. С этим трудно спорить, если думать о дистрибутивах общего назначения, ведь компании действительно выбирают из двух-трёх. Но мой опыт подсказывает, что бизнес на самом деле потребляет огромное количество специализированных дистрибутивов Linux, просто не называет их так. Внутри прошивки маршрутизатора, системы хранения, гипервизора, промышленного контроллера, медицинского прибора, телевизора и автомобиля живут дистрибутивы Linux, собранные под одну задачу, и почти никто из их владельцев не знает, что там внутри. Именно этот класс, а не очередной Ubuntu, я и имел в виду, и именно поэтому наши дистрибутивы оказались маршрутизатором, гипервизором, контроллером реального времени и шлюзом, а не тремя конкурирующими кластерами общего назначения.
В коробке лишнее: kube-proxy, DNS, kubeadm, и даже планировщик ближе к пространству пользователя. Здесь мы с Хокином полностью совпали, и в kuberoot эта мысль доведена до конца. Какие компоненты Kubernetes запускать, решает профиль дистрибутива. У маршрутизатора и гипервизора нет ни планировщика, ни kube-proxy, ни container runtime, у шлюза для датчиков тоже, DNS ставится пакетом, а жизненный цикл кластера целиком живёт в kinit и API узла, так что kubeadm не нужен вовсе. Получается, что ядро Kubernetes в нашем понимании это сервер API, хранилище и контроллеры, а всё остальное становится выбором дистрибутива.
Kubernetes выпускает готовые бинарники, а ядро Linux собирают сборщики дистрибутивов. А вот здесь мы пока остановились на полпути. Ядро Linux мы собираем сами, вместе со сторонними модулями и подписью, а сам Kubernetes берём готовыми бинарными сборками проекта, закреплёнными по версии, потому что для наших задач сборка из исходников пока ничего не давала. Но архитектура это позволяет, и если дистрибутиву понадобится свой kubelet или свой сервер API, он сможет собрать их так же, как сегодня собирает ядро.
Опции компиляции для Kubernetes имеют меньше смысла, чем для ядра, и аналогом скорее будет «используйте планировщик попроще». Это наблюдение оказалось очень точным. В kuberoot роль опций компиляции играет профиль дистрибутива, в котором решается, какие компоненты запускать и с какими настройками, каким хранилищем пользоваться и нужны ли вообще поды. Флаги компиляции нам действительно не понадобились, а вот возможность выбросить компонент или заменить его на более простой понадобилась в каждом дистрибутиве.
Строить новое ПОВЕРХ Kubernetes, а не ВНУТРИ него. И это, пожалуй, лучшая формулировка того, что получилось. Ни одна из возможностей kuberoot не потребовала менять Kubernetes. API узла подключается через агрегацию, виртуальные машины, модели, датчики и правила маршрутизации описываются своими ресурсами со своими контроллерами, пакеты ставит оператор, а агент-оператор ограничен политикой допуска, которая есть в Kubernetes из коробки. Всё это построено на Kubernetes, и ничего не встроено в него.
Восемь дистрибутивов
Теперь, когда понятно, из чего они собраны, можно пройтись по самим дистрибутивам. О каждом будет отдельная статья с подробностями и демонстрациями, а здесь я постараюсь показать, чем каждый из них интересен и как он устроен внутри.

Edge
С этого дистрибутива всё начиналось, и на нём проверялось всё остальное. Это кластер общего назначения на один или несколько узлов, и один узел может быть сам себе кластером, полноценным, с сервером API, планировщиком и пакетным менеджером, а другие узлы подключаются к нему по токенам присоединения. Такой узел удобно ставить туда, где нет инженера, но есть потребность в нескольких сервисах, например в магазин, на склад, на производственную площадку, в филиал или на базовую станцию. Узел устанавливается с загрузочной флешки, обновляется сам с откатом при неудаче, снимает резервные копии своего состояния и восстанавливается из них, а всё, что выше базы, приезжает пакетами.
Для edge собран метапакет полной платформы, который превращает небольшой кластер в самостоятельную площадку с хранилищем на DRBD через Piraeus, мониторинг и логи на VictoriaMetrics и VictoriaLogs, резервное копирование приложений через Velero, политики через Kyverno, балансировщик для сервисов и входящий трафик. На этом дистрибутиве мы гоняли тесты на соответствие, проверяли откат обновления, внезапное отключение питания управляющего узла, разрыв сети между узлами, перенос приложений через резервную копию в другой кластер и нагрузочные тесты, и именно здесь нашли больше всего ошибок, которые потом не пришлось искать в остальных дистрибутивах. Одна из них была особенно поучительной. При внезапной потере питания хранилище кластера иногда оставалось заблокированным, и пришлось научить его ждать блокировку, а не падать.
Router
Это Kubernetes, в котором нет ни одного контейнера, и мне он кажется самым наглядным доказательством того, что API Kubernetes может быть просто удобным пультом управления машиной. В профиле этого дистрибутива нет ни containerd, ни планировщика, ни kube-proxy, а kubelet остаётся только ради того, чтобы кластер знал о своих узлах. Сетевые интерфейсы и VLAN, маршруты, трансляция адресов, зоны и правила межсетевого экрана, DHCP-сервер, BGP-маршрутизатор и его соседи описываются ресурсами в группе router.kuberoot.dev, а применяет их программа на узле, которая напрямую работает с сетевой подсистемой ядра, ведёт свою таблицу nftables, запускает DHCP-сервер и BGP-демон и помечает свои маршруты собственным номером протокола, чтобы не трогать чужие.
Самое приятное в этом дистрибутиве это защита от собственной руки. Если включён Safeguard, любое изменение сначала идёт на испытание. Узел применяет его и ждёт подтверждения, и если вы случайно отрезали себе доступ и не подтвердили изменение в отведённое время, оно откатится к последней подтверждённой конфигурации само, причём откат переживает и перезагрузку узла. А чтобы не отрезать себе доступ особенно изобретательным способом, правила проброса портов не могут перехватить адреса и порты, через которые узлом управляют. Кто хоть раз ехал ночью в серверную из-за одной неверной строчки в правилах межсетевого экрана, оценит это лучше меня.
Hypervisor
Гипервизор запускает виртуальные машины прямо на KVM через cloud-hypervisor, без подов, без libvirt и без промежуточного слоя управления. Машина описывается ресурсом VirtualMachine, где указаны процессоры, память, размер диска, образ, с которого его записать, и число реплик диска, а контроллер на управляющем узле решает, где машине работать и где лежать копиям её диска.
Диски машин это тома DRBD 9 поверх файлов на узлах, и каждая запись доходит до других копий прежде, чем считается сделанной. Тома настроены так, что узел, отрезанный от большинства копий, перестаёт писать на диск совсем, и именно это спасает от раздвоения данных, ведь машина, запущенная на другом узле после аварии, никогда не будет делить диск с устаревшей копией. Машины разных узлов живут в одной сети, мост на каждом узле связан с остальными через VXLAN, а шлюз на управляющем узле раздаёт адреса и выпускает машины наружу.
Если узел выключить из розетки, контроллер через полминуты считает его мёртвым и запускает машину на узле с актуальной копией диска, и меньше чем через две минуты машина работает там с тем же диском и тем же адресом. Когда старый узел вернётся, его копия диска догонит остальные, а сам он не станет запускать машину второй раз. Мы проверяли это и выключением питания, и разрывом сети, и раздвоения данных не было ни разу.
Для обслуживания машину можно перенести на другой узел живой, без перезагрузки гостевой системы. Для этого на время переезда диск разрешается держать открытым на запись на двух узлах сразу, на целевом узле запускается пустой процесс гипервизора, который ждёт машину, исходный узел передаёт ему память работающей машины, и после переезда диск снова становится доступным на запись только на одном узле. Переезд начинается только тогда, когда все копии диска на связи, и откатывается, если не уложился в отведённое время или по дороге пропал узел.

$ kubectl patch vm part --type merge -p '{"spec":{"node":"kuberoot-af49e9"}}'
11:22:46 Migrating kuberoot-221a17 Preparing moving alive from kuberoot-221a17 to kuberoot-af49e9
11:22:53 Migrating kuberoot-221a17 Receiving
11:23:00 Migrating kuberoot-221a17 Sending
11:23:06 Running kuberoot-af49e9 moved alive from kuberoot-221a17
На наших стендах такой переезд занимал около двадцати пяти секунд. По сути это та часть vCenter, ради которой его обычно и покупают, только устроенная как обычный ресурс Kubernetes, с теми же правами доступа и той же автоматизацией, что и всё остальное.
AI
AI-дистрибутив это edge, к которому добавлено два собственных механизма, и о каждом стоит рассказать подробно.
Первый механизм это языковые модели как ресурс кластера. Вы описываете модель ресурсом Model, указываете, где взять её веса и их хеш, сколько копий нужно, какой размер контекста и сколько запросов обслуживать одновременно, и кластер делает всё остальное. Контроллер выбирает узлы, причём старается не ставить модели на управляющий узел, пока есть рабочие, узлы скачивают веса, проверяют их по хешу, запускают сервер модели обычным процессом узла и отдают модель через один общий адрес, совместимый с API OpenAI, который маршрутизирует запросы по имени модели к её готовым копиям по очереди и пропускает потоковые ответы насквозь.
Самое интересное здесь это выкатка новой версии, устроенная как канареечный выпуск. Новая версия сначала ставится на один узел, а когда она там готова, получает пробный запрос. Только если она ответила, остальные узлы переходят на неё по одному, каждый после того, как предыдущий начал обслуживать новую версию, и перед сменой версии узел выводится из маршрутизации и дожидается, пока закончатся запросы, которые он уже обслуживает. Если новая версия не запустилась, не стала готовой за пятнадцать минут или не ответила на пробный запрос, кластер возвращается к прежней сам и запоминает, что эту версию пробовать снова не надо, пока её не поменяют. На наших стендах заведомо битая версия откатилась за одиннадцать секунд, причём две другие копии модели её даже не трогали, а смена версии под нагрузкой из четырёх параллельных клиентов с длинными потоковыми ответами прошла без единого оборванного ответа.

$ kubectl patch model qwen --type merge -p '{"spec":{"source":{"url":".../qwen2.5-0.5b-q5_k_m.gguf","sha256":"041474…"}}}'
$ kubectl get model qwen -w
NAME READY PHASE SERVING MESSAGE
qwen 2/2 RollingOut 74a4da… trying the new version on kuberoot-05b48c
qwen 1/2 RollingOut 74a4da… trying the new version on kuberoot-05b48c
qwen 2/2 RollingOut 74a4da… moving to the new version
qwen 2/2 Ready 041474…
Между первой и последней строкой прошло около тридцати секунд. Во второй строке видно, что первый узел на время смены версии выведен из обслуживания, и модель отвечает с одной копии, а в третьей новая версия уже ответила на пробный запрос, и на неё переходит второй узел.
Серверы моделей живут на узле в отдельной контрольной группе с ограничением памяти, и это тоже урок, полученный на стенде. Модель, которой по ошибке задали контекст в два миллиона токенов, начала занимать память лениво, и узел, вместо того чтобы просто убить сервер, ушёл в бесконечную подкачку страниц, а это был управляющий узел. Теперь серверы моделей не могут занять память, оставленную системе, а только что запущенный сервер первым попадает под нож ядра, если память кончилась, чтобы не пострадали соседние модели, которые уже работают.
Драйвер NVIDIA в этом дистрибутиве является частью операционной системы. Открытые модули ядра собираются под наше ядро и подписываются его ключом, рядом лежат библиотеки драйвера, утилита nvidia-smi и прошивка, а узел сам создаёт файлы устройств, потому что udev здесь нет, и описывает видеокарты для container runtime по стандарту CDI, так что поды получают их обычным образом через device plugin, а в контейнере сам собой обновляется кэш загрузчика под выданные библиотеки. Узлы с видеокартами помечаются меткой, по которой их находят пакеты для видеокарт, а модели кластера могут работать на видеокартах напрямую, получая на узле свободные карты, которые узел запоминает за ними.
Программа, которая обслуживает модель на видеокарте, со встроенными библиотеками CUDA для всех поколений карт весит около гигабайта и в загрузочный слот системы просто не помещалась. Это заставило нас придумать, по-моему, более правильную вещь: движок модели теперь является частью её версии наравне с весами, скачивается узлами, которые его запускают, с проверкой хеша, а его смена выкатывается так же осторожно, как новые веса, через пробный узел и с откатом. Образ системы при этом остаётся маленьким, а движок можно обновлять без обновления ОС.
Вся остальная экосистема ставится пакетами: очереди задач Kueue и Volcano, движки инференса vLLM и Ollama, шлюз LiteLLM и веб-интерфейс Open WebUI, обучение через KubeRay и Kubeflow Trainer, JupyterHub и MLflow, векторные базы Qdrant и Milvus, PostgreSQL с pgvector, объектное хранилище для датасетов и пакеты для видеокарт, включая разделение одной карты между подами через HAMi. Большую часть из них мы ставили и проверяли вживую на стенде.
Второй механизм это агент-оператор, о котором я обязательно напишу отдельно, потому что, как мне кажется, здесь у нас получилась самая интересная архитектура в проекте. Агент это языковая модель, которая смотрит на кластер, находит проблемы вроде неготовых узлов, перезапускающихся служб или упавших серверов моделей, читает их логи и предлагает, что с этим сделать. Но сделать сама она ничего не может. Она может только создать ресурс Remedy, предложение из пяти возможных действий: перезапустить службу узла, перезапустить сервер модели, откатить модель на прежнюю версию, перезагрузить узел или передать вопрос человеку. Причём выбирает она только действие и только из тех, что допустимы для этой проблемы, потому что её ответ ограничен схемой. А то, к чему действие применяется, берётся из самой проблемы, а не из ответа модели.
Выполняет предложение обычный код, и только после того, как его одобрил человек или явно разрешила политика, не чаще заданного числа раз в час, и даже одобренное действие он откажется выполнять, если оно опасно само по себе, например если это перезагрузка управляющего узла или перезагрузка узла в тот момент, когда другой узел уже лежит. У агента своя идентичность с правами только на чтение и создание предложений, а запрет одобрять собственные предложения и менять их проверяет не он сам, а сервер API Kubernetes политикой допуска.

$ kubectl get remedies
NAME ACTION NODE TARGET PHASE REASON
restartmodelserver-xdv9t RestartModelServer kuberoot-05b48c huge Proposed The server of model huge on node kuberoot-05b48c failed, and the logs indicate a possible training context overflow...
reboot-cp RebootNode kuberoot-71a85e Refused node kuberoot-71a85e runs the control plane
В этом выводе видна и сильная, и слабая сторона подхода. Маленькая модель на полмиллиарда параметров прочитала в логе про переполнение контекста и всё равно предложила перезапуск, хотя правильнее было передать вопрос человеку, и именно поэтому по умолчанию ничто не выполняется без одобрения. Мне показалось важным заложить такую архитектуру сейчас, пока модели ещё не стали настолько убедительными, что им захочется доверить больше, чем стоит.
RT
Дистрибутив реального времени собран для промышленных контроллеров и других задач, где важно не просто посчитать, а посчитать вовремя. Ядро здесь полностью вытесняемое, с частотой системного таймера в тысячу герц, все процессорные ядра, начиная с третьего, освобождены от системного таймера, обратных вызовов RCU и фоновой работы ядра, а прерывания закреплены за первыми двумя. kubelet со статическими менеджерами процессоров и памяти раздаёт освобождённые ядра подам целиком, вместе с памятью с того же узла NUMA, так что под с задачей реального времени получает свои ядра в монопольное пользование и может работать с приоритетом реального времени.
Задержки, которые узел на самом деле обеспечивает, измеряются тоже через API узла: ресурс LatencyTest запускает на освобождённых ядрах замер с приоритетом реального времени и возвращает минимальную, среднюю и максимальную задержку и процентили по каждому ядру, так что проверить узел перед вводом в работу можно обычной командой kubectl.
$ kubectl logs -n rt-demo rt-probe2
Cpus_allowed_list: 2-3
policy : 1
prio : 19
Этот под получил ядра 2 и 3 в монопольное пользование и работает с политикой планирования SCHED_FIFO.
IoT
Шлюз для промышленных устройств снова обходится без контейнеров. Устройства, например контроллер пресса, который говорит по протоколу Modbus, описываются ресурсами Device с перечнем точек, то есть регистров с их типами, масштабом и периодом опроса, а маршруты Route описывают, при каком условии и куда отправить данные. Условия пишутся на CEL, том же языке, которым в Kubernetes пишутся правила валидации, и срабатывают по фронту, то есть тревога уходит один раз в момент, когда условие стало истинным, а не на каждом опросе. Программа на узле держит по одному соединению с каждым устройством и с каждым брокером MQTT и обходится тридцатью пятью мегабайтами памяти.
Здесь тоже есть защита от неверной конфигурации, и она ещё умнее, чем у маршрутизатора. Изменение откатывается само, если после него перестало работать то, что работало до него, например устройство перестало отвечать или брокер перестал принимать сообщения. А если испытание прошло исправно, изменение подтверждается само, без участия человека.

$ kubectl get devices,routes,safeguards
NAME PROTOCOL ADDRESS CONNECTED VALUES
press-7 modbus-tcp 10.244.156.46:5020 true temperature=91.2C cycles=42 running=true
NAME DEVICE WHEN SENT
overheat press-7 running && temperature > 80 2
NAME RUNNING ROLLEDBACK WHY
default 17b81762470b 80bcbf1eab69 the change broke what worked before it: route overheat stopped working
$ mosquitto_sub -t 'factory/#'
factory/press-7/alarm {"device":"press-7","values":{"temperature":91.2},"when":"running && temperature > 80"}
В последней строке Safeguard видно, что одно из недавних изменений, которое отправило тревоги на несуществующий брокер, было откачено само, и в поле WHY записано почему.
Узел шлюза помещается в виртуальную машину на 1,25 гигабайта памяти, и любопытно, что большую часть этого места занимает не наш код, а сам сервер API Kubernetes. Следующий шаг здесь это узел без собственного сервера API, управляемый из центрального кластера, для совсем маленьких устройств, а ещё протоколы OPC-UA и Modbus RTU.
Observability
Седьмой дистрибутив задуман как внешний кластер мониторинга, который следит за железом и программами вокруг себя и устроен так, чтобы пережить то, за чем он следит. Когда лежит дата-центр, мониторинг, живущий в этом же дата-центре и на той же платформе, лежит вместе с ним, и именно в этот момент он нужнее всего. Поэтому кластер мониторинга ставится на один, два или три небольших сервера со своей операционной системой, своими обновлениями и реплицируемым хранилищем и не зависит ни от одной системы, за которой наблюдает.
Серверы и их контроллеры управления, коммутаторы, блоки распределения питания и источники бесперебойного питания описываются теми же ресурсами Device, что и промышленные датчики, только с другими протоколами. Программа на узле опрашивает контроллеры серверов по Redfish, обходя системы, шасси и сами контроллеры и собирая их здоровье, температуры, обороты вентиляторов, потребляемую мощность и записи журналов, сетевое железо опрашивает по SNMP, включая его защищённую третью версию, а доступность сервисов снаружи проверяет пробами по HTTP, TCP и ICMP, причём недоступный сервис тоже считается показанием, а не ошибкой. Всё собранное программа сама отдаёт как метрики и сама регистрирует себя для сбора, так что россыпь отдельных экспортеров здесь не нужна, а изменение, которое сломало опрос целей, работавших до него, откатывается само, как и в шлюзе. На стенде сломанный адрес контроллера сервера откатился за полторы минуты, а сама программа заняла около сорока мегабайт.
Данные складываются в VictoriaMetrics и VictoriaLogs, а дашборды делаются в Perses и описываются ресурсами Kubernetes, так что их можно ревьюить в Git, как код, причём Perses умеет читать и метрики, и логи из VictoriaLogs. Отдельно сделан сторожевой сигнал, ресурс Heartbeat, который раз в заданный период обращается к внешнему адресу, но только пока сама система мониторинга видит свежие данные. Кластер, который перестал собирать метрики, замолкает так же, как упавший, и внешний получатель узнаёт об этом сразу, потому что самый неприятный вид аварии это та, о которой мониторинг молчит, потому что умер первым.

Workstation
Восьмой дистрибутив отвечает на вопрос, можно ли перенести в кластер рабочее место человека. Рабочий стол здесь это виртуальная машина гипервизора, описанная ресурсом Workspace, с реплицируемым диском, и она продолжает жить, когда человек закрывает ноутбук, а открывается из любого браузера или клиента RDP. У cloud-hypervisor нет графической консоли, поэтому рабочий стол поднимает сам гость, а настраивается он при первой загрузке через cloud-init, данные для которого гипервизор отдаёт каждой машине по сети, сверяясь с её адресом. Шлюз, через который браузер попадает на рабочий стол, работает процессом на управляющем узле, без подов, как и весь гипервизор, и пускает только с токеном рабочего места.

Самое приятное в этом дистрибутиве перешло в наследство от гипервизора. Открытая в браузере сессия пережила живой переезд виртуальной машины на другой узел, и за две с половиной минуты наблюдения ни один кадр рабочего стола не потерялся, а самый медленный пришёл через девять миллисекунд. По сути это VDI без Citrix и без vCenter. Шифрования на шлюзе и входа через единый каталог пользователей пока нет, а Windows с её приложениями описана только на бумаге, потому что упирается в лицензии.
Какие решения мы приняли и почему
Если собрать всё сказанное в несколько решений, получится примерно такой список, и каждое из них, как мне кажется, достойно отдельного спора.
Мы решили, что узлом нужно управлять тем же API, что и кластером, а не отдельной утилитой. Это даёт единые права доступа, единый аудит и возможность автоматизировать узлы теми же контроллерами, что и всё остальное. Платим мы за это тем, что API узла должен работать и тогда, когда кластера ещё или уже нет, поэтому узел умеет обслуживать свой API самостоятельно, со своим удостоверяющим центром.
Мы решили собирать своё ядро, разрешать ему загружать только модули, подписанные его собственным ключом, а после загрузки запрещать загрузку модулей совсем. Это закрывает целый класс атак и избавляет от расхождения версий драйверов и ядра, но означает, что любой сторонний модуль, будь то хранилище или видеокарта, нужно собирать вместе с ядром, и что ответственность за обновления безопасности ядра теперь на нас.
Мы решили, что система неизменяема, а обновляется целиком в запасной слот с испытательным сроком. Это даёт откат без участия человека и одинаковые узлы, но требует, чтобы всё изменяемое состояние жило отдельно и аккуратно переносилось между версиями, и мы уже видели, как состояние, сохранённое одной версией, мешает загрузиться другой.
Мы решили, что дистрибутив это профиль плюс метапакет, а всё общее живёт в оснастке. Это делает новые дистрибутивы дешёвыми, но требует дисциплины, чтобы оснастка не обросла частными случаями отдельных дистрибутивов.
Мы решили, что пакет это неизменённый Helm-чарт плюс описание, а не новый формат. Это позволяет пользоваться всем, что уже опубликовано, но ограничивает нас возможностями Helm там, где их не хватает.
Мы решили не тащить контейнеры туда, где они не нужны. Маршрутизатор, гипервизор и шлюз для датчиков работают без container runtime, и это делает их проще, легче и предсказуемее, но означает, что для каждой такой сущности пишется своя программа на узле.
Мы решили, что тяжёлые данные, будь то веса моделей или движки для видеокарт, не живут в образе системы, а скачиваются узлами, которым они нужны, с проверкой хеша и с той же осторожной выкаткой, что и всё остальное.
И мы решили, что машинный интеллект в инфраструктуре может только предлагать, а одобрение, аудит и запреты должны быть архитектурой, которую проверяет сервер API, а не настройкой, которую можно забыть включить.
Чем это отличается от того, что есть сегодня
Ближайший родственник kuberoot это Talos Linux, и отличие от него я уже описал: у нас узел управляется самим Kubernetes, а не своим API и своей утилитой, и у нас есть оснастка для сборки многих разных дистрибутивов, тогда как Talos это один дистрибутив с механизмом расширений. От k3s и k0s нас отличает то, что они являются дистрибутивами Kubernetes, которые ставятся на чужую операционную систему, а мы собираем операционную систему вместе с Kubernetes. От связки Ubuntu с kubeadm и системой управления конфигурацией нас отличает отсутствие того самого стыка, о котором я говорил в начале. От OpenShift и подобных платформ нас отличает то, что мы не строим одну большую платформу для всех, а даём собрать маленькую и специализированную под свою задачу.
Отдельно стоят продукты, с которыми можно сравнить отдельные дистрибутивы: VyOS для маршрутизации, vSphere и Proxmox для виртуализации, EdgeX и Node-RED для промышленных шлюзов, KubeEdge для управления устройствами на краю сети. С каждым из них мы не соревнуемся по количеству возможностей, потому что проиграем, а показываем, что та же задача может решаться в общей модели управления, где маршрутизатор, гипервизор, модели и датчики живут в одном API, с одними правами, одним аудитом и одними инструментами автоматизации.
И отдельно скажу о Cozystack, который мы развиваем в Aenix. kuberoot не задуман как его замена. Скорее это исследование того, как может выглядеть следующий слой под такими платформами, и многое из того, что здесь получится, может со временем оказаться там, если сообщество проекта сочтёт это полезным.
Что это даёт
Тем, кто эксплуатирует кластеры, это даёт узлы, которыми не нужно управлять отдельно от кластера, обновления, которые откатываются сами, и операционную систему, в которой нечего ломать вручную, потому что в ней нет ни shell, ни пакетного менеджера, ни возможности загрузить в ядро чужой модуль. Резервная копия управляющего узла и его восстановление на другой машине превращаются в два ресурса, а не в инструкцию на несколько страниц.
Тем, кто строит платформы и продукты на Kubernetes, это даёт возможность собрать свой дистрибутив под свою нишу, будь то сетевое устройство, система хранения, промышленный контроллер, кластер для инференса или устройство на краю сети, не начиная с нуля и не поддерживая форк операционной системы. И пакетный менеджер, в котором можно поставлять проверенные наборы компонентов как единое целое, а не набор отдельных чартов, совместимость которых каждый проверяет сам.
Тем, кто покупает инфраструктурные продукты, это даёт одну модель управления там, где сегодня их несколько: маршрутизаторы, гипервизоры, кластеры и датчики настраиваются одним способом, с одними правами и одним аудитом, а значит, требуют меньше узких специалистов.
А индустрии в целом, если позволите такое громкое слово, это даёт рабочую модель, в которой Kubernetes действительно становится ядром, а специфика уходит в дистрибутивы. Тогда бюджет сложности, о котором говорил Тим Хокин, тратится не в ядре проекта, а там, где эта сложность на самом деле нужна, и там же оплачивается.
Где риски
Рисков много, и важно перечислить их сразу.
Главный риск в том, что это пока небольшой проект, который развивается в свободное время и силами очень небольшой команды, и всё, что в нём сделано, проверено на наших стендах, а не в чужих продуктивных средах. Отказоустойчивый управляющий слой появился совсем недавно, и, хотя он пережил отключение лидера и прошёл тесты на соответствие, проверен он пока только на стенде, а сертификаты etcd, как и остальная инфраструктура ключей, пока не ротируются автоматически. Хранилище на kine поверх SQLite хорошо подходит для одного узла и небольших кластеров, но на нём мы уже находили неочевидные вещи вроде того, что по умолчанию оно не сжимает историю на тихих кластерах, и наверняка найдём ещё.
Своё ядро означает свою ответственность за его безопасность. Сегодня у нас есть закреплённые версии и пересборка, но нет отлаженного процесса, который выпускал бы обновление в течение нескольких дней после публикации уязвимости в ядре или в любом компоненте образа, и это то, что придётся построить раньше, чем кто-то поставит kuberoot туда, где это важно.
Поддержка железа пока узкая. Основная архитектура это amd64, для arm64 часть вещей ещё не доделана, драйвер видеокарт собран и подписан, но на настоящих видеокартах ещё не проверен, а цифры задержек реального времени мы снимали только в виртуальных машинах, где они говорят больше о гипервизоре хоста, чем о нашем ядре.
Отсутствие shell на узле это не только защита, но и неудобство при диагностике, и нам ещё предстоит доказать, что набора ресурсов API узла достаточно, чтобы разобраться в любой ночной аварии. Независимого аудита безопасности не было.
И, наконец, пакетный менеджер упирается в ту же проблему, на которой в своё время споткнулись все похожие проекты. Ценность Debian не в apt, а в архиве пакетов и людях, которые его поддерживают, и без сообщества, которое возьмёт на себя поддержку рецептов, архив пакетов останется небольшим, как бы хорошо ни был устроен сам менеджер.
Присоединяйтесь
Всё, о чём я рассказал, открыто под лицензией Apache 2.0 и живёт в организации kuberoot-dev на GitHub: сборщик и дистрибутивы в репозитории kuberoot, пакетный менеджер в kubepkg, архив пакетов в kubepkg-recipes. Мне было бы очень интересно продолжить работу не в одиночку.
Сейчас у проекта один мейнтейнер, и решения принимаю я, но это стартовая точка, а не цель. В описании того, как устроен проект, сказано, как ответственность переходит к тем, кто стабильно вкладывается в проект, и как решения станут общими, когда мейнтейнеров будет трое и больше.
Помощь нужна самая разная. Можно собрать свой дистрибутив под свою задачу и рассказать, где оснастка помешала. Можно проверить то, что мы не смогли проверить сами, на настоящих видеокартах, на arm64, на железе для задач реального времени. Можно взять на себя рецепты пакетов для тех компонентов, которыми пользуетесь сами, и это, пожалуй, самая ценная помощь. Можно посмотреть на код и архитектуру критическим взглядом, особенно на безопасность. А можно просто прийти в обсуждение и сказать, что я неправ, потому что именно с такого разговора этот проект и начался.
В следующих статьях серии я подробно расскажу, как устроен узел, которым управляют через kubectl, как собрать свой дистрибутив на нашей оснастке, как устроен пакетный менеджер и чем он отличается от предшественников, а потом пройдусь по каждому дистрибутиву отдельно, с живыми демонстрациями, и отдельно напишу про агента-оператора и про работу над хранилищем. А когда серия выйдет, мы вернёмся к тем, кто участвовал в разговоре два года назад, и спросим, что изменилось с тех пор и в индустрии, и в их собственных взглядах. Сами решения уже написаны, осталось только описать, как они устроены и работают.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.