Learning Platform
Глоссарий Troubleshooting
Урок 19.01 · 22 мин
Продвинутый
CRDapiextensionsOpenAPI schemaextensioncustom resource

CRD: Custom Resource Definitions

Когда говорят, что Kubernetes — это «API-платформа, а не оркестратор контейнеров», имеют в виду именно CustomResourceDefinition. CRD позволяет добавить в кластер новый kind (Certificate, PostgresCluster, Application) и получить вокруг него весь API-инструментарий — kubectl get, RBAC, watch streams, validation — без единой строчки кода в kube-apiserver. Это extension механизм, на котором стоит весь cloud-native ecosystem: cert-manager, Prometheus Operator, ArgoCD, Istio, Crossplane. Без CRD не было бы 80% полезных вещей, которые ставят в Kubernetes.


KRaft контроллер: консенсус без ZooKeeper

Что такое CRD концептуально

CRD — это объект, который регистрирует новый resource type в API server. Сам CRD — это обычный Kubernetes ресурс (apiVersion: apiextensions.k8s.io/v1, kind: CustomResourceDefinition), но его создание имеет особый эффект: apiserver видит этот CRD и поднимает для него новый HTTP endpoint в своём API.

После применения CRD certificates.cert-manager.io:

  • Появляется HTTP-маршрут /apis/cert-manager.io/v1/namespaces/<ns>/certificates.
  • Можно сделать kubectl create -f my-cert.yaml с kind: Certificate.
  • apiserver валидирует тело запроса по schema.openAPIV3Schema из CRD — если поле dnsNames не массив строк, придёт 400 Invalid.
  • kubectl explain certificate.spec.dnsNames начинает работать — apiserver отдаёт OpenAPI документацию из самой CRD.
  • RBAC применяется автоматически: verbs: [get, list, watch, create, update, patch, delete] на resources: [certificates] в apiGroups: [cert-manager.io].
  • kubectl api-resources показывает certificates рядом с pods, services, и т.д.
Жизненный цикл CRD: от YAML до working API
kubectl apply -f crd.yamlСоздаётся объект CustomResourceDefinition в API group apiextensions.k8s.io/v1. Это обычный API object — хранится в etcd как и всё остальное.
kube-apiserverapiserver запускает свой внутренний controller apiextensions-controller, который watches CRDs. Когда CRD создаётся — controller динамически регистрирует новый REST handler в роутере apiserver.
/apis/<group>/<v>/...Новый HTTP endpoint доступен через минуту-две. До этого момента kubectl get на новом kind вернёт 'the server doesn't have a resource type'. Готовность фиксируется в status.conditions[].type=Established.
kubectl create cr.yamlСоздаём instance кастомного ресурса (CR). apiserver принимает запрос, проверяет authn/authz, потом запускает admission и validation по схеме из CRD.
OpenAPI validationapiserver валидирует тело запроса по schema.openAPIV3Schema из CRD. Если type не совпал, required отсутствует, или property не объявлено — возвращается 400 Invalid. Без schema apiserver принимает любой YAML.
etcdОбъект сохраняется в etcd под ключом /registry/<group>/<resource>/<ns>/<name>. Это всё — без controller никакой реальной работы не происходит, объект просто лежит в БД.
NOTE

CRD без custom controller — это просто хранилище данных в etcd. Можно создать CRD PostgresCluster, сделать kubectl create -f my-pg.yaml, объект попадёт в etcd — но никакой Postgres-кластер не появится. Чтобы это работало, нужен controller, который watches PostgresCluster объекты и реконсилирует реальный мир (создаёт StatefulSet, PVC, Services) в соответствии со spec. CRD + controller вместе называются Operator.


Анатомия CRD

Минимальный полезный CRD выглядит так:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: certificates.cert-manager.io   # ОБЯЗАТЕЛЬНО формат <plural>.<group>
spec:
  group: cert-manager.io
  names:
    plural: certificates
    singular: certificate
    kind: Certificate
    shortNames: [cert]
    listKind: CertificateList
    categories: [cert-manager]
  scope: Namespaced                     # или Cluster (e.g. ClusterIssuer)
  versions:
  - name: v1
    served: true                        # отвечать ли на этот API version
    storage: true                       # ровно одна version имеет storage=true
    schema:
      openAPIV3Schema:
        type: object
        required: [spec]
        properties:
          spec:
            type: object
            required: [dnsNames, issuerRef]
            properties:
              dnsNames:
                type: array
                items:
                  type: string
              issuerRef:
                type: object
                required: [name, kind]
                properties:
                  name: {type: string}
                  kind: {type: string}
                  group: {type: string}
              duration:
                type: string
                pattern: '^(\d+(h|m|s))+$'
          status:
            type: object
            properties:
              conditions:
                type: array
                items:
                  type: object
    subresources:
      status: {}                         # отдельный endpoint /status
      scale:                             # если CR должен поддерживать kubectl scale
        specReplicasPath: .spec.replicas
        statusReplicasPath: .status.readyReplicas
    additionalPrinterColumns:            # столбцы в kubectl get
    - name: Ready
      type: string
      jsonPath: .status.conditions[?(@.type=="Ready")].status
    - name: Age
      type: date
      jsonPath: .metadata.creationTimestamp

Ключевые поля:

  • metadata.name — обязательно <plural>.<group>. Любое другое значение apiserver отвергает.
  • group — DNS-style имя своей organization (cert-manager.io, argoproj.io, monitoring.coreos.com). Избегайте *.k8s.io — это зарезервировано.
  • names.plural — нижний регистр, используется в URL: /apis/cert-manager.io/v1/certificates.
  • names.kind — PascalCase, используется в kind: манифеста и в Go-структурах controllers.
  • scopeNamespaced или Cluster. У одного CRD scope один навсегда. Менять — пересоздавать CRD.
  • versions[] — массив version-specs. Можно иметь v1alpha1, v1beta1, v1 одновременно. Apiserver конвертирует между ними через conversion webhook (или identity для simple cases).
  • served: true — apiserver отвечает на этот version. Можно временно false, чтобы deprecate version без удаления.
  • storage: true — ровно у одной version. Это canonical storage format в etcd; остальные версии конвертируются в неё при чтении/записи.

OpenAPI v3 schema: валидация на уровне apiserver

Без schema.openAPIV3Schema apiserver принимает любой YAML под spec и status. Это плохо: типичная ошибка пользователя (написал dnsName вместо dnsNames, забыл required-поле) пройдёт через apiserver молча, а controller потом упадёт с runtime ошибкой. Schema — это первая линия защиты.

Schema следует OpenAPI v3 (subset OpenAPI Specification), с некоторыми Kubernetes-specific расширениями:

  • typeobject, array, string, integer, number, boolean.
  • required: [...] — список обязательных полей.
  • properties — структура полей для objects.
  • items — описание элементов для arrays.
  • pattern: '^...$' — regex для строк.
  • enum: [...] — список допустимых значений.
  • minimum, maximum — для чисел.
  • x-kubernetes-preserve-unknown-fields: true — НЕ удалять unknown поля (по умолчанию pruning ON: всё, чего нет в schema, вырезается перед записью в etcd).
  • x-kubernetes-int-or-string: true — поле может быть int или string (как port в Service).
  • x-kubernetes-list-type: map / set / atomic — strategic merge patch behavior.
  • x-kubernetes-validations: [...] (v1.29+ GA): CEL expressions для cross-field validation (self.spec.minReplicas <= self.spec.maxReplicas).
WARNING

Без schema CRD будет создан, но apiserver автоматически добавит x-kubernetes-preserve-unknown-fields: true и выключит structural schema features: subresources (status/scale), pruning, defaulting, server-side apply. По сути CRD будет работать как «свалка YAML» без validation. С v1.16 schema обязательна для apiextensions.k8s.io/v1 — без неё CRD откажет в создании. Schema — это не опция, а необходимость.


Subresources: /status и /scale

Subresources в CRD — это отдельные API endpoints на том же объекте, требующие отдельных RBAC permissions:

  • /status — endpoint /apis/<g>/<v>/.../<name>/status. Когда включён, поле .status в обычном PUT/PATCH игнорируется apiserver-ом; обновлять status можно только через /status subresource. Это разделение: пользователь пишет .spec, controller пишет .status, и они не пересекаются.
  • /scale — endpoint /apis/<g>/<v>/.../<name>/scale. Позволяет kubectl scale my-cr --replicas=5 и интеграцию с HPA для custom resource. Поле .spec.replicas мапится через specReplicasPath в CRD.

RBAC для status и scale — отдельные subresources:

rules:
- apiGroups: [cert-manager.io]
  resources: [certificates]
  verbs: [get, list, watch, create, update, patch, delete]
- apiGroups: [cert-manager.io]
  resources: [certificates/status]   # для controller
  verbs: [get, update, patch]
- apiGroups: [cert-manager.io]
  resources: [certificates/scale]
  verbs: [get, update, patch]

printer columns: красивый kubectl get

По умолчанию kubectl get certificates показывает только NAME и AGE. additionalPrinterColumns в CRD добавляет столбцы через jsonPath по объекту:

additionalPrinterColumns:
- name: Ready
  type: string
  jsonPath: .status.conditions[?(@.type=="Ready")].status
- name: Secret
  type: string
  jsonPath: .spec.secretName
- name: Issuer
  type: string
  jsonPath: .spec.issuerRef.name
- name: Age
  type: date
  jsonPath: .metadata.creationTimestamp

Теперь kubectl get certificates сам по себе показывает status: Ready=True/False, имя secret, issuer, возраст. Это улучшает DX оператора в разы.


CKAD scope: что нужно знать про CRD

CKAD не требует уметь писать CRD вручную. На экзамене типичные задачи:

  • Узнать, какие CRDs установлены в кластере: kubectl get crd.
  • Посмотреть, какие версии и группы у конкретной CRD: kubectl get crd certificates.cert-manager.io -o yaml.
  • Получить instances кастомного ресурса: kubectl get certificates -n my-ns.
  • Понять, что данный ресурс — это не built-in, а от оператора: kubectl api-resources --api-group=cert-manager.io.
  • Объяснить, почему создание CR ничего не делает — controller не установлен.
TIP

Команды для быстрого discovery:

  • kubectl api-resources — все resources, включая custom.
  • kubectl api-resources --api-group=monitoring.coreos.com — фильтр по group.
  • kubectl get crd — список всех CRDs.
  • kubectl explain certificate — документация из schema.
  • kubectl explain certificate.spec.dnsNames — деталь поля.

Killer-моменты

  • CRD — это API server extension без extension code. Чисто декларативная регистрация нового resource type.
  • CRD без controller — мусор в etcd. Нужен controller, реконсилирующий CR в реальный мир — это и есть Operator.
  • Schema обязательна для apiextensions.k8s.io/v1. Без неё CRD не создаётся. Структурная schema включает pruning, defaulting, subresources.
  • Все CRDs регистрируются apiextensions-controller внутри apiserver. Это in-process extension, не отдельный сервер (в отличие от aggregated API server, который будет в уроке 04).
  • kubectl explain работает на любой CRD автоматически, если schema толковая. Хорошие descriptions в schema = живая документация для users.

Проверка знанийKnowledge check
Создан CRD postgresclusters.example.com. Через kubectl create применён PostgresCluster объект. Что произойдёт сразу после успешного create?
ОтветAnswer
Объект сохранится в etcd под ключом /registry/example.com/postgresclusters/<ns>/<name>. apiserver вернёт 201 Created. И всё. Никакого StatefulSet, никаких PVC, никаких Pods — потому что CRD сам по себе не делает работу. Чтобы из объекта PostgresCluster появился реальный кластер, нужен custom controller (часть Operator), который watches эти CR через Watch API, читает spec, и создаёт нужные built-in ресурсы (StatefulSet, Service, ConfigMap, Secret). Без оператора CR — это просто YAML в БД.
Проверка знанийKnowledge check
Зачем в CRD нужны subresources status и scale, и какое поведение они меняют?
ОтветAnswer
status subresource создаёт отдельный endpoint /status. Когда он включён, обычный PUT/PATCH игнорирует поле .status — обновлять его можно только через PATCH на /status. Это разделение ответственности: пользователь редактирует .spec через обычный endpoint, controller обновляет .status через subresource. Без status subresource они конкурировали бы и могли затирать друг друга. scale subresource даёт endpoint /scale: позволяет kubectl scale my-cr --replicas=5 и интеграцию с HPA для custom workloads. Оба subresources требуют отдельных RBAC permissions: resources: [mycr/status], [mycr/scale].
Проверка знанийKnowledge check
Можно ли создать CRD без schema.openAPIV3Schema в apiextensions.k8s.io/v1, и что произойдёт с валидацией?
ОтветAnswer
Нельзя. Начиная с v1.16, apiextensions.k8s.io/v1 требует обязательную structural schema. Без неё apiserver откажет в создании CRD. До v1.16 (старая v1beta1) можно было создать CRD без schema, и тогда apiserver принимал любой YAML под spec/status — это была свалка с автоматическим x-kubernetes-preserve-unknown-fields=true. Также без schema выключались structural features: subresources status/scale, server-side apply, defaulting, pruning unknown fields. Поэтому schema сейчас — не опция, а обязательная часть CRD.
Проверка знанийKnowledge check
CRD has versions: [v1alpha1, v1beta1, v1], storage=true только у v1. Что происходит, когда controller читает объект через v1beta1?
ОтветAnswer
apiserver читает canonical копию из etcd (она в формате v1, так как storage=true там). Затем применяет conversion: для simple cases — identity (имя полей и структура одинаковы), для complex — через conversion webhook, зарегистрированный в CRD spec.conversion. Результат сериализуется в v1beta1 формате и отдаётся клиенту. Когда клиент пишет объект через v1beta1, apiserver конвертирует его в v1 (storage format) и сохраняет в etcd. Это даёт совместимость старых клиентов с новой схемой. served:true должно быть у обеих версий; serve:false — version всё ещё в storage, но не отвечает по HTTP.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. В кластер применён CRD postgresclusters.example.com с правильной schema. Затем kubectl create -f my-pg.yaml с kind: PostgresCluster, name: prod. Что произойдёт?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5