Взагалі почав писати створення власного Kubernetes Operator, але вирішив винести окремо тему про те, що таке власне Kubernetes CustomResourceDefinition, і як створення CRD взагалі працює на рівні Kubernetes API та etcd.
Тобто, почати з того, як власне Kubernetes працює з ресурсами, і що відбувається, коли ми створюємо чи редагуємо ресурси.
Продовження – Kubernetes: що таке Kubernetes Operator та CustomResourceDefinition.
Зміст
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/v1″ “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.
Але це вже в наступній частині.





