Дайте мне шесть часов, чтобы срубить дерево, и первые четыре я потрачу на заточку топора.
- A. Линкольн
А я даже не знал, что есть топор… Ресурсы K8s, в этом контексте, это чертовски много дерева, которое нужно рубить. Вам лучше прийти готовым к работе. Я надеюсь, что эта информация поможет кому-то найти топор и сгладить процесс.
- Контроллеры? Операторы? CRD?
- Kubernetes — это база данных
- Начните с Kubebuilder, прочитайте книгу!
- CRD не создают метаданные по умолчанию
- Взаимодействие с CRD вне контекста контроллера не является простым
- Открытие номер один: «Динамический клиент»
- Открытие номер два: «DefaultUnstructuredConverter».
- Использование финализаторов для завершения работы внешних ресурсов
- Установка ссылки владельца на отслеживаемые объекты
- Правильно задайте группы и имя crd
Контроллеры? Операторы? 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 объект любого типа (каламбур не предназначен) и управлять им с помощью контроллера.
А как насчет попыток взаимодействия с ним вне контекста контроллера? У вас может возникнуть соблазн спросить: «Зачем кому-то это делать». Я вижу это так:
- Клиенты могут захотеть взаимодействовать с этой производственной службой, используя свой собственный контроллер или информер.
- В зависимости от построенной системы, другим частям программного обеспечения может понадобиться доступ к производимому объекту. Именно с таким сценарием использования мне пришлось иметь дело — 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 и т.д. Есть несколько причин, по которым они важны:
- Импорт осуществляется по всему проекту. Импорты должны быть a. однозначными, b. прямыми и c. названы осмысленно.
- Они будут использоваться в различных местах, например, в yaml-файлах, содержащих версии и виды API объектов. А также в других областях, таких как обсуждаемые объекты GVR или GVK (GroupVersionKind). Если есть осмысленное имя группы, отличное от «crd» или «apps», используйте его. Если нет, помните, что оно должно иметь смысл в контексте
<group-name>.company.com
как часть поляapiVersion
. - Менять их — та еще морока, особенно при работе с пользовательскими объектами ресурсов. Кроме того, что они являются частью каждой генерируемой функции или маркеров генератора kubebuilder, они, вероятно, упоминаются сотни раз по всему коду. Рефакторинг не является необходимым, но если этого не сделать, то читабельность проекта снизится. Проще говоря: не меняйте название CRD, если только вы не должны. Вас предупредили 😉.
На этом все. Пожалуйста, дайте мне знать, если у вас есть какие-либо другие «до» и «не», чтобы добавить или убрать. В любом случае, это все вещи, которые я хотел бы знать, вместо того, чтобы тратить часы на их выяснение для себя.
Спасибо, что прочитали!