10 вещей, которые я хотел бы знать перед созданием контроллера Kubernetes CRD


Дайте мне шесть часов, чтобы срубить дерево, и первые четыре я потрачу на заточку топора.

  • A. Линкольн

А я даже не знал, что есть топор… Ресурсы K8s, в этом контексте, это чертовски много дерева, которое нужно рубить. Вам лучше прийти готовым к работе. Я надеюсь, что эта информация поможет кому-то найти топор и сгладить процесс.


Контроллеры? Операторы? CRD?

Давайте внесем немного порядка в этот хаос. K8s документирует понятия контроллеров и операторов. Прочитав их оба, читатель может быть озадачен тонкими различиями между ними.

Оператор» — это красивое название, придуманное CoreOS в 2016 году для описания концепции управления инфраструктурой приложений с помощью контроллеров и пользовательских ресурсов.

K8s Custom Resource Definitions (CRDs) позволяют пользователям расширять систему с помощью тех же инструментов, которые используются для создания и управления Pods, ReplicaSets, StatefulSets и ConfigMaps. Это понятие обсуждается в книге «kubebuilder» — учебник «Создание CronJob».
Создание компонента CronJob с использованием существующего ресурса Job является невероятным примером оператора или контроллера.
В своем выступлении на KubeCon в 2018 году Мацей Шулик, создатель CronJob, фактически заявил, что его функции контроллера еще не реализованы.

Согласно описанию в документации K8s, «контроллер» — это компонент, который использует цикл управления для приведения «желаемого состояния» системы к ее фактическому состоянию. В этом смысле оператор — это контроллер с CRD и историей.


Kubernetes — это база данных

В одном из эпизодов подкаста K8s Дэниел Смит, со-тренер SIG API, объясняет мощную концепцию: «K8s больше похож на базу данных, чем на событийно-управляемую систему». Он объясняет, что вместо того, чтобы различные компоненты взаимодействовали друг с другом, K8s хранит информацию как база данных. Эта информация — состояние кластера. Обновление или создание ресурса приводит к новому желаемому состоянию. Через цикл управления контроллер заботится о достижении желаемого состояния по мере поступления событий такого рода. Посредством итераций цикла управления желаемое состояние согласовывается с существующим состоянием.

«Согласование» здесь отмечается как термин, используемый для достижения равновесия. Вы найдете цикл Reconcile в большинстве контроллеров как сердце логики и место, откуда события начинают свой путь.

K8s больше похож на базу данных, чем на систему, управляемую событиями.


Начните с Kubebuilder, прочитайте книгу!

Жаль, что никто мне этого не сказал. Существует так много ресурсов о том, как создавать операторы и контроллеры K8s, некоторые, такие как Operator Framework, автоматизируют все еще больше, помогая пользователю создавать операторы на основе родного языка, Helm и другие.

Тем не менее, Kubebuilder уже делает большую часть этой работы за вас, и, IMHO, лучше. Он поставляется с (действительно невероятной) частью документации: The Kubebuilder Book.

Обязательно к прочтению, если вы планируете создать контроллер K8s.

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

Автор уделил много внимания написанию книги, и она легко читается. Концепции, рассматриваемые в тексте, интересны и сопровождаются фрагментами кода, подчеркивающими суть.


CRD не создают метаданные по умолчанию

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

Мой собственный случай связан с созданием statefulSet как части моего CRD, который, как я думал, будет рассматриваться как гражданин первого класса. Напротив; вложенные мета-объекты будут игнорироваться, если нет особых указаний. У меня нет ответа на вопрос «почему» (хотя это и рассматривается как ошибка), только «как»:

Code Gen CLI в своей документации предлагает дополнительную возможность установить crd:generateEmbeddedObjectMeta=true для разрешения вложенных мета-объектов. В контексте генератора кода makefile это будет выглядеть примерно так:

.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
    $(CONTROLLER_GEN) rbac:roleName=manager-role 
                    crd:generateEmbeddedObjectMeta=true,maxDescLen=0 
                    webhook paths="./..." 
                    output:crd:artifacts:config=config/crd/bases
Войти в полноэкранный режим Выйти из полноэкранного режима

Вы также можете обратить внимание на maxDescLen=0; В процессе разработки (и после нее) количество огромного текста, генерируемого для каждой части документации по ресурсам, становится невыносимым. Просто попробуйте kubectl describe вашего ресурса и обнаружите, что ваш терминал медленно теряет свою историю. Это не обязательно должно быть «0», но может помочь привести строки текста к управляемой ситуации.


Взаимодействие с CRD вне контекста контроллера не является простым

CRD — это здорово. Вы можете создать в K8s объект любого типа (каламбур не предназначен) и управлять им с помощью контроллера.

А как насчет попыток взаимодействия с ним вне контекста контроллера? У вас может возникнуть соблазн спросить: «Зачем кому-то это делать». Я вижу это так:

  1. Клиенты могут захотеть взаимодействовать с этой производственной службой, используя свой собственный контроллер или информер.
  2. В зависимости от построенной системы, другим частям программного обеспечения может понадобиться доступ к производимому объекту. Именно с таким сценарием использования мне пришлось иметь дело — Daemonset pod, который обновлял данные о новых типах CRD на основе событий, ориентированных на узел.

Получить доступ ко всем родным ресурсам с помощью client-go относительно просто:

clientSet, _ := kubernetes.NewForConfig(config)
pods := clientSet.CoreV1().Pods("")
Войти в полноэкранный режим Выйти из полноэкранного режима

Пользовательские ресурсы, однако, не только требуют от вас получения соответствующих типов для объекта, как только вы это сделаете, для вас не будет экспортирован или сгенерирован клиент. Кроме того, когда вы, наконец, достигаете искомого клиента, вы обнаруживаете полноценный сырой HTTP API с неструктурированными JSON-запросами и ответами. Работать таким образом неинтересно (и небезопасно).

Открытие номер один: «Динамический клиент»

Динамический клиент для K8s хорошо описан в этой статье блога. TL;DR заключается в том, что он позволяет получить прямой доступ к любому объекту в кластере, структурированному или нет. Вот один из основных компонентов динамического клиента, который работает с неструктурированными объектами:

unstructured.Unstructured: Это специальный тип, который инкапсулирует произвольный JSON и при этом соответствует стандартным интерфейсам Kubernetes, таким как runtime.Object.

  • Динамический клиент Kubernetes

Предоставив schema.GroupVersionResource (GVR), динамический клиент получит объект CRD и вернет его в виде неструктурированного объекта данных:

ctx := context.Background()
gvr := schema.GroupVersionResource{
  Group: "group.example.com",
  Version: "v1alpha1",
  Resource: "myresource",
}
returnedObj, err := c.Resource(gvr).
                    Namespace("default").
                    Get(ctx, "myresource-sample", metav1.GetOptions{})
if err != nil {
  return
}
Вход в полноэкранный режим Выход из полноэкранного режима

Все это хорошо и замечательно, но что вы на самом деле делаете с необработанными данными, если вам нужно сделать больше, чем просто разобрать их? Согласно вышеупомянутому сообщению, вы можете изменять поля вложенных данных с помощью функций типа unstructured.NestedInt64, разбирая и изменяя необработанные данные на месте и возвращая их обратно в кластер.

Казалось, что это еще не все:

Открытие номер два: «DefaultUnstructuredConverter».

Библиотека runtime предоставляет функцию конвертера с методом FromUnstructured для преобразования данных в известный тип CRD. В результате из неструктурированных данных создается типизированная, изменяемая и доступная информация:

var myResource MyResourceType
err = runtime.
      DefaultUnstructuredConverter.
      FromUnstructured(returnedObj.UnstructuredContent(), &myResource)
if err != nil {
  return err
}
Вход в полноэкранный режим Выход из полноэкранного режима

От получения объекта до его разбора и манипулирования им вне контекста контроллера — это и есть полное взаимодействие с CRD.

Поскольку в моем примере используются клиенты, я хотел предоставить им возможность взаимодействовать со сгенерированными объектами, поэтому я открыл их через собственный клиент, используя несколько простых методов, которые обрабатывали логику CRUD:

func Get(c dynamic.Interface, nsn types.NamespacedName) (MyResource, error)
func List(c dynamic.Interface, nsn types.NamespacedName) (MyResource, error)
func Update(c dynamic.Interface, mr MyResource) (error)
func Delete(c dynamic.Interface, mr MyResource) (error)
Войти в полноэкранный режим Выход из полноэкранного режима

Использование финализаторов для завершения работы внешних ресурсов

Финализаторы — это логические процессы, которые требуются перед удалением ресурса K8s. В книге об этом говорится, но это легко пропустить. Возможно, вы заметили deletionTimestamp, добавленный в метаданные объекта, когда вы пытались его удалить. Это финализатор, который предотвращает сборку мусора на ресурсе. Например, в AWS EBS внутренняя логика контроллера EBS пытается удалить физические тома перед удалением PVC (для этого удаляется поле finalizer).
Как только финализатор удаляется, объект автоматически собирается GC и удаляется из кластера. При использовании этого метода вам, пользователю, не придется иметь дело с отходами и остатками кластера.


Установка ссылки владельца на отслеживаемые объекты

Есть еще одна очень важная концепция, но легко пропустить раздел в Kubebuilder «реализация контроллера»:
Сборщик мусора K8s знает, что нужно удалять эти объекты при удалении родительского объекта, точно так же, как CronJob создает задания. Если ваш CRD создает другие ресурсы в кластере, например, Pod или Deployment, то вы должны установить ссылку на владельца.

При создании объекта убедитесь, что он включает:

# r being the reconciler receiver
if err := ctrl.SetControllerReference(parentObj, childObj, r.Scheme); err != nil {
  return nil, err
}
Вход в полноэкранный режим Выход из полноэкранного режима

Правильно задайте группы и имя crd

Их изменение — это, мягко говоря, довольно сложная задача.
Подумайте об именах групп и ресурсов, когда вы создаете контроллер, CRD, API и т.д. Есть несколько причин, по которым они важны:

  1. Импорт осуществляется по всему проекту. Импорты должны быть a. однозначными, b. прямыми и c. названы осмысленно.
  2. Они будут использоваться в различных местах, например, в yaml-файлах, содержащих версии и виды API объектов. А также в других областях, таких как обсуждаемые объекты GVR или GVK (GroupVersionKind). Если есть осмысленное имя группы, отличное от «crd» или «apps», используйте его. Если нет, помните, что оно должно иметь смысл в контексте <group-name>.company.com как часть поля apiVersion.
  3. Менять их — та еще морока, особенно при работе с пользовательскими объектами ресурсов. Кроме того, что они являются частью каждой генерируемой функции или маркеров генератора kubebuilder, они, вероятно, упоминаются сотни раз по всему коду. Рефакторинг не является необходимым, но если этого не сделать, то читабельность проекта снизится. Проще говоря: не меняйте название CRD, если только вы не должны. Вас предупредили 😉.

На этом все. Пожалуйста, дайте мне знать, если у вас есть какие-либо другие «до» и «не», чтобы добавить или убрать. В любом случае, это все вещи, которые я хотел бы знать, вместо того, чтобы тратить часы на их выяснение для себя.

Спасибо, что прочитали!

Оцените статью
devanswers.ru
Добавить комментарий