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. Каждый запрос проходит одну и ту же цепочку:

  1. TLS и аутентификация — кто пришёл. Клиентский сертификат, подписанный CA кластера (так входят компоненты и администратор из admin.conf), токен сервисной учётной записи (так входят поды), bootstrap-токен (так входят новые узлы), внешний провайдер OIDC (единый вход через корпоративный сервер идентификации). Подробно — в части 30.
  2. Авторизация — можно ли этому субъекту это действие. В кластере kubeadm работают два модуля: Node (kubelet может читать только то, что относится к подам на его узле) и RBAC (роли и привязки ролей).
  3. Admission (допуск) — модули, которые могут изменить объект (mutating) или отклонить его (validating) уже после авторизации: подставить значения по умолчанию, проверить квоты, запретить привилегированные поды. Подробно — в части 3.
  4. Проверка схемы — поля и типы соответствуют версии API.
  5. Запись в 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), и запускает их сам, ни у кого не спрашивая.

Порядок при старте мастера:

  1. systemd запускает containerd и kubelet.
  2. kubelet находит в /etc/kubernetes/manifests четыре файла — etcd.yaml, kube-apiserver.yaml, kube-controller-manager.yaml, kube-scheduler.yaml — и запускает поды.
  3. Поднимается etcd, за ним kube-apiserver; теперь у kubelet есть с кем говорить.
  4. 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 ┼─────────────────┤                 │
    │                   │                   │                 │                 │
  1. kubectl читает kubeconfig (~/.kube/config): адрес API, CA кластера, клиентский сертификат. Строит объект Pod с одним контейнером и отправляет POST /api/v1/namespaces/default/pods.
  2. kube-apiserver проверяет сертификат клиента, RBAC разрешает создание подов. Admission-плагины дополняют объект: ServiceAccount подставляет учётную запись default и том с её токеном, DefaultTolerationSeconds — допуски на 300 секунд к запретам not-ready и unreachable из части 1. Объект проверяется по схеме и записывается в etcd по ключу /registry/pods/default/test. Клиенту уходит 201 Created — под существует, но пока это только запись, spec.nodeName пуст, статус Pending.
  3. kube-scheduler получает по watch событие ADDED о поде без узла и ставит его в очередь.
  4. Планировщик фильтрует и оценивает узлы, выбирает k8s-node-2 и отправляет POST .../pods/test/binding. kube-apiserver вписывает spec.nodeName: k8s-node-2 и добавляет условие PodScheduled=True.
  5. kubelet на k8s-node-2 подписан на поды своего узла и получает событие. Он проверяет, что под можно принять (хватает ресурсов, нет конфликтов), создаёт cgroup пода, готовит тома — в том числе файл с токеном сервисной учётной записи.
  6. kubelet вызывает по CRI RunPodSandbox. containerd запускает shim, тот через runc — контейнер pause с новым сетевым пространством имён; containerd вызывает CNI-плагин Calico, который создаёт veth, выдаёт адрес 172.20.140.5 и прописывает маршрут.
  7. kubelet вызывает PullImage для nginx:1.29 (если образа ещё нет на узле), затем CreateContainer и StartContainer: shim через runc запускает nginx в пространствах имён pause-контейнера.
  8. 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 к серверу.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.