Сразу зафиксируем главную мысль, без неё дальше будет каша. Kubernetes - это не "набор демонов, которые как-то договариваются". Это система с единственной точкой правды (etcd) и единственными воротами к ней (api-server). Всё остальное - клиенты, которые смотрят на состояние через эти ворота и пытаются подтянуть реальность к желаемому. Запомни эту картинку, она объясняет 90 процентов поведения кластера.
Control plane kubernetes: кто где живёт
Узлы кластера делятся на два сорта. Worker-узлы (где крутятся твои поды) несут на себе kubelet, kube-proxy и container runtime (containerd через CRI - dockershim выпилен ещё в 1.24, забудь про него). Control plane - это отдельный набор компонентов, обычно на выделенных master-узлах (их же зовут control plane nodes). В managed-кластерах (Yandex Managed Service for Kubernetes, VK Cloud, GKE) control plane от тебя спрятан: его держит провайдер, ты платишь и не видишь мастера вообще. В self-hosted (kubeadm, Deckhouse, k3s на своих ВМ) - управляешь сам, и тогда всё нижесказанное твоя прямая зона ответственности.
Состав control plane:
- kube-apiserver - единая точка входа, REST-фасад над состоянием кластера.
- etcd - распределённое key-value хранилище, где лежит ВСЁ состояние.
- kube-scheduler - решает, на какой узел поставить новый под.
- kube-controller-manager - пачка контроллеров, которые сводят желаемое с фактическим.
- cloud-controller-manager - контроллеры, которые ходят в API облака (балансировщики, диски, узлы).

kube-apiserver: ворота, через которые ходят все
api-server - сердце всего. Это stateless HTTP/JSON (и protobuf для внутренних клиентов) сервер, который слушает обычно 6443. Любое действие - kubectl, контроллер, kubelet, твой оператор - идёт через него. Прямого доступа к etcd нет ни у кого, кроме api-server. Это не случайность, а сознательная архитектура: одна точка для аутентификации, авторизации, валидации и аудита.
Каждый входящий запрос проходит конвейер из трёх стадий:
- Authentication - кто ты. Сертификаты (mTLS), bearer-токены ServiceAccount (JWT), OIDC, webhook. api-server не хранит пароли - он проверяет предъявленное удостоверение.
- Authorization - что тебе можно. В подавляющем большинстве кластеров это RBAC: твой (Cluster)Role даёт глаголы (get, list, watch, create) на ресурсы. Нет правила - отказ.
- Admission control - можно ли это в принципе и не подправить ли. Сначала mutating-плагины (могут менять объект - например, дописать sidecar или дефолты), потом validating (только да/нет). Сюда же встроены Pod Security Admission (преемник умерших PodSecurityPolicy), внешние Kyverno/Gatekeeper и нативная ValidatingAdmissionPolicy на CEL-выражениях - без вебхуков, прямо движком api-server.
Ещё одна ключевая способность api-server - механизм watch. Клиент открывает долгоживущее соединение и говорит "уведомляй меня обо всех изменениях подов с этой меткой". api-server держит это соединение и шлёт события (ADDED/MODIFIED/DELETED). На этом построено вообще всё: scheduler watch-ит неназначенные поды, kubelet watch-ит поды своего узла, контроллеры watch-ят свои ресурсы. Никто не опрашивает базу в цикле - все подписаны на поток событий. Поэтому api-server должен быть быстрым и его легко перегрузить тысячами watch-ей (привет, list-watch шторм при рестарте).
Посмотрим, кто вообще доступен через api:
Код: Выделить всё
kubectl api-resources | head
NAME SHORTNAMES APIVERSION NAMESPACED KIND
bindings v1 true Binding
configmaps cm v1 true ConfigMap
endpoints ep v1 true Endpoints
events ev v1 true Event
namespaces ns v1 false Namespace
nodes no v1 false Node
pods po v1 true Pod
etcd kubernetes: единственная правда кластера
etcd - это распределённое консистентное key-value хранилище. Всё, что ты создаёшь в кластере (Deployment, Secret, ServiceAccount, статус каждого пода), сериализуется и лежит ключами в etcd. Грубо говоря, /registry/pods/default/nginx -> сериализованный объект. Убери etcd - и кластер забыл вообще всё. Поэтому etcd kubernetes - это не "ещё один компонент", это твоя база данных, и относиться к ней надо как к проду с критичными данными.
Консистентность держится на консенсусе Raft. Среди членов etcd выбирается лидер, он принимает все записи, реплицирует их на фолловеров, и запись считается зафиксированной только когда её подтвердило большинство (quorum). Отсюда железное правило по числу узлов: бери НЕЧЁТНОЕ. Кворум - это N/2 + 1.
- 3 узла: кворум 2, переживает падение 1 узла.
- 5 узлов: кворум 3, переживает падение 2 узлов.
- 2 узла: кворум 2, не переживает НИ ОДНОГО падения - хуже, чем один узел. Поэтому чётные числа бессмысленны.
Проверка здоровья и снятие бэкапа (на kubeadm-узле, где etcd - статический под):
Код: Выделить всё
ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
endpoint status --write-out=table
+----------------+------------------+---------+---------+-----------+-----------+------------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | RAFT TERM | RAFT INDEX |
+----------------+------------------+---------+---------+-----------+-----------+------------+
| 127.0.0.1:2379 | 8e9e05c52164694d | 3.5.16 | 12 MB | true | 8 | 104523 |
+----------------+------------------+---------+---------+-----------+-----------+------------+
Снимаем снапшот - это и есть твой бэкап:
Код: Выделить всё
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save /backup/etcd-$(date +%F).db
{"level":"info","msg":"saved","path":"/backup/etcd-2026-06-15.db"}
kube-scheduler: куда поставить под
Когда ты создаёшь под, в нём поле spec.nodeName пустое - под "висит" в состоянии Pending без узла. Тут вступает kube-scheduler. Он watch-ит поды без nodeName и для каждого прогоняет двухфазный алгоритм:
- Filtering (предикаты) - выкидывает узлы, на которые под в принципе не влезет: не хватает CPU/памяти под requests, не подходит nodeSelector/affinity, узел кордонлен, есть taint без матчащего toleration, нет нужного тома.
- Scoring (приоритеты) - оставшиеся узлы оцениваются по баллам (балансировка нагрузки, spread по зонам, affinity-предпочтения), выбирается лучший.
Посмотреть решение scheduler-а можно прямо в событиях пода:
Код: Выделить всё
kubectl describe pod web-7d9f -n shop | grep -A4 Events
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 12s default-scheduler Successfully assigned shop/web-7d9f to node-2
Normal Pulled 11s kubelet Container image already present on machine
Контроллеры: бесконечный цикл сверки
kube-controller-manager - это один бинарь, внутри которого крутится десяток с лишним контроллеров: Deployment, ReplicaSet, Node, Job, EndpointSlice, ServiceAccount и другие. У каждого одна и та же логика - reconciliation loop (петля согласования): сравнить желаемое состояние (что в spec) с фактическим (что в status и в реальности) и сделать шаг к их совпадению. Желаешь 3 реплики, а живых 2 - ReplicaSet-контроллер создаёт ещё один под. Узел не шлёт heartbeat дольше таймаута - Node-контроллер метит его NotReady, потом эвиктит поды.
Это и есть фундаментальная идея Kubernetes - декларативность. Ты не командуешь "запусти под на node-3". Ты декларируешь "хочу 3 реплики", записываешь это в etcd через api-server, а контроллеры в фоне бесконечно сводят реальность к твоему желанию. Упал под - петля сама поднимет, потому что желаемое (3) больше фактического (2). Никто не выполняет скриптов - все просто чинят расхождение.
cloud-controller-manager - отдельный бинарь (его вынесли из kube-controller-manager, чтобы код облаков не жил в ядре). Он переводит абстракции k8s в API конкретного облака: Service type LoadBalancer -> заказать сетевой балансировщик в Yandex Cloud; PersistentVolume -> создать диск; узел удалён в облаке -> удалить Node-объект. В bare-metal/k3s его может не быть вовсе, и тогда LoadBalancer тебе даст что-то вроде MetalLB.
Все контроллеры общаются ТОЛЬКО через api-server. Scheduler не звонит etcd напрямую, controller-manager не звонит kubelet напрямую. Звезда, а не паутина: в центре api-server, остальные - спицы. Это упрощает безопасность и аудит (один контур авторизации) и развязывает компоненты - любой можно перезапустить независимо.
HA control plane и что ломается при падении каждого
В проде control plane делают отказоустойчивым: минимум 3 master-узла, каждый со своим экземпляром etcd (кворум 2 из 3), api-server за балансировщиком (он stateless - бери любой). А вот scheduler и controller-manager не должны работать параллельно - иначе два scheduler-а назначат один под дважды. Поэтому у них leader election: все экземпляры запущены, но активен один, остальные в горячем резерве и перехватывают лидерство через Lease-объект в api-server, если активный умер.
Теперь самое полезное - что конкретно отвалится при смерти каждого компонента:
- api-server упал (все экземпляры) - kubectl мёртв, ничего не создать/изменить, но УЖЕ запущенные поды продолжают работать: kubelet и kube-proxy на узлах живут своей жизнью по последнему известному состоянию. Кластер "ослеп", но трафик идёт.
- etcd потерял кворум - api-server жив, но не может писать. Чтения частично идут, записи нет. Кластер заморожен: новые поды не создаются, упавшие не заменяются. Чинится восстановлением кворума или рестором снапшота.
- scheduler упал - новые поды зависают в Pending, никто их не назначает. Уже работающие - не трогает. Подними scheduler - очередь Pending разгребётся.
- controller-manager упал - перестаёт работать самовосстановление: упавший под не заменится, Deployment не докатится, узел не пометится NotReady. Существующее живёт, но кластер теряет способность чинить себя.
- Worker-узел упал - это не control plane; Node-контроллер заметит пропажу heartbeat и переназначит поды на другие узлы (если есть куда).
Мини-лаба: пощупать мозг руками
Подними локальный кластер (kind или minikube) и пройди по слоям:
- Посмотри компоненты как поды: kubectl get pods -n kube-system - увидишь kube-apiserver, etcd, kube-scheduler, kube-controller-manager (в kind они статические поды).
- Создай под и поймай работу scheduler-а: kubectl run probe --image=nginx, затем kubectl describe pod probe - найди событие Scheduled и узел.
- Спровоцируй Pending: добавь огромный requests.cpu (например 100 ядер) в под и посмотри FailedScheduling в событиях - это filtering отбраковал все узлы.
- Понаблюдай reconciliation: kubectl create deployment web --image=nginx --replicas=3, удали один под kubectl delete pod <имя> и сразу kubectl get pods -w - увидишь, как контроллер тут же поднимает замену.
- Если кластер на VM с kubeadm - сними снапшот etcd командой из урока и проверь его snapshot status.
- Почему scheduler и controller-manager не ходят в etcd напрямую, а всё делают через api-server? Что это даёт?
- В кластере 3 узла etcd, два внезапно умерли. Что произойдёт с кластером и почему именно так?
- Чем отличается роль scheduler-а от kubelet-а в процессе запуска пода - кто решает "где", а кто делает "что"?
- api-server лёг во всех репликах, но трафик на сайт идёт. Как это возможно?
Control plane - это звезда с api-server в центре, единственной правдой в etcd и роем контроллеров вокруг, которые бесконечно сводят желаемое с фактическим. api-server фильтрует всех через authn/authz/admission и раздаёт состояние через watch. etcd хранит всё и держится на кворуме Raft - его бэкап критичен. scheduler решает где, kubelet делает что, контроллеры чинят расхождения. Поймёшь эту механику - и большинство аварий кластера будешь читать не как магию, а как сбой конкретного звена с предсказуемыми последствиями.