Взагалі почав писати створення власного Kubernetes Operator, але вирішив винести окремо тему про те, що таке власне Kubernetes CustomResourceDefinition, і як створення CRD взагалі працює на рівні Kubernetes API та
etcd
.
Тобто, почати з того, як власне Kubernetes працює з ресурсами, і що відбувається, коли ми створюємо чи редагуємо ресурси.
Зміст
Kubernetes API
Отже, вся комунікація з Kubernetes Control Plane відбувається через його головний ендпоінт – Kubernetes API, який являє собою компонент Kubernetes Control Plane – див. Cluster Architecture.
Документація – The Kubernetes API та Kubernetes API Concepts.
Через API ми комунікуємо з Kubernetes, а всі ресурси та інформація по ним зберігаються в базі даних – etcd
.
Інші компоненти Control Plane – це Kube Controller Manager з набором дефолтних контролерів, які відповідають за роботу з ресурсами, та Scheduler, що відповідає за те, як ресурси будуть розміщатись на Worker Nodes.
Kubernetes API – це звичайний HTTPS REST API, до якого ми можемо звернутись навіть з curl
.
Для доступу до кластеру можемо використати kubectl proxy
, який візьме параметри з ~/.kube/config
з адресою API Server та токеном, і створить тунель до нього.
В мене є налаштований доступ до AWS EKS – тому підключення піде до нього:
$ kubectl proxy --port=8080 Starting to serve on 127.0.0.1:8080
І звертаємось до API:
$ curl -s localhost:8080 | jq { "paths": [ "/.well-known/openid-configuration", "/api", "/api/v1", "/apis", ... "/version" ] }
Власне, що ми бачимо – це список API endpoints, які підтримує Kubernetes API:
/api/
: інформація по самому Kubernetes API та точка входу до core API Groups (див. далі)/api/v1
: core API group з Pods, ConfigMaps, Services, etc/apis/
: APIGroupList – решта API Groups в системі та їх версії, в тому числі і API Groups, створені з різних CRD- наприклад, для API Group
operator.victoriametrics.com
можемо бачити підтримку двох версій – “operator.victoriametrics.com/v
1″ “operator.victoriametrics.com/v1beta1
“
- наприклад, для API Group
/version
: інформація по версії кластера
Ну і далі вже можемо піти глибше, і подивитись що всередині кожного ендпоінту, наприклад – отримати інформацію про всі Pods в кластері:
$ curl -s localhost:8080/api/v1/pods | jq ... { "metadata": { "name": "backend-ws-deployment-6db58cc97c-k56lm", ... "namespace": "staging-backend-api-ns" "labels": { "app": "backend-ws", "component": "backend", ... "spec": { "volumes": [ { "name": "eks-pod-identity-token", ... "containers": [ { "name": "backend-ws-container", "image": "492***148.dkr.ecr.us-east-1.amazonaws.com/challenge-backend-api:v0.171.9", "command": [ "gunicorn", "websockets_backend.run_api:app", ... "resources": { "requests": { "cpu": "200m", "memory": "512Mi" } }, ...
Тут бачимо інформацію про Pod з іменем “backend-ws-deployment-6db58cc97c-k56lm“, який живе в Kubernetes Namespace “staging-backend-api-ns“, і решту інформації про нього – volumes, які в цьому поді контейнери, ресурси і т.д.
Kubernetes API Groups та Kind
API Groups – це спосіб організації ресурсів у Kubernetes. Вони групуються за групами, версіями та типами ресурсів (Kind).
Тобто структура API:
- API Group
- versions
- kind
- versions
Наприклад, в /api/v1
ми бачимо Kubernetes Core API Group, в /apis
– API Groups apps
, batch
, events
і так далі.
Структура буде такою:
/apis/<group>
– сама група та її версії/apis/<group>/<version>
– конкретна версія групи вже з конкретними resources (Kind)/apis/<group>/<version>/<resource>
– доступ до конкретного ресурсу та об’єктів в ньому
Note: Kind vs resource: Kind – це назва ресурсу, яка задається в schema цього ресурсу. А resource – це ім’я, яке використовується при побудові URI при запиті до API Server.
Наприклад, для API Group apps маємо версію v1
:
$ curl -s localhost:8080/apis/apps | jq { "kind": "APIGroup", "apiVersion": "v1", "name": "apps", "versions": [ { "groupVersion": "apps/v1", "version": "v1" } ], ...
А всередині версії – ресурси, наприклад deployments
:
$ curl -s localhost:8080/apis/apps/v1 | jq { ... { "name": "deployments", "singularName": "deployment", "namespaced": true, "kind": "Deployment", "verbs": [ "create", "delete", "deletecollection", "get", "list", "patch", "update", "watch" ], "shortNames": [ "deploy" ], "categories": [ "all" ], ...
І вже використовуючи цю групу, версію та конкретний тип ресурсу (kind) – отримуємо всі об’єкти:
$ curl -s localhost:8080/apis/apps/v1/deployments/ | jq { "kind": "DeploymentList", "apiVersion": "apps/v1", "metadata": { "resourceVersion": "1534" }, "items": [ { "metadata": { "name": "coredns", "namespace": "kube-system", "uid": "9d7f6de3-041e-4afe-84f4-e124d2cc6e8a", "resourceVersion": "709", "generation": 2, "creationTimestamp": "2025-07-12T10:15:33Z", "labels": { "k8s-app": "kube-dns" }, ...
Окей, ми звернулись до API – але звідки він бере всі ті дані, що нам відображаються?
Kubernetes та etcd
Для зберігання даних в Kubernetes маємо ще один ключовий компонент Control Plane – etcd.
Власне це просто key:value база даних з усіма даними, які і формують наш кластер – всі його налаштування, вся ресурси, всі стани цих ресурсів, RBAC-групи тощо.
Коли Kubernetes API Server отримує запит, наприклад – POST /apis/apps/v1/namespaces/default/deployments
– він спершу перевіряє відповідність обʼєкта до схеми ресурсу (валідація), і тільки після цього зберігає його в etcd
.
База etcd
складається з набору ключів. Наприклад Pod з іменем “nginx-abc” буде зберігатись в ключі з іменем /registry/pods/default/nginx-abc
.
Див. документацію Operating etcd clusters for Kubernetes.
В AWS EKS ми доступу до etcd
не маємо (і це добре), але можемо запустити Minikube, і трохи подивитись там:
$ minikube start ... 🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
Перевіряємо системні поди:
$ kubectl -n kube-system get pod NAME READY STATUS RESTARTS AGE coredns-674b8bbfcf-68q8p 0/1 ContainerCreating 0 57s etcd-minikube 1/1 Running 0 62s ...
Підключаємось в кластер:
$ minikube ssh
Якби ми використовували minikube start --driver=virtualbox
– то з minikube ssh
зайшли в інстанс VirtualBox.
Але так як у нас дефолтний драйвер docker
– то просто заходимо в контейнер minikube
.
Встановлюємо тут etcd
, аби отримати etcdctl
:
docker@minikube:~$ sudo apt update docker@minikube:~$ sudo apt install etcd
Перевіряємо:
docker@minikube:~$ etcdctl -version etcdctl version: 3.3.25
І тепер можемо подивитись що в базі:
docker@minikube:~$ sudo ETCDCTL_API=3 etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/var/lib/minikube/certs/etcd/ca.crt \ --cert=/var/lib/minikube/certs/etcd/server.crt \ --key=/var/lib/minikube/certs/etcd/server.key \ get "" --prefix --keys-only ... /registry/namespaces/kube-system /registry/pods/kube-system/coredns-674b8bbfcf-68q8p /registry/pods/kube-system/etcd-minikube ... /registry/services/endpoints/default/kubernetes /registry/services/endpoints/kube-system/kube-dns ...
Дані в ключах зберігаються в форматі Protobuf (Protocol Buffers), тому при звичайному etcdctl get KEY
дані будуть виглядати трохи криво.
Глянемо, що є в базі про Pod самого etcd
:
docker@minikube:~$ sudo ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/var/lib/minikube/certs/etcd/ca.crt --cert=/var/lib/minikube/certs/etcd/server.crt --key=/var/lib/minikube/certs/etcd/server.key get "/registry/pods/kube-system/etcd-minikube"
Результат:
Окей.
CustomResourceDefinitions та Kubernetes API
Отже, коли ми створюємо CRD – ми розширюємо Kubernetes API, створюючи власну API Group з власним ім’ям, версією та новим типом ресурсу (Kind), який описується в CRD.
Документація – Extend the Kubernetes API with CustomResourceDefinitions.
Напишемо простеньку CRD:
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: myapps.mycompany.com spec: group: mycompany.com names: kind: MyApp plural: myapps singular: myapp scope: Namespaced versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: image: type: string
Тут ми:
- використовуємо існуючу API Group
apiextensions.k8s.io
і версіюv1
- з неї беремо схему об’єкту CustomResourceDefinition
- і на основі цієї схеми – створюємо власну API Group з ім’ям
mycompany.com
- в цій API Group описуємо єдиний тип ресурсу –
kind: MyApp
- і одну версію –
v1
- далі з
openAPIV3Schema
описуємо схему нашого ресурсу – які у нього поля, їхні типи, тут жеж можна задати дефолтні значення (див. OpenAPI Specification)
- в цій API Group описуємо єдиний тип ресурсу –
З цим CRD ми зможемо створювати нові Custom Resources з маніфестом, в якому передамо поля apiVersion
, kind
, та spec.image
– зі schema.openAPIV3Schema.properties.spec.properties.image
нашого CRD:
apiVersion: mycompany.com/v1 kind: MyApp metadata: name: example spec: image: nginx:1.25
Створюємо CRD:
$ kk apply -f test-crd.yaml customresourcedefinition.apiextensions.k8s.io/myapps.mycompany.com created
Перевіряємо в Kubernetes API (можна з селектором | jq '.groups[] | select(.name == "mycompany.com")'
):
$ curl -s localhost:8080/apis/ | jq ... { "name": "mycompany.com", "versions": [ { "groupVersion": "mycompany.com/v1", "version": "v1" } ], ... } ...
І саму API Group mycompany.com
:
$ curl -s localhost:8080/apis/mycompany.com/v1 | jq { "kind": "APIResourceList", "apiVersion": "v1", "groupVersion": "mycompany.com/v1", "resources": [ { "name": "myapps", "singularName": "myapp", "namespaced": true, "kind": "MyApp", "verbs": [ "delete", "deletecollection", "get", "list", "patch", "create", "update", "watch" ], "storageVersionHash": "MZjF6nKlCOU=" } ] }
Та в etcd
:
docker@minikube:~$ sudo ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/var/lib/minikube/certs/etcd/ca.crt --cert=/var/lib/minikube/certs/etcd/server.crt --key=/var/lib/minikube/certs/etcd/server.key get "" --prefix --keys-only /registry/apiextensions.k8s.io/customresourcedefinitions/myapps.mycompany.com ... /registry/apiregistration.k8s.io/apiservices/v1.mycompany.com ...
Тут в ключі /registry/apiextensions.k8s.io/customresourcedefinitions/myapps.mycompany.com
зберігається інформація про сам новий CRD – структура CRD, її OpenAPI schema, версії, etc, а в /registry/apiregistration.k8s.io/apiservices/v1.mycompany.com
– реєструється API Service для цієї групи для доступу до групи через Kubernetes API.
Ну і звісно, ми можемо побачити CRD з kubectl`:
$ kk get crd NAME CREATED AT myapps.mycompany.com 2025-07-12T11:23:19Z
Створюємо сам CustomResource з маніфесту, що бачили вище:
$ kk apply -f test-resource.yaml myapp.mycompany.com/example created
Перевіряємо його:
$ kk describe MyApp Name: example Namespace: default Labels: <none> Annotations: <none> API Version: mycompany.com/v1 Kind: MyApp Metadata: Creation Timestamp: 2025-07-12T13:34:52Z Generation: 1 Resource Version: 4611 UID: a88e37fd-1477-4a7e-8c00-46c925f510ac Spec: Image: nginx:1.25
Але це поки що просто дані в etcd
– ніяких реальних ресурсів по типу Pods у нас нема, бо нема контролера, який оброблює ресурси з Kind: MyApp
.
Note: трохи забігаючи наперед до наступного поста: власне, Kubernetes Operator – це і є набір CRD та контролер, який “контролює” ресурси з заданими Kind
Kubernetes API Service
Колими додаємо новий CRD – Kubernetes має не тільки створити новий ключ в etcd
із новою API Group та схемою відповідних ресурсів, але й додати новий ендпоінт до свої маршрутів – як ми це робимо в Python з @app.get("/")
в FastAPI – для того, аби API-сервер знав, що на запит GET /apis/mycompany.com/v1/myapps
повертати ресурси саме цього типу.
Відповідний API Service буде в собі містити spec
з групою та версією:
$ kk get apiservice v1.mycompany.com -o yaml apiVersion: apiregistration.k8s.io/v1 kind: APIService metadata: creationTimestamp: "2025-07-12T11:53:52Z" labels: kube-aggregator.kubernetes.io/automanaged: "true" name: v1.mycompany.com resourceVersion: "2632" uid: 26fc8c6b-6770-422f-8996-3f35d86be6c7 spec: group: mycompany.com groupPriorityMinimum: 1000 version: v1 versionPriority: 100 ...
Тобто коли ми створюємо новий CRD – Kubernetes API Server створює API Service (записуючи його до /registry/apiregistration.k8s.io/apiservices/v1.mycompany.com
), і додає до свого до своїх роутів в ендпоінт /apis
.
І от тепер, маючи уявлення про те, як виглядає API та база даних, яка всі ресурси зберігає – ми можемо перейти до створення CRD та контролера, тобто – власне, написати сам Operator.
Але це вже в наступній частині.