← 18: Scheduling Constraints | 20: Authn/Authz/Admission →
Что такое kube-apiserver
kube-apiserver — центральный узел control plane и единственная дверь в кластер. kubectl, controllers, scheduler, kubelet — никто не ходит в etcd напрямую и не общается друг с другом в обход. Все идут только через API server. Это осознанное решение: одна дверь — одно место, где применяются аутентификация, авторизация, политики и throttling. Не нужно защищать десять входов; защищаешь один.
Ментальная модель: регистратура
Прежде чем нырять в детали, полезна аналогия — регистратура (front desk). Вы приносите заявление: «хочу вот такой Deployment». Регистратура последовательно:
- проверяет, кто вы (authentication),
- проверяет, можно ли вам такое подавать (authorization),
- смотрит, не переполнена ли очередь (API Priority & Fairness),
- проверяет, правильно ли заполнена форма, и при необходимости дополняет её (admission + validation),
- и записывает заявление в журнал (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 | Операции | Что проходит | Что доказывает |
|---|---|---|---|
| Read | GET, LIST | authn → authz → APF → handler → response | можно читать состояние |
| Write | CREATE, UPDATE, DELETE | authn → authz → APF → admission → validation → etcd → response | запрос принят и сохранён |
| Watch | WATCH | authn → 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:
| resourceVersion | resourceVersionMatch | Что вернёт | Стоимость |
|---|---|---|---|
| не задан | — | 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_seconds | latency обработки | P99 > 1s для write |
apiserver_current_inflight_requests | текущая нагрузка | приближение к limit |
apiserver_flowcontrol_rejected_requests_total | APF rejections | 0 → throttling активен |
apiserver_flowcontrol_request_queue_length_after_enqueue | глубина APF очередей | рост → backpressure |
apiserver_admission_webhook_rejection_count | webhook rejections | spike → проблема с policy |
apiserver_admission_controller_admission_duration_seconds | latency admission | P99 > 1s → slow webhook |
etcd_request_duration_seconds | latency обращений к etcd | P99 > 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 — этого не увидеть.
Полная сводная таблица: что на каком шаге
| Операция | TLS | Authn | Authz | APF | Mutating 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 проксирует к нему |
| Storage | etcd основного кластера | свой (может быть не-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 v2 | envelope 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 | ничего (заглушить шумные правила) |
Metadata | user, 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.