Kubernetes: Kubernetes API, API Groups, CRD та etcd

Автор |  12/07/2025
 

Взагалі почав писати створення власного 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/v1″ “operator.victoriametrics.com/v1beta1
  • /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

Наприклад, в /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)

З цим 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.

Але це вже в наступній частині.