Kubernetes для самых маленьких. Часть 2: Архитектура кластера
Все части серии «Kubernetes для самых маленьких»В первой части кластер выглядел как набор контроллеров, которые через kube-apiserver читают желаемое состояние и приводят к нему мир. Теперь разберём его по деталям: какие процессы работают на мастере и на узлах, что делает каждый, кто к кому подключается и по каким портам, как control plane запускает сам себя и что происходит за те несколько секунд, пока kubectl run превращается в работающий контейнер. Ставить кластер будем в части 4, а здесь, в разделе «Смотрим руками», — как всё это выглядит на уже готовом учебном кластере: мастер k8s-master-1 (192.168.10.17) и рабочие узлы k8s-node-1 (192.168.10.20) и k8s-node-2 (192.168.10.21).
Общая картина
┌─ k8s-master-1: control plane ────────────────────────────────────────┐
│ │
│ ┌──────────┐ ┌────────────────┐ ┌───────────────────┐ │
│ │ etcd │◄─2379──┤ kube-apiserver │◄─6443───┤ kube-scheduler │ │
│ └──────────┘ │ │◄───┐ └───────────────────┘ │
│ └────┬─────┬─────┘ │ │
│ ▲ │ │ ┌───────────────────┐ │
│ │ │ └────┤ kube-controller- │ │
│ │ │ │ manager │ │
│ │ │ └───────────────────┘ │
└──────────────────────────┼─────┼─────────────────────────────────────┘
6443: watch, │ │ 10250: logs, exec,
статусы, Lease │ │ port-forward
│ ▼
┌─ k8s-node-1, k8s-node-2 ─┴───────────────────────────────────────────┐
│ kubelet ──CRI──► containerd ──► containerd-shim-runc-v2 ──► runc │
│ │ │
│ └─► CNI-плагин Calico: veth, адрес, маршрут │
│ │
│ kube-proxy (DaemonSet) ──► правила nftables для сервисов │
│ calico-node (DaemonSet) ──► маршруты между узлами, политики │
└──────────────────────────────────────────────────────────────────────┘
Главное правило архитектуры: с etcd говорит только kube-apiserver. Все остальные компоненты — клиенты API: они подключаются к kube-apiserver по HTTPS на порт 6443, получают изменения через watch и записывают результаты своей работы обратно через API. Друг к другу компоненты напрямую не ходят. Единственное заметное исключение — kube-apiserver сам подключается к kubelet на порт 10250, когда нужно отдать логи контейнера, выполнить kubectl exec или пробросить порт.
На мастере kubelet, containerd, kube-proxy и calico-node тоже работают: control plane в кластере kubeadm сам состоит из подов, и запускать их кто-то должен. Обычные приложения на мастер не попадают из-за метки-запрета (taint) node-role.kubernetes.io/control-plane:NoSchedule, которую kubeadm ставит при установке.
| Компонент | Где и как работает | Порты | Кто подключается |
|---|---|---|---|
| kube-apiserver | мастер, static pod | 6443/TCP | все компоненты, kubectl, дополнения |
| etcd | мастер, static pod | 2379/TCP (клиенты), 2380/TCP (другие члены etcd), 2381 (метрики, только 127.0.0.1) | только kube-apiserver; по 2380 — члены кластера etcd между собой |
| kube-scheduler | мастер, static pod | 10259/TCP, HTTPS: проверки здоровья и метрики, на 127.0.0.1 | системы мониторинга |
| kube-controller-manager | мастер, static pod | 10257/TCP, то же, на 127.0.0.1 | системы мониторинга |
| kubelet | каждый узел, служба systemd | 10250/TCP — API kubelet; 10248 — проверка здоровья на 127.0.0.1 | kube-apiserver, сборщики метрик |
| kube-proxy | каждый узел, под DaemonSet | 10256/TCP — проверка здоровья; 10249 — метрики на 127.0.0.1 | внешние балансировщики, мониторинг |
| сервисы NodePort | каждый узел | 30000–32767/TCP и UDP | клиенты снаружи кластера |
Static pod (статический под) — под, который kubelet запускает по файлу на диске, без участия API; ниже разберём, зачем это нужно. DaemonSet — объект, который держит по одной копии пода на каждом узле; подробно — в части 9.
Порты 10257 и 10259 kubeadm привязывает к 127.0.0.1: снаружи к планировщику и менеджеру контроллеров ходить незачем, а каждый открытый порт — лишняя поверхность атаки. Если метрики с них нужно собирать в Prometheus (систему сбора метрик) с другой машины, адрес меняют осознанно и вместе с правилами файрвола.
kube-apiserver
kube-apiserver — HTTP-сервер, который реализует API Kubernetes. Каждый запрос проходит одну и ту же цепочку:
- TLS и аутентификация — кто пришёл. Клиентский сертификат, подписанный CA кластера (так входят компоненты и администратор из
admin.conf), токен сервисной учётной записи (так входят поды), bootstrap-токен (так входят новые узлы), внешний провайдер OIDC (единый вход через корпоративный сервер идентификации). Подробно — в части 30. - Авторизация — можно ли этому субъекту это действие. В кластере kubeadm работают два модуля: Node (kubelet может читать только то, что относится к подам на его узле) и RBAC (роли и привязки ролей).
- Admission (допуск) — модули, которые могут изменить объект (mutating) или отклонить его (validating) уже после авторизации: подставить значения по умолчанию, проверить квоты, запретить привилегированные поды. Подробно — в части 3.
- Проверка схемы — поля и типы соответствуют версии API.
- Запись в etcd и ответ клиенту.
У kube-apiserver нет своего состояния: всё лежит в etcd. Поэтому экземпляров может быть несколько за балансировщиком — так строится отказоустойчивый control plane, разобранный в серии про BGP, где адрес API держит kube-vip (под на мастерах, который поднимает общий виртуальный адрес на одном из них и анонсирует его по BGP). Внутри kube-apiserver держит кеш watch: последнюю версию каждого объекта и короткую историю изменений. Большинство чтений и все подписки обслуживаются из этого кеша, а не из etcd, — иначе тысячи контроллеров положили бы базу.
Ещё две обязанности. kube-apiserver умеет проксировать часть API во внешние серверы (aggregation layer): так, например, metrics-server, сборщик текущего потребления процессора и памяти, отдаёт свои данные по пути /apis/metrics.k8s.io/. И он распределяет свою пропускную способность между клиентами (API Priority and Fairness): запросы разбиты на уровни приоритета и очереди, чтобы один зациклившийся контроллер не занял весь сервер и не помешал kubelet отчитываться о состоянии узлов.
etcd
etcd — распределённое хранилище ключ-значение, согласованность которого обеспечивает алгоритм Raft: члены кластера etcd выбирают лидера, все записи идут через него и считаются подтверждёнными, когда их сохранило большинство членов (кворум). Поэтому членов нечётное число: три выдерживают потерю одного, пять — двух. В учебном кластере etcd один, и его потеря — потеря кластера; как его бэкапить и восстанавливать, будет в части 35.
Объекты лежат под ключами вида /registry/<ресурс>/<неймспейс>/<имя>: под test из неймспейса default — это /registry/pods/default/test, Deployment — /registry/deployments/default/web. Значения сериализованы в Protobuf (компактный двоичный формат Google), а не в JSON. У каждой записи есть номер ревизии etcd; он же, по сути, resourceVersion объекта в API, о котором в части 3.
Важная деталь безопасности: по умолчанию значения в etcd не шифруются, включая Secret. Доступ к etcd или к его файлам /var/lib/etcd — это доступ ко всем секретам кластера. Поэтому etcd слушает клиентов только по TLS с проверкой клиентского сертификата (принимает лишь сертификаты, подписанные отдельным CA etcd-ca), а шифрование секретов на уровне kube-apiserver включают отдельно — это тема части 13.
kube-scheduler
Планировщик решает одну задачу: для каждого пода, у которого не заполнено поле spec.nodeName, выбрать узел. Он подписан на такие поды через watch и для каждого проходит два этапа:
- фильтрация: отбросить узлы, куда под не помещается — не хватает ресурсов по
requests, есть запрет (taint), который под не допускает, не совпадают метки изnodeSelectorили правила affinity, занят нужный порт хоста, том нельзя подключить в этой зоне; - оценка: оставшиеся узлы получают баллы — за равномерную загрузку, за разнесение реплик одного приложения по узлам, за уже скачанный образ — и выбирается лучший.
Результат планировщик записывает в API подресурсом binding: это короткий запрос «под такой-то — на узел такой-то», после которого у пода появляется spec.nodeName. Сам он ничего не запускает и на узлы не ходит. Подробно правила размещения — в части 14.
kube-controller-manager
Один процесс, внутри которого работают десятки контроллеров — каждый со своим циклом согласования из части 1. Основные:
| Контроллер | Что делает |
|---|---|
| deployment | создаёт и обновляет ReplicaSet по шаблону Deployment |
| replicaset | держит нужное число подов |
| statefulset, daemonset, job, cronjob | то же для своих типов рабочих нагрузок |
| node-lifecycle | следит за продлением Lease узлов, ставит NotReady и запреты, выселяет поды |
| endpointslice | для каждого Service собирает список адресов готовых подов (EndpointSlice) |
| namespace | удаляет всё содержимое удаляемого неймспейса |
| garbage-collector | удаляет объекты, у которых исчез владелец (каскадное удаление) |
| serviceaccount, serviceaccount-token | создаёт сервисную учётную запись default в каждом неймспейсе |
| csrapproving, csrsigning | одобряет и подписывает запросы сертификатов (так узлы получают сертификаты при подключении) |
| bootstrapsigner, tokencleaner | подписывают данные для подключения узлов и удаляют просроченные bootstrap-токены |
| persistentvolume-binder, attachdetach | связывают заявки на тома с томами и подключают тома к узлам |
kubeadm запускает его с ключом --use-service-account-credentials=true: каждый контроллер ходит в API под своей сервисной учётной записью (system:serviceaccount:kube-system:replicaset-controller и т. д.) с минимальными правами, а не под одной общей.
Если экземпляров kube-scheduler и kube-controller-manager несколько (как в отказоустойчивом control plane), работает только один из каждой пары — лидер. Лидерство — это объект Lease в неймспейсе kube-system: лидер продлевает его каждые несколько секунд, остальные экземпляры ждут, пока аренда не истечёт без продления (по умолчанию 15 секунд). Так два планировщика никогда не назначают один под одновременно.
Для облаков есть ещё cloud-controller-manager — контроллеры, которые знают API конкретного облака: создают облачные балансировщики, узнают зону узла, удаляют узлы удалённых виртуальных машин. На своём железе его нет.
kubelet
kubelet — агент на каждом узле, единственный компонент Kubernetes, который сам создаёт контейнеры (точнее, просит об этом среду выполнения). Он работает не в контейнере, а обычной службой systemd: ему нужен полный доступ к хосту, и он должен работать, даже когда API недоступен. Его обязанности:
- регистрирует узел: при первом запуске создаёт объект Node со своими ресурсами, адресами и версиями;
- отчитывается о живости: каждые 10 секунд продлевает свой Lease в неймспейсе
kube-node-leaseи периодически обновляетstatusузла (условияReady,MemoryPressure,DiskPressure,PIDPressure); - запускает поды: подписан через watch на поды с
spec.nodeName, равным имени своего узла, и приводит контейнеры на узле к их описанию; - подключает тома, монтирует секреты и ConfigMap в каталоги под
/var/lib/kubelet/pods/<uid пода>/volumes/; - проверяет здоровье контейнеров (probes) и перезапускает упавшие;
- отдаёт API на порту 10250: логи,
exec,attach,port-forward, метрики ресурсов. Доступ — только с клиентским сертификатом или токеном, анонимные запросы kubeadm отключает; - выселяет поды при нехватке памяти или диска на узле.
Конфигурация kubelet — файл /var/lib/kubelet/config.yaml (объект KubeletConfiguration), учётные данные для API — /etc/kubernetes/kubelet.conf с клиентским сертификатом CN=system:node:k8s-node-1, O=system:nodes. Модуль авторизации Node и admission-плагин NodeRestriction следят, чтобы kubelet с таким сертификатом мог менять только свой объект Node и поды своего узла: захват одного узла не даёт управлять соседними.
Static pods: как control plane запускает сам себя
kube-apiserver, etcd, планировщик и менеджер контроллеров в кластере kubeadm — поды. Но поды создаются через API, а API ещё нет. Курицу и яйцо разводит механизм статических подов: kubelet читает манифесты подов прямо из каталога, указанного в staticPodPath (у kubeadm — /etc/kubernetes/manifests), и запускает их сам, ни у кого не спрашивая.
Порядок при старте мастера:
- systemd запускает containerd и kubelet.
- kubelet находит в
/etc/kubernetes/manifestsчетыре файла —etcd.yaml,kube-apiserver.yaml,kube-controller-manager.yaml,kube-scheduler.yaml— и запускает поды. - Поднимается etcd, за ним kube-apiserver; теперь у kubelet есть с кем говорить.
- kubelet создаёт в API зеркальные поды (mirror pods) — объекты с именами
kube-apiserver-k8s-master-1и т. п., чтобы статические поды было видно вkubectl get pods.
Зеркальный под — только отражение. kubectl delete pod kube-apiserver-k8s-master-1 удалит объект, но kubelet тут же создаст его снова, а контейнер даже не перезапустится. Изменить статический под можно только правкой файла: kubelet следит за каталогом и при изменении манифеста пересоздаёт под. Так меняют флаги kube-apiserver — и так же легко его уронить опечаткой, поэтому перед правкой файл копируют за пределы каталога манифестов: копия kube-apiserver.yaml.bak, оставленная рядом, тоже будет прочитана kubelet как манифест.
Всё остальное в kube-system — kube-proxy, CoreDNS (DNS-сервер кластера, который отвечает на имена сервисов; подробно — в части 17) — и сетевой плагин Calico — обычные поды, созданные через API объектами DaemonSet и Deployment.
Среда выполнения: CRI, containerd, shim и runc
kubelet не умеет запускать контейнеры сам. Он говорит со средой выполнения по CRI (Container Runtime Interface) — gRPC-протоколу (gRPC — механизм удалённого вызова процедур поверх HTTP/2) через Unix-сокет, у containerd это /run/containerd/containerd.sock. В CRI две службы:
- RuntimeService:
RunPodSandbox(создать «песочницу» пода — общие пространства имён и сеть),CreateContainer,StartContainer,StopContainer,RemovePodSandbox,ExecSync,ContainerStatusи т. д.; - ImageService:
PullImage,ListImages,RemoveImage,ImageFsInfo.
До версии 1.24 kubelet умел говорить ещё и с Docker через встроенную прослойку dockershim; её удалили, и теперь любая среда выполнения подключается только по CRI. Образы, собранные Docker, при этом работают как прежде: формат у них общий, OCI.
containerd обслуживает CRI встроенным плагином. Объекты Kubernetes он держит в своём пространстве имён k8s.io (это внутренняя группировка containerd, не namespaces ядра). Образы хранит в /var/lib/containerd, слои распаковывает снапшоттером overlayfs: каждый слой — каталог, а корень контейнера — их объединение через файловую систему overlay, поверх которой лежит записываемый слой контейнера.
Для каждого пода containerd запускает отдельный процесс containerd-shim-runc-v2. Shim («прокладка») становится родителем контейнеров пода: держит их stdin/stdout, собирает коды выхода, пишет логи. Благодаря ему containerd можно перезапустить или обновить, не трогая работающие контейнеры. Сам контейнер создаёт runc: shim вызывает его с описанием OCI (config.json), runc создаёт пространства имён и cgroup, запускает процесс и завершается.
Первым в каждом поде запускается контейнер pause (образ registry.k8s.io/pause): крошечный процесс, который ничего не делает, но держит сетевое пространство имён пода. Остальные контейнеры пода присоединяются к его пространствам, поэтому у пода один IP-адрес, а перезапуск прикладного контейнера не теряет сеть. Подробно — в части 7.
Контейнерам kubelet раздаёт cgroup внутри иерархии kubepods.slice: с драйвером cgroup systemd (а он единственно правильный, когда systemd управляет системой и используется cgroup v2) каждая cgroup пода — юнит systemd. Как kubelet делит её по классам QoS — в части 11.
Логи stdout и stderr контейнеров containerd пишет в файлы /var/log/pods/<неймспейс>_<под>_<uid>/<контейнер>/0.log, а kubelet отдаёт их по запросу kubectl logs. Для отладки на узле есть crictl — клиент CRI, который видит поды и контейнеры так же, как kubelet.
Сетевой плагин (CNI)
Когда containerd создаёт песочницу пода, ему нужно подключить её к сети. Как именно — он не знает и вызывает CNI-плагин: исполняемый файл из /opt/cni/bin с конфигурацией из /etc/cni/net.d. Спецификация CNI простая: плагин запускается с командой ADD, DEL или CHECK в переменной окружения, получает путь к сетевому пространству имён и конфигурацию в JSON на stdin, а в ответ печатает выданный адрес.
В учебном кластере плагин — Calico. При ADD он создаёт пару veth (виртуальный «кабель» из двух интерфейсов: один конец внутри пода как eth0, другой на узле как cali…), берёт адрес из блока, выделенного узлу из пула 172.20.0.0/16, и прописывает на узле маршрут к поду через этот интерфейс. Остальное делают поды Calico в неймспейсе calico-system:
- calico-node (DaemonSet): в нём работают Felix — агент, который настраивает интерфейсы подов и пишет правила сетевых политик в nftables, — и BIRD — демон BGP (протокола, которым маршрутизаторы сообщают друг другу, какие сети через них доступны), через который узлы обмениваются маршрутами к блокам адресов подов друг друга;
- calico-typha: посредник между API и Felix, который держит одну подписку на изменения и раздаёт их всем узлам, чтобы сотни Felix не нагружали kube-apiserver каждый своей;
- calico-kube-controllers: контроллеры Calico, например уборка адресов удалённых узлов.
Ставит и обновляет всё это оператор Tigera (tigera-operator) по объекту Installation; как — в части 4, а как ходят пакеты между подами — в части 15.
kube-proxy
kube-proxy, вопреки имени, ничего не проксирует. Он работает подом DaemonSet на каждом узле, подписан на объекты Service и EndpointSlice и превращает их в правила ядра: пакет на виртуальный адрес сервиса из сети 10.96.0.0/12 должен уйти на адрес одного из подов. В учебном кластере он работает в режиме nftables — правила лежат в таблицах nftables ip kube-proxy и ip6 kube-proxy. Режимы iptables, nftables и IPVS, их устройство и то, как Calico может заменить kube-proxy своим датаплейном на eBPF, — тема части 16.
Кто к кому ходит
| Откуда | Куда | Порт | Зачем | Как аутентифицируется |
|---|---|---|---|---|
| kube-apiserver | etcd | 2379 | чтение и запись объектов | сертификат apiserver-etcd-client |
| kube-scheduler, kube-controller-manager | kube-apiserver | 6443 | watch, запись решений | сертификаты из scheduler.conf, controller-manager.conf |
| kubelet | kube-apiserver | 6443 | watch своих подов, статусы, Lease | сертификат system:node:<имя> |
| kube-proxy, calico-node, CoreDNS | kube-apiserver | 6443 | watch сервисов, эндпойнтов, политик | токены сервисных учётных записей |
kubectl |
kube-apiserver | 6443 | всё | сертификат из kubeconfig |
| kube-apiserver | kubelet | 10250 | logs, exec, attach, port-forward |
сертификат apiserver-kubelet-client |
| kube-apiserver | вебхуки admission, агрегированные API | 443 сервиса | проверка объектов, metrics-server | TLS сервиса |
| kubelet | containerd | Unix-сокет | CRI | права на сокет (только root) |
| containerd | CNI-плагин | запуск файла | подключение пода к сети | — |
| calico-node (BIRD) | calico-node других узлов | 179 | обмен маршрутами к блокам подов по BGP | — |
Все клиенты API устроены одинаково: при старте они делают list нужных объектов, складывают их в локальный кеш и открывают watch с того места, на котором закончился список. Дальше кеш обновляется событиями, а контроллер читает объекты из кеша и ставит ключи изменившихся объектов в очередь на обработку. В библиотеке client-go эта связка называется informer. Отсюда важное свойство: контроллеры почти не читают API запросами GET, вся нагрузка — один watch на тип объектов.
Путь kubectl run
Проследим, что происходит после команды:
$ kubectl run test --image=nginx:1.29
pod/test created
kubectl kube-apiserver kube-scheduler kubelet containerd
│ │ │ │ │
├── 1. POST pods ──►│ │ │ │
│ │ 2. authn, authz, │ │ │
│ │ admission, etcd │ │ │
│◄─ 201 Created ────┤ │ │ │
│ ├── 3. watch ──────►│ │ │
│ │ │ 4. фильтр, │ │
│ │ │ оценка узлов │ │
│ │◄─ POST binding ───┤ │ │
│ ├── 5. watch ───────┼────────────────►│ │
│ │ │ ├── 6. Sandbox ──►│
│ │ │ │ pause, netns, │
│ │ │ │ CNI ADD │
│ │ │ ├── 7. Pull, Run ►│
│ │◄─ 8. PATCH status ┼─────────────────┤ │
│ │ │ │ │
- kubectl читает kubeconfig (
~/.kube/config): адрес API, CA кластера, клиентский сертификат. Строит объект Pod с одним контейнером и отправляетPOST /api/v1/namespaces/default/pods. - kube-apiserver проверяет сертификат клиента, RBAC разрешает создание подов. Admission-плагины дополняют объект: ServiceAccount подставляет учётную запись
defaultи том с её токеном, DefaultTolerationSeconds — допуски на 300 секунд к запретамnot-readyиunreachableиз части 1. Объект проверяется по схеме и записывается в etcd по ключу/registry/pods/default/test. Клиенту уходит201 Created— под существует, но пока это только запись,spec.nodeNameпуст, статусPending. - kube-scheduler получает по watch событие
ADDEDо поде без узла и ставит его в очередь. - Планировщик фильтрует и оценивает узлы, выбирает
k8s-node-2и отправляетPOST .../pods/test/binding. kube-apiserver вписываетspec.nodeName: k8s-node-2и добавляет условиеPodScheduled=True. - kubelet на
k8s-node-2подписан на поды своего узла и получает событие. Он проверяет, что под можно принять (хватает ресурсов, нет конфликтов), создаёт cgroup пода, готовит тома — в том числе файл с токеном сервисной учётной записи. - kubelet вызывает по CRI
RunPodSandbox. containerd запускает shim, тот через runc — контейнер pause с новым сетевым пространством имён; containerd вызывает CNI-плагин Calico, который создаёт veth, выдаёт адрес172.20.140.5и прописывает маршрут. - kubelet вызывает
PullImageдляnginx:1.29(если образа ещё нет на узле), затемCreateContainerиStartContainer: shim через runc запускает nginx в пространствах имён pause-контейнера. - kubelet записывает в
statusпода адрес, состояние контейнера и условияInitialized,ContainersReady,Ready— запросом к подресурсуpods/test/status. Теперьkubectl get podпоказываетRunning.
Дальше, если под подходит под селектор какого-нибудь Service, контроллер endpointslice добавит его адрес в EndpointSlice, а kube-proxy на всех узлах — в правила nftables. Обратите внимание: ни один компонент не позвал другой напрямую. Каждый увидел изменение объекта в API и сделал свой шаг.
Смотрим руками
Узлы и поды control plane:
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-master-1 Ready control-plane 12d v1.36.5 192.168.10.17 <none> Ubuntu 26.04 LTS 7.0.0-15-generic containerd://2.2.2
k8s-node-1 Ready <none> 12d v1.36.5 192.168.10.20 <none> Ubuntu 26.04 LTS 7.0.0-15-generic containerd://2.2.2
k8s-node-2 Ready <none> 12d v1.36.5 192.168.10.21 <none> Ubuntu 26.04 LTS 7.0.0-15-generic containerd://2.2.2
$ kubectl get pods -n kube-system -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-5d7c9f8b6d-7hq2x 1/1 Running 0 12d 172.20.62.130 k8s-master-1 <none> <none>
coredns-5d7c9f8b6d-wb4kz 1/1 Running 0 12d 172.20.62.129 k8s-master-1 <none> <none>
etcd-k8s-master-1 1/1 Running 0 12d 192.168.10.17 k8s-master-1 <none> <none>
kube-apiserver-k8s-master-1 1/1 Running 0 12d 192.168.10.17 k8s-master-1 <none> <none>
kube-controller-manager-k8s-master-1 1/1 Running 0 12d 192.168.10.17 k8s-master-1 <none> <none>
kube-proxy-6xk2d 1/1 Running 0 12d 192.168.10.20 k8s-node-1 <none> <none>
kube-proxy-p8vqn 1/1 Running 0 12d 192.168.10.17 k8s-master-1 <none> <none>
kube-proxy-zr5tw 1/1 Running 0 12d 192.168.10.21 k8s-node-2 <none> <none>
kube-scheduler-k8s-master-1 1/1 Running 0 12d 192.168.10.17 k8s-master-1 <none> <none>
У подов control plane и kube-proxy адрес узла, а не из сети подов: они запущены с hostNetwork: true, в сетевом пространстве имён самого узла. Суффикс -k8s-master-1 у статических подов — имя узла, который их запустил. CoreDNS работает на мастере, потому что к моменту его запуска рабочих узлов ещё не было, а допуск к запрету control plane у него есть.
Манифесты статических подов и порты на мастере:
user@k8s-master-1:~$ ls -l /etc/kubernetes/manifests/
total 16
-rw------- 1 root root 2569 Oct 9 11:02 etcd.yaml
-rw------- 1 root root 3994 Oct 9 11:02 kube-apiserver.yaml
-rw------- 1 root root 3478 Oct 9 11:02 kube-controller-manager.yaml
-rw------- 1 root root 1463 Oct 9 11:02 kube-scheduler.yaml
user@k8s-master-1:~$ sudo ss -tlnp | grep -E 'kube|etcd'
LISTEN 0 4096 127.0.0.1:10248 0.0.0.0:* users:(("kubelet",pid=1102,fd=19))
LISTEN 0 4096 127.0.0.1:10249 0.0.0.0:* users:(("kube-proxy",pid=2231,fd=13))
LISTEN 0 4096 127.0.0.1:10257 0.0.0.0:* users:(("kube-controller",pid=1688,fd=3))
LISTEN 0 4096 127.0.0.1:10259 0.0.0.0:* users:(("kube-scheduler",pid=1702,fd=3))
LISTEN 0 4096 127.0.0.1:2379 0.0.0.0:* users:(("etcd",pid=1655,fd=7))
LISTEN 0 4096 127.0.0.1:2381 0.0.0.0:* users:(("etcd",pid=1655,fd=14))
LISTEN 0 4096 192.168.10.17:2379 0.0.0.0:* users:(("etcd",pid=1655,fd=8))
LISTEN 0 4096 192.168.10.17:2380 0.0.0.0:* users:(("etcd",pid=1655,fd=6))
LISTEN 0 4096 *:10250 *:* users:(("kubelet",pid=1102,fd=22))
LISTEN 0 4096 *:10256 *:* users:(("kube-proxy",pid=2231,fd=12))
LISTEN 0 4096 *:6443 *:* users:(("kube-apiserver",pid=1671,fd=3))
Всё как в таблице: наружу открыты только 6443, 2379–2380 (etcd слушает адрес узла, чтобы в будущем к нему могли подключиться другие члены кластера etcd), 10250 и 10256. Имя kube-controller обрезано: ядро хранит имя процесса длиной до 15 символов.
Зеркальный под помнит, откуда взялся:
$ kubectl get pod -n kube-system kube-apiserver-k8s-master-1 \
-o jsonpath='{.metadata.annotations}{"\n"}{.metadata.ownerReferences[0].kind}{"\n"}'
{"kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint":"192.168.10.17:6443","kubernetes.io/config.hash":"4b8e1c0a6f2d9e7b3c5a1f0e8d6b2a9c","kubernetes.io/config.mirror":"4b8e1c0a6f2d9e7b3c5a1f0e8d6b2a9c","kubernetes.io/config.seen":"2026-10-09T11:02:41.118203547+03:00","kubernetes.io/config.source":"file"}
Node
config.source: file — под из файла, config.mirror — признак зеркального пода, а владелец — объект Node: удалится узел, удалится и отражение.
Конфигурация kubelet и служба systemd:
user@k8s-node-2:~$ sudo grep -E '^(cgroupDriver|clusterDNS|clusterDomain|rotateCertificates|staticPodPath)' -A1 /var/lib/kubelet/config.yaml
cgroupDriver: systemd
clusterDNS:
- 10.96.0.10
clusterDomain: cluster.local
rotateCertificates: true
staticPodPath: /etc/kubernetes/manifests
user@k8s-node-2:~$ systemctl status kubelet --no-pager | head -5
● kubelet.service - kubelet: The Kubernetes Node Agent
Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; preset: enabled)
Drop-In: /usr/lib/systemd/system/kubelet.service.d
└─10-kubeadm.conf
Active: active (running) since Fri 2026-10-09 11:14:20 MSK; 1 week 5 days ago
Каталог статических подов на рабочем узле пуст, но kubelet смотрит в него так же, как на мастере.
Аренды узлов и лидеров:
$ kubectl get lease -n kube-node-lease
NAME HOLDER AGE
k8s-master-1 k8s-master-1 12d
k8s-node-1 k8s-node-1 12d
k8s-node-2 k8s-node-2 12d
$ kubectl get lease -n kube-system
NAME HOLDER AGE
apiserver-q7bx3mfl2kcnq4zt6r5h2dvw4e apiserver-q7bx3mfl2kcnq4zt6r5h2dvw4e_1c9d4a2e-5b7f-4c3a-9e8d-2f6a1b0c7e45 12d
kube-controller-manager k8s-master-1_8f2e6c1a-4d3b-4a7e-b9c0-5e1f2a3d6b78 12d
kube-scheduler k8s-master-1_3a7d9e0b-2c4f-4b1a-8e6d-7c5b9a0f1e23 12d
$ kubectl get lease -n kube-node-lease k8s-node-2 -o jsonpath='{.spec.renewTime}{" "}{.spec.leaseDurationSeconds}{"\n"}'
2026-10-21T08:15:42.906114Z 40
renewTime обновляется каждые 10 секунд — четверть от leaseDurationSeconds. Lease apiserver-… — отметка каждого экземпляра kube-apiserver: по ним экземпляры узнают друг о друге.
Что лежит в etcd. Утилита etcdctl есть в образе etcd, сертификаты — в каталоге /etc/kubernetes/pki/etcd, смонтированном в под:
$ kubectl exec -n kube-system etcd-k8s-master-1 -- etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
--key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
get /registry/pods/default --prefix --keys-only
/registry/pods/default/test
Без сертификата etcd не ответит вовсе; это и есть та граница, за которую никто, кроме kube-apiserver, не ходит.
Под test на узле глазами containerd:
user@k8s-node-2:~$ sudo crictl pods --name test
POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME
3f1b0c9d2e8a4 3 minutes ago Ready test default 0 (default)
user@k8s-node-2:~$ sudo crictl ps --pod 3f1b0c9d2e8a4
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE
9c4e7a1b5d2f6 6a2f8c1d9e0b3 3 minutes ago Running test 0 3f1b0c9d2e8a4 test default
user@k8s-node-2:~$ ps -eo pid,ppid,cmd --forest | grep -A3 '[c]ontainerd-shim-runc-v2 -namespace k8s.io -id 3f1b'
4711 1 /usr/bin/containerd-shim-runc-v2 -namespace k8s.io -id 3f1b0c9d2e8a4c7f0e5b9d1a6c3e8f2b4d7a0c9e1f5b3d8a6c2e4f7b9d0a1c3e -address /run/containerd/containerd.sock
4735 4711 \_ /pause
4802 4711 \_ nginx: master process nginx -g daemon off;
4840 4802 \_ nginx: worker process
Родитель shim — PID 1 (systemd), а не containerd: shim отделён от него и переживёт его перезапуск. Под shim два контейнера — pause и nginx, а crictl ps показывает только прикладной: песочница пода — отдельная сущность CRI.
События пода собирают весь путь из раздела выше:
$ kubectl get events --field-selector involvedObject.name=test
LAST SEEN TYPE REASON OBJECT MESSAGE
3m12s Normal Scheduled pod/test Successfully assigned default/test to k8s-node-2
3m11s Normal Pulling pod/test Pulling image "nginx:1.29"
3m7s Normal Pulled pod/test Successfully pulled image "nginx:1.29" in 4.212s (4.212s including waiting). Image size: 72178381 bytes.
3m7s Normal Created pod/test Created container: test
3m7s Normal Started pod/test Started container test
Первое событие записал планировщик, остальные — kubelet. И наконец, сам kube-apiserver может рассказать о своём здоровье по частям:
$ kubectl get --raw='/readyz?verbose' | head -8
[+]ping ok
[+]log ok
[+]etcd ok
[+]etcd-readiness ok
[+]informer-sync ok
[+]poststarthook/start-apiserver-admission-initializer ok
[+]poststarthook/generic-apiserver-start-informers ok
[+]poststarthook/priority-and-fairness-config-consumer ok
Убираем тестовый под: kubectl delete pod test.
Дальше — API изнутри: из чего состоит объект, как устроены группы и версии API, что такое resourceVersion и watch, как работает server-side apply и как подсмотреть каждый запрос kubectl к серверу.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.