Модуль 16 / 19
Розширення API: CRD, controllers та operator pattern
Зміст
1. Цілі та результат модуля
Ви розширите Kubernetes API власним типом, реалізуєте один reconciliation cycle і простежите spec, generation, status, ownerReferences та finalizer.
- CRD додає новий API resource
- custom resource зберігає desired state
- controller спостерігає й узгоджує стан
- status повідомляє спостережений результат
- operator поєднує domain knowledge з control loop
2. Теорія: розширення API
CRD реєструє group, versions, scope, names та OpenAPI schema. Після Established ресурс бере участь у discovery, RBAC, watch, admission і storage API Server. CRD сам не створює Deployment і не виконує business logic.
Version із served доступна клієнтам, storage визначає формат в etcd. Зміна версій потребує conversion strategy та сумісності даних; v1alpha1 повідомляє нестабільність контракту.
3. Schema, status і printer columns
Structural OpenAPI schema валідовує та pruning невідомих полів. status subresource відокремлює записи controller від user-owned spec. additionalPrinterColumns покращують get, але не замінюють Conditions.
Status має бути спостереженням, а не другим desired state. observedGeneration показує, до якої версії spec належить status.
4. Reconciliation loop
Controller watch/list отримує події, кладе keys у work queue та багаторазово приводить actual state до desired state. Reconcile має бути ідемпотентним: повторний виклик після timeout чи duplicate event дає той самий результат.
Production controller враховує retries/backoff, caches, conflicts, rate limits, leader election, shutdown і Conditions. Shell-скрипт лабораторії виконує один цикл лише для прозорості механіки.
5. ownerReferences і garbage collection
Namespaced owner може володіти namespaced dependent у тому самому namespace. UID захищає від збігу імен після повторного створення. controller:true позначає керівного owner; blockOwnerDeletion бере участь у foreground deletion.
Owner reference не означає reconciliation і не підходить для зовнішніх ресурсів. Garbage collector працює лише з Kubernetes objects та коректними scope/UID.
6. Finalizers і видалення
Delete об’єкта з finalizer встановлює deletionTimestamp, але зберігає об’єкт, доки controller не завершить cleanup і не видалить свій finalizer. Finalizer — opaque key, а не виконуваний hook.
Забутий finalizer лишає Terminating object. Видаляти його вручну можна лише після перевірки cleanup або свідомого прийняття ризику витоку.
7. Методичні вказівки
- Проєктуйте API contract до controller code
- Додавайте schema і status subresource
- Порівнюйте generation з observedGeneration
- Робіть reconcile ідемпотентним
- Використовуйте UID в ownerReferences
- Обробляйте update conflicts
- Не зберігайте нескінченні дані в status
- Не знімайте finalizer без перевірки cleanup
8. Лабораторна робота: CRD і custom resource
Зареєструйте namespaced CourseApp, дочекайтеся Established і дослідіть schema через discovery/explain. Потім створіть desired state.
mkdir -p /tmp/k8s-m16
cat > /tmp/k8s-m16/crd.yaml <<'EOF'
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata: {name: courseapps.training.goretskiy.pro}
spec:
group: training.goretskiy.pro
scope: Namespaced
names: {plural: courseapps, singular: courseapp, kind: CourseApp, shortNames: [capp]}
versions:
- name: v1alpha1
served: true
storage: true
subresources: {status: {}}
additionalPrinterColumns:
- {name: Ready, type: string, jsonPath: .status.readyReplicas}
- {name: Observed, type: integer, jsonPath: .status.observedGeneration}
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: [image, replicas]
properties:
image: {type: string, minLength: 1}
replicas: {type: integer, minimum: 1, maximum: 5}
status:
type: object
properties:
observedGeneration: {type: integer, format: int64}
readyReplicas: {type: integer, format: int32}
url: {type: string}
EOF
kubectl apply --dry-run=server -f /tmp/k8s-m16/crd.yaml
kubectl apply -f /tmp/k8s-m16/crd.yaml
kubectl wait --for=condition=Established crd/courseapps.training.goretskiy.pro --timeout=60s
kubectl api-resources --api-group=training.goretskiy.pro
kubectl explain courseapp.speckubectl create namespace k8s-m16
cat > /tmp/k8s-m16/instance.yaml <<'EOF'
apiVersion: training.goretskiy.pro/v1alpha1
kind: CourseApp
metadata: {name: demo, namespace: k8s-m16}
spec: {image: nginx:1.29-alpine, replicas: 2}
EOF
kubectl apply --dry-run=server -f /tmp/k8s-m16/instance.yaml
kubectl apply -f /tmp/k8s-m16/instance.yaml
kubectl -n k8s-m16 get courseapp demo -o yaml9. Лабораторна робота: один reconciliation cycle
Скрипт читає CourseApp, server-side застосовує дочірні Deployment/Service з ownerReferences і записує status через окремий subresource.
cat > /tmp/k8s-m16/reconcile.sh <<'EOF'
#!/bin/sh
set -eu
ns=${1:-k8s-m16}; name=${2:-demo}
uid=$(kubectl -n "$ns" get courseapp "$name" -o jsonpath='{.metadata.uid}')
generation=$(kubectl -n "$ns" get courseapp "$name" -o jsonpath='{.metadata.generation}')
image=$(kubectl -n "$ns" get courseapp "$name" -o jsonpath='{.spec.image}')
replicas=$(kubectl -n "$ns" get courseapp "$name" -o jsonpath='{.spec.replicas}')
cat > /tmp/k8s-m16/children.yaml <<YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: $name
namespace: $ns
ownerReferences:
- {apiVersion: training.goretskiy.pro/v1alpha1, kind: CourseApp, name: $name, uid: $uid, controller: true, blockOwnerDeletion: true}
spec:
replicas: $replicas
selector: {matchLabels: {app: $name}}
template:
metadata: {labels: {app: $name}}
spec:
containers:
- {name: nginx, image: $image, ports: [{name: http, containerPort: 80}]}
---
apiVersion: v1
kind: Service
metadata:
name: $name
namespace: $ns
ownerReferences:
- {apiVersion: training.goretskiy.pro/v1alpha1, kind: CourseApp, name: $name, uid: $uid, controller: true, blockOwnerDeletion: true}
spec:
selector: {app: $name}
ports: [{name: http, port: 80, targetPort: http}]
YAML
kubectl apply --server-side --field-manager=courseapp-controller -f /tmp/k8s-m16/children.yaml
kubectl -n "$ns" rollout status "deployment/$name" --timeout=120s
ready=$(kubectl -n "$ns" get deployment "$name" -o jsonpath='{.status.readyReplicas}')
kubectl -n "$ns" patch courseapp "$name" --subresource=status --type=merge \
-p "{"status":{"observedGeneration":$generation,"readyReplicas":${ready:-0},"url":"http://$name.$ns.svc"}}"
EOF
chmod +x /tmp/k8s-m16/reconcile.sh
/tmp/k8s-m16/reconcile.sh
kubectl -n k8s-m16 get courseapp,deployment,service,pod
kubectl -n k8s-m16 get deployment demo -o jsonpath='{.metadata.ownerReferences}{"\n"}'10. Лабораторна робота: generation і drift
Після зміни spec generation випереджає observedGeneration до наступного reconcile. Потім ручний scale створює drift, який повторний ідемпотентний цикл виправляє.
kubectl -n k8s-m16 patch courseapp demo --type=merge -p '{"spec":{"replicas":3}}'
kubectl -n k8s-m16 get courseapp demo -o jsonpath='before: generation={.metadata.generation} observed={.status.observedGeneration}{"\n"}'
/tmp/k8s-m16/reconcile.sh
kubectl -n k8s-m16 get courseapp demo -o jsonpath='after: generation={.metadata.generation} observed={.status.observedGeneration} ready={.status.readyReplicas}{"\n"}'
kubectl -n k8s-m16 get deployment demo11. Діагностика: validation, finalizer і deletion
replicas=0 відхиляється API й не змінює stored desired state. Ручний drift Deployment видно під час порівняння CR і child.
kubectl -n k8s-m16 patch courseapp demo --type=merge -p '{"spec":{"replicas":0}}' || true
kubectl -n k8s-m16 get courseapp demo -o jsonpath='desired={.spec.replicas} generation={.metadata.generation}{"\n"}'
kubectl -n k8s-m16 scale deployment demo --replicas=1
kubectl -n k8s-m16 get courseapp,deployment
/tmp/k8s-m16/reconcile.sh
kubectl -n k8s-m16 get courseapp,deploymentДодайте finalizer: delete лише ставить deletionTimestamp. Після імітації cleanup зніміть finalizer і спостерігайте garbage collection дочірніх об’єктів.
kubectl -n k8s-m16 patch courseapp demo --type=merge \
-p '{"metadata":{"finalizers":["training.goretskiy.pro/cleanup"]}}'
kubectl -n k8s-m16 delete courseapp demo --wait=false
kubectl -n k8s-m16 get courseapp demo -o jsonpath='deletionTimestamp={.metadata.deletionTimestamp}{" finalizers="}{.metadata.finalizers}{"\n"}'
kubectl -n k8s-m16 get deployment,service
# Имитируем завершение внешнего cleanup контроллером.
kubectl -n k8s-m16 patch courseapp demo --type=merge -p '{"metadata":{"finalizers":null}}'
kubectl -n k8s-m16 wait --for=delete deployment/demo --timeout=60s
kubectl -n k8s-m16 get courseapp,deployment,service 2>&1 || true12. Контроль, критерії засвоєння та cleanup
- Що робить CRD без controller?
- served проти storage version?
- Навіщо status subresource?
- Що доводить observedGeneration?
- Чому reconcile ідемпотентний?
- Навіщо owner UID?
- Чи видаляє GC зовнішній DNS?
- Що робить deletionTimestamp?
- Finalizer є hook-скриптом?
- Чим operator відрізняється від загального controller?
kubectl delete namespace k8s-m16 --ignore-not-found --wait=true
kubectl delete crd courseapps.training.goretskiy.pro --wait=true
rm -rf /tmp/k8s-m16
kubectl api-resources --api-group=training.goretskiy.pro
kubectl get crd courseapps.training.goretskiy.pro 2>&1 || trueМодуль засвоєно, якщо ви розширюєте API, виправляєте drift повторним reconcile й безпечно проводите об’єкт через finalizer та garbage collection.