Learning Platform
Глоссарий Troubleshooting
Урок 03.02 · 25 мин
Продвинутый
kube-apiserverREST APIRBACAdmissionWatch streamsAPI groups

kube-apiserver: REST API, auth, admission, watch streams

kube-apiserver — мозг кластера. Это HTTP/HTTPS REST API сервер, через который проходят все запросы: команды от kubectl, watch-стримы kubelet-ов, write-запросы controllers, аутентификация ServiceAccount-ов. Один процесс, один порт :6443, и при этом единственная точка, имеющая read/write доступ к etcd.

В этом уроке препарируем kube-apiserver до уровня конкретных HTTP-запросов, фаз admission control, и того, как watch-стримы реально работают на сетевом уровне.


Анатомия HTTP-запроса: что внутри request и response

kube-apiserver как HTTP-сервер

Структурно это монолитный Go-процесс, обслуживающий несколько HTTP endpoint-групп.

GET  /healthz                — liveness check (200 если жив)
GET  /readyz                 — readiness check (готов принимать traffic)
GET  /livez                  — расширенный health для подсистем
GET  /version                — версия API сервера (gitVersion, buildDate, ...)
GET  /metrics                — Prometheus metrics

GET  /api/v1/pods            — core/v1 API group: pods, services, configmaps, ...
GET  /apis/apps/v1/deployments  — apps API group: deployments, replicasets, daemonsets
GET  /apis/batch/v1/jobs     — batch API group: jobs, cronjobs
GET  /apis/networking.k8s.io/v1/networkpolicies  — networking API group

GET  /openapi/v3             — OpenAPI 3.0 schema всех ресурсов
GET  /api/v1/namespaces/{ns}/pods/{name}/log    — proxy на kubelet logs
POST /api/v1/namespaces/{ns}/pods/{name}/exec   — proxy на kubelet exec (WebSocket / SPDY)
NOTE

core API group исторически живёт под /api/v1 (без слова “core” в URL). Все остальные группы — под /apis/<group>/<version>. Это историческая аномалия: первая версия API имела только /api, добавление groups сделали через /apis, чтобы не ломать совместимость.


API groups и versions

Каждый ресурс Kubernetes принадлежит к group/version. Это позволяет независимо эволюционировать разные части API.

API groups в v1.35
core/v1Самая старая группа. Pods, Services, ConfigMaps, Secrets, Nodes, Namespaces, PersistentVolumes, PersistentVolumeClaims, Events, ServiceAccounts. URL: /api/v1/... — без префикса /apis/.
apps/v1Workload resources. Deployments, ReplicaSets, DaemonSets, StatefulSets. URL: /apis/apps/v1/...
batch/v1Job, CronJob. URL: /apis/batch/v1/...
networking.k8s.io/v1NetworkPolicy, Ingress, IngressClass. URL: /apis/networking.k8s.io/v1/...
rbac.authorization.k8s.io/v1Role, ClusterRole, RoleBinding, ClusterRoleBinding. URL: /apis/rbac.authorization.k8s.io/v1/...
storage.k8s.io/v1StorageClass, CSIDriver, CSINode, VolumeAttachment. URL: /apis/storage.k8s.io/v1/...
gateway.networking.k8s.io/v1Gateway API ресурсы — Gateway, HTTPRoute, GRPCRoute. Замена Ingress. External CRD-проект SIG-Network (v1.0 GA — октябрь 2023, на 2026 актуальна v1.2/v1.3); CRDs ставятся отдельно от Kubernetes-релиза.
coordination.k8s.io/v1Lease — для leader election. Заменил endpoints/configmap-based locks. holderIdentity + acquireTime + renewTime.
apiextensions.k8s.io/v1CustomResourceDefinition (CRD) — определение собственных типов ресурсов. Регистрирует новую group в API server.

Полный список групп можно получить через kubectl api-resources (показывает все ресурсы и их groups) или kubectl api-versions (только список group/version пар).


kubectl — это просто HTTP клиент

Здесь живёт ключевое понимание: kubectl ничего не делает сам. Это удобная обёртка, которая превращает команды в HTTP-запросы. Всё, что доступно через kubectl, доступно через curl к api-server-у.

# kubectl get pods
GET https://api-server:6443/api/v1/namespaces/default/pods

# kubectl get deployments -n kube-system
GET https://api-server:6443/apis/apps/v1/namespaces/kube-system/deployments

# kubectl create -f pod.yaml
POST https://api-server:6443/api/v1/namespaces/default/pods
Content-Type: application/json
# (YAML внутри kubectl сконвертирован в JSON)

# kubectl delete pod nginx
DELETE https://api-server:6443/api/v1/namespaces/default/pods/nginx

# kubectl patch deployment myapp -p '{"spec":{"replicas":5}}'
PATCH https://api-server:6443/apis/apps/v1/namespaces/default/deployments/myapp
Content-Type: application/strategic-merge-patch+json

# kubectl get pods --watch
GET https://api-server:6443/api/v1/namespaces/default/pods?watch=true&resourceVersion=12345
TIP

Запустите kubectl get pods -v=8 2>&1 | grep -E '(GET|POST|PUT|DELETE|PATCH)' — увидите все HTTP-запросы, которые kubectl делает. --v=9 покажет ещё и тела запросов/ответов. Это незаменимо для отладки RBAC и admission-проблем.


Watch streams — самое интересное

Watch — фундаментальный примитив Kubernetes. Через него работают абсолютно все компоненты: kubelet наблюдает за своими Pod-ами, scheduler — за unscheduled, controllers — за своими ресурсами.

Технически watch — это HTTP GET с ?watch=true, который не возвращает ответ сразу. Соединение остаётся открытым, и сервер шлёт events чанками по мере их появления.

GET /api/v1/namespaces/default/pods?watch=true&resourceVersion=12345&allowWatchBookmarks=true HTTP/2

HTTP/2 200 OK
Content-Type: application/json
Transfer-Encoding: chunked

{"type":"ADDED","object":{"kind":"Pod","metadata":{"name":"nginx-abc","resourceVersion":"12346",...}}}
{"type":"MODIFIED","object":{"kind":"Pod","metadata":{"name":"nginx-abc","resourceVersion":"12350",...}}}
{"type":"BOOKMARK","object":{"kind":"Pod","metadata":{"resourceVersion":"12400"}}}
{"type":"DELETED","object":{"kind":"Pod","metadata":{"name":"nginx-abc","resourceVersion":"12410",...}}}
...

Каждая строка — отдельный event. Транспорт — HTTP/1.1 chunked transfer encoding (или HTTP/2 streaming). Клиент держит соединение открытым потенциально часами. Если оно рвётся — клиент переоткрывает с указанием resourceVersion последнего полученного event, и сервер шлёт всё, что было после.

Resource version и watch контракт

resourceVersion — это монотонно растущее число, фактически revision из etcd. Каждое изменение объекта получает новый, больший resourceVersion. Это даёт строгую гарантию упорядоченности.

Контракт watch:

  1. Клиент делает LIST → получает текущий snapshot всех объектов + последний resourceVersion коллекции.
  2. Клиент делает WATCH с этим resourceVersion → сервер шлёт все события, начиная сразу после этого revision.
  3. Клиент гарантированно увидит каждое изменение хотя бы раз (но порядок ADDED/MODIFIED/DELETED строго упорядочен).
  4. Если клиент отстал слишком сильно (compaction в etcd выкинул нужный revision) → сервер возвращает 410 Gone, клиент должен сделать новый LIST.
WARNING

“Хотя бы раз” — это about MODIFIED events. Если объект изменили 5 раз быстро подряд, клиент может увидеть только последнее значение. Watch не гарантирует, что вы увидите КАЖДОЕ промежуточное состояние — только что финальное состояние будет правильным. Это называется “level-triggered” vs “edge-triggered” подход.

BOOKMARK events

Чтобы клиент знал актуальный resourceVersion даже без изменений в watched-ресурсах, сервер периодически шлёт BOOKMARK-события — синтетические события с обновлённым resourceVersion. Это позволяет клиенту переоткрыть соединение с свежим версионным якорем при потере коннекта.


Authentication: кто это к нам пришёл

Каждый запрос к API server проходит цепочку: authenticationauthorizationadmission control → запись в etcd. Если что-то возвращает deny на любом этапе — запрос отказывается.

Auth pipeline в kube-apiserver
HTTP requestЗапрос приходит на :6443 TLS endpoint. TLS handshake может включать client certificate — это первый authentication метод.
authn
Client certificatesX.509 cert подписан CA, которому apiserver доверяет (--client-ca-file). Common Name = username, Organization = groups. Используется для kubelet, controller-manager, scheduler, и admin kubectl.
Bearer tokenAuthorization: Bearer <token>. Самый частый случай — ServiceAccount JWT (выпускается TokenController-ом, подписан key-ом apiserver-а). Также сюда попадают bootstrap tokens.
OIDCOpenID Connect — для интеграции с внешними IdP (Google, Azure AD, Okta, Keycloak). Конфигурируется через --oidc-issuer-url, --oidc-client-id и т.д. Получает JWT, валидирует через JWKS.
WebhookAuthentication webhook — apiserver делает HTTP POST на внешний сервис с токеном/cert, тот отвечает username+groups. Используется когда стандартные методы не подходят.
AnonymousЕсли ни один authenticator не сработал и --anonymous-auth=true (default) → запрос обрабатывается как system:anonymous в группе system:unauthenticated. Полезно для /healthz, но опасно — RBAC должен явно запрещать.

Результат authentication — структура UserInfo: username, groups, UID, extra-fields. Дальше она передаётся в authorization.

ServiceAccount token — самый частый случай

Каждый Pod автоматически получает токен ServiceAccount через projected volume в /var/run/secrets/kubernetes.io/serviceaccount/token. Это JWT, подписанный приватным ключом apiserver. Содержит:

{
  "iss": "https://kubernetes.default.svc.cluster.local",
  "sub": "system:serviceaccount:default:my-app",
  "aud": ["https://kubernetes.default.svc.cluster.local"],
  "exp": 1715000000,
  "kubernetes.io": {
    "namespace": "default",
    "pod": {"name": "my-app-abc", "uid": "..."},
    "serviceaccount": {"name": "my-app", "uid": "..."}
  }
}

С v1.22 (BoundServiceAccountTokenVolume GA) это bound projected tokens — короткоживущие (по умолчанию 1 час), привязанные к Pod-у (если Pod удалили — токен невалиден). Старые long-lived токены через автогенерируемые Secret отключены по умолчанию с v1.24 — apiserver их больше не создаёт автоматически.


Authorization: RBAC

После authentication apiserver знает (username, groups). Дальше — авторизация: имеет ли этот user право выполнить именно этот action на именно этом ресурсе.

В v1.35 default-режим — RBAC, основанный на четырёх типах объектов:

# Role / ClusterRole — описывает РАЗРЕШЕНИЯ
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [""]               # core API group
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["pods/log"]       # subresource
  verbs: ["get"]
# RoleBinding / ClusterRoleBinding — связывает РАЗРЕШЕНИЯ с СУБЪЕКТАМИ
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: default
  name: alice-can-read-pods
subjects:
- kind: User
  name: alice
  apiGroup: rbac.authorization.k8s.io
- kind: ServiceAccount
  name: my-app
  namespace: default
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

Разница Role vs ClusterRole: Role — namespaced (разрешения только в одном namespace), ClusterRole — cluster-wide. ClusterRoleBinding даёт ClusterRole во всех namespace. Можно использовать ClusterRole в RoleBinding — тогда разрешения ограничены namespace биндинга. Это позволяет переиспользовать одни и те же permission-наборы.

NOTE

Кроме RBAC есть Node authorizer (специальный — позволяет kubelet делать только то, что нужно своим Pod-ам) и Webhook authorizer (внешний сервис принимает решение). ABAC формально не deprecated, но на практике почти не используется и не рекомендуется — RBAC покрывает все стандартные сценарии. Несколько authorizer-ов работают последовательно — если первый говорит deny, проверяется следующий, и так до конца.


Admission control: mutating и validating

Если authorization прошла — запрос идёт на admission controllers. Это набор плагинов и webhooks, которые могут модифицировать или отклонить запрос до записи в etcd.

Admission pipeline
authenticated + authorized requestПрошёл authentication и RBAC. Объект в декодированном виде, валидирован против OpenAPI schema.
фаза 1
Mutating admissionМожет ИЗМЕНИТЬ объект. Список: built-in (NamespaceLifecycle, DefaultStorageClass, ...) + MutatingWebhookConfiguration. Каждый webhook — POST на внешний URL, возвращает JSON patch. Порядок важен: вебхуки выполняются один за другим, каждый видит результат предыдущих.
фаза 2
Object schema validationВалидация объекта против OpenAPI schema после всех mutations. Проверяет типы полей, required-поля, enum-значения.
фаза 3
Validating admissionНе может изменять — только allow/deny. Built-in (ResourceQuota, PodSecurity, ...) + ValidatingWebhookConfiguration + ValidatingAdmissionPolicy (CEL-based, в v1.35 GA). Параллельное выполнение — порядок не важен.
если все allow
Write to etcdФинальный шаг — apiserver делает Put в etcd под ключом /registry/<resource>/<ns>/<name>. Получает обратно new resourceVersion. Возвращает 201 Created клиенту.

Зачем нужны admission webhooks

Классические юзкейсы:

  • PodSecurity — встроенный validating, проверяет соответствие Pod-а уровню (privileged / baseline / restricted) для namespace.
  • OPA Gatekeeper / Kyverno — внешние validating webhooks для policy-as-code: запретить latest tag, требовать labels, и т.д.
  • Istio sidecar injection — mutating webhook автоматически добавляет sidecar container с Envoy в Pod-ы определённых namespace.
  • cert-manager — mutating webhook валидирует и нормализует Certificate ресурсы.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: enforce-image-policy
webhooks:
- name: deny-latest.example.com
  rules:
  - apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
    operations: ["CREATE", "UPDATE"]
  clientConfig:
    service:
      name: image-policy-webhook
      namespace: policy-system
      path: /validate
    caBundle: <base64-encoded-CA>
  admissionReviewVersions: ["v1"]
  sideEffects: None
  failurePolicy: Fail
  timeoutSeconds: 5
WARNING

failurePolicy: Fail означает, что если webhook недоступен — apiserver отклоняет запрос. Это создаёт single point of failure: если ваш validating webhook упал, нельзя создавать Pod-ы вообще. failurePolicy: Ignore безопаснее, но ослабляет policy enforcement. Тщательно настраивайте namespaceSelector и objectSelector, чтобы webhook не вызывался для kube-system.


Aggregated API server

API server расширяется двумя способами:

  1. CustomResourceDefinitions (CRD) — добавляют новые типы ресурсов в встроенный API. apiserver сам их обслуживает (хранит в etcd, валидирует через schema).
  2. APIService / Aggregation Layer — apiserver проксирует запросы на внешний API server для определённой group/version.

Aggregation layer используется когда стандартного CRD недостаточно: нужна custom storage (не etcd), specific endpoints (/scale, /exec-like proxy), сложная business logic.

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1beta1.metrics.k8s.io
spec:
  service:
    name: metrics-server
    namespace: kube-system
  group: metrics.k8s.io
  version: v1beta1
  insecureSkipTLSVerify: true
  groupPriorityMinimum: 100
  versionPriority: 100

Это говорит apiserver: «все запросы к /apis/metrics.k8s.io/v1beta1/... проксируй на Service metrics-server в kube-system». Так работает metrics-server — он реализует metrics.k8s.io API группу, но storage и runtime — отдельный pod.

Прокси работает прозрачно для клиента: kubectl top nodes делает GET /apis/metrics.k8s.io/v1beta1/nodes на основной apiserver, тот форвардит на metrics-server, и ответ возвращается клиенту как будто он пришёл от kube-apiserver.


Killer-момент: что реально летит при kubectl get pods

Соберём всю цепочку в один пример. Команда:

kubectl get pods -n production --watch

Что происходит на уровне HTTP:

# Шаг 1. LIST — снять начальный снапшот
GET /api/v1/namespaces/production/pods?limit=500 HTTP/2
Authorization: Bearer eyJhbGciOi...
Accept: application/vnd.kubernetes.protobuf, application/json

HTTP/2 200 OK
Content-Type: application/json
{
  "kind": "PodList",
  "metadata": {"resourceVersion": "874523"},
  "items": [...]
}
# Шаг 2. WATCH с того же resourceVersion — слушать изменения
GET /api/v1/namespaces/production/pods?watch=true&resourceVersion=874523&allowWatchBookmarks=true HTTP/2
Authorization: Bearer eyJhbGciOi...

HTTP/2 200 OK
Content-Type: application/json
Transfer-Encoding: chunked

{"type":"ADDED","object":{"kind":"Pod","metadata":{"name":"web-7","resourceVersion":"874524",...}}}
{"type":"MODIFIED","object":{"kind":"Pod","metadata":{"name":"web-7","resourceVersion":"874527",...}}}
{"type":"BOOKMARK","object":{"kind":"Pod","metadata":{"resourceVersion":"874600"}}}
# ...пока не Ctrl+C или сетевой обрыв

Внутри apiserver-а это не два независимых запроса. Запросы попадают в storage layer: первый делает etcd.Get с range query на префикс /registry/pods/production/, второй открывает etcd.Watch стрим начиная с revision 874523. Apiserver кэширует объекты в watch cache (in-memory), чтобы не дёргать etcd на каждый одинаковый запрос — все клиенты получают один и тот же стрим из shared cache.

Это объясняет масштабируемость: 1000 kubelet-ов могут одновременно держать watch на свои Pod-ы, и для etcd это всё ещё один Raft Watch стрим внутри apiserver-а.


Проверка знанийKnowledge check
Почему watch требует resourceVersion из предыдущего LIST? Что произойдёт, если открыть watch без resourceVersion?
ОтветAnswer
resourceVersion обеспечивает гарантию упорядоченности и continuity между LIST и WATCH. После LIST клиент имеет snapshot с конкретным resourceVersion — последний revision, отражённый в этом снапшоте. WATCH с этим resourceVersion говорит серверу: 'шли мне всё, что произошло ПОСЛЕ этого revision'. Без resourceVersion (или с resourceVersion=0) — сервер шлёт текущий список как серию ADDED-events и потом продолжает с актуальными изменениями, но это создаёт race condition между LIST и началом WATCH, в котором события между двумя запросами могут быть пропущены. Поэтому правильный паттерн — всегда LIST → WATCH-from-resourceVersion. Это контракт informer-ов в client-go SDK.

Проверьте понимание

Результат: 0 из 0
Аналитический
Вопрос 1 из 5. Что произойдёт, если клиент откроет watch без resourceVersion (или с resourceVersion=0) после LIST?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6