Каждый раз, когда мы вводим в консоли kubectl apply -f deployment.yaml, кажется, что происходит что-то простое: утилита обратилась по HTTPS к мастер-ноде, передала манифест, и он аккуратно лег в базу данных etcd. Но если бы все было так примитивно, распределенные системы не вызывали бы столько вопросов при траблшутинге.
На самом деле за фасадом простой команды скрывается строго детерминированный конвейер проверок и трансформаций. kube-apiserver — единственный компонент в control plane Kubernetes, имеющий прямой доступ к etcd. Ни планировщик, ни контроллеры, ни агенты kubelet на рабочих узлах не смеют напрямую читать или писать в базу. Все они — точно такие же клиенты API server, как и твой локальный терминал.
Давай пройдем весь маршрут сетевого пакета от момента запуска команды до транзакционной фиксации ключа в Raft-логе etcd.
Фаза 0: От терминала до HTTP-хендлера
Все начинается на твоем ноутбуке. Утилита kubectl считывает контекст из ~/.kube/config, парсит манифест и формирует HTTP-запрос. В современных кластерах клиент чаще всего отправляет PATCH-запрос по протоколу Server-Side Apply или POST на эндпоинт ресурса:
POST /apis/apps/v1/namespaces/default/deployments HTTP/2
Host: k8s-master.corp.local:6443
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJSUzI1Ni...
Когда запрос прилетает на порт 6443, kube-apiserver терминирует TLS-соединение. Сервер написан на Go и построен как цепочка вложенных middleware. Первыми срабатывают служебные фильтры: навешивается audit ID, запускаются таймеры метрик, проверяются лимиты API Priority and Fairness (APF). Если сервер перегружен, APF может сразу вернуть статус 429 Too Many Requests. Если ресурсов хватает, запрос идет на аутентификацию.
Фаза 1: Аутентификация — «Кто ты такой?»
Главная задача этапа Authentication — установить личность того, кто постучался в API. При этом Kubernetes принципиально не хранит в своей базе учетные записи пользователей (Users): пользователь — это просто имя и список групп, подтвержденные внешним источником доверия.
Сервер прогоняет входящий запрос через цепочку настроенных аутентификаторов:
- X.509 клиентские сертификаты: имя пользователя извлекается из Common Name (CN), а группы — из Organization (O).
- Bearer-токены: проверка токенов сервисных аккаунтов через открытые ключи кластера.
- OpenID Connect (OIDC): валидация внешних JWT-токенов от корпоративных провайдеров (Keycloak, Okta).
- Webhook Token Authenticator: делегирование проверки внешней службе через TokenReview API.
Как только один из аутентификаторов успешно опознает клиента, он формирует структуру user.Info (имя пользователя, UID, группы) и прикрепляет ее к контексту запроса. Если ни один плагин не опознал клиента, запрос прерывается с ошибкой 401 Unauthorized.
Фаза 2: Авторизация — «Имеешь ли ты право?»
Теперь сервер знает имя гостя. Следующий вопрос: разрешено ли пользователю выполнять операцию create над ресурсом deployments в пространстве имен default?
За это отвечает авторизационный конвейер. Авторизаторы опрашиваются последовательно:
- Node: специальный авторизатор, ограничивающий права kubelet только ресурсами его собственного узла.
- RBAC: сопоставляет кортеж
{user, groups, verb, resource, namespace}с правиламиClusterRoleBindingиRoleBinding. - Webhook: обращение к внешним сервисам авторизации.
Правило работает по принципу первого совпадения (first-match allow): если хотя бы один модуль разрешил действие, запрос идет дальше. Если все модули ответили отказом, клиент получает статус 403 Forbidden.
Фаза 3: Контроллеры допуска (Admission Controllers)
После RBAC запрос переходит к контроллерам допуска, разделенным на две фазы.

Мутирующий допуск (Mutating Admission)
Мутирующие контроллеры дополняют манифест значениями по умолчанию или модифицируют его. Здесь работают как встроенные плагины, так и внешние вебхуки MutatingAdmissionWebhook. Например, Service Mesh (Istio) перехватывает описание пода и внедряет в него sidecar-контейнеры.
Вот пример минимального обработчика вебхука на Go, добавляющего служебный лейбл:
package main
import (
"encoding/json"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
)
func handleMutate(w http.ResponseWriter, r *http.Request) {
var review admissionv1.AdmissionReview
if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
patchOps := []map[string]interface{}{
{"op": "add", "path": "/metadata/labels/audit-verified", "value": "true"},
}
patchBytes, _ := json.Marshal(patchOps)
patchType := admissionv1.PatchTypeJSONPatch
response := admissionv1.AdmissionResponse{
UID: review.Request.UID,
Allowed: true,
Patch: patchBytes,
PatchType: &patchType,
}
review.Response = &response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(review)
}
Мутирующие вебхуки вызываются последовательно. Если вебхук завис или медленно отвечает по сети, весь конвейер создания ресурсов блокируется.
Валидация схемы и Validating Admission
После мутаций сервер проверяет схему объекта на соответствие типам Go или OpenAPI v3 для CRD. Если в поле передано некорректное значение, запрос отклоняется с кодом 422 Unprocessable Entity.
Затем вызываются валидирующие вебхуки (ValidatingAdmissionWebhook), например OPA Gatekeeper или Kyverno. Они работают параллельно и проверяют политики безопасности:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: security-policy-checker
webhooks:
- name: check-privileged.corp.local
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
clientConfig:
service:
name: policy-engine
namespace: security
path: "/validate-pods"
admissionReviewVersions: ["v1"]
sideEffects: None
timeoutSeconds: 3
failurePolicy: Fail
При failurePolicy: Fail сбой вебхука полностью блокирует деплой.
Фаза 4: Сериализация и запись в etcd
Манифест преодолел все КПП. Внутреннее хранилище RESTStorage преобразует Go-структуру ресурса в версию постоянного хранения (storage version) и сериализует ее в Protocol Buffers.
Запись происходит через gRPC-вызов к etcd по ключу:
/registry/deployments/default/my-awesome-app
Транзакция выполняется с оптимистичной блокировкой: сверяется resourceVersion. Если за время обработки объект успел измениться, etcd отклонит транзакцию, а API server вернет 409 Conflict. О том, как декларативное владение полями в Server-Side Apply минимизирует такие конфликты, мы говорили отдельно.
После фиксации транзакции в Raft-логе сервер отправляет клиенту 201 Created. И только в этот момент через механизм Watch API контроллеры в kube-controller-manager и планировщик узнают о новом ресурсе и начинают физический запуск подов на узлах.
