Как мы построили динамический Kubernetes API server для слоя агрегации API в Cozystack
Как мы построили динамический Kubernetes API server для слоя агрегации API в Cozystack
Привет! Я Андрей Квапиль, но в сообществах, посвящённых Kubernetes и cloud-native инструментам, вы можете знать меня как @kvaps. В этой статье я хочу рассказать, как мы реализовали собственный расширенный API server в open-source PaaS-платформе Cozystack.
Kubernetes по-настоящему восхищает меня своими мощными возможностями расширения. Вероятно, вы уже знакомы с концепцией контроллера и фреймворками вроде kubebuilder и operator-sdk, которые помогают его реализовать. Если коротко, они позволяют расширять кластер Kubernetes, определяя пользовательские ресурсы (CRD) и написав дополнительные контроллеры, которые реализуют вашу бизнес-логику для согласования и управления такими ресурсами. Этот подход хорошо задокументирован, и в интернете доступно множество информации о том, как разрабатывать собственные операторы.

Однако это не единственный способ расширить Kubernetes API. Для более сложных сценариев — таких как реализация императивной логики, управление подресурсами и динамическая генерация ответов — эффективной альтернативой служит слой агрегации Kubernetes API. С помощью слоя агрегации вы можете разработать собственный расширенный API server и без труда интегрировать его в общую структуру Kubernetes API.
В этой статье я рассмотрю слой агрегации API, типы задач, для которых он хорошо подходит, случаи, когда он может быть менее уместен, и то, как мы использовали эту модель для реализации собственного расширенного API server в Cozystack.
Что такое слой агрегации API?
Сначала давайте разберёмся с определениями, чтобы избежать путаницы в дальнейшем. Слой агрегации API — это возможность Kubernetes, тогда как расширенный API server — это конкретная реализация API server для слоя агрегации. Расширенный API server ничем не отличается от стандартного Kubernetes API server, за исключением того, что он запускается отдельно и обрабатывает запросы к вашим специфическим типам ресурсов.
Итак, слой агрегации позволяет написать собственный расширенный API server, легко интегрировать его в Kubernetes и напрямую обрабатывать запросы к ресурсам определённой группы. В отличие от механизма CRD, расширенный API регистрируется в Kubernetes как APIService, сообщая Kubernetes о необходимости учитывать этот новый API server и признавать, что он обслуживает определённые API.
Вы можете выполнить эту команду, чтобы вывести список всех зарегистрированных apiservice:
kubectl get apiservices.apiregistration.k8s.io
Пример APIService:
NAME SERVICE AVAILABLE AGE
v1alpha1.apps.cozystack.io cozy-system/cozystack-api True 7h29m
Как только Kubernetes api-server получает запросы к ресурсам группы v1alpha1.apps.cozystack.io, он перенаправляет все эти запросы нашему расширенному API server, который может обрабатывать их на основе встроенной в него бизнес-логики.
Когда использовать слой агрегации API
Слой агрегации API помогает решить несколько задач, с которыми обычного механизма CRD может быть недостаточно. Давайте разберём их.
Императивная логика и подресурсы
Помимо обычных ресурсов, в Kubernetes есть так называемые подресурсы.
В Kubernetes подресурсы — это дополнительные действия или операции, которые можно выполнять над основными ресурсами (такими как Pod, Deployment, Service) через Kubernetes API. Они предоставляют интерфейсы для управления отдельными аспектами ресурсов, не затрагивая объект целиком.
Простой пример — status, который традиционно предоставляется как отдельный подресурс, доступный независимо от родительского объекта. Поле status не предназначено для изменения
Но помимо /status, у Pod в Kubernetes есть и такие подресурсы, как /exec, /portforward и /log. Что интересно, вместо привычных декларативных ресурсов Kubernetes они представляют собой конечные точки для императивных операций — таких как просмотр логов, проксирование соединений, выполнение команд в работающем контейнере и так далее.
Чтобы поддержать такие императивные команды в собственном API, нужно реализовать расширенный API и расширенный API server. Вот несколько хорошо известных примеров:
- KubeVirt: дополнение для Kubernetes, расширяющее возможности его API для запуска традиционных виртуальных машин. Расширенный API server, созданный в составе KubeVirt, обрабатывает такие подресурсы виртуальных машин, как
/restart,/consoleи/vnc. - Knative: дополнение для Kubernetes, расширяющее его возможности для бессерверных вычислений и реализующее подресурс
/scaleдля настройки автомасштабирования своих типов ресурсов.
Кстати, хотя логика подресурсов в Kubernetes может быть императивной, управлять доступом к ним можно декларативно, используя стандартную модель RBAC Kubernetes.
Например, вот так можно управлять доступом к подресурсам /log и /exec объекта типа Pod:
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: pod-and-pod-logs-reader
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
Вы не привязаны к использованию etcd
Обычно Kubernetes API server использует в качестве бэкенда etcd. Однако реализация собственного API server не обязывает вас использовать только etcd. Если хранить состояние вашего сервера в etcd не имеет смысла, вы можете хранить информацию в любой другой системе и генерировать ответы на лету. Вот несколько примеров для иллюстрации:
- metrics-server — стандартное расширение для Kubernetes, которое позволяет просматривать метрики ваших узлов и подов в реальном времени. Оно определяет альтернативные типы Pod и Node в собственном API metrics.k8s.io. Запросы к этим ресурсам преобразуются в метрики напрямую из Kubelet. Поэтому, когда вы выполняете
kubectl top nodeилиkubectl top pod, metrics-server в реальном времени получает метрики из cAdvisor. Затем он возвращает эти метрики вам. Поскольку информация формируется в реальном времени и актуальна только в момент запроса, хранить её в etcd не нужно. Такой подход экономит ресурсы. - При необходимости можно использовать бэкенд, отличный от etcd. Вы даже можете реализовать для него Kubernetes-совместимый API. Например, если вы используете Postgres, можно создать прозрачное представление его сущностей в Kubernetes API. Скажем, базы данных, пользователи и права (grants) внутри Postgres будут отображаться как обычные ресурсы Kubernetes благодаря вашему расширенному API server. Управлять ими можно с помощью
kubectlили любого другого Kubernetes-совместимого инструмента. В отличие от контроллеров, которые реализуют бизнес-логику через пользовательские ресурсы и методы согласования, расширенный API server избавляет от необходимости в отдельных контроллерах для каждого типа. Это означает, что вам не нужно синхронизировать состояние между Kubernetes API и вашим бэкендом.
Одноразовые ресурсы
- В Kubernetes есть специальный API, предназначенный для предоставления пользователям информации об их правах. Он реализован с помощью API SelfSubjectAccessReview. Одна необычная деталь этих ресурсов в том, что их нельзя просмотреть с помощью глаголов get или list. Их можно только создавать (с помощью глагола create) и получать в ответ информацию о том, к чему у вас есть доступ в данный момент.
- Если попытаться напрямую выполнить
kubectl get selfsubjectaccessreviews, вы просто получите ошибку вроде такой:
Error from server (MethodNotAllowed): the server does not allow this method on the requested resource
- Причина в том, что Kubernetes API server не поддерживает никакого другого взаимодействия с этим типом ресурсов (их можно только СОЗДАВАТЬ).
- API SelfSubjectAccessReview поддерживает команды вроде
kubectl auth can-i create deployments --namespace dev - Когда вы выполняете приведённую выше команду,
kubectlсоздаёт SelfSubjectAccessReview через Kubernetes API. Это позволяет Kubernetes получить список возможных прав для вашего пользователя. Затем Kubernetes в реальном времени формирует персонализированный ответ на ваш запрос. Эта логика отличается от сценария, где такой ресурс просто хранится в etcd. - Аналогично, в расширении
CDI (Containerized Data Importer) от KubeVirt, которое позволяет загружать файлы в PVC с локальной машины с помощью инструмента
virtctl, перед началом процесса загрузки требуется специальный токен. Этот токен генерируется путём создания ресурса UploadTokenRequest через Kubernetes API. Kubernetes направляет (проксирует) все запросы на создание ресурса UploadTokenRequest расширенному API server CDI, который генерирует и возвращает токен в ответ.
Полный контроль над преобразованием, валидацией и форматированием вывода
- Ваш собственный API server может обладать всеми возможностями стандартного Kubernetes API server. Ресурсы, которые вы создаёте в своём API server, могут проходить валидацию сразу на стороне сервера без дополнительных webhook’ов. Хотя CRD тоже поддерживают серверную валидацию с помощью Common Expression Language (CEL) для декларативной валидации и ValidatingAdmissionPolicies без необходимости в webhook’ах, собственный API server при необходимости позволяет реализовать более сложную и специализированную логику валидации.
- Kubernetes позволяет обслуживать несколько версий API для каждого типа ресурса, традиционно
v1alpha1,v1beta1иv1. В качестве версии хранилища можно указать только одну версию. Все запросы к другим версиям должны автоматически преобразовываться в версию, указанную как версия хранилища. В случае CRD этот механизм реализуется с помощью conversion webhooks. Тогда как в расширенном API server вы можете реализовать собственный механизм преобразования, смешивать разные версии хранилища (один объект может сериализоваться какv1, другой — какv2) или полагаться на внешний обслуживающий API. - Прямая реализация Kubernetes API позволяет форматировать табличный вывод так, как вам угодно, и не заставляет следовать логике
additionalPrinterColumnsиз CRD. Вместо этого вы можете написать собственный форматтер, который форматирует табличный вывод и пользовательские поля в нём. Например, при использованииadditionalPrinterColumnsможно отображать значения полей только по логике JSONPath. В собственном API server вы можете генерировать и вставлять значения на лету, форматируя табличный вывод как пожелаете.
Динамическая регистрация ресурсов
Ресурсы, обслуживаемые расширенным API server, не обязательно предварительно регистрировать как CRD. Как только ваш расширенный API server зарегистрирован через APIService, Kubernetes начинает опрашивать его для обнаружения API и ресурсов, которые он может обслуживать. Получив ответ discovery, Kubernetes API server автоматически регистрирует все доступные типы для этой группы API. Хотя это не считается распространённой практикой, вы можете реализовать логику, которая динамически регистрирует нужные вам типы ресурсов в кластере Kubernetes.
Когда не стоит использовать слой агрегации API
Существует несколько антипаттернов, при которых использовать слой агрегации API не рекомендуется. Давайте пройдёмся по ним.
Нестабильный бэкенд
Если ваш API server по какой-то причине перестанет отвечать — из-за недоступного бэкенда или других проблем, — это может заблокировать часть функциональности Kubernetes. Например, при удалении пространств имён Kubernetes будет ждать ответа от вашего API server, чтобы проверить, не остались ли какие-либо ресурсы. Если ответ не придёт, удаление пространства имён будет заблокировано.
Кроме того, вы могли сталкиваться с
ситуацией, когда при недоступности metrics-server после каждого запроса к API (даже не связанного с метриками) в stderr появляется дополнительное сообщение о том, что metrics.k8s.io недоступен. Это ещё один пример того, как использование слоя агрегации API может приводить к проблемам, когда обрабатывающий запросы api-server недоступен.
Медленные запросы
Если вы не можете гарантировать мгновенный ответ на запросы пользователей, лучше рассмотреть использование CustomResourceDefinition и контроллера. Иначе вы можете сделать свой кластер менее стабильным. Многие проекты реализуют расширенный API server только для ограниченного набора ресурсов, в частности для императивной логики и подресурсов. Эта рекомендация также упоминается в официальной документации Kubernetes.
Зачем это понадобилось нам в Cozystack
Напомню, мы разрабатываем open-source PaaS-платформу Cozystack, которую также можно использовать как фреймворк для построения собственного частного облака. Поэтому возможность легко расширять платформу критически важна для нас.
Cozystack построен поверх FluxCD. Любое приложение упаковывается в собственный Helm-чарт, готовый к развёртыванию в пространстве имён арендатора. Развёртывание любого приложения на платформе выполняется путём создания ресурса HelmRelease с указанием имени чарта и параметров приложения. Всю остальную логику берёт на себя FluxCD. Этот паттерн позволяет нам легко расширять платформу новыми приложениями и даёт возможность создавать новые приложения, которые нужно лишь упаковать в соответствующий Helm-чарт.

Интерфейс платформы Cozystack
Итак, в нашей платформе всё настраивается через ресурсы HelmRelease. Однако мы столкнулись с двумя проблемами: ограничениями модели RBAC и необходимостью в публичном API. Давайте разберём их подробнее
Ограничения модели RBAC
Широко используемая система RBAC в Kubernetes не позволяет ограничивать доступ к списку ресурсов одного типа на основе меток или конкретных полей в spec. При создании роли вы можете ограничить доступ к ресурсам одного типа только путём указания конкретных имён ресурсов в resourceNames. Для глаголов вроде get или update это работает. Однако фильтрация по resourceNames с глаголом list работает иначе. Таким образом, вы можете ограничить вывод списка определённых ресурсов по типу, но не по имени.
- В Kubernetes есть специальный API, предназначенный для предоставления пользователям информации об их правах. Он реализован с помощью API SelfSubjectAccessReview. Одна необычная деталь этих ресурсов в том, что их нельзя просмотреть с помощью глаголов get или list. Их можно только создавать (с помощью глагола create) и получать в ответ информацию о том, к чему у вас есть доступ в данный момент.
Поэтому мы решили ввести новые типы ресурсов на основе имён Helm-чартов, которые они используют, и генерировать список доступных типов динамически во время выполнения в нашем расширенном API server. Так мы можем повторно использовать стандартную модель RBAC Kubernetes для управления доступом к конкретным типам ресурсов.
Необходимость в публичном API
Поскольку наша платформа предоставляет возможности для развёртывания различных управляемых сервисов, мы хотим организовать публичный доступ к API платформы. Однако мы не можем позволить пользователям напрямую взаимодействовать с ресурсами вроде HelmRelease, так как это дало бы им возможность указывать произвольные имена и параметры для развёртывания Helm-чартов, потенциально компрометируя нашу систему.
Мы хотели дать пользователям возможность развёртывать конкретный сервис, просто создавая ресурс соответствующего типа в Kubernetes. Тип этого ресурса должен называться так же, как чарт, из которого он развёртывается. Вот несколько примеров:
kind: Kubernetes→chart: kuberneteskind: Postgres→chart: postgreskind: Redis→chart: rediskind: VirtualMachine→chart: virtual-machine
Более того, мы не хотим каждый раз добавлять новый тип в codegen и перекомпилировать наш расширенный API server при добавлении нового чарта, чтобы он начал обслуживаться. Обновление схемы должно происходить динамически или предоставляться администратором через ConfigMap.
Двустороннее преобразование
На данный момент у нас уже есть интеграции и панель управления, которые продолжают использовать ресурсы HelmRelease. На этом этапе мы не хотели терять возможность поддерживать этот API. Учитывая, что мы просто преобразуем один ресурс в другой, поддержка сохраняется и работает в обе стороны. Если вы создадите HelmRelease, вы получите пользовательский ресурс в Kubernetes, а если создадите пользовательский ресурс в Kubernetes, он также будет доступен как HelmRelease.
У нас нет никаких дополнительных контроллеров, которые синхронизируют состояние между этими ресурсами. Все запросы к ресурсам в нашем расширенном API server прозрачно проксируются в HelmRelease и наоборот. Это устраняет промежуточные состояния и необходимость писать контроллеры и логику синхронизации.
Реализация
Чтобы реализовать API агрегации, можно рассмотреть в качестве отправной точки следующие проекты:
- apiserver-builder: в настоящее время находится в стадии alpha и не обновлялся два года. Он работает подобно kubebuilder, предоставляя фреймворк для создания расширенного API server и позволяя последовательно создавать структуру проекта и генерировать код для ваших ресурсов.
- sample-apiserver: готовый пример реализованного API server на основе официальных библиотек Kubernetes, который можно использовать как основу для вашего проекта.
По практическим соображениям мы выбрали второй проект. Вот что нам нужно было сделать:
Отключить поддержку etcd
В нашем случае она не нужна, так как все ресурсы хранятся напрямую в Kubernetes API.
Отключить опции etcd можно, передав nil в RecommendedOptions.Etcd:
Сгенерировать общий тип ресурса
Мы назвали его Application, и выглядит он так:
Это универсальный тип, используемый для любого типа приложения, и логика его обработки одинакова для всех чартов.
Настроить загрузку конфигурации
Поскольку мы хотим настраивать наш расширенный API server через файл конфигурации, мы сформировали структуру конфигурации на Go:
Мы также изменили логику регистрации ресурсов так, чтобы создаваемые нами ресурсы регистрировались в scheme с разными значениями Kind:
В результате мы получили конфигурацию, в которой можно передать все возможные типы и указать, во что они должны отображаться:
Реализовать собственный реестр
Чтобы хранить состояние не в etcd, а напрямую преобразовывать его в ресурсы HelmRelease Kubernetes (и наоборот), мы написали функции преобразования из Application в HelmRelease и из HelmRelease в Application:
Мы реализовали логику фильтрации ресурсов по имени чарта, sourceRef и префиксу в имени HelmRelease:
Затем, используя эту логику, мы реализовали методы Get(), Delete(), List(), Create().
Полный пример можно посмотреть здесь:
В конце каждого метода мы устанавливаем правильный Kind и возвращаем объект unstructured.Unstructured{}, чтобы Kubernetes сериализовал объект корректно. Иначе он всегда сериализовал бы их с kind: Application, чего мы не хотим.
Чего мы достигли?
В Cozystack все наши типы из ConfigMap теперь доступны в Kubernetes как есть:
kubectl api-resources | grep cozystack
buckets apps.cozystack.io/v1alpha1 true Bucket
clickhouses apps.cozystack.io/v1alpha1 true ClickHouse
etcds apps.cozystack.io/v1alpha1 true Etcd
ferretdb apps.cozystack.io/v1alpha1 true FerretDB
httpcaches apps.cozystack.io/v1alpha1 true HTTPCache
ingresses apps.cozystack.io/v1alpha1 true Ingress
kafkas apps.cozystack.io/v1alpha1 true Kafka
kuberneteses apps.cozystack.io/v1alpha1 true Kubernetes
monitorings apps.cozystack.io/v1alpha1 true Monitoring
mysqls apps.cozystack.io/v1alpha1 true MySQL
natses apps.cozystack.io/v1alpha1 true NATS
postgreses apps.cozystack.io/v1alpha1 true Postgres
rabbitmqs apps.cozystack.io/v1alpha1 true RabbitMQ
redises apps.cozystack.io/v1alpha1 true Redis
seaweedfses apps.cozystack.io/v1alpha1 true SeaweedFS
tcpbalancers apps.cozystack.io/v1alpha1 true TCPBalancer
tenants apps.cozystack.io/v1alpha1 true Tenant
virtualmachines apps.cozystack.io/v1alpha1 true VirtualMachine
vmdisks apps.cozystack.io/v1alpha1 true VMDisk
vminstances apps.cozystack.io/v1alpha1 true VMInstance
vpns apps.cozystack.io/v1alpha1 true VPN
Мы можем работать с ними так же, как с обычными ресурсами Kubernetes.
Список S3-бакетов:
kubectl get buckets.apps.cozystack.io -n tenant-kvaps
Пример вывода:
NAME READY AGE VERSION
foo True 22h 0.1.0
testaasd True 27h 0.1.0
Список кластеров Kubernetes:
kubectl get kuberneteses.apps.cozystack.io -n tenant-kvaps
Пример вывода:
NAME READY AGE VERSION
abc False 19h 0.14.0
asdte True 22h 0.13.0
Список дисков виртуальных машин:
kubectl get vmdisks.apps.cozystack.io -n tenant-kvaps
Пример вывода:
NAME READY AGE VERSION
docker True 21d 0.1.0
test True 18d 0.1.0
win2k25-iso True 21d 0.1.0
win2k25-system True 21d 0.1.0
Список экземпляров виртуальных машин:
kubectl get vminstances.apps.cozystack.io -n tenant-kvaps
Пример вывода:
NAME READY AGE VERSION
docker True 21d 0.1.0
test True 18d 0.1.0
win2k25 True 20d 0.1.0
Мы можем создавать, изменять и удалять каждый из них, и любое взаимодействие с ними будет преобразовано в ресурсы HelmRelease с применением структуры ресурса и префикса в имени.
Чтобы увидеть все связанные Helm-релизы:
kubectl get helmreleases -n tenant-kvaps -l cozystack.io/ui
Пример вывода:
NAME AGE READY
bucket-foo 22h True
bucket-testaasd 27h True
kubernetes-abc 19h False
kubernetes-asdte 22h True
redis-test 18d True
redis-yttt 12d True
vm-disk-docker 21d True
vm-disk-test 18d True
vm-disk-win2k25-iso 21d True
vm-disk-win2k25-system 21d True
vm-instance-docker 21d True
vm-instance-test 18d True
vm-instance-win2k25 20d True
Дальнейшие шаги
Мы не собираемся останавливаться на достигнутом с нашим API. В будущем мы планируем добавить новые возможности:
- Добавить валидацию на основе спецификации OpenAPI, сгенерированной напрямую из Helm-чартов.
- Разработать контроллер, который собирает примечания к релизам (release notes) из развёрнутых релизов и показывает пользователям информацию о доступе к конкретным сервисам.
- Переработать нашу панель управления для прямой работы с новым API.
Заключение
Слой агрегации API позволил нам быстро и эффективно решить нашу задачу, предоставив гибкий механизм для расширения Kubernetes API динамически регистрируемыми ресурсами и их преобразования на лету. В конечном счёте это сделало нашу платформу ещё более гибкой и расширяемой без необходимости писать код для каждого нового ресурса.
Вы можете сами протестировать API в open-source PaaS-платформе Cozystack, начиная с версии v0.18.