← Back to notes

kube-apiserver: путь запроса
и внутреннее устройство


18: Scheduling Constraints | 20: Authn/Authz/Admission


Что такое kube-apiserver

kube-apiserver — центральный узел control plane и единственная дверь в кластер. kubectl, controllers, scheduler, kubelet — никто не ходит в etcd напрямую и не общается друг с другом в обход. Все идут только через API server. Это осознанное решение: одна дверь — одно место, где применяются аутентификация, авторизация, политики и throttling. Не нужно защищать десять входов; защищаешь один.

Ментальная модель: регистратура

Прежде чем нырять в детали, полезна аналогия — регистратура (front desk). Вы приносите заявление: «хочу вот такой Deployment». Регистратура последовательно:

  1. проверяет, кто вы (authentication),
  2. проверяет, можно ли вам такое подавать (authorization),
  3. смотрит, не переполнена ли очередь (API Priority & Fairness),
  4. проверяет, правильно ли заполнена форма, и при необходимости дополняет её (admission + validation),
  5. и записывает заявление в журнал (etcd).

На этом её работа заканчивается. Саму работу — создать Pod-ы, найти им ноду, скачать образы, запустить контейнеры — делают уже другие сотрудники (controllers, scheduler, kubelet), асинхронно, читая тот же журнал.

Persistence, но не convergence

Отсюда главное разделение, вокруг которого построена вся заметка:

  • Persistence — это делает apiserver: надёжно записать желаемое состояние в etcd.
  • Convergence — это делают не apiserver: привести реальный кластер к этому состоянию.

Всё, что происходит после 200 OK — ответственность controllers, scheduler и kubelet. Apiserver сказал «записал», а не «сделал». Это различие — источник половины путаницы на собеседованиях: «я сделал apply, вернулось 200, почему Pod не запущен?» Потому что 200 подтверждает запись заявления в журнал, а не запуск контейнера. К этому мы вернёмся в разделе про kubectl apply.

source: kubernetes.io/docs/concepts/overview/components/#kube-apiserver


Три пути через API server

Не всякий запрос проходит одинаковый путь. Регистратура обрабатывает «прочитать из журнала», «внести запись в журнал» и «подписаться на изменения журнала» по-разному — и failure modes у них разные. Отсюда три типа запросов, три длины pipeline.

PathОперацииЧто проходитЧто доказывает
ReadGET, LISTauthn → authz → APF → handler → responseможно читать состояние
WriteCREATE, UPDATE, DELETEauthn → authz → APF → admission → validation → etcd → responseзапрос принят и сохранён
WatchWATCHauthn → authz → APF → long-lived streamизменения доставляются downstream

Обратите внимание: read короче write — у чтения нет admission и записи в etcd (нечего проверять и нечего сохранять). А watch вообще не завершается сразу — это подписка, а не разовый ответ. Практический вывод, который любят на собеседованиях:

  • Успешный GET не доказывает, что write path работает (чтение может идти из кэша, пока запись в etcd лежит).
  • Успешный write не доказывает, что система сконвергировала к желаемому состоянию (заявление записано, работа ещё не сделана).

Дальше разберём каждый путь по отдельности — от самого длинного (write) к самому хитрому (watch).


Полный write path

Client (kubectl, controller, kubelet)
  
  
TLS termination
  
  
Authentication ──── 401 Unauthorized
  
  
Authorization ───── 403 Forbidden
  
  
API Priority & Fairness ── 429 Too Many Requests
  
  
Mutating Admission Webhooks ── reject
  
  
Object Schema Validation ───── reject
  
  
Validating Admission Webhooks ── reject
  
  
Persist to etcd (через Raft consensus)
  
  
Response 200/201 to client

Каждая стрелка — точка, где запрос может не дойти до storage. Запрос проходит все стадии синхронно: клиент получает ответ только после записи в etcd.

Что делает каждая стадия — одной строкой:

СтадияЗадачаЗаворачивает с кодом
TLS terminationрасшифровать соединение, проверить клиентский сертификат(handshake fail)
Authenticationустановить, кто прислал запрос (username, groups)401
Authorizationпроверить, можно ли этому кому-то это действие403
API Priority & Fairnessне пустить, если очередь этого класса переполнена429
Mutating admissionдополнить/изменить объект (defaults, sidecar-инъекция)reject
Schema validationпроверить, что объект структурно корректен после мутацийreject (422)
Validating admissionфинальная проверка политик, объект уже не меняетсяreject
Persist to etcdзаписать через Raft, дождаться quorum ack(5xx при сбое etcd)

Порядок здесь не случаен. Дешёвые и отбраковывающие проверки идут раньше дорогих: нет смысла звать admission webhook, если запрос всё равно завернёт authz. И mutating всегда до validating — чтобы валидация проверяла уже финальный объект (с подставленными defaults и внедрёнными sidecar-ами), а не то, что прислал клиент. Подробный разбор admission-фаз — в заметке 20.

source: kubernetes.io/docs/reference/access-authn-authz/controlling-access/


Read path и watch cache

Read path короче — admission webhooks не вызываются:

Client
  
  
TLS  Authentication  Authorization  APF
  
  
Handler читает из in-memory cache или etcd
  
  
Response

Прежде чем разбирать таблицу — простая интуиция.

У etcd есть один сквозной счётчик изменений: revision. Любая запись в хранилище (создали Pod, обновили Deployment, удалили ConfigMap) увеличивает его на единицу. resourceVersion объекта — это, по сути, снимок значения этого счётчика на момент, когда объект последний раз менялся. Думайте о нём как о штампе версии на всём магазине, а не на отдельном товаре: resourceVersion: 45021 у Pod-а означает «этот Pod соответствует состоянию хранилища на revision 45021».

Зачем это клиенту? Затем, что при чтении он может указать, насколько свежие данные ему нужны — и это прямой размен: свежее стоит дороже.

  • Нужно самое свежее состояние на момент запроса — apiserver обязан сходить к etcd и подтвердить, что кэш не отстал. Дороже.
  • Готов принять чуть устаревшее («мне не критичны последние миллисекунды») — apiserver отвечает прямо из своей памяти. Почти бесплатно.

Бытовая аналогия: «назовите цену прямо сейчас, сверьтесь с центральным прайсом» против «цена из вашей распечатки утренней — сойдёт». Второе быстрее, но распечатка могла устареть.

Этот размен и задаётся парой resourceVersion + resourceVersionMatch:

resourceVersionresourceVersionMatchЧто вернётСтоимость
не заданMost Recent: consistent read, самое свежее состояниедороже (потенциально quorum read)
"0"Any: любое достаточно свежее из кэшадёшево, может быть stale
"<rv>"NotOlderThanданные не старее <rv> (обычно из watch cache)дёшево
"<rv>"Exactточно на версии <rv> или 410 Goneдорого, для пагинации снапшота

Ключевой механизм оптимизации — consistent reads from watch cache. Раньше правило было жёстким: нужно самое свежее — иди в etcd за quorum read. Проблема в том, что «тяжёлые» LIST (например, все Pod-ы на одной ноде) били по etcd напрямую и создавали основную нагрузку на хранилище. Теперь apiserver умеет отдавать даже consistent LIST из своего in-memory watch cache: он спрашивает у etcd лишь текущую revision (дёшево) и, убедившись что кэш не отстаёт, отвечает из памяти. Feature gate ConsistentListFromCache: beta (включён по умолчанию) с 1.31, GA и залочен на true в 1.34.

source: kubernetes.io/docs/reference/using-api/api-concepts/#semantics-for-get-and-list

# Most Recent consistent read: самое свежее состояние на момент обработки запроса
kubectl get pods -n default --resource-version=""

# Дёшево, допускается stale: чтение из кэша
kubectl get --raw '/api/v1/pods?resourceVersion=0'

410 Gone при LIST/WATCH означает, что запрошенная resourceVersion уже вне окна компакции etcd — клиент обязан сделать полный re-list с чистого состояния.

Как это использует реальный клиент (informer)

Абстракция становится понятнее на конкретном паттерне, который выполняют все контроллеры под капотом (через client-go informer). Он не «поллит» apiserver в цикле — он делает LIST один раз, потом WATCH:

1. LIST всех объектов (например, resourceVersion=0  дёшево, из кэша)
    получает полный снимок + текущую resourceVersion, скажем 45021
2. WATCH начиная с resourceVersion=45021
    apiserver присылает ТОЛЬКО дельты после 45021
3. Каждое событие обновляет локальный кэш информера и двигает его "позицию"
4. Соединение оборвалось  переподключиться с последней известной rv
   ├─ rv ещё в окне  продолжить с дельт (дёшево)
   └─ 410 Gone  вернуться к шагу 1, полный re-list

Отсюда два вывода, которые часто спрашивают:

  • Контроллеры не создают постоянную нагрузку на apiserver поллингом. После стартового LIST идёт дешёвый поток дельт.
  • 410 Gone — не ошибка клиента, а штатный сигнал «ты отстал слишком сильно, начни заново». Грамотный клиент обязан его обрабатывать re-list-ом, а не падать.

Watch path

Watch — это long-lived HTTP streaming connection. Informers в controllers, scheduler и kubelet используют watch для получения изменений в реальном времени.

Client (informer)
  
  
TLS  Authentication  Authorization  APF
  
  
API server открывает watch stream
  
  
При каждом изменении объекта в etcd:
  etcd watch  API server  serialize event  отправить клиенту

Если watch разрывается, informer переподключается и делает re-list для синхронизации. Это level-triggered design — и это принципиально.

Разница на пальцах. Edge-triggered (по фронту) — «сработай на событие»: пропустил событие, пока был отключён — потерял его навсегда. Level-triggered (по уровню) — «сверься с текущим состоянием»: даже если пропустил промежуточные события, при следующей сверке увидишь актуальную картину и доедешь до неё. Kubernetes выбрал level-triggered: контроллер всегда сравнивает желаемое состояние с текущим и действует по разнице, а не по потоку отдельных «нажатий». Поэтому потеря пары watch-событий не ломает систему — реконнект с re-list-ом восстанавливает полную правду. Это же делает контроллеры устойчивыми к рестартам: поднялся, сделал LIST, увидел мир как есть, довёл до нужного.

Watch bookmarks и watch cache

Чтобы не терять позицию при разрыве, apiserver периодически шлёт события типа BOOKMARK — они не несут изменения объекта, а только сообщают текущую resourceVersion. Клиент при реконнекте продолжает с последнего bookmark, а не делает дорогой re-list. Запрашивается через allowWatchBookmarks=true.

Watch cache — общий in-memory буфер apiserver, из которого обслуживаются все watch- и (при ConsistentListFromCache) многие read-запросы. Один watch на etcd мультиплексируется в тысячи watch-соединений клиентов — etcd не знает о количестве informers в кластере.

Streaming list (WatchList) решает проблему памяти. Обычный большой LIST apiserver собирает в памяти целиком перед отправкой — это пик потребления, который на больших коллекциях способен уронить apiserver. WatchList вместо этого отдаёт начальный снапшот потоком watch-событий (sendInitialEvents=true) с финальным bookmark, а затем переходит в обычный watch — память не копится.

История версий тут поучительна и показывает, как двигаются feature gates: WatchList дошёл до beta в 1.32, в 1.33 был выключен по умолчанию в пользу более прозрачного механизма — streaming collection encoding (StreamingCollectionEncodingToJSON/ToProtobuf, GA в 1.34), который даёт ту же экономию памяти без изменений в клиентах, — а в 1.34 WatchList снова включили по умолчанию (всё ещё beta). Практический вывод: экономия памяти на больших LIST теперь работает из коробки; какой именно gate её обеспечивает — деталь реализации, которая продолжает меняться от релиза к релизу.

source: kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes


API Priority and Fairness (APF)

APF (GA с Kubernetes 1.29) стоит между authorization и обработкой запроса. Задача — защитить API server от перегрузки и обеспечить fair access между разными клиентами и типами запросов.

Мысленная модель: представьте аэропорт с несколькими линиями досмотра разного приоритета. Запрос сперва классифицируется (FlowSchema решает, в какую линию его отправить), затем попадает в очередь этой линии (PriorityLevelConfiguration задаёт, сколько «окошек» у линии и какой длины очередь). Если окошки заняты — запрос либо ждёт, либо разворачивается с 429. Ключевое: перегрузка в одной линии не блокирует другие — «шумный» клиент выжигает свой уровень, а не весь apiserver.

source: kubernetes.io/docs/concepts/cluster-administration/flow-control/

Как работает

Входящий запрос
  
  
FlowSchema (классификация)
    Матчит по: user, group, namespace, resource, verb
  
  
PriorityLevelConfiguration (очередь)
    Определяет: concurrency shares, queue depth, hand size
  
  ├── capacity есть  обработать запрос
  
  └── capacity нет  поставить в очередь или reject 429

Пример FlowSchema

apiVersion: flowcontrol.apiserver.k8s.io/v1
kind: FlowSchema
metadata:
  name: system-leader-election
spec:
  priorityLevelConfiguration:
    name: leader-election        # высокий приоритет
  matchingPrecedence: 100
  rules:
  - subjects:
    - kind: ServiceAccount
      serviceAccount:
        name: "*"
        namespace: kube-system
    resourceRules:
    - verbs: ["get", "update"]
      apiGroups: ["coordination.k8s.io"]
      resources: ["leases"]

Встроенные уровни приоритета

Уровни делятся на две группы. Обязательные (exempt, catch-all) всегда присутствуют и не редактируются — это защита от того, чтобы админ случайно не сломал APF. Рекомендуемые создаются по умолчанию, но их можно переопределить под свой кластер.

PriorityLevelГруппаЧто обслуживаетConcurrency
exemptобязательныйsystem:masters — проходят вообще без очереди и лимитовбез ограничений
catch-allобязательныйзапрос, не пойманный ни одной FlowSchemaнамеренно крошечная — чтобы неклассифицированный трафик не съел кластер
node-highрекомендуемыйhealth-обновления от нодвысокий
systemрекомендуемыйsystem components (kubelet и др.)высокий
leader-electionрекомендуемыйlease operationsвысокий
workload-highрекомендуемыйважные non-system запросысредний
workload-lowрекомендуемыйостальные запросынизкий
global-defaultрекомендуемыйвсё прочее, что не поймали другие FlowSchemaминимальный

Разница между catch-all и global-default тонкая, но важная: global-default — обычная (переопределяемая) FlowSchema, ловящая большую часть «прочего»; catch-all — обязательный последний рубеж для запросов, не совпавших вообще ни с чем, с минимальной concurrency.

Seats и shuffle sharding

Единица параллелизма в APF — seat («место»). Обычный запрос занимает 1 seat, но:

  • LIST занимает несколько seats пропорционально оценке числа возвращаемых объектов — большой LIST «весит» дороже.
  • WATCH удерживает seat только на время рассылки initial events, затем освобождает.
  • WRITE добавляет seat-время на рассылку уведомлений в watch-стримы после записи.

Внутри одного priority level запросы распределяются по очередям через shuffle sharding. Каждый flow (пара «FlowSchema + flow distinguisher»: user / namespace / none) попадает не в одну очередь, а в случайное подмножество очередей — размер этого подмножества задаёт handSize («сколько карт раздаём на руку»).

Зачем не просто хэш в одну очередь? Потому что при простом хэше «шумный» клиент и невезучий сосед, попавшие в ту же очередь, страдают вместе. Shuffle sharding резко снижает шанс полного пересечения. Грубая интуиция: если у уровня 128 очередей и handSize=8, то у двух разных клиентов все 8 очередей совпадут с вероятностью примерно 1 к нескольким миллиардам (порядка 1 / C(128,8)). Значит почти для любого соседа найдётся хотя бы одна не разделяемая с шумным клиентом очередь, через которую его запросы продолжат обслуживаться. Шумный клиент выжигает пересечение своих очередей — но не весь уровень.

Не всё проходит через APF: long-running запросы (exec, attach, port-forward, log --follow) не подчиняются concurrency-лимитам APF. При этом WATCH — подчиняется (в отличие от старого --max-requests-inflight).

flowcontrol.apiserver.k8s.io/v1 — GA с 1.29; v1beta3 включён, но deprecated.

APF создаёт неочевидную ситуацию: API server работает, healthz отвечает 200, но пользовательские запросы получают 429 Too Many Requests. Мониторинг одного healthz endpoint недостаточен.


Что реально происходит при kubectl apply

Разберём kubectl apply -f deployment.yaml по шагам:

1. TLS handshake
2. Authentication: kubeconfig  client cert  username=developer, groups=[dev-team]
3. Authorization: RBAC  verb=create, resource=deployments, namespace=app  Allow
4. APF: classify  workload-low  capacity есть  пропустить
5. Mutating Admission:
   - LimitRanger: добавляет default resource limits
   - Istio webhook: inject sidecar container
   - Custom webhook: добавляет labels
6. Schema Validation: проверяет spec после mutations
7. Validating Admission:
   - PodSecurity: проверяет security context
   - ResourceQuota: проверяет, что namespace quota не превышена
8. Persist: запись в etcd через Raft  quorum ack  commit
9. Response: 201 Created

Deployment создан в etcd. Но Pod-ы ещё не существуют — это задача controller-manager. Контейнеры не запущены — это задача scheduler и kubelet.

Почему 200 OK не означает "workload работает"

kubectl apply -f deployment.yaml
#  200 OK                           API server сохранил объект

# Дальше (всё асинхронно):
#  Deployment controller создаёт ReplicaSet
#  ReplicaSet controller создаёт Pod objects
#  Scheduler выбирает node и делает bind
#  kubelet скачивает image и запускает контейнеры

# Между "200 OK" и "контейнер запущен" может пройти
# от секунд до минут, или workload может вообще не запуститься.

Сигналы мониторинга

МетрикаЧто показываетНа что смотреть
apiserver_request_totalзапросы по verb, resource, codeрост 4xx/5xx
apiserver_request_duration_secondslatency обработкиP99 > 1s для write
apiserver_current_inflight_requestsтекущая нагрузкаприближение к limit
apiserver_flowcontrol_rejected_requests_totalAPF rejections
0 → throttling активен
apiserver_flowcontrol_request_queue_length_after_enqueueглубина APF очередейрост → backpressure
apiserver_admission_webhook_rejection_countwebhook rejectionsspike → проблема с policy
apiserver_admission_controller_admission_duration_secondslatency admissionP99 > 1s → slow webhook
etcd_request_duration_secondslatency обращений к etcdP99 > 100ms → etcd degradation

Комбинация, которую легко пропустить:

apiserver_request_total{code="200", verb="GET"}     растёт (reads OK)
apiserver_request_total{code="500", verb="POST"}    растёт (writes fail)
apiserver_request_total{code="429"}                 растёт (throttling)

API "работает", но кластер неуправляем. GET отвечает, writes ломаются. Без разделения метрик по verb и code — этого не увидеть.


Полная сводная таблица: что на каком шаге

ОперацияTLSAuthnAuthzAPFMutating Adm.Schema Val.Validating Adm.etcd write
GET/LIST
WATCH
CREATE
UPDATE
DELETE

DELETE проходит через admission, но schema validation обычно не применяется (нет тела объекта). Admission webhooks могут заблокировать удаление — типичный use case: защита critical объектов от случайного удаления.


Расширение API: CRD vs Aggregation Layer

API server интересен тем, что его можно дописать своими типами — не форкая Kubernetes. Возвращаясь к аналогии с регистратурой: можно либо (а) завести новый вид бланка, который та же регистратура принимает и кладёт в тот же журнал, либо (б) поставить рядом отдельное окно со своим сотрудником, а главная регистратура просто перенаправляет к нему подходящие заявления. Первое — CRD, второе — Aggregation Layer.

kube-apiserver можно расширить двумя этими способами — оба добавляют новые ресурсы, но по-разному.

КритерийCustomResourceDefinition (CRD)Aggregation Layer (APIService)
Что этоДекларативная схема нового типа, хранится в том же etcdОтдельный HTTP-сервер, kube-apiserver проксирует к нему
Storageetcd основного кластерасвой (может быть не-etcd)
ВалидацияOpenAPI schema + CEL (ValidatingAdmissionPolicy) + webhooksпроизвольная логика в коде сервера
Когда нужен95% случаев: операторы, конфиги, CRD-контроллерыкастомная логика хранения/вычисления, imperative subresources
Примерыcert-manager, Argo, большинство операторовmetrics-server, custom/external metrics adapters

CRD регистрируется в группе apiextensions.k8s.io. Aggregation layer — объект APIService (apiregistration.k8s.io): он говорит kube-apiserver, что запросы к группе-версии (например metrics.k8s.io/v1beta1) нужно проксировать на указанный Service. Именно поэтому kubectl top nodes работает через тот же kube-apiserver, хотя данные считает metrics-server.

source: kubernetes.io/docs/concepts/extend-kubernetes/api-extension/apiserver-aggregation/

# Список aggregated API и их доступность
kubectl get apiservices
# v1beta1.metrics.k8s.io   kube-system/metrics-server   True

# CRD в кластере
kubectl get crds

Aggregated APIService со статусом Available: False ломает kubectl discovery: команды вроде kubectl get all могут падать целиком, потому что discovery обходит все группы. Классический сбой — упавший metrics-server с failurePolicy-подобным эффектом на discovery.

Encryption at rest

По умолчанию объекты (включая Secret-ы) лежат в etcd в открытом виде — кто получил доступ к etcd-дампу, получил все секреты. EncryptionConfiguration включает шифрование на уровне apiserver перед записью в etcd.

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
    providers:
      - kms:                     # KMS v2  рекомендуемый provider
          apiVersion: v2
          name: myKmsProvider
          endpoint: unix:///tmp/kms.sock
      - identity: {}             # fallback: без шифрования (для чтения старых данных)

Порядок provider-ов важен: первый используется для записи (шифрования), все перечисленные — для чтения (дешифрования). identity: {} первым = шифрование выключено. Передаётся флагом --encryption-provider-config.

ProviderСтойкостьКлюч где
identityнет (plaintext)
aescbc / secretbox / aesgcmсимметричное шифрованиеключ в самом конфиге на диске control plane
kms v2envelope encryption, ротация KEK без re-encryptвнешний KMS (Vault, cloud KMS)

KMS v2 — GA с Kubernetes 1.29. Использует envelope encryption: DEK деривится из seed, seed ротируется при ротации KEK; DEK кэшируются, что даёт заметно лучшую производительность, чем KMS v1. KMS v1 — deprecated с 1.28 и выключен по умолчанию с 1.29 (требует --feature-gates=KMSv1=true).

source: kubernetes.io/docs/tasks/administer-cluster/kms-provider/

Важно: включение шифрования не перешифровывает уже записанные объекты. После смены конфига нужно принудительно переписать данные:

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

Audit logging

Audit фиксирует «кто, что, когда, откуда и с каким результатом» сделал в API server. Это единственный слой, который видит запрос до и после admission. Настраивается Policy (флаг --audit-policy-file).

Стадии (один запрос порождает событие на нескольких):

СтадияКогдаЗаметка
RequestReceivedзапрос принят, до обработкичасто отключают через omitStages
ResponseStartedотправлены заголовки ответатолько для long-running (watch)
ResponseCompleteтело ответа отправлено полностьюосновное событие для аудита
Panicпаника при обработке

Уровни (сколько данных писать):

LevelЧто пишется
Noneничего (заглушить шумные правила)
Metadatauser, verb, resource, timestamp — без тела
Request
  • тело запроса
RequestResponse
  • тело запроса и ответа (максимум)

Правила в Policy проверяются по порядку, первое совпадение задаёт уровень. Пустой список правил недопустим.

apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
  - "RequestReceived"       # не писать шумную первую стадию
rules:
  - level: RequestResponse  # секреты  полностью
    resources:
      - group: ""
        resources: ["secrets"]
  - level: Metadata         # всё остальное  только метаданные

source: kubernetes.io/docs/tasks/debug/debug-cluster/audit/

Практическое применение: когда webhook или RBAC молча блокирует запрос, audit log показывает точный verb/resource/user и решение authz — быстрее, чем гадать по симптомам.

Когда использовать

Прямое взаимодействие с API server актуально когда:

  • Raw API запрос, который kubectl не поддерживает: kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes
  • Отладка APF throttling: FlowSchema/PriorityLevel объекты в кластере
  • Webhook блокирует запросы: включить audit logging на API server
  • Кастомный клиент: client-go informer pattern, не polling

Когда APF важен:

  • Много операторов/контроллеров — конкурируют за capacity
  • CI/CD с массовыми kubectl apply — могут выжечь workload-low

source: kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/


Типичные ошибки

1. Считать healthz=200 признаком здорового кластера

healthz проверяет только жизнеспособность процесса, не работоспособность write path. Кластер может быть read-only (etcd потерял quorum) при healthz=200.

2. Игнорировать 429 в метриках

APF rejection — это не "кто-то делает слишком много запросов". Это сигнал, что API server перегружен. Обычная причина — слишком много controllers или операторов без rate limiting.

3. Не учитывать async nature после apply

kubectl apply + 200 OK ≠ workload running. Нужен kubectl rollout status или проверка через conditions, а не просто выход команды apply.

4. Webhook с failurePolicy: Fail без high availability

Один инстанс webhook + failurePolicy: Fail = потенциальный deadlock кластера при рестарте webhook pod. Всегда минимум 2 реплики с podAntiAffinity.

5. Неправильное понимание read consistency

resourceVersion задаёт требования к свежести чтения. Для операций, где важно самое свежее состояние на момент запроса, явно запрашивай consistent read и учитывай, что apiserver может обслужить его из подтверждённо свежего watch cache, а не обязательно напрямую из etcd.


Альтернативы

ValidatingAdmissionPolicy (CEL) вместо validating webhooks

С Kubernetes 1.30 (GA) — политики валидации без внешних HTTP сервисов. Работают in-process в API server, нет network dependency, нет availability risk. Ограничение: только validation, только CEL expressions.

Aggregated API Server

Для кастомных API (не CRD) — отдельный API server, зарегистрированный через APIService. Запросы проксируются через kube-apiserver. Используется metrics-server, custom metrics adapters.


18: Scheduling Constraints | 20: Authn/Authz/Admission

kube-apiserver: путь запроса и внутреннее устройство | Aleksandr Suprun