CRD: свой тип ресурса в кластере
Как кластер узнаёт про новый kind, чем определение типа отличается от объекта и почему без контроллера CRD остаётся просто записью в базе.
Зачем нужен свой тип ресурса
Встроенных типов в Kubernetes конечное число: Pod, Service, ConfigMap и ещё несколько десятков. Когда приложению нужен объект, которого в этом списке нет, его добавляют через CustomResourceDefinition.
CustomResourceDefinition (CRD) - это определение нового типа ресурса.
После применения CRD кластер начинает понимать новый kind, и с ним
работают те же kubectl get, kubectl apply, kubectl describe, та же
RBAC-модель и тот же формат манифеста.
Так устроен почти весь сторонний софт для Kubernetes. cert-manager
добавляет тип Certificate, Prometheus Operator - ServiceMonitor,
Argo CD - Application. Ни один из них не патчит исходники Kubernetes.
Все они регистрируют свои CRD, и API кластера расширяется на лету.
Проверить, какие типы кластер знает прямо сейчас, можно одной командой:
kubectl api-resources
kubectl api-resources --api-group=cert-manager.io
Всё, что ниже, повторяется на любом кластере, где у вас есть права администратора, включая локальный кластер.
CRD и CR - разные вещи
Два понятия легко перепутать, а разница принципиальная.
- CRD - определение типа. Один раз описывает, что в кластере теперь
существует, например,
Widget. - CR (CustomResource) - конкретный объект этого типа. Их может быть
много:
widget-a,widget-b.
Аналогия из программирования: CRD - это class, CR - объект класса.
Сначала регистрируют тип, потом создают объекты. Разными будут и права:
на создание CRD нужен доступ ко всему кластеру, на создание Widget
хватит прав в одном namespace.
Анатомия CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: widgets.demo.example.com
spec:
group: demo.example.com
scope: Namespaced
names:
plural: widgets
singular: widget
kind: Widget
shortNames: [wd]
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
size:
type: integer
color:
type: string
group
group - пространство имён API, к которому относится тип. У встроенных
ресурсов это либо пустая строка (Pod, Service), либо короткое имя
(apps у Deployment). Для своих типов берут доменное имя, чтобы не
столкнуться с чужими: demo.example.com.
name и names
metadata.name обязан быть в формате <plural>.<group>, то есть
widgets.demo.example.com. Если написать иначе, apiserver вернёт ошибку
и CRD не создастся.
Блок names задаёт, как тип называется в командах:
kind: Widget- имя в манифесте (полеkind).plural: widgets- форма дляkubectl get widgets.singular: widget- форма дляkubectl get widget my-widget.shortNames: [wd]- короткий алиас,kubectl get wd.
scope
scope определяет, где живут объекты этого типа.
Namespaced- объект принадлежит namespace, как Pod.kubectl get widgets -n team-aпокажет виджеты только этого namespace.Cluster- объект общий на весь кластер, как Node или сам CRD.
Сам CRD всегда cluster-scoped: он меняет API всего кластера. Поэтому создать его может только тот, у кого есть права на весь кластер, и поэтому в managed-кластерах установка операторов обычно идёт от администратора, а не от команды разработки.
kubectl delete crd уносит с собой все объекты этого типа во всех
namespace сразу. Перед удалением смотрят kubectl get widgets -A.
versions и schema
versions - список версий API типа. Ровно одна из них помечена
storage: true: в этом формате объект лежит в etcd. served: true
означает, что версия отдаётся через API. Когда версий несколько,
apiserver конвертирует объект между ними, а для нетривиальных случаев
подключают conversion webhook.
schema.openAPIV3Schema описывает поля объекта. Без схемы apiserver не
примет CRD: в apiextensions.k8s.io/v1 она обязательна. Схема работает
как валидация: если объявить size как integer, строка туда не
запишется.
Объект нового типа
После применения CRD кластеру нужно несколько секунд, чтобы
зарегистрировать тип. Готовность видна по условию Established:
kubectl get crd widgets.demo.example.com
kubectl wait --for=condition=Established crd/widgets.demo.example.com --timeout=30s
Объект, созданный сразу после применения CRD, получает
no matches for kind "Widget". Тип ещё не Established, и помогает
как раз kubectl wait.
Дальше объект создаётся как обычный манифест. apiVersion собирается из
group и version, kind берётся из names.kind:
apiVersion: demo.example.com/v1
kind: Widget
metadata:
name: my-widget
spec:
size: 3
color: blue
kubectl apply -f widget.yaml
kubectl get widgets
kubectl get widget my-widget -o jsonpath='{.spec.size}'
Что ещё умеет схема
Схема нужна не только для валидации, из неё вырастает вся остальная работа с типом.
kubectl explain читает ту же схему и объясняет поля кастомного типа
так же, как поля Pod:
kubectl explain widget.spec
kubectl explain widget.spec.size
Поля, которых в схеме нет, apiserver вырезает при записи. Это называется
pruning и включено по умолчанию в apiextensions.k8s.io/v1. Симптом
характерный: kubectl apply проходит без ошибки, а после kubectl get -o yaml поля в объекте нет. Если произвольные ключи нужны намеренно,
их разрешают через x-kubernetes-preserve-unknown-fields: true.
Колонки в выводе kubectl get тоже настраиваются. По умолчанию
показывается только имя и возраст, но additionalPrinterColumns внутри
версии добавляет свои:
versions:
- name: v1
served: true
storage: true
additionalPrinterColumns:
- name: Size
type: integer
jsonPath: .spec.size
- name: Color
type: string
jsonPath: .spec.color
spec, status и subresources
У встроенных ресурсов есть разделение: spec - чего хочет человек,
status - что реально происходит. Для кастомных типов это разделение
включают явно:
subresources:
status: {}
После этого status живёт отдельным подресурсом. Запись в основной
объект не меняет status, а запись в status не трогает spec.
Практический смысл: человек правит spec через kubectl apply,
контроллер пишет status через отдельный вызов, и они не затирают
работу друг друга.
Второй подресурс, scale, позволяет применять к кастомному типу
kubectl scale и HorizontalPodAutoscaler, если в схеме есть поля с
числом реплик.
Контроллер: что превращает запись в действие
Сам по себе CRD только хранит данные. Объект Widget лежит в etcd, и
без внешнего кода на его появление ничего не происходит.
Действия добавляет контроллер - процесс, который подписан на события по объектам своего типа и приводит кластер к описанному состоянию. Работает он циклом согласования (reconcile):
- Прочитать желаемое состояние из
specобъекта. - Посмотреть на фактическое состояние в кластере.
- Сделать шаги, которые сокращают разницу.
- Записать результат в
status.
Важное свойство этого цикла: он level-triggered, то есть смотрит на текущее состояние, а не на список произошедших событий. Одно пропущенное событие ничего не ломает, следующий проход всё равно увидит расхождение. Отсюда требование к reconcile: он должен быть идемпотентным. Повторный запуск на неизменившемся объекте не должен ничего создавать заново.
Такой контроллер обычно ещё вешает на объект finalizer - запись в
metadata.finalizers, которая не даёт удалить объект окончательно, пока
контроллер не приберёт за собой внешние ресурсы. Пока finalizer на
месте, объект после kubectl delete остаётся в состоянии Terminating.
Оператор
Связку «CRD + контроллер» называют оператором. Пример - cert-manager:
он регистрирует Certificate, контроллер видит новый объект, идёт в
Let's Encrypt, кладёт сертификат в Secret и пишет в status дату
истечения. Пишут операторы поверх каркасов controller-runtime,
Kubebuilder, Operator SDK, которые снимают рутину и оставляют
разработчику функцию reconcile.
CRD без контроллера тоже полезен: так хранят конфигурацию в kubectl-совместимом виде, с валидацией схемой и обычной RBAC. Только до появления контроллера это база данных, а не автоматизация. Зачем операторы появились, какие встречаются в проде и во что обходятся - в уроке Операторы.
Что обычно ломается
metadata.nameне совпадает с<plural>.<group>. apiserver отклоняет CRD с ошибкой про несоответствие имени.- Поле не описано в схеме и молча исчезает после apply. Это pruning, а не баг kubectl.
- Прав не хватает: CRD cluster-scoped, из namespace-роли его не создать.
Домашка
Теорию закрепляет задание на своём кластере: 2 шага с проверкой. Кластер поднимается по треку Kubernetes локально, задания лежат в репозитории k8s-practice.
./run.sh start k8s-crd # развернуть стартовое состояние
./run.sh verify k8s-crd # проверить текущий шаг
Коротко
- CRD регистрирует новый
kind, после чего с ним работают обычные команды kubectl и обычная RBAC. - CRD - определение типа, CR - объект этого типа; CRD всегда cluster-scoped.
metadata.nameстрого<plural>.<group>, схема в версииv1обязательна, поля вне схемы вырезаются.subresources.statusразделяет намерение человека и результат работы контроллера.- Контроллер крутит цикл reconcile: сравнивает
specс фактом и сокращает разницу; связка CRD с таким контроллером и есть оператор.
Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс