В конце 2024 года клиенты и потенциальные пользователи Oxide активно интересовались запуском Kubernetes на платформе, но поддерживаемых интеграций для этого не существовало.

Kubernetes и Oxide хорошо дополняют друг друга. Kubernetes описывает ожидаемое поведение инфраструктуры через стандартные точки расширения, а Oxide предоставляет через API примитивы, необходимые для реализации этого поведения. Фундамент для интеграции уже был — не хватало только программного обеспечения и понимания, какие именно интеграции нужны клиентам.

Такая ситуация сложилась к моменту, когда Мэттью Санабрия присоединился к Oxide в качестве первого Solutions Software Engineer[1], сосредоточившись на разработке ПО для решения клиентских задач. Первым заданием стало упрощение развёртывания и эксплуатации Kubernetes на Oxide.

В первую неделю в распоряжении оказалось два ресурса для старта:

  1. Присланный клиентом pull request с драйвером узла для Rancher

  2. Ранний черновик RFD 493 Initial Kubernetes Integrations

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

Ниже проблемы рассматриваются в порядке жизненного цикла Kubernetes, а не в строго хронологическом. Разные сценарии развёртывания привели к работе с Rancher, Omni и Cluster API. Эксплуатация кластеров потребовала согласования состояния инфраструктуры, публикация приложений вскрыла пробелы в сетевой части, а stateful-нагрузки обнажили ограничения хранилища. На каждом этапе рабочие процессы клиентов открывали следующий пробел, определяя как построенные интеграции, так и оставшуюся работу над платформой.

Как развернуть кластер Kubernetes на Oxide?

Первым пробелом, за который взялась команда, стало развёртывание. Непосредственной целью было разблокировать клиента, приславшего pull request с драйвером узла для Rancher. Работа над этим кейсом также дала команде опыт создания кластеров Kubernetes на Oxide и помогла выявить следующие задачи.

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

Rancher Node Driver

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

Rancher node driver — это исполняемый плагин, который учит Rancher создавать и управлять виртуальными машинами на конкретной инфраструктурной платформе. Rancher node driver для Oxide транслирует эти операции в запросы к API Oxide. После установки в Rancher он позволяет клиентам развёртывать инстансы Oxide как узлы в управляемых Rancher кластерах Kubernetes.

Тестирование подтвердило, что реализация клиента работала. Pull request был смёржен, добавлены улучшения CI/CD и документации, опубликован первый релиз. У Oxide официально появилась первая интеграция с Kubernetes — и клиент уже успешно использовал её в продакшене!

Тем, кто работает с Rancher и хочет запускать Kubernetes на Oxide, стоит обратиться к руководству по Rancher.

Omni Infrastructure Provider

Клиенты проявляли интерес к использованию Omni от Sidero Labs для развёртывания кластеров Kubernetes на базе Talos Linux. Omni подключается к инфраструктурным платформам через инфраструктурных провайдеров — программы, которые создают инстансы Talos Linux и регистрируют их в Omni.

За несколько месяцев до KubeCon North America 2025 появилась возможность партнёрства с Sidero Labs для создания и демонстрации инфраструктурного провайдера Oxide для Omni. На завершение работы перед совместным мероприятием Oxide+Sidero было семь недель[2]. Работа со второй платформой развёртывания также стала проверкой API Oxide на разных клиентских сценариях.

Работа над интеграцией выявила несколько проблем в Omni и Talos Linux. Они были вынесены на обсуждение с Sidero Labs в siderolabs/omni#1633, где команда Sidero охотно включилась в совместную работу — приятное напоминание о RFD 68 Partnership as Shared Values.

Самой запоминающейся стала проблема siderolabs/talos#11948. Oxide использует файловую систему FAT12 для cloud-init user-data, а не ISO 9660, но проверка файловой системы в Talos пыталась прочитать только суперблок ISO 9660 с диска конфигурации NoCloud. Когда это чтение не удавалось, проверка останавливалась, вместо того чтобы пробовать другие форматы вроде VFAT или MS-DOS. В результате Talos никогда не читал user-data Oxide с конфигурацией, необходимой для подключения к Omni. Исправление не успевало выйти к KubeCon, так что пришлось прибегнуть к довольно забавному обходному пути.

Обходное решение сейчас — дополнить user-data комментариями, чтобы увеличить его размер настолько, что будет использоваться суперблок ISO 9660.

К моменту KubeCon состоялось мероприятие Oxide+Sidero, на котором был представлен инфраструктурный провайдер Oxide для Omni. Теперь клиенты могут использовать этот провайдер для развёртывания инстансов Oxide с Talos Linux как узлов в управляемых Omni кластерах Kubernetes.

Тем, кто работает с Omni или Talos Linux и хочет запускать Kubernetes на Oxide, стоит обратиться к руководству по Omni.

Cluster API Provider

Идея создать инфраструктурного провайдера для Kubernetes Cluster API (CAPI) появилась ещё при написании RFD 493 Initial Kubernetes Integrations. Cluster API предлагает то, чего не было в первых двух интеграциях — upstream-совместимый, расширяемый провайдерами API для управления кластерами без необходимости в стороннй платформе вроде Rancher или Omni.

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

Со временем ситуация изменилась в обеих составляющих. Клиенты начали запрашивать провайдер CAPI, а команда Solutions Software Engineering выросла. Коллеги Джош и Брэндон взяли на себя эту работу и выпустили Cluster API Provider Oxide (CAPOx), дав клиентам Kubernetes-нативный способ развёртывания кластеров на Oxide.

Рабочий процесс Cluster API также задействует несколько других интеграций, позволяя протестировать сквозной процесс работы с кластером на собственном опыте[3]. Kubernetes Image Builder использует Packer-плагин для создания готовых к CAPI образов VM Oxide, которые CAPOx применяет при развёртывании инстансов. Кластеры, развёрнутые с помощью CAPOx, также используют отдельно устанавливаемый Oxide cloud controller manager (CCM) для интеграции Kubernetes с Oxide во время работы.

Тем, кто хочет развёртывать кластеры Kubernetes на Oxide с помощью Cluster API, стоит обратиться к руководству по Cluster API.

Как Kubernetes отслеживает инстансы Oxide?

Интеграции для развёртывания создают и управляют инстансами Oxide, но не согласуют их с объектами Node в Kubernetes. Без такого согласования кластер не мог бы надёжно определить, временно ли недоступен узел Kubernetes или его инстанс Oxide уже удалён.

Требовался компонент, работающий в каждом кластере, взаимодействующий с API Oxide и непрерывно согласующий инфраструктуру Oxide с состоянием Kubernetes. Для этого в Kubernetes есть стандартная точка расширения — cloud controller manager (CCM). CCM позволяет специфичным для инфраструктуры контроллерам интегрировать ресурсы Kubernetes с API провайдера инфраструктуры, не добавляя провайдеро-специфичный код в сам Kubernetes.

Был создан Oxide cloud controller manager для связи Kubernetes с Oxide. Его node-контроллер поддерживает синхронизацию объектов Node Kubernetes с соответствующими инстансами Oxide, фиксируя такие детали, как ID инстансов и сетевые адреса, и сообщая, работает ли инстанс, остановлен или уже не существует. Kubernetes использует эту информацию для инициализации узлов и безопасного удаления при удалении соответствующих инстансов.

CCM не создаёт инстансы и не разворачивает кластеры — эта работа остаётся за интеграциями развёртывания вроде Rancher node driver, Omni infrastructure provider и CAPOx. Вместо этого CCM предоставляет общую runtime-интеграцию для всех этих рабочих процессов.

Важно, что создание CCM дало устойчивую точку расширения внутри каждого кластера. По мере развития Oxide новые контроллеры, осведомлённые об инфраструктуре, можно добавлять в CCM, а не обновлять каждую интеграцию развёртывания по отдельности.

С этой runtime-точкой расширения появилась возможность заняться следующим уровнем работы с Kubernetes — публикацией приложений. Архитектура CCM также определяет service-контроллер для сервисов Kubernetes типа LoadBalancer, что дало место для решения следующей клиентской задачи.

Как использовать сервисы LoadBalancer?

Одна из возможностей, которую клиенты ожидают от облачно-интегрированного Kubernetes — поддержка объектов Service типа LoadBalancer. Когда пользователь создаёт такой объект, Kubernetes просит service-контроллер облачного провайдера развернуть необходимую инфраструктуру и опубликовать её адрес в статусе Service. Проблема была одна: нативного балансировщика нагрузки у Oxide на тот момент не было.

Зато у Oxide были floating IP — адреса из внешних IP-пулов стойки, которые можно присоединять к инстансам и отсоединять от них, делая инстансы доступными извне их VPC. Использование floating IP давало способ разблокировать сервисы LoadBalancer: floating IP доставлял бы трафик на один узел Kubernetes, а дата-плейн Service распределял бы его по нужным подам.

Для реализации потребовалось учесть, как floating IP Oxide выглядят с точки зрения инстанса. Они прозрачны для гостевой системы в двух важных отношениях. Во-первых, Oxide транслирует адрес назначения входящего трафика во внутренний IP инстанса перед отправкой трафика на него. Во-вторых, у инстанса нет сетевого интерфейса, настроенного на floating IP.

Итоговый поток трафика выглядит так:

Поток трафика к сервису LoadBalancer через floating IP.

┌────────────────────────────────────────────────────────────┐
│ Client                                                     │
│ Request to floating IP: 45.154.216.233:80                  │
└────────────────────────────────────────────────────────────┘
                               │
                               ▼
┌────────────────────────────────────────────────────────────┐
│ Oxide networking                                           │
│ Translates destination to internal IP: 172.30.0.5:80       │
└────────────────────────────────────────────────────────────┘
                               │
                               ▼
┌────────────────────────────────────────────────────────────┐
│ Kubernetes node                                            │
│ Packet arrives at internal IP: 172.30.0.5:80               │
└────────────────────────────────────────────────────────────┘
                               │
                               ▼
┌────────────────────────────────────────────────────────────┐
│ Kubernetes Service dataplane                               │
│ Selects a Service endpoint                                 │
└────────────────────────────────────────────────────────────┘
                               │
                               ▼
┌────────────────────────────────────────────────────────────┐
│ Pod                                                        │
│ Receives traffic on its target port                        │
└────────────────────────────────────────────────────────────┘

Такая трансляция адресов создала небольшую проблему интеграции. Дата-плейну Service Kubernetes нужно было воспринимать внутренний IP узла как фронтенд Service, поскольку именно этот адрес назначения фактически несли пакеты, доходя до гостевой системы. Поэтому service-контроллер публикует две записи в status.loadBalancer.ingress[4]:

  1. Присоединённый floating IP в режиме Proxy

  2. Внутренний IP узла в режиме VIP

Записи статуса выглядят так:

status:
  loadBalancer:
    ingress:
      - ip: 45.154.216.233
        ipMode: Proxy
      - ip: 172.30.0.5
        ipMode: VIP

В результате вывод kubectl выглядит несколько необычно:

$ kubectl get service nginx
NAME    TYPE           CLUSTER-IP       EXTERNAL-IP                 PORT(S)        AGE
nginx   LoadBalancer   10.106.122.233   45.154.216.233,172.30.0.5   80:30605/TCP   37h

Пользователи видят и floating IP, и внутренний IP узла в колонке EXTERNAL-IP, хотя доступен извне только floating IP. Это несовершенная абстракция, но она позволяет поддержать распространённый сценарий работы Kubernetes, пока нативный балансировщик Oxide ещё не готов.

Текущая реализация поддерживает externalTrafficPolicy: Cluster[5], что позволяет выбранному узлу пересылать трафик к любому endpoint Service в кластере. Если этот узел исчезает, CCM переносит floating IP на другой подходящий узел и обновляет внутренний адрес в статусе Service.

Когда у Oxide появится нативный сервис балансировки нагрузки, service-контроллер можно будет обновить для его использования без изменения интерфейса Kubernetes. Клиенты продолжат создавать те же сервисы LoadBalancer, изменится только инфраструктура за ними.

Для установки Oxide CCM на кластер стоит обратиться к руководству по CCM.

Как использовать хранилище Oxide в Kubernetes?

После того как кластеры стали развёртываться, согласовываться с Oxide и быть доступными извне VPC, следующим уровнем стало хранилище для stateful-нагрузок. Пользователи Kubernetes запрашивают постоянное хранилище через объекты PersistentVolumeClaim и ожидают, что драйвер Container Storage Interface (CSI) создаст, присоединит и смонтирует нужные тома. У Oxide были диски, но у Kubernetes не было нативного способа управлять их жизненным циклом.

Без CSI-драйвера Oxide клиенты могли развернуть стороннюю систему хранения для Kubernetes, например Longhorn. Longhorn предоставляет собственный CSI-драйвер и реплицирует данные между дисками, присоединёнными к воркерам Kubernetes. Однако использование Longhorn означало размещение его реплик на распределённых дисках Oxide, которые уже хранят три реплики на разных sled-узлах.

Наложение одной реплицируемой системы хранения на другую может создавать существенное расширение записи. Когда трёхреплик Longhorn-том опирается на трёхреплик распределённые диски Oxide, одна запись приложения может превратиться в до девяти операций записи на диск. Точный коэффициент физического усиления записи зависит от нагрузки и конфигурации, но клиенты хотели избежать такого дублирования репликации.

Появление локальных дисков Oxide дало способ убрать второй уровень репликации. Локальные диски не имеют встроенной репликации и привязаны к своему sled-узлу, что хорошо подходит для систем вроде Longhorn, реплицирующих данные между узлами Kubernetes. Демонстрационный проект с Rancher уже использует такой подход сегодня. Это позволяет избежать наложения двух реплицируемых систем хранения, хотя жизненным циклом хранилища по-прежнему управляет Longhorn, а не нативная интеграция Oxide.

Для нативной интеграции коллега Луис написал RFD 595 Oxide CSI Plugin. На бумаге рабочий процесс выглядел просто. Когда пользователь создаёт PersistentVolumeClaim, CSI-контроллер создаёт распределённый диск Oxide. После того как Kubernetes планирует под, контроллер присоединяет этот диск к выбранному инстансу Oxide, а CSI node-плагин форматирует и монтирует его для пода. Если под перепланируется на другой узел, контроллер отсоединяет диск и присоединяет его к новому узлу.

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

Прежде чем выпускать CSI-плагин, нужно добавить поддержку горячего подключения дисков по всему стеку Oxide — от гипервизора до API. То, что начиналось как интеграция с Kubernetes, превратилось в проект, затрагивающий сразу несколько слоёв программного стека Oxide.

На момент написания материала горячее подключение дисков и CSI-плагин Oxide находятся в активной разработке. Пока клиенты могут использовать такое ПО, как Longhorn, вместе с локальными дисками Oxide для динамически выделяемого постоянного хранилища без наложения двух уровней репликации. Когда нативный CSI-плагин выйдет, клиенты смогут пользоваться привычными API хранилища Kubernetes, опирающимися напрямую на распределённые диски Oxide со встроенной репликацией и надёжностью.

Что дальше?

Результатом стала не единичная интеграция с Kubernetes, а растущая экосистема. Rancher, Omni и Cluster API предоставляют разные пути развёртывания, а Oxide CCM обеспечивает общую runtime-интеграцию для согласования узлов и сервисов LoadBalancer. Некоторые из этих интеграций клиенты уже используют в продакшене, а часть используется во внутренних продакшен-нагрузках самой компании. Вместе они образуют прочный фундамент для дальнейшего развития.

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

Работы ещё немало. В ближайших планах — завершить горячее подключение дисков и выпустить CSI-плагин, добавить поддержку автомасштабирования, расширить service-контроллер CCM для поддержки внешних подсетей. В более отдалённой перспективе, с появлением тегирования ресурсов, поддержки OIDC и нативной балансировки нагрузки, интеграции с Kubernetes будут расширены с учётом этих возможностей.

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

Эта работа также даёт возможность опробовать собственные SDK и API с точки зрения клиентов и превращать возникающие у них трудности в улучшения продукта. Именно такой цикл обратной связи будет и дальше двигать развитие экосистемы. Потребности клиентов сформировали каждую из описанных интеграций — и сформируют следующую.

Демонстрация

Чтобы увидеть интеграции Cluster API и cloud controller manager в действии, можно посмотреть видео ниже с развёртыванием кластера Kubernetes на Oxide.


[1] Теперь под эту работу есть целая команда! Подробнее — в выпуске подкаста Oxide and Friends Solutions Software Engineering with Matthew Sanabria.

[2] Мероприятие Oxide+Sidero состоялось 12 ноября 2025 года. Работа над инфраструктурным провайдером Omni началась 24 сентября.

[3] Dogfooding — практика использования собственных продуктов или сервисов компании. В офисе Oxide есть стойка под названием dogfood, посвящённая именно этому.

[4] Kubernetes использует ipMode, чтобы указать, доходит ли трафик до узла с адресом балансировщика в качестве адреса назначения (VIP) или после трансляции адреса назначения (Proxy).

[5] Поддержка externalTrafficPolicy: Local потребовала бы, чтобы floating IP следовал за узлами с локальными endpoint'ами Service по мере перепланирования подов, что привело бы к большему числу операций присоединения и отсоединения.