Kubernetes. Логика работы клиентов с kube-api: что под капотом?
29.05.24
Редакция Factory5
На связи представитель блока поддержки платформы в Factory5. Обучая своих падаванов устройству kubernetes, я трачу много времени на то, чтобы объяснить, что это не «черная коробка», которая «хранит ямлы» и каким-то образом переваривает это в запущенные приложения, которые «сами себя чинят», а просто удачный набор компонентов, который обеспечивает централизованное управление ресурсами. Главное здесь — именно ресурсы. Экземпляры объектов групп аpi, которые декларативно описывают некоторое состояние инфраструктуры, к которому будут стремиться контроллеры, отвечающие за конкретные группы аpi. И сердцем всего этого является компонент kube-apiserver. По сути своей api-gateway является центральной точкой общения всех компонентов кластера. В данной статье попробую описать логику работы клиентов с kube-api и варианты того, как мы можем этим воспользоваться.
Как клиенты взаимодействуют с kube-apiserver?
Рассмотрим самый очевидный случай — kubectl. Как только вы вводите «kubectl get $some», kubectl идет в kubeconfig (по умолчанию в ~/.kube/config или указывается флагом --kubeconfig $path) и делает запросы в /api и /apis с соответствующими параметрами безопасного подключения, среди которых есть сертификаты, токен и basic-auth.

Пойдем по порядку. В /api возвращается ресурс с kind:APIVersions и списком доступных версий групп апи. В этих группах апи собраны core-ресурсы куба, такие как немспейсы, ноды и другие. В /apis возвращается ресурс с kind:APIGroupList, в котором лежит список api-групп расширений kube-api, а также доступных для них версий. Когда в kube добавляется какая-то новая фича, она попадает в эту группу с заданной версией. Как правило, все фичи проходят этапы alpha, beta, stable или deprecated, если по какой-то причине она становится не актуальной. В alpha фичи, как правило, отключены по умолчанию. В beta (опять же, как правило) они уже включаются по умолчанию. Управлять включением и отключением фичами можно через флаг kube-apiserver --feature-gate=..., подробности здесь.

Однако эти alpha, beta и прочие стадии — не конкретная версия api. Это лишь описание некоего жизненного цикла фичи. Версией же является то, что указано у ресурса в поле apiVersion. Это и есть путь, внутри /apis (/api в случае с core-ресурсами). Также в списке групп api можно указать атрибут «namespaces», который определяет иерархию запроса до конкретного экземпляра ресурса — будет ли там указание немспейса или ресурс будет, что называется, «cluster wide». По фича-гейтам я в какой-то момент начал вести визуализацию в виде таблички, ее можно посмотреть по ссылке.

Визуализацию наглядного примера того, как размещены api можно найти в этом репозитории. Я не ставил задачу вместить в него все ресурсы, так что это просто пример. Однако, даже в таком состоянии он является рабочим. Если у кого-то возникнет желание дополнить недостающие примеры, то МР приветствуются.

После того, как kubectl получил список всех доступных ресурсов в кластере, он определяет внутри себя разрешенные аргументы, например, для команд create, edit, get, delete. Далее он уже делает запрос того, что вы от него хотели, если запрошенный ресурс существует. Команда get преобразуется в HTTP GET запрос, create — в POST, edit — в последовательные GET и PATCH, а delete — в DELETE. Все достаточно логично. Подробнее со списком ресурсов и доступных для них HTTP-методов вы можете ознакомиться здесь (следите за версией). Это лучшая документация с описанием всех возможностей kube-apiserver.

Отметим и такую вещь, как subresources, или сабресурсы. В rest иерархии запросов это дополнительная секция после имени ресурса. Например, .../some_namespace/nginx/logs. Сабресурсы используются не для описания какого-то состояния, к которому контроллеры должны привести нашу инфраструктуру, а для выполнения определенных одномоментных действий. Это могут быть, например, встроенные сабресурсы /logs, /status, /eviction и прочие, которые можно обнаружить в API reference.
Таким образом мы определили, что взаимодействие с kube-api достаточно стандартизировано и прозрачно. Если вы не поленились заглянуть в мой репозиторий, то, наверняка, заметили, что иерархия объектов выглядит достаточно просто.
Эмуляция с nginx
А нельзя ли эмулировать поведение kube-apiserver простым nginx-ом, просто подкладывая файлики в папочку? Да, собственно, можно. Вы даже можете в kubeconfig клиента использовать http и заигнорировать аутентификацию (точнее сказать, вы можете указать basic-auth с любым содержимым в kubeconfig и просто игнорировать в nginx заголовок Authorization). Просто клонируйте мой репозиторий в любой каталог для server/location nginx’а, натравите на него kubeconfig и можете пользоваться, например, командами kubectl get nodes. Что положите — то и будет работать. В иных случаях он будет грустно сообщать об отсутствии ресурса. Только не забудьте в конфиге локейшена указать 'types {} default_type "application/json; charset=utf-8";' для того, чтобы kubectl не ругался на кодировку (Это нужно потому, что в своей дальновидности я использую в репозитории с примерами расширение .html).

На этом этапе вы можете использовать сию конструкцию, например, для какого-то тестирования, не заморачиваясь с etcd, а также версионируя доступные объекты в kube-apiserver. Однако, очевидно, что кое-чего все же здесь не хватает. Конечно же, любые запросы, кроме GET, дружно сообщат вам об ошибке. Если быть точнее, то они сообщат, что всё OK, однако изменения не доедут. Ведь эти файлики за nginx, а он не умеет ими управлять. Если мы хотим заставить всё это работать, то можем вооружиться встроенным джаваскриптом, lua или чем-то еще, чтобы обеспечить функционирование этих методов. Или можно взять fastCGI и накрутить поверх него определенное поведение. Но зачем усложнять минималистичную конструкцию? В таком случае будет проще поставить apache и использовать там нативно php, а то и CGI скрипты. Если вам это интересно, то можете побаловаться самостоятельно. Я же предпочел просто взять первый попавшийся язык программирования и набросать там простое веб-приложение.

Так что же нам нужно сделать? Очевидно, в первую очередь следует обеспечить работу /api и /apis с соответствующим форматом ответов. Для полноценной эмуляции kube-api стоит также реализовать эндпоинты /openapi и /version. /openapi/v2 можно просто забить пустым файлом. Вы можете написать с нуля свой kube-apiserver, если у вас много свободного времени. А можете смело выкинуть вообще все ресурсы и создать пустой список APIVersions в APIGroupList, сформировать какой-то кастомный ресурс и организовать любое поведение вашего самописного kube api. Оно будет работать. К примеру, можете описать ресурс postgres-databases, работу HTTP методов для создания, просмотра, редактирования и удаления баз данных где-то там в существующей постгре, и спокойно после этого управлять ими через kubectl.

Однако, советую в /api реализовать хотя бы ресурс namespaces, он вам явно может пригодится для группировки ваших ресурсов. Но... зачем эти шаманства? Ну, к примеру, вы можете организовать gitops, например, через argocd, вообще не имея куба в инфраструктуре. Вам всего лишь нужно написать свои контроллеры для всего, что вы хотите покрыть этой модной методологией. (Правда в argocd есть нюанс. При регистрации кластера через cli он пытается достучаться до токена, с которым впоследствии будет обращаться к kube-api. Так что вам придется реализовать roles и secrets. Ну, создать какие-то фиксированные, если вы такой же любитель кривых костылей, как я). Допустим, что мы настолько отчаянные, что даже написали все наши контроллеры, и теперь у нас свой собственный kube-apiserver, с которым работают kubectl, helm, argocd, jsonnet и все, что вообще умеет взаимодействовать с кубом. Также мы можем воспроизвести еще две полезные фичи kubernetes. Это веб хуки валидации и мутации. Реализуется это через Admission Controllers.

В целом это работает следующим образом: где-то вертится контроллер, по сути еще одно api, в которое перенаправляется любой запрос к kube-apiserver перед тем, как зафиксировать это в etcd. Есть два типа обработки: Validating и Mutating. С помощью валидации мы можем, к примеру, запретить добавлять в кластер ресурсы с определенными атрибутами в спеке. А мутация позволяет автоматически изменять применяемые манифесты. Классический случай — применение сайдкаров при наличии какой-либо аннотации во всяких там service mesh решениях. В кубе это делается через регистрацию такого контроллера через ресурс Admission Configuration. Подробно описывать этот процесс я не буду, можете ознакомиться с этим здесь.

Но кажется, мы все же еще кое о чем забыли. Это конечно же аутентификация и авторизация, а также генерация токенов. Тут тоже вдаваться в подробности не буду. Достаточно сказать что в kube api есть ресурсы role, определяющие некоего «юзера». Не совсем так, правда, потому что в кубе есть и ресурс user. Если мне не изменяет память, он используется для oauth аутентифицированных клиентов в kube-apiserver. Также есть ресурс Group, для группировки. Во время аутентификации по сертификатам, сопоставление роли будет использоваться CommonName (cn) аттрибут в сертификате. Сопоставление с группой идет по атрибуту Organizatin (o). Наглядно это можно посмотреть в сертификатах kubelet в опусе Келси Хайтауера kubernetes the hard way.

Нюансы относительно этих ресурсов, биндингов и прочего, с вашего позволения, я опущу. Так как это является одним из базовых знаний, которые вы уже должны знать, если интересуетесь углубленной работой kube. С необходимостью реализовать эти компоненты мы уже столкнулись, пытаясь натравить argocd на наш новоявленный куб-кластер. Подождите, кластер? Похоже что для кластера нам тоже далеко, ведь нам нужно как-то синхронизировать состояние между репликами. А если мы хорошо вкатились в kube, то знаем что у нас должны быть еще валидация, ивенты, веб хуки валидации и мутации, а также много чего еще... Кажется, что писать свои костыли было все же не самой хорошей идеей. Но не пропадать же нашим знаниям даром? Давайте все же раскатаем kube, но прицепим к нему наше новоявленное api как расширение. Делается это с помощью механизма API Aggregation.

Полностью это описано здесь, но краткая суть сводится примерно к следующему. Мы определяем во флагах kube-apiserver сертификат и необходимые заголовки для подключения к нашему api, после чего создаем ресурс APIService, в котором описываем нашу группу ресурсов, версию, параметры подключения и все такое. После чего kube api становится нашим api-гейтвеем или прокси, если угодно, обслуживая запросы на указанную в ресурсе группу api, перенаправляя запросы в наш новорожденный контроллер.

На этом моменте я все же сдамся и расскажу страшный секрет. Здесь тоже можно упростить себе жизнь, если вы гошник. В репозитории вы можете обнаружить описание как можно собрать расширение для kubeapi. Если же вы вдруг решили, что махинации с nginx-ом или иной проксей можно использовать, чтобы каким-то образом управлять запросами перед kube api, например, чтобы организовать что-то вроде наколеночного мультитенанта, то такое уже тоже есть. Например, как я понял, так работает это приложение. Если же вас взбудоражила возможность отказаться от etcd как бэкенда, то и пример такой реализации уже есть.Так что и тут покостылять не удастся. Но все же никто не может вам этого запретить!

Однако, в большинстве случаев у нас не возникает в этом необходимости. Потому что мейнстримом сейчас считается «паттерн» операторов, которые работают на основе CustomResourceDefinitions (CRD). Это такие кубовые ресурсы, с помощью которых можно создать группу api и версию, а также openapi-схему для спеки вашего ресурса, после чего вы можете через kubectl или любой клиент создавать экземпляры этих ресурсов в кластере. Обычно, далее их подхватывает оператор и на базе них уже генерирует стандартные ресурсы. Самым элементарным примером для такого будет уже не раз упомянутый argocd, который полностью конфигурируется через его CRD-хи. Разница между расширением api заключается в том, что работа с crd получается асинхронной, тогда как в расширении api вы можете управлять запросом как вам угодно. Однако, очевидно, это более трудоемко.
Вывод
Таким образом мы можем заключить следующее. Api куба является единой точкой взаимодействия компонентов кластера kubernetes. При этом кластер не обязательно может быть заточен именно на оркестрации контейнеров, а может быть расширен любой логикой, которую реализуют различные контроллеры. Сам kube-apiserver является просто интерфейсом для создания и управления ресурсами определенных типов. Мы можем расширять функционал нашего kubernetes-кластера за счет механизмов CRD и API Aggregation. Перед тем, как объекты попадут в стейт (etcd), можно валидировать данные через validation webhooks, а также изменять их при помощи mutation webhooks. После чего ресурсы считаются добавленными в кластер и контроллеры подхватывают их, обеспечивят уже некую полезную работу, согласно спеке ресурса, а спека определяется группой api и ее версией. Сам по себе kube-apiserver, строго говоря, пассивен и не выполняет какой-то фоновой работы. Кворум «мастер-нод» определяется требованиями etcd, а не kube-apiserver. Вы можете разворачивать произвольное количество реплик api-сервера и даже разворачивать внутри вашего кластера еще один экземпляр api, например, с другими параметрами --feature-gate или сертификатами авторизации. Теперь вы знаете как это работает в ванильном кластере, опеншифте и любом операторе или любой другой интерпретации kubernetes. И, пожалуйста, не изобретайте велосипеды.