Предыдущая статья — про унификацию доставки приложений заказчику.

Я исходил не из вопроса “какой оркестратор выбрать”. Я исходил из боли поставки: одно и то же приложение нужно доставить в десяток совершенно разных инфраструктур и потом это поддерживать. Kubernetes здесь не антагонист, а один из основных вариантов запуска. Ключевая мысль, которую я хочу зафиксировать: принести Kube-way туда, где Kubernetes под запретом. Horchestra не исключает Kubernetes, а позволяет использовать их совместно.

Задача: не «нет Kubernetes», а фрагментация доставки

Настоящая проблема — не отсутствие одной платформы, а разнородность сред, в которые нужно доставить одно и то же приложение. Каждая среда тянет доставку к своему механизму, а инфраструктурный код — тот самый «второй API» с сотнями параметров установки — форкается под каждую цель.

Стоимость здесь не в отдельном пути доставки, а в необходимости поддерживать их N штук и удерживать в согласованном состоянии. Установочный интерфейс обладает всеми свойствами публичного API — потребитель, обратная совместимость, версионирование, контрактные тесты, — но, размноженный по средам, перестаёт поддаваться сопровождению. Molecule и Testinfra проверяют процесс установки в каждой из них, но не единство интерфейса поверх всех.

OCI уже унифицировал одну ось — упаковку: артефакт один и тот же везде. Фрагментированной осталась другая — модель управления жизненным циклом: как приложение описывается и приводится к нужному состоянию после того, как упаковано. Именно её я и хотел свести к одной.

Цель: свести доставку к одной модели

Чтобы схлопнуть N способов доставки в один, нужна единая абстракция «описать приложение и привести к желаемому состоянию», работающая на всех целях сразу. В качестве такой абстракции я выбрал модель Kubernetes — декларативные манифесты, kubectl, сверка с желаемым состоянием — по двум причинам.

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

Horchestra не заменяет Kubernetes — он расширяет его охват

Это центральное уточнение. Horchestra не альтернатива Kubernetes; они сосуществуют.

Там, где Kubernetes есть или разрешён, приложение доставляется и запускается как обычно — манифесты или Helm, кластер заказчика. Там, где Kubernetes отсутствует или под запретом, Horchestra предоставляет тот же Kube-way на голых Linux-хостах: та же форма манифеста, тот же kubectl, та же декларативная сверка — но исполняемые через systemd и OCI, без платформы. В смешанном парке оба работают рядом: часть нагрузок на кластере заказчика, часть на хостах под управлением Horchestra, и всё описывается и управляется одной моделью.

Отсюда и формулировка тезиса. Не «Kubernetes без Kubernetes» как замена платформы, а перенос подхода Kubernetes туда, куда платформа не дотягивается. Приложение описывается один раз и доставляется во все среды сразу, включая те, где Kubernetes запрещён. В этом и состоит выигрыш унификации, ради которого проект и появился.

Почему именно Kube-way, а не собственный формат

Унификация требует не «похожей», а той же самой модели, что и на реальном кластере. Поэтому интерфейс сознательно совместим с kubectl и повторяет форму манифестов: kubectl apply, get, logs, единый ресурс Application вместо связки Pod/Deployment/StatefulSet. В целевом сценарии — одно приложение, один процесс, одна нода — выбор между этими сущностями не несёт смысла и лишь создаёт пространство для ошибки.

Показательно, что Application — ресурс, которого нет в ванильном Kubernetes: kubectl get app против случайно оставленного продакшен-kubeconfig просто не отработает, и оператор сразу увидит, что контекст не тот. Смысл совпадения API-поверхности не в имитации, а в том, что одни и те же манифесты и одна и та же оперативная привычка покрывают и реальный кластер, и хост под Horchestra. Это и делает доставку единой.

Почему нельзя просто расширить Kubernetes

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

Для службы ИБ внутренний рантайм несуществен — предметом аудита становится Kubernetes как таковой: control-plane, etcd, сетевые политики, RBAC, поверхность CVE экосистемы, компетенции сопровождения. Все причины, по которым платформа в этой среде отсутствует, при расширении сохраняются. Расширение Kubernetes не может войти в инфраструктуру, которая Kubernetes запрещает. Поэтому Horchestra заимствует модель, а не платформу: желаемое состояние, агент, цикл сверки — и ничего из тяжёлого распределённого ядра.

Транспорт и сетевая модель

Из требований сред, куда предстоит доставлять, напрямую следует архитектура связи. Взаимодействие Controller ↔ Agent построено на gRPC bidirectional streaming поверх mTLS: агент сам инициирует единственное исходящее соединение, открывает один Session-поток и переустанавливает его с backoff при обрыве. По этому каналу идут оба направления — вниз желаемое состояние (закреплённые за нодой Application и PersistentVolume), вверх статус, события и потоковая передача журналов.

Ключевое следствие: на управляемой ноде не публикуется ни одного входящего порта. Даже kubectl logs реализован без отдельного HTTP-API на агенте — контроллер резолвит spec.node приложения и открывает поток к ноде поверх уже установленной сессии, а агент отдаёт journalctl -u horchestra-app-<name>. Для корпоративной сетевой модели это снимает целый класс согласований. Идентичность ноды берётся из CN клиентского сертификата в mTLS-рукопожатии, а сессия отклоняется, если сертификат не несёт группу system:nodes.

Встраивание в инфраструктуру: dex работает, Kerberos встраивается глубже

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

Kubernetes API умеет OIDC, но не умеет напрямую ни Kerberos, ни то, на чём фактически держится корпоративная identity. Канонический путь — поставить рядом dex: он федерирует AD или LDAP и выдаёт OIDC-токены, понятные kube-apiserver. Схема рабочая, но по существу это переходник — заказчик приносит свою identity, а её приходится оборачивать в OIDC, потому что другого языка платформа не знает, — и это ещё один компонент под развёртывание, настройку и аудит ИБ.

С точки зрения безопасника на AD или FreeIPA Kerberos — не новинка, а основа: KDC как доверенный центр, билеты как аудируемая механика, SSO по тикету как норма. Поскольку я не связан моделью аутентификации kube-apiserver и сам определяю, что говорит control-plane, есть архитектурная свобода встроиться в этот механизм напрямую — GSSAPI/SPNEGO поверх существующего KDC, без промежуточного OIDC-стека. Позиционирование принципиально разное: Kubernetes с dex говорит «принеси свою identity, я оберну её в OIDC»; свободный от платформы оркестратор — «я говорю на Kerberos, который твоя ИБ уже знает». Оговорюсь: сегодня идентичность операторов реализована через mTLS с RBAC/Casbin, а Kerberos — направление, которое архитектура делает возможным именно потому, что API-поверхность контролирую я, а не унаследованный apiserver.

Исполнительный слой: systemd и OCI без собственного рантайма

Как Kube-way исполняется без платформы — второе принципиальное решение: не писать собственный механизм управления процессами, а стать тонким декларативным слоем над Linux.

Ресурсные ограничения Application отображаются на cgroup самого unit: requests становятся CPUWeight (относительная доля) и MemoryLow (защита от вытеснения), limitsCPUQuota (жёсткий процентный потолок ядра) и MemoryMax (жёсткий предел с OOM за границей). Изоляция задаётся директивами hardening-юнита — RootDirectory, User, NoNewPrivileges, ProtectSystem, ProtectHome, PrivateTmp — поверх read-only overlay-rootfs, собранного из общего для хоста oci-layout с дедупликацией слоёв между приложениями. PersistentVolume живёт отдельным жизненным циклом и bind-монтируется в иначе неизменяемый rootfs, будучи выведенным из-под ProtectSystem=strict.

Сверка построена как level-driven self-heal, а не как оптимистичный diff. Источник истины для «что должно работать» — контроллер; для «что работает» — сами systemd-юниты ноды; агент не хранит персистентной записи о развёрнутом. Каждое приложение сходится идемпотентно: образ подтягивается только при отсутствии, rootfs перемонтируется при необходимости, unit переписывается при расхождении, сервис стартует, если не активен. Отсюда самовосстановление после перезагрузки: включённый unit агента поднимается, переподключается и заново сходит каждое приложение из образов и данных томов на диске. Мультиарх-тег резолвится только под платформу хоста, а рекультивация томов намеренно консервативна — пустой список PV трактуется как возможная потеря состояния контроллера, а не как команда «удалить всё».

Спектр изоляции и вопрос «контейнера»

Здесь стоит снять типичное возражение: раз есть overlay-rootfs, cgroups и namespaces — это контейнер без ярлыка, и значит запрет на контейнеры обходится подменой понятий. Возражение неточно.

В текущем варианте развёртывания сетевая изоляция не используется: своего data-plane у оркестратора нет, приложение доступно на адресе своей ноды на объявленных портах. По способу запуска это ближе к приложению, установленному из пакетов и запущенному через systemd с chroot, чем к контейнеру. Более того, вместо обычного образа можно взять scratch или distroless, если приложение это допускает: результат по существу совпадает с установкой из пакетов — бинарник, зависимости, точка входа, ничего сверх. Такой запуск уже некорректно квалифицировать как «использование контейнеров»: нет ни контейнерного рантайма, ни сетевой изоляции, ни демона.

Оставшаяся изоляция работает не на маскировку, а на усиление защиты. Read-only rootfs, NoNewPrivileges, ProtectSystem, ProtectHome, PrivateTmp ограничивают приложение сильнее, чем штатная установка из пакета. Для запуска недоверенного или сомнительного кода это выигрыш, а не лазейка: прав выдаётся меньше, а не больше. Практически корректнее говорить не «контейнер или нет», а о настраиваемом спектре — от «эквивалент установки из пакета» до «жёстко ограниченный sandbox» — по модели угроз конкретного заказчика.

Позиционирование: почему альтернативы ломают унификацию

Соседние инструменты правильно оценивать не по вопросу «есть ли оркестратор полегче», а по единственному критерию: сохраняют ли они одну модель доставки поверх реального Kubernetes заказчика и закрытых для платформы хостов одновременно.

k3s / k0s — это Kubernetes в компактной поставке. Там, где Kubernetes запрещён, запрещён и k3s: число бинарников не меняет предмета ИБ-аудита. Целевой — «запретный» — случай он не решает вовсе.

Podman + Quadlet — отличный примитив «OCI-образ как systemd-юнит», и именно его Horchestra использует внизу. Но это другая модель: одна машина, без control-plane и без Kube-way. Приняв Quadlet как способ доставки, вы форкаете модель заново — рядом с кубовой у заказчика появляется вторая, несовместимая, — и унификация рушится.

HashiCorp Nomad — зрелый оркестратор, но со своей экосистемой, языком и интерфейсом. Это ещё одна модель, которую пришлось бы поддерживать параллельно с кластером заказчика; снова форк, снова две кодовые базы доставки.

Различие сводится к одному: только сохранение самой модели Kubernetes позволяет одному описанию покрыть оба мира — и реальный кластер, и ограниченный хост. Именно эту ось перечисленные инструменты не держат, а Horchestra держит по построению.

Границы и дальнейшее развитие

Ограничение «одно приложение — одна нода» — сознательное сужение для проверки исходной гипотезы, а не следствие незавершённости. При этом честнее обозначить настоящие обрывы сложности, чем декларировать плавную дорожную карту. Планировщик размещения, отказоустойчивость control-plane (сегодня состояние в embedded BoltDB, что делает контроллер одноузловым) и управление секретами (сейчас окружение через spec.env, что для чувствительных данных требует монтируемого файла) — это не доработки текущего слоя, а отдельные подсистемы. Фильтрацию узлов, учёт ресурсов, affinity/anti-affinity, taints и tolerations разумно адаптировать из решений Kubernetes, не воспроизводя платформу целиком, — но путь от идемпотентной сверки одного unit до распределённого планировщика следует оценивать как самостоятельный объём работы.

Итог

OCI унифицировал упаковку приложения. Horchestra ставит целью унифицировать модель доставки поверх неё, взяв за общий язык подход Kubernetes. Проект не вытесняет Kubernetes: где платформа есть — она и остаётся нативной целью; где платформа под запретом — Horchestra переносит тот же Kube-way на голый Linux, и в смешанном парке они работают совместно. k3s оставляет заказчику Kubernetes и потому не входит в закрытые среды; Quadlet и Nomad вводят другую модель и потому ломают единство доставки. Суть же в одном: описать приложение один раз и доставить его во все среды сразу — включая те, где Kubernetes не может быть развёрнут. Станет ли это чем-то большим, чем исследовательский проект, определит практика — но ось, по которой его следует оценивать, теперь названа явно: единство доставки, а не соревнование с платформой.