Kubernetes для самых маленьких. Часть 28: Что такое CRD и операторы?
Все части серии «Kubernetes для самых маленьких»В части 1 мы определили Kubernetes как набор контроллеров, каждый из которых приводит фактическое состояние к желаемому, а в части 3 — его API как единообразный набор ресурсов с spec и status, которые можно читать, менять и на изменения которых можно подписаться. Обе идеи открыты для расширения: в API можно добавить свой тип ресурса, а рядом запустить свой контроллер, который будет его обслуживать. Вместе это называется оператором, и мы уже пользовались двумя: Tigera operator ставил Calico в части 4, cert-manager выпускал сертификаты в части 19. Сегодня — как это устроено: CustomResourceDefinition, схема и валидация, контроллер и его цикл, а затем два настоящих оператора для баз данных и очередей и то, как оценивать оператор перед установкой. Kustomize из части 26 и Helm из части 27 здесь тоже пригодятся: операторы обычно ставятся манифестами выпуска или чартами.
Всё в этой части проверено на учебном кластере: CRD и контроллер на controller-runtime, CloudNativePG 1.30.1 и RabbitMQ Cluster Operator 2.23.2. Без проверки остались резервное копирование PostgreSQL в S3, плагин kubectl cnpg, конверсия версий CRD и Messaging Topology Operator — о них по документации.
Ресурс без кода в apiserver
CustomResourceDefinition (CRD) — встроенный ресурс группы apiextensions.k8s.io, который описывает новый тип: его группу, имя, версии и схему. Создали CRD — и через секунду kube-apiserver обслуживает новый путь REST, хранит объекты в etcd, проверяет их по схеме, отдаёт watch, поддерживает kubectl get, apply, explain, RBAC и всё остальное, что есть у встроенных ресурсов. Ни строчки кода в apiserver при этом не пишется и ничего не перезапускается.
Чего CRD не делает — не придаёт объекту поведения. Объект Greeting будет лежать в etcd и ничего не произойдёт, пока какой-то контроллер не начнёт за ним следить. В этом его отличие от встроенных ресурсов только количественное: Deployment тоже «просто данные», которые обрабатывает контроллер внутри kube-controller-manager.
Есть и второй, более старый механизм расширения — агрегация API: отдельный сервер API регистрируется объектом APIService, и kube-apiserver проксирует к нему запросы своей группы. Так работает metrics-server (часть 32). Это нужно, когда данные хранятся не в etcd или нужна своя логика хранения; для всего остального используют CRD.
Пример на всю часть — ресурс Greeting: «хочу приложение hello такой-то версии, с таким-то приветствием, в стольких-то репликах, на таком-то хосте». Контроллер будет превращать его в Deployment, Service и Ingress.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: greetings.demo.example.com # <plural>.<group>, обязательно так
spec:
group: demo.example.com
scope: Namespaced # или Cluster
names:
kind: Greeting
listKind: GreetingList
plural: greetings
singular: greeting
shortNames: [gr]
categories: [demo] # kubectl get demo покажет все ресурсы категории
versions:
- name: v1
served: true
storage: true
subresources:
status: {}
scale:
specReplicasPath: .spec.replicas
statusReplicasPath: .status.readyReplicas
labelSelectorPath: .status.selector
additionalPrinterColumns:
- name: Version
type: string
jsonPath: .spec.version
- name: Replicas
type: integer
jsonPath: .spec.replicas
- name: Ready
type: integer
jsonPath: .status.readyReplicas
- name: URL
type: string
jsonPath: .status.url
priority: 1 # только в -o wide
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
selectableFields:
- jsonPath: .spec.version
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [version]
properties:
version:
type: string
pattern: '^[0-9]+\.[0-9]+(\.[0-9]+)?$'
description: Тег образа ghcr.io/example/hello
message:
type: string
maxLength: 200
default: hello
replicas:
type: integer
minimum: 1
maximum: 10
default: 2
host:
type: string
maxLength: 253
x-kubernetes-validations:
- rule: self == oldSelf
message: host нельзя менять, создайте новый Greeting
x-kubernetes-validations:
- rule: "!has(self.host) || self.host.endsWith('.apps.example.com')"
message: host должен быть в зоне apps.example.com
fieldPath: .host
- rule: "!has(self.host) || self.replicas >= 2"
messageExpression: "'публичному приложению нужно минимум 2 реплики, задано %d'.format([self.replicas])"
reason: FieldValueForbidden
status:
type: object
properties:
readyReplicas:
type: integer
selector:
type: string
url:
type: string
observedGeneration:
type: integer
format: int64
conditions:
type: array
x-kubernetes-list-type: map
x-kubernetes-list-map-keys: [type]
items:
type: object
required: [type, status]
properties:
type: {type: string}
status: {type: string, enum: ["True", "False", "Unknown"]}
reason: {type: string}
message: {type: string}
lastTransitionTime: {type: string, format: date-time}
observedGeneration: {type: integer, format: int64}
Разберём по частям.
Группа, версии, served и storage
Группа — доменное имя, которым вы владеете (demo.example.com), чтобы не столкнуться с чужими CRD. Группы *.k8s.io и *.kubernetes.io зарезервированы за проектом Kubernetes и требуют аннотации со ссылкой на одобренное предложение.
Версии — у одного ресурса их может быть несколько, как у встроенных (v1alpha1, v1beta1, v1). У каждой два флага:
served— отдаётся ли эта версия по API. Выключивserved, версию «снимают с обслуживания»: запросы к ней получат 404, но объекты в etcd не трогаются.storage— в какой версии объекты записываются в etcd. Ровно одна версия имеетstorage: true.
Если версий несколько и схемы различаются, объекты приходится конвертировать: клиент пишет в v1, а в etcd хранится v2, или наоборот. spec.conversion.strategy: None годится, только когда схемы совпадают (меняется лишь apiVersion). Иначе — Webhook: apiserver отправляет объект на HTTPS-сервис (обычно в том же поде, что контроллер) запросом ConversionReview и получает обратно объект нужной версии. Конверсия должна быть без потерь в обе стороны, иначе чтение и запись через разные версии будут портить данные.
Объекты, записанные в старой версии хранения, так и остаются в ней, пока их не перезапишут. Список версий, реально присутствующих в etcd, показывает status.storedVersions у CRD, и удалить старую версию из CRD можно, только когда её там нет: для этого все объекты переписывают (миграцию хранения выполняет сам оператор при обновлении или утилита вроде kube-storage-version-migrator), а затем правят storedVersions.
Схема: structural schema, значения по умолчанию, валидация
С apiextensions.k8s.io/v1 схема обязательна и должна быть структурной: у корня, у каждого поля-объекта и у каждого элемента массива указан непустой type, а всё, что описано внутри allOf/anyOf/oneOf, описано и снаружи. Это ограничение не прихоть: зная тип каждого узла, apiserver может делать три вещи.
Pruning (обрезка) — поля, которых нет в схеме, при записи молча выбрасываются. Опечатка replica: 3 вместо replicas: 3 не сохранится, а kubectl apply с --validate=strict (поведение по умолчанию) вернёт ошибку: Greeting in version "v1" cannot be handled as a Greeting: strict decoding error: unknown field "spec.replica". Если какому-то поддереву нужен произвольный YAML (например, кусок конфигурации программы), его помечают x-kubernetes-preserve-unknown-fields: true.
Значения по умолчанию — default: 2 у replicas подставляется при чтении из etcd и при записи, так что контроллер всегда видит заполненное поле.
Валидация — по ключевым словам OpenAPI (type, pattern, minimum, maxLength, enum, required) и по правилам на CEL в x-kubernetes-validations. CEL (Common Expression Language) — язык выражений от Google без циклов и побочных эффектов, гарантированно завершающийся за ограниченное время; apiserver вычисляет его у себя, без внешнего вызова. Валидация на CEL стабильна с Kubernetes 1.29.
Поля правила:
| Поле | Назначение |
|---|---|
rule |
выражение; self — значение поля, где правило стоит, oldSelf — его прежнее значение при обновлении |
message |
текст ошибки |
messageExpression |
текст ошибки, вычисляемый CEL-выражением (важнее message) |
reason |
машинная причина: FieldValueInvalid (по умолчанию), FieldValueForbidden, FieldValueRequired, FieldValueDuplicate |
fieldPath |
на какое поле указать в ошибке |
optionalOldSelf |
вычислять правило с oldSelf и при создании объекта |
Выражения CEL apiserver оценивает заранее, при создании CRD: каждое правило получает оценку «стоимости» по худшему случаю (длина строк, число элементов списков), и схема, которая не укладывается в бюджет, отвергается целиком. Первая версия этой CRD собирала сообщение конкатенацией '… задано ' + string(self.replicas), и apiserver её не принял:
The CustomResourceDefinition "greetings.demo.example.com" is invalid:
* spec.validation.openAPIV3Schema.properties[spec].x-kubernetes-validations[1].messageExpression: Forbidden: estimated messageExpression cost exceeds budget by factor of more than 100x (try simplifying the rule, or adding maxItems, maxProperties, and maxLength where arrays, maps, and strings are declared)
…
Длину строки, полученной из числа, оценщик не знает и считает её неограниченной, а склейка с ней — тоже. string(self.replicas) без склейки проходит, и проходит форматирование '… задано %d'.format([self.replicas]) — его и оставили. Лекарство в общем случае подсказывает сама ошибка: maxLength, maxItems, maxProperties у полей, с которыми работают правила.
Правило с oldSelf — правило перехода: self == oldSelf на host делает поле неизменяемым после создания, ровно как у встроенных spec.selector. Ещё одна полезная деталь — ratcheting (стабилен с 1.33): если вы ужесточили схему, а в кластере уже лежат объекты, которые новым правилам не соответствуют, их можно обновлять, не исправляя старые нарушения, — проверяются только изменённые поля. Без этого ужесточение схемы ломало бы все существующие объекты.
selectableFields — поля, по которым работает kubectl get greetings --field-selector spec.version=1.0 (стабильно с 1.32); раньше для CRD селектор полей работал только по metadata.name и metadata.namespace. Фильтрует сам apiserver: --field-selector spec.version=1.1 на стенде вернул No resources found in demo namespace., а с 1.0 — объект hello.
Printer columns — колонки kubectl get: что показывать, вместо одного NAME AGE. Колонки с priority больше нуля видны только в -o wide.
Подресурсы status и scale
subresources.status: {} включает подресурс /status, тот же, что у встроенных типов. Последствия:
- запись в основной ресурс игнорирует изменения
status, а запись в/status— всё, кромеstatus. Пользователь пишет желаемое, контроллер — наблюдаемое, и они не затирают друг друга; - права разделяются: пользователю —
updateнаgreetings, контроллеру — ещё и наgreetings/status(RBAC и подресурсы — в части 29); metadata.generationувеличивается только при изменении полей внеmetadataиstatus, то есть фактически при изменении желаемого состояния. Контроллер записывает вstatus.observedGeneration, какую генерацию он уже обработал, и любой клиент сравнением двух чисел понимает, догнал ли контроллер последние изменения. Ровно такkubectl rollout statusследит за Deployment.
subresources.scale подключает ресурс к общему механизму масштабирования: kubectl scale greeting/hello --replicas=4 и HorizontalPodAutoscaler (автомасштабирование по метрикам, часть 32) работают с Greeting так же, как с Deployment. Нужно указать, где в объекте лежат желаемое число реплик, фактическое и селектор подов в строковом виде.
Смотрим руками: CRD
$ kubectl apply -f greeting-crd.yaml
customresourcedefinition.apiextensions.k8s.io/greetings.demo.example.com created
$ kubectl get crd greetings.demo.example.com -o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\n"}{end}'
NamesAccepted=True
Established=True
$ kubectl api-resources --api-group=demo.example.com
NAME SHORTNAMES APIVERSION NAMESPACED KIND
greetings gr demo.example.com/v1 true Greeting
$ kubectl explain greeting.spec.version
GROUP: demo.example.com
KIND: Greeting
VERSION: v1
FIELD: version <string>
DESCRIPTION:
Тег образа ghcr.io/example/hello
Established=True — apiserver начал обслуживать новый путь. Посмотрим на сам путь и на валидацию:
$ kubectl get --raw /apis/demo.example.com/v1/namespaces/demo/greetings
{"apiVersion":"demo.example.com/v1","items":[],"kind":"GreetingList","metadata":{"continue":"","resourceVersion":"1261791"}}
$ cat <<'EOF' | kubectl apply -f -
apiVersion: demo.example.com/v1
kind: Greeting
metadata:
name: hello
namespace: demo
spec:
version: "1.0"
replicas: 1
host: hello.example.org
EOF
The Greeting "hello" is invalid:
* spec.host: Invalid value: host должен быть в зоне apps.example.com
* spec: Forbidden: публичному приложению нужно минимум 2 реплики, задано 1
Обе ошибки пришли от apiserver, контроллера ещё нет. Исправленный объект:
$ cat <<'EOF' | kubectl apply -f -
apiVersion: demo.example.com/v1
kind: Greeting
metadata:
name: hello
namespace: demo
spec:
version: "1.0"
message: привет
host: hello.apps.example.com
EOF
greeting.demo.example.com/hello created
$ kubectl -n demo get gr
NAME VERSION REPLICAS READY AGE
hello 1.0 2 1s
REPLICAS 2 — значение по умолчанию из схемы, READY пуст: статус заполнять некому. Правило перехода на host тоже проверяет apiserver:
$ kubectl -n demo patch gr hello --type=merge -p '{"spec":{"host":"hello2.apps.example.com"}}'
The Greeting "hello" is invalid: spec.host: Invalid value: "hello2.apps.example.com": host нельзя менять, создайте новый Greeting
Где объект лежит на самом деле — в etcd, под ключом по группе и ресурсу; в отличие от встроенных ресурсов, которые apiserver хранит в Protobuf, пользовательские хранятся как JSON. Утилита etcdctl есть в образе etcd, поэтому её проще всего запустить внутри контейнера через crictl exec, как в части 12; jq оставляет от объекта главное:
user@k8s-master-1:~$ sudo crictl exec "$(sudo crictl ps --name etcd -q)" etcdctl \
--endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key \
get /registry/demo.example.com/greetings/demo/hello --print-value-only \
| jq -c '{apiVersion, kind, metadata: {name: .metadata.name, namespace: .metadata.namespace, generation: .metadata.generation, uid: .metadata.uid}, spec}'
{"apiVersion":"demo.example.com/v1","kind":"Greeting","metadata":{"name":"hello","namespace":"demo","generation":1,"uid":"1cccbbb4-fcb6-4617-a2a5-2a91cbc31c37"},"spec":{"host":"hello.apps.example.com","message":"привет","replicas":2,"version":"1.0"}}
replicas: 2 записан в etcd: значение по умолчанию подставлено ещё при записи. Для сравнения — начало значения встроенного объекта, неймспейса demo: вместо JSON там двоичный Protobuf с префиксом k8s\0:
user@k8s-master-1:~$ sudo crictl exec "$(sudo crictl ps --name etcd -q)" etcdctl … \
get /registry/namespaces/demo --print-value-only | head -c 12 | od -c | head -1
0000000 k 8 s \0 \n 017 \n 002 v 1 022 \t
Утилита etcdctl и устройство etcd — тема части 34; здесь достаточно увидеть, что CRD ничем не отличается от остальных данных кластера.
Контроллер
Контроллер — программа, которая следит за объектами через API и приводит мир в соответствие с ними. Устроена она всегда одинаково:
kube-apiserver
▲ │ list + watch (Greeting, Deployment, Service, Ingress)
create/update│ ▼
patch status │ ┌──────────────┐
│ │ informer │ локальный кеш объектов,
│ │ (кеш) │ события add / update / delete
│ └──────┬───────┘
│ │ ключ "demo/hello"
│ ▼
│ ┌──────────────┐
│ │ work queue │ очередь ключей: дубликаты схлопываются,
│ │ │ ошибки — повтор с нарастающей задержкой
│ └──────┬───────┘
│ ▼
│ ┌──────────────┐
└─────┤ Reconcile │ прочитать желаемое (из кеша), посмотреть,
│ (ключ) │ что есть, создать / поправить / удалить,
└──────────────┘ записать status
Informer держит watch на нужные типы и локальную копию объектов в памяти. Контроллер читает только из этого кеша, а не из apiserver напрямую: десятки контроллеров, каждую секунду перечитывающих все объекты, положили бы apiserver. При старте informer делает list, потом получает изменения через watch с resourceVersion (часть 3) и периодически сверяется заново (resync).
Work queue получает не события, а ключи namespace/name. Если объект поменялся пять раз за секунду, в очереди окажется один ключ, и Reconcile выполнится один раз — с последней версией. Если Reconcile вернул ошибку, ключ вернётся в очередь с экспоненциальной задержкой. Один ключ никогда не обрабатывается двумя потоками одновременно.
Reconcile — функция «привести объект с этим ключом к желаемому состоянию». Главное её свойство — она не знает, что изменилось. Ей не передают «поле replicas поменялось с 2 на 3», ей передают имя объекта, и она каждый раз заново сравнивает желаемое с действительным. Такой подход называется level-triggered (по уровню), в противоположность edge-triggered (по фронту, по событию), и он делает контроллер устойчивым: пропущенное событие, перезапуск контроллера, ручная правка Deployment — следующий вызов Reconcile всё исправит. Отсюда требование идемпотентности: вызов Reconcile десять раз подряд должен давать тот же результат, что и один.
Контроллер следит не только за своими объектами, но и за дочерними: если кто-то удалит Deployment, созданный для Greeting, событие удаления Deployment должно поставить в очередь ключ его владельца. Связь хранится в самом Deployment.
ownerReferences и сборка мусора
У каждого объекта, созданного контроллером, в metadata.ownerReferences записывается владелец:
metadata:
name: hello
namespace: demo
ownerReferences:
- apiVersion: demo.example.com/v1
kind: Greeting
name: hello
uid: 1cccbbb4-fcb6-4617-a2a5-2a91cbc31c37
controller: true
blockOwnerDeletion: true
Этим пользуются двое. Контроллер — чтобы по событию дочернего объекта найти владельца (controller: true означает «управляющий» владелец, он может быть только один). И сборщик мусора в kube-controller-manager: когда владелец удалён, он удаляет все объекты, у которых этот владелец записан. Так удаление Deployment удаляет его ReplicaSet-ы, а те — поды; так же удаление Greeting удалит Deployment, Service и Ingress без единой строчки кода удаления в нашем контроллере.
Режим удаления выбирается при удалении владельца (kubectl delete --cascade=…): background (по умолчанию: владелец удаляется сразу, дети — следом), foreground (владелец остаётся с deletionTimestamp, пока не удалены дети с blockOwnerDeletion: true), orphan (дети остаются, ссылки на владельца с них снимаются). Ограничение: владелец и дочерний объект должны быть в одном неймспейсе, либо владелец — кластерный. Объект в неймспейсе не может владеть кластерным.
Finalizers
Сборщик мусора убирает объекты внутри кластера. Но контроллер может создавать и внешние ресурсы: запись в DNS, бакет в S3, учётную запись в базе. О них сборщик не знает. Для этого есть finalizers — список строк в metadata.finalizers.
Пока список не пуст, объект не удаляется: kubectl delete лишь проставляет metadata.deletionTimestamp, объект остаётся видимым, и это изменение — событие для контроллера. Контроллер видит deletionTimestamp, чистит внешние ресурсы и убирает свою строку из finalizers. Когда список опустел, apiserver удаляет объект окончательно.
kubectl delete greeting hello
│
▼
apiserver: finalizers не пуст → ставит deletionTimestamp, объект остаётся
│
▼
контроллер: Reconcile видит deletionTimestamp
│ → удалить запись DNS (внешний ресурс)
│ → PATCH: убрать "demo.example.com/cleanup" из finalizers
▼
apiserver: finalizers пуст → объект удалён
│
▼
сборщик мусора: удалить Deployment, Service, Ingress по ownerReferences
Обратная сторона: если контроллер удалён или сломан, объекты с его финализатором висят в Terminating вечно, и неймспейс с ними тоже не удалится. Типичная картина после удаления оператора раньше его ресурсов. Снять финализатор руками (kubectl patch … --type=merge -p '{"metadata":{"finalizers":null}}') можно, но тогда внешние ресурсы останутся неубранными — это осознанное решение, а не способ «починить». Правильный порядок удаления оператора: сначала его ресурсы, потом сам оператор, потом CRD.
status и conditions
status — то, что контроллер наблюдает: число готовых реплик, адрес, ошибки. Для сообщения о состоянии принят общий формат conditions — список условий, у каждого:
| Поле | Значение |
|---|---|
type |
имя условия в PascalCase: Ready, Available, Progressing, Degraded |
status |
True, False или Unknown |
reason |
машинная причина в PascalCase: DeploymentNotReady, ImagePullFailed |
message |
текст для человека |
lastTransitionTime |
когда status последний раз менялся (не когда условие перезаписывали) |
observedGeneration |
к какой генерации объекта относится |
Условие Ready — то, что проверяют kubectl wait --for=condition=Ready, kstatus в Helm 4 (часть 27) и GitOps-инструменты. Положительная полярность (Ready=True — хорошо) читается легче, чем Failed=False. Status должен целиком восстанавливаться из наблюдения мира: контроллер не хранит в нём ничего, чего не может вычислить заново, — иначе потеря статуса (а при резервном копировании etcd через kubectl get -o yaml и восстановлении он теряется) потеряет и данные.
Минимальный контроллер на controller-runtime
Писать informer-ы и очереди руками не нужно. client-go — официальная библиотека Go для работы с API, в ней есть informer-ы и work queue. controller-runtime — библиотека проекта Kubernetes SIGs поверх client-go: менеджер с общим кешем, регистрация контроллеров, вебхуки, выбор лидера при нескольких репликах. На ней написано большинство операторов. Kubebuilder — генератор проектов на controller-runtime: по Go-структурам с аннотациями-маркерами генерирует CRD со схемой и CEL-правилами, RBAC, заготовку контроллера и тесты. Operator SDK — проект Red Hat поверх Kubebuilder: те же Go-операторы плюс операторы на Ansible и Helm и интеграция с OLM (Operator Lifecycle Manager — менеджером установки и обновления операторов, встроенным в OpenShift, дистрибутив Kubernetes от Red Hat).
Суть контроллера Greeting без генерированного окружения:
func (r *GreetingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var g demov1.Greeting
if err := r.Get(ctx, req.NamespacedName, &g); err != nil {
// объект удалён — дочерние уберёт сборщик мусора
return ctrl.Result{}, client.IgnoreNotFound(err)
}
labels := map[string]string{"app.kubernetes.io/name": "hello", "app.kubernetes.io/instance": g.Name}
// Желаемый Deployment. CreateOrUpdate читает текущий, вызывает функцию
// и пишет, только если что-то изменилось.
dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: g.Name, Namespace: g.Namespace}}
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, dep, func() error {
dep.Labels = labels
dep.Spec.Replicas = g.Spec.Replicas
dep.Spec.Selector = &metav1.LabelSelector{MatchLabels: labels}
dep.Spec.Template.Labels = labels
dep.Spec.Template.Spec.Containers = []corev1.Container{{
Name: "hello",
Image: "ghcr.io/example/hello:" + g.Spec.Version,
Env: []corev1.EnvVar{{Name: "GREETING", Value: g.Spec.Message}},
Ports: []corev1.ContainerPort{{Name: "http", ContainerPort: 8080}},
}}
// ownerReference: Greeting — управляющий владелец Deployment
return controllerutil.SetControllerReference(&g, dep, r.Scheme)
})
if err != nil {
return ctrl.Result{}, err // ключ вернётся в очередь с задержкой
}
// ... так же Service и Ingress ...
// Status — отдельным запросом в подресурс /status.
g.Status.ReadyReplicas = dep.Status.ReadyReplicas
g.Status.Selector = "app.kubernetes.io/instance=" + g.Name
g.Status.URL = "https://" + g.Spec.Host
g.Status.ObservedGeneration = g.Generation
ready := metav1.ConditionFalse
if dep.Status.ReadyReplicas == *g.Spec.Replicas {
ready = metav1.ConditionTrue
}
meta.SetStatusCondition(&g.Status.Conditions, metav1.Condition{
Type: "Ready", Status: ready, Reason: "DeploymentStatus",
Message: fmt.Sprintf("%d/%d replicas ready", dep.Status.ReadyReplicas, *g.Spec.Replicas),
ObservedGeneration: g.Generation,
})
return ctrl.Result{}, r.Status().Update(ctx, &g)
}
func (r *GreetingReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&demov1.Greeting{}). // свои объекты
Owns(&appsv1.Deployment{}). // события дочерних → ключ владельца
Owns(&corev1.Service{}).
Owns(&networkingv1.Ingress{}).
Complete(r)
}
Этот код собран на стенде с controller-runtime v0.25.2 (библиотеки k8s.io/* v0.37) и Go 1.27.3. Типы Greeting и GreetingList вместо Kubebuilder написаны руками — Go-структуры с полями spec и status и функциями глубокого копирования DeepCopyObject, которые требует интерфейс runtime.Object; функция main создаёт менеджер (ctrl.NewManager), регистрирует в его схеме встроенные типы и Greeting и запускает контроллер. Service и Ingress создаются так же, как Deployment, и с теми же метками.
Здесь видны все идеи предыдущих разделов: Reconcile получает только имя, читает объект из кеша, приводит Deployment к желаемому виду идемпотентно, проставляет владельца, пишет status через подресурс. Owns подписывает контроллер на дочерние Deployment, и их изменения ставят в очередь ключ владельца. Удаление вообще не обрабатывается — его делает сборщик мусора. Финализатор понадобился бы, только если бы контроллер создавал что-то вне кластера.
Контроллер в кластере — это Deployment с ServiceAccount, которому через ClusterRole (или Role, если он следит за одним неймспейсом) разрешены get, list, watch на greetings, update на greetings/status и полный набор на Deployment, Service и Ingress. Kubebuilder генерирует эти права из маркеров // +kubebuilder:rbac:… над функцией Reconcile. Если реплик контроллера больше одной, активна только одна: они соревнуются за объект Lease в coordination.k8s.io, как kube-scheduler и kube-controller-manager.
Смотрим руками: контроллер в работе
Чтобы попробовать контроллер, его не обязательно собирать в образ и ставить Deployment-ом: controller-runtime находит kubeconfig так же, как kubectl, и программу можно запустить прямо на машине администратора (go run . или собранный бинарник) — так контроллеры и отлаживают. На стенде бинарник запускался на мастере. Собирать его лучше не там: компиляция зависимостей client-go заняла оба ядра мастера на несколько минут, kube-apiserver не прошёл проверку живучести и перезапустился, а kube-controller-manager и kube-scheduler успели посидеть в CreateContainerError — рабочая станция для сборки подходит больше. Объект hello из прошлого раздела уже есть, и через несколько секунд после старта:
$ kubectl -n demo get deploy,svc,ingress -l app.kubernetes.io/instance=hello
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/hello 1/2 2 1 64s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/hello ClusterIP 10.102.187.61 <none> 80/TCP 64s
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/hello traefik hello.apps.example.com 80 64s
$ kubectl -n demo get gr -o wide
NAME VERSION REPLICAS READY URL AGE
hello 1.0 2 2 https://hello.apps.example.com 9m49s
$ kubectl -n demo get gr hello -o jsonpath='{.status.conditions[0].message}{"\n"}'
2/2 replicas ready
$ kubectl -n demo get deploy hello -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name} controller={.metadata.ownerReferences[0].controller}{"\n"}'
Greeting/hello controller=true
Колонка ADDRESS у Ingress пуста, а PORTS — только 80: на стенде не было Ingress-контроллера, а наш контроллер не создаёт секцию tls; в кластере с Traefik из части 18 адрес появится. В журнале контроллера среди строк reconcile нашлась и ошибка:
{"level":"error","msg":"Reconciler error","controller":"greeting",…,"error":"Operation cannot be fulfilled on deployments.apps \"hello\": the object has been modified; please apply your changes to the latest version and try again"}
Это оптимистическая блокировка из части 3: между чтением Deployment из кеша и записью его успел изменить kube-controller-manager, и запись с устаревшим resourceVersion отвергнута. Reconcile вернул ошибку, ключ вернулся в очередь с задержкой, следующий проход прошёл чисто — ровно так, как задумано. Устойчивость к ручным правкам — level-triggered в действии:
$ kubectl -n demo scale deploy hello --replicas=5
deployment.apps/hello scaled
$ kubectl -n demo get deploy hello -w
NAME READY UP-TO-DATE AVAILABLE AGE
hello 2/5 2 2 79s
hello 2/2 2 2 80s
hello 2/2 2 2 80s
hello 2/2 2 2 81s
hello 2/2 5 2 82s
hello 2/2 2 2 83s
Контроллер получил событие изменения Deployment, поставил в очередь demo/hello, Reconcile вернул replicas: 2 из Greeting — в пределах секунды, лишние поды даже не успели стать готовыми. Масштабирование через сам ресурс, благодаря подресурсу scale:
$ kubectl -n demo scale greeting hello --replicas=3
greeting.demo.example.com/hello scaled
$ kubectl -n demo wait greeting/hello --for=condition=Ready --timeout=60s
greeting.demo.example.com/hello condition met
$ kubectl -n demo get greeting hello -o jsonpath='{.metadata.generation} {.status.observedGeneration}{"\n"}'
2 2
$ kubectl get --raw /apis/demo.example.com/v1/namespaces/demo/greetings/hello/scale
{"kind":"Scale","apiVersion":"autoscaling/v1","metadata":{"name":"hello","namespace":"demo",…},"spec":{"replicas":3},"status":{"replicas":3,"selector":"app.kubernetes.io/instance=hello"}}
Генерация 2, контроллер обработал генерацию 2 — состояние догнало желаемое. Объект Scale по адресу …/greetings/hello/scale apiserver собирает сам по путям из subresources.scale: с ним и работают kubectl scale и HorizontalPodAutoscaler.
Финализатор можно увидеть в действии и без кода — поставив его руками. Контроллер его не обрабатывает, поэтому снимать придётся тоже руками:
$ kubectl -n demo patch greeting hello --type=merge -p '{"metadata":{"finalizers":["demo.example.com/cleanup"]}}'
greeting.demo.example.com/hello patched
$ kubectl -n demo delete greeting hello --wait=false
greeting.demo.example.com "hello" deleted from demo namespace
$ kubectl -n demo get greeting hello -o jsonpath='{.metadata.deletionTimestamp} {.metadata.finalizers}{"\n"}'
2026-10-31T05:06:33Z ["demo.example.com/cleanup"]
$ kubectl -n demo get deploy -l app.kubernetes.io/instance=hello
NAME READY UP-TO-DATE AVAILABLE AGE
hello 3/3 3 3 2m4s
kubectl пишет deleted, но объект на месте: у него только появился deletionTimestamp, и дочерний Deployment работает. Снимаем финализатор — и apiserver удаляет объект, а сборщик мусора следом убирает Deployment, Service и Ingress:
$ kubectl -n demo patch greeting hello --type=merge -p '{"metadata":{"finalizers":null}}'
greeting.demo.example.com/hello patched
$ kubectl -n demo get greeting
No resources found in demo namespace.
$ kubectl -n demo get deploy,svc,ingress -l app.kubernetes.io/instance=hello
No resources found in demo namespace.
Оператор
Оператор — контроллер с CRD, в которые вложено знание администратора конкретной системы. Термин придумали в 2016 году в CoreOS — компании, создавшей etcd: «оператор» делает то, что раньше делал человек-оператор, — разворачивает кластер базы, заменяет упавший узел, обновляет версию в правильном порядке, делает бэкапы. Наш Greeting — контроллер, но не оператор: в нём нет знаний сверх «создай Deployment». Разница видна на настоящих примерах.
CloudNativePG
CloudNativePG (CNPG) — оператор PostgreSQL, проект в песочнице CNCF. Ставится официальным манифестом выпуска или Helm-чартом в неймспейс cnpg-system; актуальная версия — 1.30.1:
$ kubectl apply --server-side -f https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v1.30.1/cnpg-1.30.1.yaml
…
deployment.apps/cnpg-controller-manager serverside-applied
mutatingwebhookconfiguration.admissionregistration.k8s.io/cnpg-mutating-webhook-configuration serverside-applied
validatingwebhookconfiguration.admissionregistration.k8s.io/cnpg-validating-webhook-configuration serverside-applied
--server-side здесь не прихоть: CRD clusters.postgresql.cnpg.io в манифесте занимает около 460 КиБ, poolers — около 660 КиБ, а клиентский apply записал бы копию каждого объекта в аннотацию last-applied-configuration, тогда как все аннотации объекта вместе ограничены 256 КиБ. Главный ресурс — Cluster:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg
namespace: demo
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:18.6-standard-trixie
storage:
storageClass: local-path
size: 20Gi
bootstrap:
initdb:
database: app
owner: app
affinity:
topologyKey: kubernetes.io/hostname
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters:
barmanObjectName: s3-store
Образ PostgreSQL — из официального набора проекта: minimal (только PostgreSQL), standard (плюс локали и популярные расширения) и устаревший system со встроенным Barman Cloud для старого способа резервного копирования. Без imageName CNPG 1.30.1 берёт 18.6-system-trixie; с плагином резервного копирования (о нём ниже) достаточно standard. Класс хранилища local-path — из части 25.
Что делает оператор по этому объекту — и что раньше делал бы администратор:
- Разворачивает кластер PostgreSQL из трёх экземпляров: первичный и две реплики с потоковой репликацией (PostgreSQL передаёт журнал предзаписи, WAL, на реплики по сети). Он не использует StatefulSet: поды
pg-1,pg-2,pg-3и их PVC он создаёт сам, потому что ему нужно управлять каждым экземпляром отдельно — какой первичный, какой пересоздать, к какому тому прикрепить. - Создаёт сервисы
pg-rw(всегда указывает на первичный),pg-ro(только реплики) иpg-r(любой экземпляр). Приложение подключается кpg-rw.demo.svc, и при переключении ему не нужно знать, кто теперь главный. Пароль пользователяapp— в Secretpg-app(типkubernetes.io/basic-auth, там же готовые строки подключения); рядом оператор создаёт свой удостоверяющий центрpg-ca, сертификаты сервераpg-serverи репликацииpg-replicationи два PodDisruptionBudget —pgдля реплик иpg-primaryдля первичного. - Переключает при отказе (failover): если первичный недоступен, оператор выбирает реплику с наиболее свежим WAL, повышает её и перенаправляет
pg-rw. Плановое переключение (switchover) — например, перед обслуживанием узла — делается плагиномkubectl cnpg promote pg pg-2. - Обновляет PostgreSQL в правильном порядке: при смене
imageNameсначала по одной пересоздаёт реплики, дожидаясь готовности каждой, и только потом берётся за первичный. Как именно — задаётprimaryUpdateMethod: по умолчаниюrestart— первичный перезапускается на месте с новым образом (на стенде запись была недоступна около 20 секунд), аswitchoverсначала переключает роль на уже обновлённую реплику, и пауза сокращается до времени переключения.primaryUpdateStrategy: supervisedвместо умолчанияunsupervisedоставляет этот последний шаг администратору. - Делает резервные копии: непрерывный архив WAL и базовые копии в объектное хранилище S3 (протокол объектного хранилища Amazon, который реализуют и многие другие системы), что даёт восстановление на любой момент времени (PITR, point-in-time recovery). Начиная с версии 1.26 встроенная настройка
barmanObjectStoreобъявлена устаревшей, а копирование вынесено в плагин Barman Cloud (актуальная версия 0.15.1) со своим ресурсомObjectStore. Плагин ставится отдельным манифестом в тот жеcnpg-systemи требует cert-manager из части 19: оператор и плагин общаются по gRPC (протоколу удалённого вызова процедур поверх HTTP/2) со взаимной проверкой TLS-сертификатов, и манифест плагина заказывает их объектамиIssuerиCertificate. На стенде схемы обоих ресурсов ниже провереныkubectl apply --dry-run=serverс установленными CRD, а сами бэкапы — нет:
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: s3-store
namespace: demo
spec:
retentionPolicy: "30d"
configuration:
destinationPath: s3://pg-backups/
endpointURL: https://s3.example.com
s3Credentials:
accessKeyId:
name: pg-backup-s3
key: ACCESS_KEY_ID
secretAccessKey:
name: pg-backup-s3
key: ACCESS_SECRET_KEY
wal:
compression: gzip
---
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: pg-nightly
namespace: demo
spec:
schedule: "0 0 2 * * *" # формат cron с секундами: каждый день в 02:00:00
backupOwnerReference: self
cluster:
name: pg
method: plugin
pluginConfiguration:
name: barman-cloud.cloudnative-pg.io
Восстановление — новый Cluster с bootstrap.recovery и источником из того же хранилища; так же поднимают копию для проверки бэкапов, и это стоит делать регулярно. Хранилище для данных PostgreSQL — локальные диски узлов или блочные тома, а не NFS: почему, разбиралось в части 23 и части 25. Репликацию на уровне PostgreSQL оператор и так обеспечивает, поэтому локальные тома без собственной репликации здесь — нормальный выбор.
На стенде Cluster создавался без секции plugins (плагин не ставился) и сначала без imageName:
$ kubectl -n demo get cluster pg
NAME AGE INSTANCES READY STATUS PRIMARY
pg 4m13s 3 3 Cluster in healthy state pg-1
$ kubectl -n demo get pods -l cnpg.io/cluster=pg -L cnpg.io/instanceRole -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES INSTANCEROLE
pg-1 1/1 Running 0 2m36s 172.20.172.33 k8s-node-2 <none> <none> primary
pg-2 1/1 Running 0 69s 172.20.211.41 k8s-node-1 <none> <none> replica
pg-3 1/1 Running 0 28s 172.20.211.42 k8s-node-1 <none> <none> replica
$ kubectl -n demo get svc,sts
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/pg-r ClusterIP 10.100.227.180 <none> 5432/TCP 4m13s
service/pg-ro ClusterIP 10.106.229.62 <none> 5432/TCP 4m12s
service/pg-rw ClusterIP 10.101.30.144 <none> 5432/TCP 4m12s
StatefulSet нет, экземпляры создаются по одному: сначала Job pg-1-initdb инициализирует базу на томе первого, потом реплики клонируют его. pg-2 и pg-3 оказались на одном узле: воркеров на стенде два, а анти-аффинити CNPG по умолчанию preferred — с podAntiAffinityType: required третий экземпляр ждал бы в Pending третьего узла. Удаляем первичный под и смотрим за кластером раз в две секунды:
$ kubectl -n demo delete pod pg-1
pod "pg-1" deleted from demo namespace
05:12:16 pg 4m29s 3 2 Failing over pg-1
05:12:21 pg 4m33s 3 2 Failing over pg-2
05:12:23 pg 4m36s 3 2 Waiting for the instances to become active pg-2
05:12:45 pg 4m57s 3 3 Cluster in healthy state pg-2
$ kubectl -n demo get pods -l cnpg.io/cluster=pg -L cnpg.io/instanceRole
NAME READY STATUS RESTARTS AGE INSTANCEROLE
pg-1 1/1 Running 0 24s replica
pg-2 1/1 Running 0 113s primary
pg-3 1/1 Running 0 72s replica
Через пять секунд первичным стал pg-2, а пересозданный pg-1 вернулся репликой; кластер полностью здоров через полминуты. Затем смена образа — imageName: …:18.6-standard-trixie патчем (выборка тех же строк по изменению статуса):
05:13:35 Waiting for the instances to become active pg-3 пересоздаётся (replica)
05:14:03 Waiting for the instances to become active pg-3 готов, следом pg-1 (replica)
05:14:33 Primary instance is being restarted without a switchover pg-2 (primary)
05:14:54 Cluster in healthy state первичный — по-прежнему pg-2
Реплики обновлены по одной, первичный перезапущен на месте — умолчание primaryUpdateMethod: restart. Удаление Cluster удаляет и его PVC: через секунды после kubectl delete cluster pg в неймспейсе не осталось ни подов, ни томов.
RabbitMQ Cluster Operator
RabbitMQ Cluster Operator — официальный оператор брокера сообщений RabbitMQ от его разработчиков. Ставится одним манифестом выпуска (актуальный — 2.23.2) и создаёт неймспейс rabbitmq-system, но в кластере уже должен быть cert-manager из части 19: сертификат для своих вебхуков манифест заказывает объектами Issuer и Certificate. Без него kubectl apply создаёт всё остальное и сообщает no matches for kind "Certificate" in version "cert-manager.io/v1", под оператора ждёт несуществующий Secret с сертификатом, а любой RabbitmqCluster отвергается — failed calling webhook "mrabbitmqcluster-v1beta1.kb.io" … connect: connection refused. Ресурс — RabbitmqCluster:
apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
name: mq
namespace: demo
spec:
replicas: 3
persistence:
storageClassName: local-path
storage: 10Gi
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
memory: 1Gi
rabbitmq:
additionalConfig: |
default_queue_type = quorum
По нему оператор создаёт StatefulSet mq-server (здесь стабильные имена подов как раз нужны: узлы RabbitMQ знают друг друга по имени), сервис mq для клиентов и безголовый mq-nodes для связи узлов между собой, Secret mq-default-user с учётными данными администратора, ConfigMap с конфигурацией и Secret mq-erlang-cookie — общий секрет Erlang, без которого узлы не объединяются в кластер:
$ kubectl -n demo get rabbitmqcluster,sts,svc,secret,cm -o wide | grep mq
rabbitmqcluster.rabbitmq.com/mq True True 2m27s
statefulset.apps/mq-server 3/3 2m26s rabbitmq rabbitmq:4.3.4-management
service/mq ClusterIP 10.111.195.78 <none> 5672/TCP,15672/TCP,15692/TCP 2m27s app.kubernetes.io/name=mq
service/mq-nodes ClusterIP None <none> 4369/TCP,25672/TCP 2m27s app.kubernetes.io/name=mq
secret/mq-default-user Opaque 8 2m26s
secret/mq-erlang-cookie Opaque 1 2m27s
configmap/mq-plugins-conf 1 2m26s
configmap/mq-server-conf 2 2m26s
Порты mq: 5672 — AMQP (двоичный протокол, по которому клиенты публикуют сообщения в брокер и получают их из очередей), 15672 — веб-интерфейс управления, 15692 — метрики Prometheus; у mq-nodes 4369 — служба имён Erlang (epmd; RabbitMQ написан на Erlang, и его узлы находят друг друга через неё) и 25672 — связь узлов кластера. Дальше — то, что администратор делал бы вручную по инструкции: при обновлении версии или конфигурации поды перезапускаются по одному, и хук preStop каждого пода не даёт ему остановиться, пока у кворумных очередей (реплицируемых очередей RabbitMQ на алгоритме Raft — согласованной репликации с выбором лидера, где запись подтверждается большинством копий) не останется достаточно живых копий. Сам хук — несколько команд rabbitmq-upgrade:
$ kubectl -n demo get sts mq-server -o jsonpath='{.spec.template.spec.containers[0].lifecycle.preStop.exec.command[2]}'
if [ ! -z "$(cat /etc/pod-info/skipPreStopChecks)" ]; then exit 0; fi; rabbitmq-upgrade await_online_quorum_plus_one -t 604800 && (rabbitmq-upgrade await_online_synchronized_mirror -t 604800 || true) && rabbitmq-upgrade drain -t 604800
await_online_quorum_plus_one ждёт, пока у каждой кворумной очереди останется кворум и после остановки этого узла, а drain переводит узел в режим обслуживания — клиенты и лидеры очередей уходят с него до остановки. После изменения кластера оператор выполняет rabbitmq-queues rebalance all — перераспределяет лидеров очередей между узлами (отключается полем skipPostDeploySteps). Память под лимит пода оператор вычисляет сам: в конфигурацию попадает total_memory_available_override_value = 858993460 — 80 % от лимита 1 ГиБ, и уже от этого значения RabbitMQ считает свой порог vm_memory_high_watermark (по умолчанию 0,6), при котором перестаёт принимать сообщения. Оставшиеся 20 % — запас под память, которую RabbitMQ не учитывает, чтобы контейнер не упёрся в лимит и не был убит OOM раньше, чем брокер начнёт сдерживать отправителей.
Очереди, обмены, пользователи и права — отдельным оператором Messaging Topology Operator с ресурсами Queue, Exchange, Binding, User, Permission, Policy: тогда вся топология брокера описывается YAML-ом в Git рядом с приложением.
Как оценивать оператор перед установкой
Оператор — это код с широкими правами, который будет месяцами работать в кластере без присмотра и принимать решения за вас, в том числе удалять данные. Перед установкой стоит проверить:
Зрелость. Сколько лет проекту, как часто выходят релизы, есть ли политика поддержки версий, кто стоит за проектом (фонд, вендор, один энтузиаст). Полезная шкала — уровни возможностей операторов из Operator SDK: базовая установка → бесшовные обновления → полный жизненный цикл (бэкапы, восстановление, отказ) → метрики и анализ → автопилот (самонастройка). Оператор, который умеет только установку, — по сути чарт с лишним процессом.
Права. Посмотреть ClusterRole оператора до установки (helm template или манифест) и после — что он реально может:
$ kubectl auth can-i --list --as=system:serviceaccount:cnpg-system:cnpg-manager \
| grep -E '^(Resources|configmaps|secrets|services|pods|pods/exec|persistentvolumeclaims|jobs.batch|nodes) '
Resources Non-Resource URLs Resource Names Verbs
configmaps [] [] [create delete get list patch update watch]
secrets [] [] [create delete get list patch update watch]
services [] [] [create delete get list patch update watch]
persistentvolumeclaims [] [] [create delete get list patch watch]
pods/exec [] [] [create delete get list patch watch]
pods [] [] [create delete get list patch watch]
jobs.batch [] [] [create delete get list patch watch]
nodes [] [] [get list watch]
$ kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:cnpg-system:cnpg-manager
yes
Оператор базы данных, читающий Secret во всех неймспейсах, — обычное дело, но это значит, что его компрометация — компрометация всех секретов кластера. Многие операторы умеют работать в режиме одного или нескольких неймспейсов с Role вместо ClusterRole — если он подходит, его стоит предпочесть. Как читать эти права и почему secrets, pods и create опасны — в части 29.
Вебхуки. Операторы часто ставят ValidatingWebhookConfiguration и MutatingWebhookConfiguration — регистрацию своего HTTPS-сервиса, который apiserver вызывает при создании объектов. С failurePolicy: Fail и широким objectSelector упавший оператор блокирует создание объектов далеко за пределами своих CRD. Проверьте, на что именно подписан вебхук, — kubectl get validatingwebhookconfigurations -o yaml. У CNPG, например, все девять вебхуков (пять проверяющих и четыре изменяющих) с failurePolicy: Fail, но подписаны только на собственные ресурсы — clusters, backups, scheduledbackups, databases, poolers: упавший оператор не даст создать Cluster, но поды и Deployment остальных приложений не заденет.
Обновление CRD. Как обновляются CRD при новой версии: отдельным манифестом, шаблоном чарта, самим оператором при старте? Есть ли конверсионный вебхук и мигрируют ли storedVersions? Что из этого нужно делать руками — обычно написано в заметках к релизу, и пропуск шага приводит к тому, что новый оператор не понимает старые объекты.
Удаление. Что будет с данными при удалении Cluster или самого оператора, есть ли финализаторы, удаляются ли PVC. У CNPG, например, PVC по умолчанию удаляются вместе с Cluster (на стенде — за секунды), и бэкап — единственное, что останется.
Совместимость. С какими версиями Kubernetes оператор проверен, и не помешает ли он обновлению кластера (часть 35).
Знакомые операторы
Теперь можно по-новому посмотреть на то, что уже стоит на стенде:
- Tigera operator из части 4 следит за ресурсом
Installation(operator.tigera.io/v1) и по нему разворачивает calico-node, calico-typha, calico-kube-controllers и их конфигурацию. Обновление Calico — смена версии оператора, а дальше он сам в правильном порядке обновляет компоненты на узлах. - cert-manager из части 19 — набор контроллеров:
Certificate→CertificateRequest→Order→Challenge(для ACME) → Secret с сертификатом. Вstatus.conditionsуCertificate—Ready, и к нему же применима вся механика owner references: удалитеCertificate, и сборщик удалит промежуточные объекты. - MetalLB из части 17 настраивается ресурсами
IPAddressPoolиL2Advertisement— CRD, по которым его контроллер и speaker-ы на узлах раздают адреса.
На стенде в момент, когда стояли Calico, cert-manager, CNPG с CRD плагина, оператор RabbitMQ и наш Greeting, CRD по группам выглядели так:
$ kubectl get crd -o custom-columns=GROUP:.spec.group --no-headers | sort | uniq -c | sort -rn
22 crd.projectcalico.org
11 postgresql.cnpg.io
9 operator.tigera.io
4 cert-manager.io
2 acme.cert-manager.io
1 rabbitmq.com
1 policy.networking.k8s.io
1 demo.example.com
1 barmancloud.cnpg.io
52 типа, из которых свой — один. В кластере, где стоит десяток компонентов, CRD бывает больше сотни, и почти весь «Kubernetes», с которым работает администратор, — это уже не встроенные ресурсы, а чужие расширения API.
Уборка
Удалять — в обратном порядке, как говорилось выше: сначала Cluster и RabbitmqCluster (оператор удалит поды и PVC), потом операторы теми же манифестами (kubectl delete -f …; CRD уходят вместе с ними, а с CRD — все объекты этих типов), потом наш CRD и cert-manager. Объекты Lease выбора лидера cert-manager в kube-system (cert-manager-controller и cert-manager-cainjector-leader-election) манифест не удаляет — их убирают руками.
Источники
- Kubernetes: Custom Resources и Extend the Kubernetes API with CustomResourceDefinitions — CRD, структурная схема, обрезка, значения по умолчанию, правила CEL и их стоимость, ratcheting,
selectableFields, подресурсы, колонки; Versions in CustomResourceDefinitions —served,storage, конверсия иstoredVersions. - Kubernetes: Common Expression Language in Kubernetes и cel.dev — язык CEL, функции строк и
format, оценка стоимости. - Kubernetes: Controllers и Operator pattern — цикл согласования и операторы.
- Kubernetes: Owners and Dependents, Garbage Collection и Finalizers —
ownerReferences, режимы каскадного удаления, финализаторы. - Kubernetes API Conventions —
specиstatus,observedGeneration, формат conditions; Kubernetes API Concepts —resourceVersion, watch и оптимистическая блокировка. - controller-runtime (справочник пакетов), The Kubebuilder Book и Operator SDK: Operator Capability Levels — библиотека, генератор и уровни зрелости операторов.
- CloudNativePG (выпуск v1.30.1): Installation and upgrades, Rolling Updates —
primaryUpdateMethodиprimaryUpdateStrategy, Failover; Barman Cloud Plugin —ObjectStore, требования к cert-manager; образы PostgreSQL —minimal,standard,system. - RabbitMQ Cluster Operator: установка и использование, выпуск v2.23.2; Quorum Queues и Memory Threshold and Limit — кворумные очереди,
total_memory_available_override_valueиvm_memory_high_watermark.
Дальше — аутентификация и RBAC: как kube-apiserver узнаёт, кто к нему пришёл, почему в Kubernetes нет объекта «пользователь», как выдать доступ администратору сертификатом и приложению — токеном ServiceAccount, и какие права на самом деле опасны.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.