Kubernetes: От архитектурных основ до экспертных ответов на техническом интервью

Интенсивный курс, выстраивающий целостное понимание оркестрации от устройства Control Plane до стратегий отладки и безопасности. Программа сфокусирована на выявлении причинно-следственных связей в работе кластера, необходимых для прохождения глубокого технического собеседования.

1. Декларативный подход и архитектура Control Plane: как Kubernetes управляет состоянием системы

Декларативный подход и архитектура Control Plane: как Kubernetes управляет состоянием системы

Представьте, что в вашем кластере работает 100 контейнеров с критически важным приложением. В 3 часа ночи один из серверов физически выходит из строя, унося с собой 15 контейнеров. В традиционной инфраструктуре система мониторинга разбудила бы дежурного инженера, которому пришлось бы вручную запускать упавшие процессы на других серверах. В Kubernetes инженер продолжит спать, а к утру система сама восстановит нужное количество контейнеров. Этот фокус возможен благодаря фундаментальной концепции, лежащей в основе Kubernetes — декларативному подходу к управлению состоянием.

Императивный vs Декларативный подход

Чтобы понять Kubernetes, нужно изменить способ мышления с ответов на вопрос «Как сделать?» на ответ «Что я хочу получить?».

Императивный подход — это последовательность команд. Вы пишете скрипт: скачай образ, создай сеть, запусти контейнер А, затем контейнер Б. Если скрипт прервется на середине, система останется в непредсказуемом состоянии. Скрипт нужно писать с учетом всех возможных ошибок.

Декларативный подход — это описание конечного результата. Вы не говорите системе, как запускать контейнеры. Вы передаете ей манифест (обычно в формате YAML), в котором зафиксировано желаемое состояние (Desired State).

> Декларативность в Kubernetes означает, что администратор описывает, как должна выглядеть система в идеале, а сама платформа непрерывно работает над тем, чтобы привести текущее состояние (Actual State) в соответствие с желаемым.

| Характеристика | Императивный подход (скрипты, CLI) | Декларативный подход (Kubernetes YAML) | | :--- | :--- | :--- | | Суть | Инструкция к действию (Do this, then that) | Чертеж результата (Make it look like this) | | Обработка сбоев | Требует сложной логики if/else в скриптах | Встроена в систему: отклонение от чертежа вызывает автоисправление | | Идемпотентность | Повторный запуск скрипта может вызвать ошибку (контейнер уже существует) | Повторное применение манифеста безопасно: система скажет «уже сделано» |

Пример декларативного требования:

Здесь мы заявляем: «В любой момент времени должно работать ровно 3 копии web-server». Если их 2 — система создаст еще одну. Если 4 — убьет лишнюю.

Но кто именно читает этот файл и принимает решения? Для этого нам нужно заглянуть под капот кластера.

Архитектура кластера: Мозг и Мышцы

Любой кластер Kubernetes логически разделен на две части: Control Plane (управляющий слой, «мозг») и Worker Nodes (рабочие узлы, «мышцы»).

!Архитектура Kubernetes: компоненты Control Plane и Worker Nodes

Control Plane: Управляющий слой

Control Plane принимает глобальные решения по кластеру (например, планирование) и обнаруживает события кластера (например, запуск новых подов при падении старых). На технических интервью архитектуру Control Plane спрашивают почти всегда. Она состоит из четырех ключевых компонентов.

1. kube-apiserver (API-сервер) Это входная дверь в кластер. Когда вы выполняете команду kubectl apply -f config.yaml, запрос идет именно сюда. * Это единственный компонент, который имеет право напрямую читать и писать в базу данных кластера. * Все остальные компоненты (как управляющие, так и рабочие) общаются друг с другом только через API-сервер. Это обеспечивает единую точку валидации и авторизации запросов.

2. etcd (Хранилище состояний) Это распределенное, надежное key-value хранилище. Это «память» кластера. * Здесь хранится вся конфигурация и всё желаемое состояние системы. * Если данные не сохранены в etcd — для Kubernetes их не существует. Интервью-факт:* etcd использует алгоритм консенсуса Raft, поэтому для обеспечения отказоустойчивости кластера (High Availability) требуется нечетное количество узлов etcd (обычно 3 или 5), чтобы они могли выбрать лидера.

3. kube-controller-manager (Менеджер контроллеров) Это набор фоновых процессов (контроллеров), которые непрерывно следят за состоянием кластера. * Именно они реализуют ту самую декларативность. Они сравнивают Desired State (из etcd) с Actual State (реальным положением дел). * Например, Node Controller следит за тем, живы ли серверы. ReplicaSet Controller следит, чтобы количество запущенных копий приложения совпадало с цифрой replicas в манифесте.

4. kube-scheduler (Планировщик) Его единственная задача — находить дом для новых контейнеров. * Когда вы просите создать новый Pod (группу контейнеров), он изначально не привязан ни к какому серверу. * Scheduler анализирует требования пода (нужно 2 ГБ памяти и 1 ядро CPU), смотрит на свободные ресурсы рабочих узлов и назначает под на наиболее подходящий узел. Сам запускать ничего он не умеет — он только ставит «штамп» с именем узла.

Worker Nodes: Рабочие узлы

Это серверы, на которых непосредственно выполняются ваши приложения.

1. kubelet «Капитан» рабочего узла. Это агент, который работает на каждом сервере и общается с kube-apiserver. Он получает инструкции (какие поды должны здесь работать) и следит за тем, чтобы контейнеры были запущены и здоровы. Если контейнер падает, kubelet пытается его перезапустить локально.

2. kube-proxy Сетевой прокси-сервер на каждом узле. Он поддерживает сетевые правила (через iptables или IPVS), которые позволяют сетевому трафику достигать ваших контейнеров как изнутри кластера, так и снаружи.

3. Container Runtime (Среда выполнения контейнеров) Программа, которая непосредственно запускает контейнеры (изолирует процессы, выделяет им память и сеть). Kubernetes поддерживает различные среды: containerd, CRI-O.

Сердце системы: Цикл согласования (Reconciliation Loop)

Все эти компоненты не ждут прямых команд друг от друга. Они работают по принципу подписки на события (Watch) и образуют бесконечный цикл согласования — Reconciliation Loop.

Архитектура Kubernetes построена на модели конечных автоматов, работающих асинхронно. Процесс состоит из трех шагов:

  • Observe (Наблюдение): Контроллер опрашивает API-сервер о текущем состоянии.
  • Diff (Сравнение): Сравнивает его с желаемым состоянием.
  • Act (Действие): Отправляет API-серверу команды для устранения разницы.
  • !Цикл согласования в Kubernetes

    Давайте посмотрим, как компоненты взаимодействуют при развертывании приложения:

  • Вы отправляете YAML с replicas: 3 в kube-apiserver.
  • API-сервер проверяет ваши права и сохраняет манифест в etcd. На этом ваш запрос выполнен, вы получаете ответ created. Дальше система работает асинхронно.
  • kube-controller-manager видит новую запись. Он понимает: «Нужно 3 пода, а сейчас 0». Он создает 3 объекта типа Pod в API-сервере (пока без привязки к узлам).
  • kube-scheduler замечает 3 новых пода без узлов. Он вычисляет, куда их поместить, и обновляет их записи в API-сервере, добавляя имя узла (например, node-1 и node-2).
  • kubelet на node-1 постоянно слушает API-сервер. Он видит: «О, появился под, назначенный на мой узел!». Kubelet обращается к Container Runtime, скачивает образ и запускает контейнеры.
  • Kubelet рапортует API-серверу: «Под запущен, состояние Running». API-сервер обновляет статус в etcd.
  • Если node-1 сгорит, kubelet перестанет отправлять сигналы жизни (heartbeats). Controller Manager через некоторое время заметит это, поймет, что Actual State (остался 1 живой под на node-2) больше не совпадает с Desired State (нужно 3), и цикл начнется заново: контроллер создаст новые поды, планировщик найдет для них живые узлы, а новые kubelet-ы их запустят.

    Именно этот асинхронный, основанный на событиях дизайн делает Kubernetes такой устойчивой к сбоям системой. В следующих главах мы подробнее разберем, как именно рождается Pod и какие конкретно контроллеры обеспечивают самовосстановление системы.

    10. Сетевые политики (Network Policies): изоляция трафика и защита микросервисной архитектуры

    Сетевые политики (Network Policies): изоляция трафика и защита микросервисной архитектуры

    Представьте, что злоумышленник проэксплуатировал уязвимость в публичном Nginx-поде вашего кластера. Благодаря строгому SecurityContext он не может повысить привилегии до root на рабочем узле и вырваться из контейнера. Однако ему это и не нужно. Находясь внутри скомпрометированного пода, он выполняет простую команду curl http://payment-db:5432 и беспрепятственно скачивает базу данных с платежной информацией клиентов. Это происходит потому, что по умолчанию Kubernetes реализует плоскую сетевую модель: любой под имеет право инициировать соединение с любым другим подом в кластере.

    Модель RBAC надежно защищает API-сервер от несанкционированных запросов, но она никак не контролирует трафик между самими приложениями. Чтобы остановить горизонтальное перемещение злоумышленника (Lateral Movement), необходимо внедрить концепцию Zero Trust («нулевое доверие») на сетевом уровне.

    Анатомия Network Policy и роль CNI

    За управление доступом на уровнях L3 (IP-адреса) и L4 (TCP/UDP-порты) отвечает абстракция NetworkPolicy. Это манифест, который описывает, какому поду разрешено общаться с другими объектами.

    Здесь кроется один из самых частых подвохов на технических собеседованиях. API-сервер Kubernetes с радостью примет и сохранит в etcd любой валидный YAML-манифест NetworkPolicy. Но сам Kubernetes не применяет эти правила. Исполнение сетевых политик — это эксклюзивная задача CNI-плагина.

    Если в кластере используется базовый CNI (например, Flannel), созданные политики будут просто лежать в базе данных мертвым грузом. Для их работы требуется CNI с поддержкой сетевой фильтрации (Calico, Cilium, Weave Net). Плагин считывает объекты NetworkPolicy и транслирует их в правила межсетевого экрана на рабочих узлах (через iptables, IPVS или программы eBPF).

    Направления трафика: Ingress и Egress

    При проектировании политик важно четко разделять направления трафика относительно целевого пода.

    > Ingress (Входящий трафик) — сетевые пакеты, которые направляются к поду от других источников. > > Egress (Исходящий трафик) — сетевые пакеты, которые инициируются самим подом и направляются наружу.

    Важно не путать направление трафика Ingress с одноименным API-объектом Ingress, который мы разбирали при маршрутизации внешнего HTTP-трафика. Объект Ingress — это L7-балансировщик, а Ingress в контексте NetworkPolicy — это просто вектор движения пакетов на уровне L3/L4.

    Математика изоляции: аддитивная модель

    По умолчанию все поды находятся в состоянии Non-isolated (неизолированные). Они принимают и отправляют любой трафик.

    Как только к поду применяется хотя бы одна NetworkPolicy, он переходит в состояние Isolated (изолированный) для того направления (Ingress или Egress), которое описано в политике. С этого момента весь трафик, не разрешенный явно, блокируется.

    Kubernetes использует аддитивную модель (модель белых списков). Если к одному поду применяется несколько политик, их правила складываются:

    Где:

  • — итоговый набор разрешенных сетевых маршрутов для пода.
  • — маршруты, разрешенные отдельной политикой.
  • — математический оператор объединения множеств.
  • Если Политика А разрешает входящий трафик по порту 80, а Политика Б — по порту 443, итоговый под будет принимать трафик на оба порта. Запрещающих правил (Deny) в Kubernetes Network Policies не существует в принципе — блокировка происходит исключительно за счет отсутствия разрешающего правила.

    Проектирование защиты: паттерн Default Deny

    Построение безопасной архитектуры всегда начинается с полного запрета. Паттерн Default Deny подразумевает создание политики, которая выбирает все поды в неймспейсе и не содержит ни одного разрешающего правила.

    После применения этого манифеста неймспейс production становится полностью глухим. Ни один под не может ни принять запрос, ни отправить его. Теперь мы можем точечно открывать доступы.

    Трехуровневая архитектура: практический кейс

    Рассмотрим классическую микросервисную цепочку:

  • Frontend (Nginx, принимает трафик от пользователей).
  • Backend (Node.js, обрабатывает бизнес-логику).
  • Database (PostgreSQL, хранит данные).
  • Задача: Frontend должен общаться только с Backend. Backend должен общаться только с Database. Database не должен иметь доступа в интернет.

    Шаг 1: Разрешаем трафик к Frontend

    Нам нужно впустить трафик извне кластера (от Ingress Controller) к подам Frontend.

    Здесь мы используем namespaceSelector для фильтрации источника трафика. Запрос будет пропущен только в том случае, если он пришел из неймспейса, где работает Ingress Controller.

    Шаг 2: Связываем Frontend и Backend

    Теперь Frontend должен отправить запрос к Backend. Для этого требуются две вещи: разрешить Egress из Frontend и разрешить Ingress в Backend.

    Обратите внимание на podSelector внутри блока ingress.from. Мы явно указываем, что Backend принимает трафик только от подов с лейблом app: frontend. Если наш взломанный Nginx попытается достучаться напрямую до Database, пакет будет уничтожен на уровне ядра ОС узла, так как для базы данных нет разрешающего правила от Frontend.

    Ловушка Egress и проблема DNS

    Когда инженеры настраивают строгий Egress (исходящий трафик), они часто сталкиваются с ситуацией: Frontend имеет разрешение на отправку трафика в Backend, но приложение всё равно выдает ошибку соединения.

    Причина кроется в механизме Service. Приложение во Frontend обращается к Backend не по IP-адресу (который эфемерен), а по DNS-имени, например http://backend-svc. Чтобы преобразовать это имя в IP-адрес, под должен отправить запрос к внутреннему DNS-серверу кластера (CoreDNS).

    Если мы заблокировали весь Egress политикой Default Deny, под не сможет выполнить DNS-резолв. Запрос будет заблокирован еще до попытки установить TCP-соединение с Backend.

    Решение — добавить глобальное правило, разрешающее исходящий трафик на порт 53 (UDP/TCP) к системному неймспейсу:

    Сравнение механизмов безопасности

    Чтобы не путаться в зонах ответственности различных механизмов защиты, сведем их в единую матрицу:

    | Механизм | Что защищает | На каком уровне работает | Пример использования | | :--- | :--- | :--- | :--- | | RBAC | API кластера (etcd) | HTTP/API (Авторизация) | Запретить ServiceAccount удалять поды. | | SecurityContext | Ядро ОС рабочего узла | Системные вызовы Linux | Запретить контейнеру запускаться от имени root. | | NetworkPolicy | Сетевое взаимодействие | L3/L4 (IP и Порты) | Запретить Frontend обращаться к Database. |

    Сетевые политики замыкают контур безопасности приложения. Возвращаясь к исходному сценарию: даже если злоумышленник захватит контроль над Nginx, в условиях настроенных Network Policies он окажется в сетевом вакууме. Любая попытка просканировать сеть кластера или обратиться к базе данных напрямую завершится таймаутом соединения. База данных просто не получит эти пакеты, так как CNI-плагин отбросит их еще на выходе из сетевого неймспейса скомпрометированного пода.

    11. Наблюдаемость и мониторинг: сбор метрик, логов и работа Probes для проверки здоровья системы

    Наблюдаемость и мониторинг: сбор метрик, логов и работа Probes для проверки здоровья системы

    Пользователи жалуются, что ваш сервис возвращает ошибку 502 Bad Gateway. Вы открываете консоль, вводите kubectl get pods и видите идеальную картину: все поды находятся в статусе Running, никаких перезапусков нет. В этот момент разрушается главная иллюзия новичков в Kubernetes: статус Running означает лишь то, что процесс в контейнере (PID 1) запущен и не завершился с ошибкой. Он ничего не говорит о том, может ли ваше приложение обрабатывать запросы, не зависло ли оно в deadlock'е и не потеряло ли подключение к базе данных.

    Чтобы кластер мог принимать решения на основе реального состояния приложения, а не только статуса процесса, ему нужны органы чувств. В Kubernetes эту роль играют механизмы проверок здоровья (Probes), сбор метрик и агрегация логов.

    Probes: как Kubernetes проверяет пульс приложения

    Вместо того чтобы ждать, пока процесс упадет сам, Kubernetes активно опрашивает контейнеры. Эту задачу выполняет kubelet — агент, работающий на каждом узле. Он может выполнять проверки тремя способами: отправлять HTTP/HTTPS-запросы, открывать TCP-сокеты или выполнять команды (exec) прямо внутри контейнера через Container Runtime.

    В зависимости от того, на какой вопрос мы хотим получить ответ, Kubernetes использует три типа проверок (Probes).

    Liveness Probe: «Ты вообще жив?»

    Liveness-проба отвечает на вопрос, не зашло ли приложение в тупиковое состояние, из которого не сможет выйти самостоятельно (например, взаимная блокировка потоков или бесконечный цикл).

    Если Liveness-проба завершается неудачей заданное количество раз подряд (по умолчанию ), kubelet принимает радикальное решение: он убивает контейнер и перезапускает его в соответствии с политикой restartPolicy.

    > Главное правило Liveness-пробы: она должна быть максимально легковесной. > Если Liveness-проба делает сложный SQL-запрос в базу данных, то при временном замедлении БД kubelet начнет массово убивать и перезапускать абсолютно здоровые поды, что только усугубит аварию.

    Readiness Probe: «Ты готов принимать трафик?»

    Приложение может быть живо, но не готово к работе: оно прогревает кэш, устанавливает соединения с внешними API или выполняет тяжелую миграцию. Отправлять пользовательский трафик на такой под нельзя.

    Здесь в игру вступает Readiness-проба. Если она завершается неудачей, контейнер не перезапускается. Вместо этого контроллер конечных точек (Endpoint Controller) немедленно удаляет IP-адрес этого пода из объекта Endpoints (или EndpointSlice).

    Как только IP-адрес исчезает из Endpoints, kube-proxy обновляет правила маршрутизации на узлах, и абстракция Service перестает направлять новые запросы на этот под. Когда Readiness-проба снова станет успешной (), под вернется в балансировку. Это тот самый механизм, который управляет микро-статусом Ready в условиях (Pod Conditions).

    Startup Probe: защита для тяжеловесов

    Представьте монолитное Java-приложение на Spring Boot, которому для старта требуется 40 секунд. Если вы настроите Liveness-пробу с таймаутом в 10 секунд и порогом падений , kubelet убьет контейнер на 30-й секунде. Приложение попадет в бесконечный цикл перезапусков (CrashLoopBackOff), так и не успев загрузиться.

    Startup-проба решает эту проблему. Пока она не завершится успехом, Liveness и Readiness пробы отключены. Она дает приложению необходимую фору на старт, а после первого успешного ответа навсегда отключается, передавая эстафету Liveness и Readiness.

    Парадокс сетевых политик и проверок здоровья

    Поскольку проверки инициируются компонентом kubelet, возникает интересное пересечение с сетевыми политиками (Network Policies), которые ограничивают Ingress-трафик к подам.

    Если вы реализовали паттерн Default Deny (запрет всего входящего трафика), заблокируют ли эти правила HTTP и TCP запросы от kubelet?

    По архитектурному замыслу Kubernetes, трафик от kubelet для проверок здоровья должен обходить стандартные Network Policies. Большинство современных CNI-плагинов (например, Calico или Cilium) автоматически распознают и разрешают такие пакеты, так как они приходят из сетевого пространства имен самого узла.

    Однако при использовании нестандартных сетевых конфигураций или жестких правил файрвола на уровне хоста (например, HostEndpoints) можно столкнуться с ситуацией, когда приложение внутри работает идеально, но поды постоянно перезапускаются. Причина — kubelet не может достучаться до порта приложения из-за блокировки на уровне ядра Linux узла. В таких случаях требуется явное разрешение Ingress-трафика с IP-адресов рабочих узлов.

    Метрики: от узла до кластера

    Проверки здоровья говорят нам о состоянии «здесь и сейчас». Но для понимания трендов, планирования ресурсов и автомасштабирования кластеру нужны числовые показатели — метрики.

    В Kubernetes сбор метрик разделен на два концептуальных уровня: системный (Core) и пользовательский (Custom).

    Core Metrics и Metrics Server

    Каждый kubelet содержит встроенный модуль cAdvisor (Container Advisor). Он непрерывно читает данные из cgroups ядра Linux, собирая точную статистику по потреблению CPU, оперативной памяти, сети и диска каждым контейнером.

    Чтобы эти данные стали доступны кластеру, используется Metrics Server — легковесный агрегатор. Он периодически опрашивает все kubelet в кластере и кэширует данные в оперативной памяти. Именно Metrics Server обрабатывает запросы, когда вы вводите команду kubectl top nodes или kubectl top pods.

    Важное ограничение: Metrics Server хранит только текущий срез данных. Он не имеет базы данных и не помнит, сколько памяти потреблял под пять минут назад. Его главная задача — быстро отдавать текущие значения для внутренних механизмов Kubernetes (например, для автомасштабирования).

    Custom Metrics и Prometheus

    Для долговременного хранения, построения графиков и сбора бизнес-метрик (например, количества регистраций в секунду) стандартом де-факто является Prometheus.

    Prometheus работает по pull-модели: он сам ходит по кластеру и скачивает (скрапит) метрики по HTTP-адресу (обычно /metrics). Чтобы Prometheus знал, где искать метрики динамически создаваемых подов, используется паттерн ServiceMonitor — специальный CRD-объект, который связывает лейблы подов с конфигурацией сбора метрик.

    | Характеристика | Metrics Server | Prometheus | | :--- | :--- | :--- | | Источник данных | cAdvisor (cgroups) | HTTP-эндпоинты /metrics приложений | | Хранение | Только в оперативной памяти (текущий срез) | Долговременная Time-Series база данных | | Тип метрик | Только CPU и RAM | Любые (включая бизнес-метрики и задержки) | | Основной потребитель | Внутренние контроллеры Kubernetes | Инженеры (через Grafana) и системы алертинга |

    Логирование: эфемерность и централизация

    Метрики показывают, что сломалось (например, график памяти ушел вверх). Логи показывают, почему это сломалось (Stacktrace ошибки OutOfMemory).

    Kubernetes не предоставляет встроенного решения для централизованного хранения логов. Его зона ответственности заканчивается на уровне узла. Когда приложение пишет логи в стандартные потоки вывода (stdout и stderr), Container Runtime перехватывает их, оборачивает в JSON (добавляя временные метки) и сохраняет в директорию /var/log/containers/ на рабочем узле. kubelet следит за размером этих файлов и выполняет их ротацию.

    Если под удаляется или узел выходит из строя, файлы логов теряются навсегда. Чтобы этого избежать, применяется архитектура агрегации логов на базе DaemonSet.

  • На каждый узел кластера разворачивается агент логирования (например, Fluentbit или Promtail) в виде DaemonSet.
  • Этому агенту монтируется директория узла /var/log/containers/ (через механизм Volume Mounts).
  • Агент непрерывно читает новые строки в файлах логов, обогащает их метаданными Kubernetes (добавляет имя пода, неймспейс, лейблы).
  • Обогащенные логи отправляются по сети в централизованное хранилище (Elasticsearch, Loki или OpenSearch), где они индексируются и становятся доступны для поиска, независимо от судьбы исходного пода.
  • Кульминация: анатомия сбоя

    Рассмотрим, как все эти механизмы работают вместе при реальной аварии.

    Допустим, в вашем приложении появилась утечка памяти. Сначала Prometheus, собирающий метрики с JVM, фиксирует плавный рост использования Heap-памяти. На дашборде в Grafana линия ползет вверх. Спустя время запускается процесс сборки мусора (Garbage Collection), который блокирует потоки приложения (Stop-the-World). Приложение перестает отвечать на запросы вовремя.

    В этот момент Readiness-проба, ожидающая ответа за сек, завершается неудачей. Endpoint Controller мгновенно убирает IP пода из Service. Пользователи перестают получать ошибки 502, так как трафик перенаправляется на здоровые реплики.

    Приложение окончательно зависает. Теперь начинает падать Liveness-проба. После трех неудач kubelet отправляет команду Container Runtime на принудительное завершение процесса. Контейнер перезапускается, память очищается, под снова готов к работе.

    Чтобы найти причину, разработчик открывает централизованную систему логирования (например, Kibana). Благодаря тому, что DaemonSet-агент успел прочитать и отправить логи до перезапуска контейнера, разработчик находит стек-трейс java.lang.OutOfMemoryError, выброшенный за секунду до того, как Liveness-проба убила процесс.

    Связка метрик, логов и проверок здоровья превратила потенциально фатальное зависание системы в локальный инцидент с автоматическим восстановлением и сохраненными данными для расследования.

    12. Стратегии деплоя: реализация Rolling Update, Canary и Blue-Green без простоя сервиса

    Стратегии деплоя: реализация Rolling Update, Canary и Blue-Green без простоя сервиса

    Представьте высоконагруженный интернет-магазин в период распродажи, обрабатывающий тысячи транзакций в секунду. Команда разработки выкатывает критичное обновление корзины. Вы выполняете kubectl apply, и если кластер на секунду оборвет текущие соединения или направит трафик на еще не загрузившийся код, бизнес потеряет реальные деньги. Задача инженера — сделать так, чтобы замена старого кода на новый прошла для пользователя абсолютно бесшовно.

    Нулевое время простоя (Zero Downtime) не возникает в Kubernetes по умолчанию. Это результат точной оркестрации механизмов, которые мы разобрали ранее: контроллеров, маршрутизации и проверок здоровья. Прежде чем убить старый контейнер, кластер должен убедиться, что новый готов принимать трафик. Именно Readiness-пробы служат сигналом для объекта Endpoints добавить новый IP-адрес в ротацию. Но в каком порядке, в каком объеме и по каким правилам происходит эта замена? Ответ на этот вопрос дают стратегии деплоя.

    Rolling Update: постепенная замена «на лету»

    По умолчанию контроллер Deployment использует стратегию Rolling Update. Ее суть — постепенная, порционная замена подов старой версии (старого ReplicaSet) на поды новой версии (нового ReplicaSet). Две версии приложения временно работают параллельно, а балансировщик распределяет запросы между ними.

    Скорость и безопасность этого процесса регулируются двумя математическими параметрами в манифесте Deployment:

    Чтобы понять, как контроллер вычисляет допустимое состояние кластера в любой момент обновления, рассмотрим два ограничения:

    Где:

  • — общее количество существующих подов (старых и новых вместе).
  • — желаемое количество реплик, указанное в манифесте.
  • — максимальное количество дополнительных подов, которые можно создать сверх желаемого лимита.
  • — максимальное количество подов, которые могут быть недоступны относительно желаемого лимита.
  • — количество подов, успешно прошедших Readiness Probe.
  • Рассмотрим конкретный пример. У нас есть Deployment с 10 репликами. Мы задаем абсолютные значения: maxSurge: 2 и maxUnavailable: 1.

  • В начале обновления Kubernetes имеет право создать 2 новых пода (так как подов в сумме разрешено).
  • Одновременно он имеет право убить 1 старый под (так как подов обязаны оставаться доступными).
  • Как только один из новых подов проходит Readiness-пробу и переходит в статус Ready, увеличивается. Это дает контроллеру математическое право убить еще один старый под, не нарушая лимит maxUnavailable.
  • Цикл повторяется, пока все 10 подов не будут заменены на новую версию.
  • > Rolling Update — это компромисс между скоростью обновления и потреблением ресурсов. Если выставить maxSurge: 100%, кластер мгновенно создаст полную копию приложения, но потребует в два раза больше свободных узлов (CPU/RAM).

    Главная проблема Rolling Update заключается в несовместимости версий. Если новая версия приложения требует новой структуры базы данных, а старая версия падает при попытке с ней работать, параллельное существование двух версий приведет к ошибкам у части пользователей. Для таких случаев требуется атомарное переключение.

    Blue-Green Deployment: мгновенное переключение рубильника

    Стратегия Blue-Green решает проблему обратной несовместимости. Идея в том, чтобы развернуть новую версию приложения (Green) параллельно со старой (Blue), но не пускать на нее реальный трафик до полной готовности.

    Kubernetes не имеет встроенного контроллера «BlueGreenDeployment». Эта стратегия реализуется на уровне сетевой абстракции Service.

    В кластере создаются два независимых объекта Deployment. Оба масштабированы до 100% необходимых реплик. Объект Service, который принимает клиентский трафик, изначально смотрит только на старую версию благодаря Label Selector:

    В этот момент инженеры или автотесты могут обращаться к версии Green через отдельный, тестовый Service, чтобы проверить работоспособность интеграций и миграций БД.

    Когда команда готова к релизу, происходит атомарная операция — обновление манифеста основного Service: селектор version: v1 меняется на version: v2. Kube-proxy мгновенно переписывает правила маршрутизации, и 100% пользователей начинают попадать на новую версию. Старый Deployment остается в кластере на случай необходимости экстренного отката (Rollback) — для этого достаточно вернуть селектор обратно на v1.

    | Характеристика | Rolling Update | Blue-Green | | --- | --- | --- | | Потребление ресурсов | Оптимальное (зависит от maxSurge) | Высокое (x2 ресурсов на время деплоя) | | Скорость отката | Медленная (требует пересоздания подов) | Мгновенная (смена селектора в Service) | | Параллельная работа версий | Да (трафик балансируется между ними) | Нет (атомарное переключение 100% трафика) |

    Но что, если в версии Green скрыт баг, который не отловили автотесты? При переключении Service 100% пользователей столкнутся с ошибкой. Чтобы минимизировать радиус поражения, применяют третью стратегию.

    Canary Deployment: разведка боем

    «Канареечный» релиз получил свое название от шахтеров, которые брали в забой канареек для проверки уровня токсичных газов. В IT это означает перевод небольшой доли реального пользовательского трафика (например, 5%) на новую версию приложения. Если метрики ошибок (500 HTTP) не растут, доля трафика постепенно увеличивается до 100%.

    В Kubernetes реализовать Canary можно двумя путями: на уровне реплик (нативно, но грубо) и на уровне Ingress (профессионально).

    Подход 1: Балансировка на основе количества реплик

    Мы создаем два Deployment, но на этот раз они имеют одинаковые лейблы, на которые смотрит единый Service. Балансировка трафика происходит естественно: если у нас 9 подов версии v1 и 1 под версии v2, то математически новая версия будет получать примерно 10% запросов.

    Этот подход прост, но имеет жесткие ограничения. Чтобы пустить на новую версию ровно 1% трафика, вам придется держать в кластере минимум 100 подов, что бессмысленно для небольших микросервисов.

    Подход 2: Управление весами через Ingress Controller

    Современный подход переносит ответственность за разделение трафика с L4 (Service) на L7 (Ingress). Мы создаем два независимых Service (один для старой версии, другой для новой). Затем мы создаем дополнительный объект Ingress для новой версии и используем аннотации Ingress-контроллера (например, NGINX), чтобы указать вес маршрутизации.

    В этой конфигурации NGINX сам решает, куда отправить запрос, игнорируя количество подов за каждым сервисом. Если мониторинг (Prometheus) показывает, что метрики новой версии в норме, CI/CD пайплайн обновляет аннотацию canary-weight до 10, 20, 50 и, наконец, 100%.

    Итог: как сделать выбор

    Архитектура нулевого простоя требует осознанного выбора инструмента. Если ваше приложение не имеет строгих привязок к схеме БД и вы хотите сэкономить ресурсы — используйте встроенный Rolling Update. Если вам критически важно избежать смешивания версий и иметь возможность мгновенного отката — стройте Blue-Green через подмену Label Selectors в Service. Если цена ошибки колоссальна и вы хотите протестировать гипотезу на живых пользователях — реализуйте Canary через веса Ingress-контроллера.

    Все эти стратегии работают безупречно в статичной среде. Но ситуация усложняется, когда в процесс вмешивается динамическое изменение нагрузки, и кластер начинает самостоятельно добавлять или удалять реплики прямо во время деплоя.

    13. Автомасштабирование: работа HPA, VPA и Cluster Autoscaler в условиях меняющейся нагрузки

    Автомасштабирование: работа HPA, VPA и Cluster Autoscaler в условиях меняющейся нагрузки

    Представьте, что маркетинговый отдел запустил масштабную рекламную кампанию, и трафик на ваш сервис вырос в десять раз за три минуты. Если вы начнете масштабировать систему вручную, вы потеряете пользователей из-за таймаутов, пока будете вводить команды. Если же вы превентивно будете держать сотни реплик запущенными круглосуточно, инфраструктурные счета быстро сожгут бюджет проекта. Современная инфраструктура должна «дышать» вместе с нагрузкой, автоматически расширяясь в пиковые моменты и сжимаясь во время простоя.

    В Kubernetes за это «дыхание» отвечают три независимых, но тесно связанных механизма: Horizontal Pod Autoscaler (HPA), Vertical Pod Autoscaler (VPA) и Cluster Autoscaler (CA).

    Горизонтальное масштабирование: Horizontal Pod Autoscaler (HPA)

    HPA — это самый популярный инструмент автомасштабирования. Он работает на уровне приложений (подов) и решает проблему нехватки ресурсов путем изменения количества реплик в контроллерах, таких как Deployment или StatefulSet.

    HPA работает как классический контроллер в цикле согласования (Reconciliation Loop). Каждые 15 секунд (по умолчанию) он запрашивает данные у Metrics Server (для базовых метрик CPU/RAM) или у Prometheus (для пользовательских метрик, таких как количество HTTP-запросов или длина очереди сообщений).

    > HPA не управляет подами напрямую. Он изменяет поле replicas в манифесте целевого ресурса (например, Deployment), оставляя непосредственное создание подов контроллеру ReplicaSet.

    Математика HPA

    Когда HPA получает текущие значения метрик, он вычисляет желаемое количество реплик по строго определенной формуле:

    Где:

  • — итоговое количество реплик, которое HPA установит в контроллере.
  • — текущее количество работающих подов.
  • — усредненное значение метрики со всех текущих подов.
  • — целевое значение метрики, которое мы хотим поддерживать (задается в манифесте HPA).
  • — математическое округление вверх до ближайшего целого числа.
  • Разберем на примере. Допустим, у нас работает 2 реплики Nginx. Мы указали в HPA целевую утилизацию CPU в 50%. Внезапно пошел трафик, и текущее среднее потребление CPU выросло до 200% (каждый под потребляет в два раза больше своего Request). Считаем: . HPA обновит Deployment, установив 8 реплик.

    Конфликт HPA и стратегий деплоя

    Внедрение HPA требует изменения подхода к написанию манифестов. Если вы используете HPA, вам необходимо удалить поле replicas из манифеста Deployment.

    Если этого не сделать, при каждом Rolling Update (например, при обновлении версии образа) Deployment будет сбрасывать количество подов до значения, жестко зашитого в манифесте. Если HPA масштабировал приложение до 50 реплик из-за высокой нагрузки, а в манифесте Deployment написано replicas: 3, то при деплое новой версии система сначала убьет 47 подов, приложение упадет от перегрузки, и только потом HPA мучительно начнет заново масштабировать новые поды вверх.

    Вертикальное масштабирование: Vertical Pod Autoscaler (VPA)

    Не все приложения можно масштабировать горизонтально. Монолитные базы данных, legacy-системы или stateful-приложения со сложной репликацией часто требуют не больше подов, а более крупные поды — с большими лимитами CPU и оперативной памяти. Эту задачу решает VPA.

    Механизм VPA состоит из трех компонентов:

  • Recommender — анализирует исторические данные о потреблении ресурсов (из Metrics Server или Prometheus) и вычисляет оптимальные значения Requests и Limits.
  • Updater — следит за работающими подами. Если их текущие Requests сильно отличаются от рекомендаций, Updater принудительно завершает (Evict) такие поды.
  • Admission Controller (Mutating Webhook) — перехватывает запрос на создание нового пода (взамен убитого) и на лету переписывает его Requests и Limits на рекомендованные значения перед сохранением в etcd.
  • > Ключевой инсайт: В Kubernetes невозможно изменить Requests и Limits у уже запущенного контейнера. Поэтому VPA в режиме Auto всегда убивает поды для применения новых ресурсов. Это деструктивный процесс, требующий корректной настройки Pod Disruption Budgets (PDB), чтобы не получить даунТайм.

    Столкновение HPA и VPA

    На технических интервью часто задают вопрос: «Можно ли использовать HPA и VPA одновременно для одного приложения?»

    Короткий ответ: на одних и тех же метриках — категорически нельзя.

    Представьте, что оба контроллера смотрят на утилизацию CPU:

  • Нагрузка растет, CPU достигает 80%.
  • HPA реагирует первым и добавляет новые реплики.
  • Нагрузка размазывается по новым подам, потребление CPU падает до 20%.
  • VPA видит, что поды потребляют мало CPU, решает, что они слишком большие, убивает их и пересоздает с урезанными лимитами.
  • Урезанные поды мгновенно задыхаются от текущего трафика, CPU снова прыгает до 80%.
  • Цикл повторяется. Возникает архитектурное «перетягивание каната» (Thrashing).
  • Совмещать их можно только в одном случае: если HPA реагирует на пользовательские метрики (например, запросы в секунду), а VPA управляет CPU и RAM.

    Инфраструктурное масштабирование: Cluster Autoscaler (CA)

    HPA и VPA оперируют абстракциями внутри кластера. Но что произойдет, если HPA запросит 20 новых реплик, а на физических узлах (Worker Nodes) закончатся свободные ресурсы (Requests)?

    Планировщик (kube-scheduler) не сможет разместить эти поды, и они перейдут в статус Pending. Именно этот статус является триггером для Cluster Autoscaler.

    CA — это компонент, который связывает логику Kubernetes с API облачного провайдера (AWS Auto Scaling Groups, GCP Managed Instance Groups и т.д.).

    Логика Scale-Up (Расширение)

    CA постоянно следит за подами в состоянии Pending. Если такие поды появляются, CA симулирует работу планировщика: он проверяет, поможет ли добавление нового узла разместить эти поды (учитывая Taints, Tolerations и Node Affinity). Если ответ положительный, CA отправляет API-запрос облачному провайдеру на создание новой виртуальной машины.

    Логика Scale-Down (Сжатие)

    Добавление узлов — это просто. Настоящая магия кроется в их безопасном удалении для экономии бюджета. Каждые 10 секунд CA проверяет узлы на соответствие критериям удаления:
  • Сумма всех Requests на узле меньше 50% от его вместимости.
  • Все поды на этом узле могут быть безопасно перемещены на другие существующие узлы.
  • Если узел проходит проверку, CA помечает его как нежелательный. Если ситуация не меняется в течение 10 минут (защита от кратковременных спадов), CA запускает процесс Cordon (запрет на планирование новых подов) и Drain (мягкое выселение текущих подов). После того как узел опустеет, CA командует облаку уничтожить виртуальную машину.

    | Характеристика | HPA | VPA | Cluster Autoscaler | | :--- | :--- | :--- | :--- | | Что масштабирует | Количество подов | Размеры подов (Requests/Limits) | Количество узлов (Nodes) | | Триггер для действия | Превышение метрики (CPU/RAM/Custom) | Исторический анализ потребления ресурсов | Поды в статусе Pending (нехватка места) | | Влияние на приложение | Создает новые поды (без простоя) | Убивает и пересоздает поды (возможен простой) | Мигрирует поды при удалении узлов |

    Архитектура в действии: полный цикл

    Свяжем все компоненты в единый сценарий прохождения пиковой нагрузки:

  • Рост нагрузки: Трафик на интернет-магазин резко возрастает. Потребление CPU в подах веб-сервера превышает целевые 70%.
  • Реакция HPA: HPA вычисляет по формуле, что нужно увеличить количество реплик с 10 до 30. Он обновляет манифест Deployment.
  • Исчерпание ресурсов: ReplicaSet создает 20 новых подов. Планировщик находит место для 5 подов, а оставшиеся 15 переводит в статус Pending, так как кластер заполнен.
  • Реакция CA: Cluster Autoscaler замечает 15 Pending подов. Он обращается к AWS и запрашивает 3 новых EC2-инстанса.
  • Стабилизация: Новые узлы загружаются, присоединяются к кластеру. Планировщик размещает на них ожидающие поды. Трафик успешно обрабатывается 30 репликами.
  • Спад нагрузки: Акция заканчивается. Трафик падает. Среднее потребление CPU опускается до 15%.
  • Сжатие HPA: HPA видит падение метрик и снижает replicas в Deployment обратно до 10. ReplicaSet корректно завершает 20 лишних подов.
  • Сжатие CA: Узлы, на которых работали удаленные поды, становятся пустыми (утилизация < 50%). Cluster Autoscaler ждет 10 минут, переносит оставшиеся системные поды на соседние узлы и удаляет лишние серверы, экономя деньги компании.
  • Именно так работает полностью автоматизированная, эластичная инфраструктура, где каждый контроллер выполняет свою узкую задачу, опираясь на декларативное состояние системы.

    14. Диагностика и Troubleshooting: алгоритм поиска неисправностей в компонентах кластера и приложениях

    Диагностика и Troubleshooting: алгоритм поиска неисправностей в компонентах кластера и приложениях

    Выкатывается минорное обновление микросервиса. CI/CD пайплайн зелёный, kubectl get pods радостно рапортует, что все поды в статусе Running. Но пользователи обрывают поддержку жалобами на ошибку «502 Bad Gateway». Случайный поиск по логам превращается в иголку в стоге сена, а перезагрузка деплоймента не решает проблему. В распределенных системах интуитивный поиск не работает — нужен строгий детерминированный алгоритм исключения.

    В этой главе мы разберем пошаговый процесс диагностики Kubernetes-кластера: от защиты инфраструктуры до вскрытия «черных ящиков» дистролесс-контейнеров.

    Защита от саморазрушения: Pod Disruption Budgets (PDB)

    Прежде чем чинить сломанное, нужно убедиться, что кластер не ломает себя сам. В прошлой главе мы остановились на проблеме выселения подов (Eviction), которое инициируют Vertical Pod Autoscaler (VPA) для обновления ресурсов или Cluster Autoscaler (CA) при сжатии кластера.

    Если контроллеры начнут выселять поды бесконтрольно, сервис уйдет в даунтайм. Чтобы этого избежать, используется механизм Pod Disruption Budget (PDB).

    > Pod Disruption Budget — это политика, ограничивающая количество подов приложения, которые могут быть одновременно недоступны из-за добровольных сбоев (Voluntary Disruptions), таких как обновление узлов или автомасштабирование.

    PDB перехватывает запросы к Eviction API. Если выселение пода нарушит заданный бюджет, API-сервер отклонит запрос, и Cluster Autoscaler будет вынужден ждать.

    Логика работы PDB описывается простым условием:

    Где:

  • — допустимое количество недоступных подов.
  • — общее желаемое количество реплик (replicas).
  • — минимально необходимое количество работающих подов, указанное в манифесте PDB.
  • Пример: У вас 5 реплик приложения (). В PDB указано minAvailable: 4. Это значит, что . Если Cluster Autoscaler попытается удалить узел, на котором работают сразу 2 пода этого приложения, Eviction API заблокирует операцию, спасая сервис от деградации.

    Алгоритм локализации проблемы

    Когда инцидент уже произошел, траблшутинг в Kubernetes строится по принципу «снизу вверх» (Bottom-Up). Мы начинаем с самого гранулярного элемента и поднимаемся до глобальных абстракций.

    | Уровень проверки | Цель | Ключевые команды | | :--- | :--- | :--- | | 1. Pod | Убедиться, что процесс внутри контейнера запущен и не падает. | kubectl get pods, kubectl describe pod, kubectl logs | | 2. Сеть и Service | Проверить, что трафик доходит до пода и балансируется. | kubectl get endpoints, kubectl get svc | | 3. Ingress | Убедиться, что внешняя маршрутизация L7 работает корректно. | kubectl describe ingress, логи Ingress Controller | | 4. Узел (Node) | Исключить аппаратные проблемы и сбои системных агентов (kubelet, CNI). | kubectl get nodes, kubectl describe node |

    Рассмотрим каждый уровень детально.

    Уровень Pod: расшифровка состояний

    Статус пода — это первый индикатор проблемы. API-сервер предоставляет нам высокоуровневую картину, но реальная причина всегда кроется в деталях (микро-статусах и событиях).

    Иллюзия работы: Pod в статусе Running, но не Ready

    Возвращаясь к ситуации из начала статьи: под Running, но пользователи видят 502 ошибку. Статус Running означает лишь то, что контейнер запущен и его PID существует в пространстве имен ядра Linux. Но это не значит, что приложение готово обрабатывать запросы.

    Если в выводе kubectl get pods в колонке READY вы видите 0/1, значит, приложение провалило Readiness-пробу. Как мы помним, это автоматически исключает IP-адрес пода из объекта Endpoints. Трафик на него не пойдет. Решение: смотреть логи приложения (kubectl logs) — скорее всего, потерян доступ к базе данных или не завершились миграции.

    CrashLoopBackOff: петля перезапусков

    Это самый частый статус при неисправностях. Важно понимать: CrashLoopBackOff — это не сама ошибка, это реакция kubelet на то, что процесс в контейнере завершается (с кодом ошибки или даже с кодом 0) почти сразу после старта.

    Чтобы не перегружать Control Plane и Container Runtime бесконечными немедленными перезапусками, kubelet применяет алгоритм экспоненциальной задержки:

    Где:

  • — время ожидания перед следующим перезапуском (в секундах).
  • — жесткий лимит задержки (5 минут).
  • — количество последовательных падений контейнера.
  • При первом падении под перезапустится через 10 секунд, затем через 20, 40, 80, 160 и, наконец, упрется в потолок 300 секунд. Диагностика:

  • kubectl describe pod <name> — смотрим секцию Events и код возврата (Exit Code) в секции Containers.
  • kubectl logs <name> --previous — критически важный флаг. Он показывает логи предыдущего (упавшего) инстанса контейнера, а не текущего, который, возможно, прямо сейчас висит в задержке.
  • ImagePullBackOff и ErrImagePull

    Контейнер даже не начал создаваться. Kubelet не может скачать образ из реестра (Registry). Причины:

  • Опечатка в теге образа (nginx:1.2.3 вместо nginx:1.23).
  • Отсутствие прав доступа к приватному реестру. Необходимо проверить, привязан ли корректный Secret типа kubernetes.io/dockerconfigjson к ServiceAccount пода (через поле imagePullSecrets).
  • Проблемы с DNS на самом узле (kubelet не может отрезолвить адрес реестра).
  • Инструмент последней надежды: Ephemeral Containers

    В современных production-средах стандартом де-факто стали Distroless образы. Это контейнеры, из которых удалено всё, кроме самого бинарного файла приложения и его зависимостей. В них нет пакетных менеджеров, утилит ping, curl и даже оболочки bash или sh.

    Если такое приложение ведет себя странно (например, не может подключиться к Redis), вы не сможете выполнить классический kubectl exec -it <pod> -- /bin/sh, так как /bin/sh просто не существует.

    Для решения этой проблемы в Kubernetes введены Ephemeral Containers (Эфемерные контейнеры).

    > Ephemeral Container — это временный отладочный контейнер, который динамически прикрепляется к уже работающему поду, разделяя с ним сетевое пространство имен и пространство процессов (PID).

    Как это работает на практике: Вы запускаете команду kubectl debug:

    API-сервер модифицирует спецификацию работающего пода на лету, добавляя в него контейнер busybox. Этот контейнер запускается внутри тех же неймспейсов Linux, что и основное приложение. Теперь у вас есть полноценный shell, curl и nslookup, и вы можете проверить сеть глазами проблемного пода, не перезапуская его и не меняя его исходный дистролесс-образ.

    Уровень сети: когда пропадают эндпоинты

    Если под жив, готов принимать трафик (Ready), но запросы до него не доходят, проблема лежит на уровне абстракции Service.

    Алгоритм проверки сети:

  • Проверка Endpoints:
  • Выполняем kubectl get endpoints <service-name>. Если список IP-адресов пуст, значит Service не нашел поды. Причина в 99% случаев — несовпадение Label Selectors. Проверьте, что лейблы в Deployment строго соответствуют селекторам в Service.
  • Проверка DNS:
  • Используем эфемерный контейнер, чтобы сделать nslookup <service-name>. Если IP-адрес сервиса не возвращается, проблема в CoreDNS.
  • Проверка Network Policies:
  • Если IP-адреса резолвятся, но curl отдает таймаут, скорее всего, трафик блокируется сетевыми политиками. Ищите правила, которые запрещают Ingress для целевого пода или Egress для пода-источника.

    Уровень узла: когда рушится фундамент

    Иногда поды массово переходят в статус Unknown или застревают в Pending без видимых причин. В этом случае нужно спуститься на уровень рабочего узла.

    Команда kubectl get nodes может показать статус NotReady. Это означает, что компонент kubelet на этом сервере перестал отправлять heartbeat-сигналы API-серверу.

    Для диагностики узла используется kubectl describe node <node-name>. Нас интересует секция Conditions (условия состояния узла). Kubelet постоянно мониторит здоровье хоста и выставляет флаги:

  • MemoryPressure — на узле заканчивается оперативная память (скоро начнется OOMKilled).
  • DiskPressure — заканчивается место на диске (kubelet начнет агрессивно удалять неиспользуемые образы контейнеров).
  • PIDPressure — исчерпан лимит идентификаторов процессов в ядре Linux.
  • NetworkUnavailable — CNI-плагин не смог сконфигурировать сеть на узле.
  • Если узел находится в состоянии DiskPressure, kubelet превентивно помечает его Taint-меткой node.kubernetes.io/disk-pressure:NoSchedule. Планировщик перестанет назначать туда новые поды, пока проблема с местом не будет решена администратором на уровне ОС.

    Системный подход к траблшутингу — от защиты через PDB до пошагового спуска от пода к узлу — позволяет не угадывать причину сбоя, а математически точно ее вычислять. В следующей, заключительной главе мы соберем воедино все слои архитектуры Kubernetes для разбора самых сложных кейсов, с которыми вы столкнетесь на технических интервью.

    15. Подготовка к интервью: разбор сложных архитектурных кейсов и типичных вопросов работодателей

    Подготовка к интервью: разбор сложных архитектурных кейсов и типичных вопросов работодателей

    Вы приходите на финальное техническое интервью, и архитектор рисует на доске простую схему: Ingress, Service, Deployment из трех подов. Затем он говорит: «Пользователи получают 504 Gateway Timeout. Все поды в статусе Running, метрики CPU в норме, OOMKilled нет. Назови пять причин, почему это происходит, и как это починить». В этот момент проверяется не знание отдельных команд kubectl, а ваше умение видеть кластер как единый живой организм, где сеть, хранилище, безопасность и планировщик неразрывно связаны.

    Технические собеседования на позиции уровня Middle+ и Senior редко затрагивают изолированные концепции. Работодателя интересует, понимаете ли вы стыки технологий — те самые «слепые зоны», где метрики показывают норму, а система при этом лежит. Разберем три классических архитектурных кейса, которые требуют синтеза всех знаний, накопленных при изучении Kubernetes.

    Иллюзия готовности: стык сети и проверок здоровья

    Возвращаясь к задаче с ошибкой 504 Gateway Timeout: казалось бы, статус Running гарантирует работоспособность. Но этот статус — лишь макро-фаза, означающая, что процесс внутри контейнера запущен. Маршрутизация трафика опирается на объект Endpoints, который заполняется только при успешном прохождении Readiness Probe.

    Первая скрытая проблема кроется в фиктивных проверках здоровья. Разработчики часто настраивают Readiness Probe на легковесный эндпоинт /health, который всегда возвращает 200 OK, если веб-сервер жив.

    > Истинная готовность приложения (Readiness) должна отражать его способность обработать запрос от начала и до конца, включая доступность всех критичных внешних зависимостей.

    Если база данных, к которой обращается под, перестает отвечать, фиктивная проба /health продолжит возвращать 200 OK. Kubelet считает под здоровым, Endpoints содержит его IP-адрес, и Ingress Controller направляет туда пользовательский трафик. Внутри пода приложение зависает в ожидании ответа от базы данных. Когда таймаут ожидания на стороне Ingress Controller истекает, пользователь получает 504 Gateway Timeout. Решение: Readiness Probe должна выполнять легковесный тестовый запрос к БД (например, SELECT 1).

    Вторая причина той же ошибки лежит на стыке Network Policies и Ingress. Допустим, Readiness Probe настроена идеально, и под действительно готов. Однако в кластере внедрен паттерн Default Deny. Вы написали сетевую политику, разрешающую входящий трафик к подам только из неймспейса frontend. Но физически трафик к поду маршрутизирует Ingress Controller (например, NGINX), который развернут в системном неймспейсе ingress-nginx.

    В результате CNI-плагин молча отбрасывает пакеты на уровне узла. Ingress Controller не может установить TCP-соединение с подом и по истечении таймаута отдает клиенту 504 ошибку. Для решения необходимо явно разрешить Ingress-трафик от лейблов неймспейса Ingress-контроллера.

    Географический капкан: стык планировщика, дисков и автомасштабирования

    Другой частый вопрос на интервью звучит так: «При резком росте нагрузки HPA увеличил количество реплик. Новые поды повисли в статусе Pending. Cluster Autoscaler увидел это и заказал новый рабочий узел у облачного провайдера. Узел добавился в кластер, ресурсы появились, но поды всё равно остались в Pending. Почему?»

    Здесь пересекаются механизмы динамического выделения хранилища (CSI) и топологии кластера. Проблема возникает при работе со stateful-нагрузками в мультизональных кластерах (например, развернутых в трех зонах доступности AWS: us-east-1a, us-east-1b, us-east-1c).

    Когда создается объект Persistent Volume Claim (PVC), контроллер хранилища по умолчанию немедленно обращается к облачному провайдеру для создания физического диска (например, AWS EBS). Диск создается в конкретной зоне доступности — допустим, в us-east-1a.

    Тем временем kube-scheduler пытается найти узел для нового пода. Если узлы в зоне us-east-1a перегружены, под переходит в Pending. Cluster Autoscaler анализирует ситуацию и запрашивает новый узел. Но Autoscaler может быть настроен на балансировку узлов между зонами и поднять новую машину в us-east-1b.

    Возникает патовая ситуация:

  • Узел готов к приему нагрузки в зоне us-east-1b.
  • Физический диск EBS жестко привязан к зоне us-east-1a (сетевые диски блочного хранилища нельзя примонтировать к виртуальной машине в другой зоне).
  • Планировщик не может назначить под на новый узел, так как нарушается жесткое правило Node Affinity для тома.
  • Ключ к решению — отложенное связывание томов. В StorageClass необходимо изменить параметр volumeBindingMode с Immediate на WaitForFirstConsumer. В этом режиме CSI-драйвер не будет создавать физический диск до тех пор, пока kube-scheduler не выберет конкретный узел для пода. Как только под будет назначен на новый узел в us-east-1b, драйвер создаст диск именно в этой зоне, обеспечив успешный запуск.

    Побег из песочницы: стык RBAC и безопасности ядра

    Третий блок вопросов проверяет понимание границ безопасности. Интервьюер предлагает ситуацию: «Вы выдали команде разработки полные административные права (ClusterRole admin через RoleBinding) строго в рамках неймспейса dev. Могут ли они случайно или намеренно сломать весь кластер?»

    Правильный ответ: да, могут, если в кластере не настроены ограничения уровня ядра (SecurityContext) и политики допуска (Pod Security Admission). Изоляция неймспейсов в Kubernetes логическая, а не физическая.

    Вектор атаки строится на эскалации привилегий через конфигурацию пода. Разработчик, имея право создавать любые поды в своем неймспейсе, может написать манифест с использованием тома типа hostPath. Он монтирует корневую файловую систему рабочего узла (/) в директорию внутри своего контейнера (например, /host-root).

    Зайдя в контейнер, разработчик получает доступ к файлам самого узла. Он может прочитать файл /host-root/etc/kubernetes/pki/ca.crt или извлечь kubeconfig компонента kubelet. Поскольку kubelet обладает правами на чтение и изменение состояния узла через API-сервер, злоумышленник (или неопытный разработчик) выходит за пределы своего неймспейса, получая контроль над инфраструктурой.

    Другой вариант обхода логической изоляции — флаг hostNetwork: true. Контейнер начинает использовать сетевое пространство имен самого узла, игнорируя CNI-плагин и Network Policies. Это позволяет прослушивать весь нешифрованный трафик, проходящий через физический интерфейс машины.

    Для предотвращения таких сценариев архитекторы применяют профили Pod Security Standards (в частности, Restricted), которые на уровне API-сервера запрещают создание подов с hostPath, hostNetwork и запуском процессов от имени root.

    Математика сетевой емкости узла

    Наконец, интервьюеры любят проверять понимание лимитов системы за пределами CPU и памяти. Вопрос: «У нас есть сверхмощный сервер на 128 ядер и 1 ТБ оперативной памяти. Наши поды потребляют по 100 милликор CPU и 50 МБ RAM. Сможем ли мы запустить на этом узле 1000 подов?»

    Если опираться только на Requests и Limits, математика сходится. Но узким местом становится сетевая архитектура CNI-плагина, в частности выделенный узлу диапазон IP-адресов.

    Каждому рабочему узлу при инициализации выдается собственный пул IP-адресов для подов — podCIDR. Количество доступных адресов жестко ограничивает максимальное число подов на узле и вычисляется по классической формуле расчета емкости подсети:

    Где: * — количество доступных IP-адресов для подов на узле. * — длина маски подсети podCIDR (например, 24). * — общее количество бит в IPv4-адресе. * Вычитание двойки обусловлено резервированием адреса самой сети и широковещательного (broadcast) адреса.

    Если кластер сконфигурирован так, что каждому узлу выдается podCIDR с маской /24, то . Подставляем в формулу: .

    Даже если узел обладает колоссальными вычислительными ресурсами, CNI-плагин физически не сможет выдать IP-адреса более чем 254 подам. Оставшиеся 746 подов из запрошенной тысячи навсегда зависнут в состоянии ContainerCreating с ошибкой networkPlugin cni failed to set up pod network: failed to allocate IP. Для решения этой архитектурной проблемы потребуется пересоздание кластера с расширенным диапазоном сети или изменение маски podCIDR на /22 (что даст 1022 адреса на узел).

    Успешное прохождение технического интервью по Kubernetes заключается не в заучивании параметров манифестов, а в понимании того, как изменение одной переменной резонирует через всю архитектуру кластера.

    2. Жизненный цикл Pod: от манифеста в etcd до запуска контейнеров в среде выполнения

    Жизненный цикл Pod: от манифеста в etcd до запуска контейнеров в среде выполнения

    «Что произойдет, если я выполню команду kubectl run nginx --image=nginx?» — это, пожалуй, самый частый вопрос на технических собеседованиях по Kubernetes. Он играет ту же роль, что и классический вопрос про ввод URL в адресную строку браузера. Интервьюер не ждет ответа «запустится контейнер». Он хочет увидеть, понимаете ли вы скрытую хореографию компонентов кластера в те миллисекунды, которые проходят между нажатием Enter и появлением процесса в операционной системе рабочего узла.

    В прошлой главе мы разобрали, как компоненты Control Plane общаются через API-сервер, стремясь привести текущее состояние к желаемому. Теперь давайте возьмем эту архитектуру и пропустим через нее один конкретный объект — Pod, проследив каждый шаг его создания.

    От YAML до etcd: Полоса препятствий API-сервера

    Когда вы отправляете манифест в кластер, он попадает в kube-apiserver. Но API-сервер не просто слепо сохраняет данные в базу. Запрос проходит через строгий конвейер проверок и модификаций.

  • Аутентификация и Авторизация: Система проверяет, кто вы такой и есть ли у вас права на создание Pod в указанном пространстве имен (namespace).
  • Admission Control (Контроллеры допуска): Это критически важный этап, на котором манифест может быть изменен или отклонен. Он делится на две фазы:
  • > Admission Controllers — это плагины, перехватывающие запросы к API-серверу после авторизации, но до сохранения объекта в etcd.

    Существует два основных типа вебхуков допуска:

    | Тип Admission Controller | Задача | Пример из практики | | :--- | :--- | :--- | | Mutating (Мутирующие) | Изменяют входящий манифест «на лету». | Автоматическое добавление sidecar-контейнера (например, прокси Envoy для Service Mesh) в ваш Pod. Вы отправляли манифест с одним контейнером, а API-сервер получил с двумя. | | Validating (Валидирующие) | Проверяют манифест на соответствие политикам компании. Отклоняют запрос при нарушении. | Запрет на запуск контейнеров от имени root (пользователя с UID 0) или требование обязательного наличия лейбла environment: production. |

    Только если манифест успешно прошел обе фазы Admission Control, API-сервер сериализует его и сохраняет в etcd. С этого момента Pod официально существует в кластере. Его статус устанавливается в Pending (В ожидании).

    Планирование (Scheduling): Поиск идеального узла

    Pod сохранен в базе данных, но у него пока нет дома — поле nodeName в его спецификации пустое.

    Здесь просыпается kube-scheduler. Благодаря механизму watch (о котором мы говорили ранее), он моментально узнает о появлении нового Pod без привязки к узлу. Задача планировщика — найти оптимальный Worker Node.

    Процесс выбора узла проходит в два этапа:

  • Отфильтровывание (Filtering): Планировщик отбрасывает узлы, которые физически не могут принять Pod. Например, если Pod требует 4 ГБ памяти, а на узле свободно только 2 ГБ, этот узел исключается.
  • Оценка (Scoring): Оставшимся узлам выставляются баллы. Например, узел, на котором уже запущены поды этого же приложения, может получить меньше баллов (чтобы размазать нагрузку для отказоустойчивости), а узел с наибольшим количеством свободных ресурсов — больше.
  • Выбрав победителя, kube-scheduler не запускает Pod сам. Он отправляет API-серверу специальный объект Binding, который говорит: «Назначь этот Pod на узел Node-A». API-сервер обновляет запись в etcd, заполняя поле nodeName.

    Kubelet и среда выполнения: От абстракции к процессам Linux

    Как только в etcd обновляется поле nodeName, kubelet, работающий на узле Node-A, видит, что ему назначена работа.

    Важно понимать: сам kubelet не умеет запускать контейнеры. Он лишь менеджер, который делегирует эту задачу среде выполнения контейнеров (например, containerd или CRI-O) через стандартизированный интерфейс CRI (Container Runtime Interface).

    Процесс запуска внутри узла строго регламентирован и таит в себе главную ловушку для кандидатов на интервью — инфраструктурный контейнер.

    1. Рождение Pause-контейнера

    Когда kubelet приказывает CRI создать Pod, первым запускается не ваш Nginx или база данных. Первым стартует невидимый для пользователя Pause container (инфраструктурный контейнер).

    > Pause container — это крошечный контейнер (написанный на C или Ассемблере), чья единственная задача — захватить и удерживать Linux-неймспейсы (в первую очередь сетевой — Network Namespace) для всего Pod.

    Почему это гениальное архитектурное решение? Если бы сетевой интерфейс и IP-адрес принадлежали вашему приложению, то при падении приложения (OOM Killed или ошибка в коде) контейнер бы перезапустился, и IP-адрес был бы утерян. Pause-контейнер работает вечно (он буквально выполняет бесконечный цикл pause()). Все остальные контейнеры в Pod присоединяются к его сетевому пространству. Именно поэтому контейнеры внутри одного Pod могут общаться друг с другом по localhost.

    2. Выполнение Init-контейнеров

    После создания «песочницы» с помощью Pause-контейнера, запускаются Init-контейнеры (если они описаны в манифесте). Их ключевые особенности:

  • Они запускаются строго последовательно, один за другим.
  • Следующий Init-контейнер не запустится, пока предыдущий не завершится успешно (с кодом 0).
  • Если Init-контейнер падает, Pod будет перезапускаться до тех пор, пока он не отработает.
  • Практический пример: Вашему бэкенд-приложению нужна база данных. Вы можете добавить Init-контейнер с простым bash-скриптом, который делает ping базы данных. Основное приложение не запустится, пока база не ответит, что исключает ошибки подключения на старте.

    3. Запуск основных контейнеров (App Containers)

    Только когда все Init-контейнеры успешно завершили работу, kubelet дает команду запустить основные контейнеры приложения. В отличие от Init-контейнеров, они стартуют параллельно.

    Анатомия состояний: Фазы против Условий

    На интервью часто просят объяснить разницу между Pod Phase и Pod Condition. Смешивание этих понятий — верный признак нехватки практического опыта.

    Pod Phase (Фаза) — это высокоуровневое, макроскопическое состояние Pod. Это то, что вы видите в колонке STATUS при выполнении kubectl get pods.

  • Pending (В ожидании) — Pod принят системой, но контейнеры еще не созданы (идет планирование или скачивание образов).
  • Running (Работает) — Pod привязан к узлу, и как минимум один основной контейнер запущен (или перезапускается).
  • Succeeded (Успешно завершен) — Все контейнеры завершили работу с кодом 0 (актуально для Job).
  • Failed (Ошибка) — Все контейнеры завершили работу, и хотя бы один упал с ошибкой.
  • Unknown (Неизвестно) — API-сервер потерял связь с kubelet на узле.
  • Pod Conditions (Условия) — это детальный чек-лист внутри фазы. Это массив логических значений (True/False), объясняющий, почему Pod находится в текущей фазе.

  • PodScheduled — смог ли планировщик найти узел?
  • Initialized — успешно ли отработали все Init-контейнеры?
  • ContainersReady — готовы ли все контейнеры?
  • Ready — готов ли Pod принимать пользовательский трафик?
  • Сценарий для понимания: Если ваше приложение запустилось, но зависло и не может обрабатывать запросы, фаза Pod будет Running (процесс жив), но условие Ready перейдет в False (трафик на него отправлять нельзя).

    Кульминация: Полный цикл за 5 секунд

    Соберем все элементы в единую картину, проследив хронологию создания нашего Nginx:

  • Выполняем kubectl apply -f nginx.yaml.
  • API-сервер проверяет права. Mutating Webhook ничего не добавляет, Validating Webhook одобряет запуск.
  • Манифест сохраняется в etcd. Фаза Pod: Pending.
  • Scheduler замечает Pod без узла. Проводит фильтрацию и скоринг, выбирает worker-node-2. Создает объект Binding.
  • API-сервер обновляет etcd. Условие PodScheduled становится True.
  • Kubelet на worker-node-2 замечает новый Pod.
  • Kubelet через CRI приказывает containerd создать Pause-контейнер. Выделяется IP-адрес.
  • Запускаются Init-контейнеры (если есть). После их успеха условие Initialized становится True.
  • Kubelet скачивает образ nginx:latest и запускает основной контейнер. Фаза меняется на Running.
  • Система проверяет здоровье приложения. Если все в порядке, условия ContainersReady и Ready становятся True. Pod начинает принимать трафик.
  • Этот цикл — фундамент, на котором строится вся отказоустойчивость Kubernetes. Понимание того, как контроллеры и узлы передают друг другу ответственность, позволяет не гадать при сбоях, а точно знать, в каком компоненте искать проблему.

    3. Контроллеры и операторы: механизмы обеспечения отказоустойчивости и самовосстановления приложений

    Контроллеры и операторы: механизмы обеспечения отказоустойчивости и самовосстановления приложений

    Представьте ситуацию: сервер в дата-центре внезапно теряет питание. Агент kubelet на этом узле перестает отвечать, а все запущенные на нем Pod'ы безвозвратно уничтожаются. API-сервер фиксирует потерю узла, но сам по себе он не будет перезапускать ваши приложения — это не его зона ответственности. Чтобы система самовосстанавливалась, в кластере работают непрерывные циклы согласования, которые постоянно сравнивают текущую реальность с желаемой. Эту работу выполняют контроллеры.

    Отвязка от инфраструктуры: Labels и Selectors

    В Kubernetes контроллеры не владеют Pod'ами через жесткие ссылки или внутренние идентификаторы (UUID). Связь между управляющим ресурсом и исполняемым контейнером строится на основе гибкой системы меток — Labels и Selectors.

    Лейбл — это простая пара «ключ-значение» (например, app: frontend или env: production), прикрепленная к Pod'у. Селектор — это правило фильтрации, которое контроллер использует для поиска «своих» Pod'ов.

    !Механизм работы Label Selectors

    Контроллер постоянно опрашивает API-сервер, используя математическую логику множеств. Если контроллеру задано поддерживать реплики, он выполняет запрос: найти все Pod'ы, где app == frontend. Если результат выборки , контроллер инициирует создание недостающих Pod'ов. Если — удаляет лишние.

    > Интервью-кейс: «Усыновление» и «Сиротство» (Adoption & Orphaning) > > Если вы вручную измените лейбл у работающего Pod'а так, что он перестанет попадать под селектор, контроллер посчитает этот Pod пропавшим и немедленно создаст новый. Старый Pod продолжит работать, но станет «сиротой» — он больше не управляется контроллером. Этот трюк часто используют Senior-инженеры для изоляции проблемного Pod'а от живого трафика ради дебага, не снижая при этом общую доступность сервиса.

    Чтобы сборщик мусора (Garbage Collector) понимал, какие объекты можно удалять при удалении самого контроллера, Kubernetes использует механизм OwnerReference — скрытое поле в метаданных Pod'а, указывающее на его создателя. Однако первичное управление количеством реплик всегда опирается именно на Label Selectors.

    ReplicaSet: Базовый уровень отказоустойчивости

    ReplicaSet — это простейший контроллер, чья единственная задача заключается в поддержании точного количества идентичных копий Pod'а.

    Он состоит из трех ключевых элементов:

  • Selector: правило, по которому контроллер находит Pod'ы.
  • Replicas: желаемое количество ().
  • Template: шаблон Pod'а (включая образ контейнера, порты и переменные), который будет использован, если потребуется создать новую реплику.
  • На практике инженеры крайне редко создают ReplicaSet напрямую. Проблема этого контроллера в том, что он не умеет обновлять приложения. Если вы измените версию образа в Template у существующего ReplicaSet, он не станет перезапускать текущие Pod'ы. Шаблон будет применен только к новым Pod'ам, если старые вдруг умрут и потребуется замена. Для управляемого обновления версий требуется абстракция более высокого уровня.

    Deployment: Управление менеджерами

    Deployment — это контроллер, который управляет контроллерами ReplicaSet. Его главная задача — обеспечить безопасный процесс обновления приложения без даунтайма (Rolling Update).

    Когда вы создаете Deployment, он генерирует хэш от вашего шаблона Pod'а и создает ReplicaSet с этим хэшем в имени (например, frontend-7b9c6d8f). Если вы меняете образ контейнера с v1 на v2, Deployment не редактирует старый ReplicaSet. Вместо этого он:

  • Создает новый ReplicaSet для v2.
  • Начинает плавно увеличивать количество реплик в новом ReplicaSet.
  • Синхронно уменьшает количество реплик в старом ReplicaSet.
  • !Процесс Rolling Update в Deployment

    Благодаря такой архитектуре Deployment позволяет легко выполнить Rollback (откат) к предыдущей версии — старый ReplicaSet не удаляется сразу, он просто масштабируется до нуля () и ждет, сохраняя в себе предыдущий рабочий шаблон.

    Зоопарк встроенных контроллеров

    Deployment отлично подходит для stateless-приложений (не сохраняющих состояние), но архитектурные задачи бывают разными. Kubernetes предоставляет набор встроенных контроллеров под разные паттерны нагрузки.

    | Контроллер | Главная особенность | Типичный Use Case | | :--- | :--- | :--- | | Deployment | Поддерживает реплик, обновляет их без простоя. Pod'ы взаимозаменяемы. | Веб-серверы, API-бэкенды, микросервисы. | | DaemonSet | Гарантирует, что на каждом (или выбранном) узле кластера запущена ровно одна копия Pod'а. | Агенты сбора логов (Fluentd), мониторинг (Prometheus Node Exporter), сетевые плагины. | | StatefulSet | Запускает Pod'ы строго по очереди (0, 1, 2). Дает каждому Pod'у стабильное сетевое имя и привязывает к конкретному диску. | Базы данных (PostgreSQL, MongoDB), брокеры сообщений (Kafka). | | Job / CronJob | Запускает Pod'ы не для постоянной работы, а до успешного завершения задачи. | Миграции баз данных, генерация отчетов по расписанию, бэкапы. |

    Паттерн Operator: Обучение Kubernetes новым трюкам

    Встроенные контроллеры универсальны, но они ничего не знают о бизнес-логике ваших приложений.

    Представьте, что вы разворачиваете кластер базы данных PostgreSQL. Если узел с Primary-базой падает, стандартный контроллер просто создаст новый Pod. Но для базы данных этого недостаточно: нужно выбрать нового лидера среди реплик (Failover), перенастроить маршрутизацию и только потом вводить новый Pod как чистую реплику. Стандартный контроллер не обладает такими доменными знаниями.

    Здесь на сцену выходит Operator Pattern — концепция расширения Kubernetes, позволяющая инкапсулировать опыт инженера-эксплуататора (оператора) в программный код.

    Оператор состоит из двух частей:

  • CRD (Custom Resource Definition) — расширение API Kubernetes. Вы добавляете в кластер новый тип объекта. Вместо стандартного kind: Deployment вы сможете писать kind: PostgreSQLCluster.
  • Custom Controller — ваш собственный фоновый процесс (чаще всего написанный на Go), который работает прямо в кластере.
  • !Архитектура паттерна Operator

    Когда вы отправляете YAML с kind: PostgreSQLCluster в API-сервер, он сохраняется в etcd. Ваш Custom Controller замечает этот новый ресурс и начинает выполнять заложенную в него специфичную логику: он сам создаст нужные StatefulSet'ы, настроит между ними репликацию, сделает первоначальный бэкап и будет следить за здоровьем базы данных, реагируя на сбои по правилам, специфичным именно для PostgreSQL.

    Паттерн Operator превратил Kubernetes из простого оркестратора контейнеров в универсальную платформу (Control Plane) для управления абсолютно любой инфраструктурой — от сложных баз данных до облачных ресурсов и CI/CD пайплайнов.

    4. Сетевая модель кластера: реализация взаимодействия между подами и роль CNI-плагинов

    Сетевая модель кластера: реализация взаимодействия между подами и роль CNI-плагинов

    Представьте кластер из 50 узлов и 1000 подов. Pod A с IP-адресом 10.244.1.5 на первом узле отправляет запрос к Pod B с адресом 10.244.2.10 на втором узле. В традиционной архитектуре Docker для этого потребовалась бы сложная система проброса портов (Port Forwarding) и трансляции адресов (NAT), потому что контейнеры изолированы во внутренних сетях своих хостов. Но в Kubernetes пакет просто уходит на адрес 10.244.2.10 и достигает цели в первозданном виде. Никакого NAT, никаких конфликтов портов. Как система создает иллюзию единой плоской сети поверх сложной распределенной инфраструктуры?

    Фундаментальные правила сетевой модели

    Архитектура Kubernetes не диктует, какие именно технологии использовать для построения сети, но жестко задает правила, которым эта сеть должна подчиняться. Это так называемая сетевая модель Kubernetes, состоящая из трех постулатов:

  • Любой Pod может связываться с любым другим Pod'ом в кластере без использования NAT.
  • Любой узел (Worker Node) может связываться с любым Pod'ом (и наоборот) без использования NAT.
  • IP-адрес, который Pod видит внутри себя, в точности совпадает с IP-адресом, который видят другие компоненты кластера при обращении к нему.
  • > Эти правила решают главную проблему миграции legacy-приложений в контейнеры: разработчикам больше не нужно управлять динамическим распределением портов. Внутри кластера каждый Pod ведет себя так, будто это отдельная виртуальная машина со своим уникальным IP-адресом в огромной локальной сети.

    Чтобы понять, как эти правила реализуются на практике, необходимо проследить путь сетевого пакета от изоляции внутри пода до выхода на физический сетевой интерфейс узла.

    Локальная маршрутизация: как Pod выходит в сеть узла

    Как мы уже разбирали, сетевое пространство имен (Network Namespace) создается и удерживается инфраструктурным Pause-контейнером. Основные контейнеры приложения лишь подключаются к этому пространству. Но само по себе пространство имен изолировано. Чтобы выпустить трафик наружу, используется механизм виртуальных патч-кордов — veth pair (Virtual Ethernet).

    veth pair — это виртуальный сетевой кабель с двумя концами:

  • Первый конец (eth0) помещается внутрь сетевого пространства имен Pause-контейнера. Именно ему назначается IP-адрес пода.
  • Второй конец (vethX) остается в корневом пространстве имен самого узла (Worker Node).
  • Однако на узле могут работать десятки подов, и у каждого есть свой торчащий конец vethX. Чтобы они могли общаться между собой на одном узле, их подключают к виртуальному коммутатору — мосту (обычно он называется cni0 или docker0).

    Когда Pod A хочет отправить пакет Pod'у B, находящемуся на том же узле, пакет выходит через его eth0, попадает по veth-кабелю на мост cni0, а мост, зная MAC-адреса, пересылает пакет в нужный veth-интерфейс Pod'а B. Трафик даже не покидает пределы узла.

    Но кто именно создает эти интерфейсы, настраивает мосты и выдает IP-адреса? Этим занимается CNI.

    Роль CNI (Container Network Interface)

    Kubernetes — это оркестратор, а не сетевой маршрутизатор. В его исходном коде нет логики создания veth-интерфейсов или настройки таблиц маршрутизации Linux. Вместо этого Kubernetes использует стандарт CNI.

    CNI — это спецификация и набор библиотек, которые определяют, как плагины должны настраивать сеть для контейнеров.

    Процесс выглядит так:

  • kubelet получает задачу запустить новый Pod.
  • Он обращается к Container Runtime (через CRI) для создания Pause-контейнера и его пространств имен.
  • До запуска основных контейнеров kubelet вызывает бинарный файл CNI-плагина, передавая ему параметры в формате JSON (какой IP выдать, к какому мосту подключить).
  • CNI-плагин выполняет всю грязную работу в операционной системе: создает veth pair, подключает к мосту, прописывает маршруты по умолчанию и назначает IP.
  • Существует множество реализаций CNI-плагинов: Flannel, Calico, Cilium, Weave Net. Все они решают одну задачу — обеспечивают связность между подами, но делают это совершенно разными способами, когда речь заходит о трафике между разными узлами.

    Меж-узловое взаимодействие: Overlay против Underlay

    Когда пакет попадает на виртуальный мост узла, но целевой IP-адрес принадлежит поду на другом узле, в игру вступает глобальная сетевая архитектура CNI-плагина. Глобально плагины делятся на два лагеря: использующие оверлейные (Overlay) сети и маршрутизируемые (Underlay).

    Overlay-сети (Инкапсуляция)

    Оверлейная сеть строит виртуальную сеть поверх существующей физической. Самый популярный протокол для этого — VXLAN (Virtual eXtensible Local Area Network). Так работает, например, плагин Flannel.

    Механика VXLAN:

  • Пакет от Pod A (IP 10.0.1.2) к Pod B (IP 10.0.2.3) доходит до маршрутизатора на Узле 1.
  • Узел 1 понимает, что подсеть 10.0.2.0/24 находится на Узле 2 (IP 192.168.1.101).
  • CNI-плагин берет исходный пакет и упаковывает его целиком внутрь нового UDP-пакета.
  • Внешний UDP-пакет отправляется по физической сети от Узла 1 к Узлу 2.
  • Узел 2 получает UDP-пакет, распаковывает его, достает исходный пакет и отправляет его в Pod B.
  • При использовании инкапсуляции возникает накладной расход на размер пакета, так как добавляются дополнительные заголовки. Это влияет на размер полезной нагрузки, что описывается формулой:

    где — максимальный размер полезной нагрузки (Maximum Transmission Unit) для сетевого интерфейса внутри пода, — MTU физического интерфейса узла (стандартно 1500 байт), а — размер заголовка протокола инкапсуляции (например, 50 байт для VXLAN). Если не учесть это при настройке, пакеты будут фрагментироваться, что приведет к деградации производительности сети.

    Underlay-сети (Прямая маршрутизация)

    Вместо того чтобы прятать пакеты друг в друга, можно научить физическую сеть понимать, где находятся IP-адреса подов. Так работает плагин Calico в режиме BGP.

    BGP (Border Gateway Protocol) — это протокол динамической маршрутизации, на котором работает весь интернет. В этой модели каждый Worker Node становится полноценным маршрутизатором. Когда на Узле 1 появляется новый Pod, CNI-плагин сообщает всем остальным узлам (или центральному роутеру) по протоколу BGP: "Эй, если вам нужен IP 10.0.1.2, отправляйте трафик мне!".

    Пакет идет от Pod A к Pod B без какой-либо упаковки. Узел 1 просто смотрит в свою таблицу маршрутизации Linux и пересылает чистый IP-пакет на Узел 2.

    Сравнение подходов

    | Характеристика | Overlay (напр. Flannel, VXLAN) | Underlay (напр. Calico, BGP) | | :--- | :--- | :--- | | Принцип работы | Инкапсуляция (пакет в пакете) | Прямая маршрутизация (BGP) | | Требования к сети | Минимальные (нужен только L3 между узлами и открытый UDP-порт) | Высокие (сеть должна пропускать BGP-трафик, отсутствие NAT между узлами) | | Производительность | Ниже (тратятся ресурсы CPU на упаковку/распаковку, снижается MTU) | Выше (работает на скорости физической сети) | | Сложность диагностики | Сложнее (в tcpdump на физическом интерфейсе видны только UDP-пакеты узлов) | Проще (в tcpdump видны реальные IP-адреса подов) |

    Жизненный цикл пакета: от отправки до получения

    Соберем все элементы воедино на примере отправки HTTP-запроса между узлами в сети Calico (Underlay):

  • Приложение в Pod A генерирует HTTP-запрос к IP-адресу Pod B.
  • Пакет проходит через сетевой стек ядра Linux внутри сетевого пространства имен Pause-контейнера.
  • Пакет вылетает через интерфейс eth0 и по виртуальному кабелю veth pair попадает в корневое пространство узла.
  • Ядро Узла 1 сверяется с таблицей маршрутизации (которую ранее заботливо заполнил CNI-плагин через BGP).
  • Ядро видит маршрут: "Чтобы попасть в подсеть Pod B, нужно отправить пакет на MAC-адрес Узла 2".
  • Пакет уходит в физический коммутатор дата-центра и достигает Узла 2.
  • Ядро Узла 2 получает пакет, смотрит целевой IP-адрес (IP Pod B), находит соответствующий veth-интерфейс и проталкивает пакет в него.
  • Пакет появляется на eth0 внутри Pod B.
  • Вся эта магия происходит на уровне L3/L4 модели OSI. Сетевая модель гарантирует, что поды могут общаться по IP-адресам. Однако IP-адреса подов эфемерны: при пересоздании ReplicaSet или Deployment поды получат новые адреса. Взаимодействовать напрямую по IP в динамической среде невозможно. Для решения этой проблемы поверх CNI строится следующий архитектурный слой — система абстракций для маршрутизации трафика и балансировки нагрузки.

    5. Абстракции Service и Ingress: маршрутизация внешнего и внутреннего трафика в динамической среде

    Абстракции Service и Ingress: маршрутизация внешнего и внутреннего трафика в динамической среде

    В классической сетевой модели прямого взаимодействия узлов достаточно знать IP-адрес назначения. Благодаря CNI-плагинам, каждый под в кластере получает собственный уникальный IP-адрес, и пакеты успешно маршрутизируются между узлами. Но здесь возникает фундаментальное противоречие: IP-адреса опираются на статику, а Kubernetes — это система непрерывного изменения. При каждом обновлении Deployment, масштабировании нагрузки или падении узла старые поды уничтожаются, а новые рождаются уже с совершенно другими IP-адресами. Если фронтенд-приложение попытается запомнить IP-адрес бэкенда, связь оборвется при первом же цикле согласования состояния.

    Для решения этой проблемы Kubernetes вводит слой абстракции над сетевыми адресами, разделяя логическое имя приложения и физические адреса его экземпляров.

    Service: стабильный якорь для эфемерных подов

    Чтобы приложения могли стабильно общаться друг с другом, Kubernetes предоставляет ресурс Service.

    > Service (Сервис) — это абстрактный объект Kubernetes, который определяет логический набор подов и политику доступа к ним, предоставляя единый неизменный IP-адрес и DNS-имя на всё время своей жизни.

    Service не ищет поды по их IP-адресам. Он использует уже знакомый нам механизм Label Selectors. Если у пода есть метки, совпадающие с селектором сервиса, этот под автоматически становится целью для маршрутизации трафика.

    Однако Service не маршрутизирует трафик в поды напрямую. Между ними существует промежуточное звено — ресурс Endpoints (или его современная, более масштабируемая версия EndpointSlice).

    Когда вы создаете Service с селектором, контроллер в Control Plane непрерывно опрашивает API-сервер на предмет подов с нужными метками, которые находятся в состоянии Ready. IP-адреса этих готовых подов записываются в объект Endpoints. Если под падает или перестает отвечать на проверки здоровья, его IP мгновенно удаляется из Endpoints, и Service перестает отправлять туда трафик.

    Типы Service: от внутреннего к внешнему

    В зависимости от того, откуда должен приходить трафик, Service может принимать один из нескольких типов. Каждая следующая ступень включает в себя функциональность предыдущей.

    | Тип Service | Зона доступа | Как работает | Применение | | :--- | :--- | :--- | :--- | | ClusterIP (по умолчанию) | Только внутри кластера | Выдает виртуальный IP-адрес, доступный только изнутри кластерной сети. | Взаимодействие между микросервисами (например, фронтенд бэкенд БД). | | NodePort | Внутри и снаружи | Открывает статический порт (обычно в диапазоне 30000–32767) на каждом рабочем узле кластера. Трафик на IP узла и этот порт перенаправляется в ClusterIP. | Прямой доступ к приложению извне (чаще для отладки) или интеграция с внешними балансировщиками. | | LoadBalancer | Внутри и снаружи | Автоматически запрашивает создание внешнего балансировщика нагрузки у облачного провайдера (AWS, GCP, Yandex Cloud), который направляет трафик на NodePort. | Публикация production-приложений в интернет в облачных средах. |

    Магия kube-proxy: как Service работает под капотом

    На технических интервью часто задают вопрос: «Является ли Service процессом или контейнером, через который проходит трафик?»

    Ответ: нет. Service — это исключительно запись в базе данных etcd. За фактическую пересылку пакетов на узлах отвечает компонент kube-proxy, который работает на каждом Worker Node.

    Kube-proxy слушает API-сервер, отслеживая создание, изменение или удаление объектов Service и Endpoints. Заметив изменения, он обновляет правила маршрутизации в ядре Linux на своем узле. Исторически и архитектурно kube-proxy может работать в нескольких режимах, два из которых являются основными:

  • Режим iptables (стандартный). Kube-proxy создает длинные цепочки правил брандмауэра iptables. Когда пакет с адресом назначения ClusterIP попадает в сетевой стек узла, iptables перехватывает его, случайным образом выбирает один из адресов из списка Endpoints и подменяет IP-адрес назначения (выполняет DNAT).
  • Режим IPVS (высокопроизводительный). Использует модуль ядра Linux IP Virtual Server, специально созданный для балансировки нагрузки.
  • Разница между ними становится критичной в больших кластерах. В режиме iptables правила оцениваются последовательно. Если в кластере тысячи сервисов, время поиска нужного правила растет линейно: , где — количество правил. В режиме IPVS используется хеш-таблица, что обеспечивает константное время поиска маршрута: , независимо от размера кластера, а также позволяет использовать сложные алгоритмы балансировки (например, Least Connections), недоступные в iptables.

    Ingress: маршрутизация на прикладном уровне (L7)

    Использование типа LoadBalancer отлично работает, но имеет существенный экономический и архитектурный недостаток. Если у вас в кластере 50 независимых микросервисов, которые нужно выставить наружу, создание 50 облачных балансировщиков обойдется очень дорого. Кроме того, Service работает на транспортном уровне (L4 модели OSI) — он понимает только IP-адреса и порты, но ничего не знает о HTTP-заголовках, путях (URL) или SSL-сертификатах.

    Для умной маршрутизации HTTP/HTTPS трафика используется Ingress.

    > Ingress — это API-объект, который содержит правила маршрутизации внешнего HTTP/HTTPS трафика к внутренним сервисам кластера на основе URL-путей или доменных имен.

    Важно понимать разделение зон ответственности: сам по себе манифест Ingress ничего не делает. Это просто набор правил. Чтобы эти правила заработали, в кластере должен быть запущен Ingress Controller.

    Ingress Controller — это обычное приложение в кластере (чаще всего на базе Nginx, HAProxy, Envoy или Traefik), которое развернуто как Deployment и выставлено наружу через один Service типа LoadBalancer. Контроллер читает правила из объектов Ingress и динамически перестраивает конфигурацию своего внутреннего прокси-сервера.

    Как Ingress разделяет трафик

    Имея всего один внешний IP-адрес, Ingress Controller может обслуживать сотни приложений, используя правила двух типов:

    * Host-based (по домену): Запрос на api.example.com направляется в Service A, а запрос на web.example.com — в Service B. * Path-based (по пути): Запрос на example.com/v1/users уходит в сервис пользователей, а example.com/v1/orders — в сервис заказов.

    Кроме того, Ingress Controller берет на себя задачу SSL-терминации: он расшифровывает входящий HTTPS-трафик, используя сертификаты (хранящиеся в кластере), и далее внутри кластера передает уже обычный HTTP-трафик, снимая вычислительную нагрузку с самих подов.

    Полный путь запроса: от браузера до контейнера

    Давайте сведем воедино все слои сетевой абстракции и проследим путь пакета от пользователя в интернете до конкретного контейнера:

  • Пользователь вводит в браузере https://api.example.com/users.
  • DNS направляет запрос на внешний IP-адрес облачного LoadBalancer.
  • LoadBalancer перенаправляет трафик на один из узлов кластера, на открытый NodePort.
  • Правила ядра Linux (настроенные kube-proxy) перехватывают трафик на этом порту и отправляют его в под Ingress Controller'а (например, Nginx).
  • Nginx расшифровывает SSL, читает HTTP-заголовок Host: api.example.com и путь /users.
  • Сверяясь с правилами Ingress, Nginx определяет, что этот трафик предназначен для внутреннего Service users-backend.
  • Nginx отправляет пакет на ClusterIP сервиса users-backend.
  • Правила iptables/IPVS на узле перехватывают запрос к ClusterIP, выбирают один из доступных IP-адресов из объекта Endpoints и делают DNAT.
  • Пакет по сети CNI (через оверлей или прямую маршрутизацию) доставляется на узел, где работает целевой под.
  • Пакет проходит через veth pair в сетевой неймспейс пода и попадает в контейнер приложения.
  • Эта многослойная архитектура позволяет Kubernetes скрывать хаос постоянно меняющихся IP-адресов подов от внешнего мира, обеспечивая надежную, масштабируемую и гибкую маршрутизацию как внутри кластера, так и на его границах.

    6. Управление состоянием: механизмы работы Persistent Volumes и специфика StatefulSet

    Управление состоянием: механизмы работы Persistent Volumes и специфика StatefulSet

    Контейнеры по своей природе эфемерны. Если процесс внутри контейнера падает, или рабочий узел выходит из строя, контроллер создает новый Pod на другом узле. Для stateless-приложений (например, веб-серверов) это не проблема: новый экземпляр просто начинает принимать трафик. Но если внутри пода работала база данных, вместе с контейнером навсегда исчезнут и накопленные на его локальной файловой системе данные. Эфемерность среды выполнения вступает в прямой конфликт с фундаментальной потребностью бизнеса — надежно хранить состояние.

    Чтобы решить эту проблему, Kubernetes выносит управление хранением данных за пределы жизненного цикла подов, используя абстракции томов, и предоставляет специализированный контроллер для управления приложениями, которым важна уникальность и порядок.

    Разделение зон ответственности: PV, PVC и StorageClass

    В ранних системах оркестрации разработчикам приходилось жестко прописывать параметры физических дисков прямо в конфигурации приложения. Это нарушало переносимость: манифест, работающий с дисками AWS EBS, ломался при переносе в локальный кластер на базе NFS.

    Kubernetes решает это через паттерн разделения зон ответственности между администратором инфраструктуры и разработчиком приложения.

  • Persistent Volume (PV) — это физический ресурс хранения в кластере. Он создается администратором или автоматикой и содержит технические детали подключения (IP-адрес NFS-сервера, ID диска в облаке, параметры монтирования). PV существует независимо от подов.
  • Persistent Volume Claim (PVC) — это запрос разработчика на выделение хранилища. В нем описываются только бизнес-требования: нужный объем (например, 50 Гб) и режим доступа (например, чтение и запись только одним узлом — ReadWriteOnce).
  • Когда разработчик создает PVC, контроллер Kubernetes ищет свободный PV, который удовлетворяет запросу, и связывает их (процесс Binding). Pod монтирует не сам диск, а ссылается на PVC.

    > Разделение на PV и PVC аналогично бронированию столика в ресторане. PVC — это ваш запрос («нужен столик на четверых у окна»), а PV — это конкретный физический стол (стол №12), который хостес (Kubernetes) закрепляет за вами.

    Динамическое выделение и StorageClass

    Создавать PV вручную под каждый запрос неудобно. Для автоматизации этого процесса введен StorageClass. Это профиль хранилища, который описывает, как именно нужно создавать новые диски.

    Если разработчик создает PVC с указанием определенного StorageClass (например, fast-ssd), а готового PV нет, Kubernetes обращается к внешнему провайдеру хранилища и просит его динамически создать диск нужного размера, после чего автоматически формирует для него PV.

    Важнейший параметр StorageClass для мультизональных кластеров — volumeBindingMode: WaitForFirstConsumer. Если диск будет создан моментально при появлении PVC, облачный провайдер может разместить его в Зоне А. Но если планировщик (Scheduler) позже решит запустить Pod в Зоне Б из-за нехватки ресурсов в Зоне А, Pod не сможет примонтировать диск. Режим отложенного связывания заставляет систему ждать, пока планировщик не выберет узел для пода, и только тогда создает диск в той же зоне доступности.

    Интерфейс CSI: как Kubernetes общается с «железом»

    Исторически код для работы с различными типами хранилищ (AWS, GCP, Ceph) был вшит прямо в исходный код Kubernetes (in-tree). Это раздувало ядро системы и требовало обновления всего кластера ради исправления бага в одном драйвере.

    Сегодня используется CSI (Container Storage Interface) — стандартный протокол взаимодействия между оркестратором и внешними драйверами хранилищ. CSI-драйвер разворачивается в кластере как обычное приложение (часто через DaemonSet) и берет на себя три задачи:

  • Создание и удаление физических дисков (Provisioning).
  • Прикрепление диска к виртуальной машине рабочего узла (Attaching).
  • Форматирование и монтирование файловой системы внутри узла для передачи в Pod (Mounting).
  • Анатомия StatefulSet: больше, чем просто Deployment

    Мы уже знаем, что контроллер Deployment идеально подходит для stateless-приложений. Он управляет взаимозаменяемыми репликами, которые имеют случайные имена (например, web-7b94d5f-x2q) и делят общий трафик.

    Но распределенным базам данных (PostgreSQL, MongoDB, Kafka) такой подход не годится. Им нужны гарантии, которые предоставляет контроллер StatefulSet.

    | Характеристика | Deployment | StatefulSet | | :--- | :--- | :--- | | Идентификация | Случайные хеши в именах (app-84f9b...) | Предсказуемые порядковые индексы (app-0, app-1) | | Порядок запуска | Все поды создаются параллельно | Строго последовательно: от до | | Порядок удаления | Случайный | Строго в обратном порядке: от до | | Связь с хранилищем | Все поды могут делить один PVC | Каждый Pod получает свой уникальный PVC (Sticky Storage) |

    Механизм Sticky Storage (volumeClaimTemplates)

    Это главная магия StatefulSet. Вместо того чтобы ссылаться на заранее созданный PVC, манифест StatefulSet содержит секцию volumeClaimTemplates — шаблон, по которому контроллер генерирует уникальный PVC для каждого создаваемого пода.

    Если у нас есть StatefulSet mongo с двумя репликами, система создаст:

  • Pod mongo-0 и привязанный к нему PVC data-mongo-0.
  • Pod mongo-1 и привязанный к нему PVC data-mongo-1.
  • Если узел, на котором работал mongo-0, сгорает, StatefulSet создает новый Pod с точно таким же именем mongo-0 на другом узле. Благодаря совпадению имен, новый Pod автоматически подхватывает старый PVC data-mongo-0. CSI-драйвер отключает диск от мертвого узла, подключает к новому, и база данных запускается с того же места, где остановилась, без потери байта информации.

    Headless Service: прямая маршрутизация к подам

    В предыдущей главе мы разбирали Service как точку балансировки нагрузки. Но кластеру базы данных балансировщик часто вредит. Если приложение хочет записать данные, оно должно обратиться строго к Primary-узлу, а не к случайной реплике. Узлам базы данных нужно общаться друг с другом для синхронизации, зная точные адреса соседей.

    Для этого используется Headless Service — сервис, у которого в манифесте указано clusterIP: None.

    При создании такого сервиса kube-proxy не выделяет ему виртуальный IP-адрес и не настраивает правила iptables/IPVS. Вместо этого внутренний DNS-сервер кластера (CoreDNS) создает A-записи, возвращающие реальные IP-адреса самих подов.

    В связке со StatefulSet это дает каждому поду стабильное DNS-имя формата: [имя-пода].[имя-headless-сервиса].[неймспейс].svc.cluster.local

    Например, mongo-0.mongo-svc.default.svc.cluster.local. Даже если IP-адрес пода изменится после пересоздания на другом узле, его DNS-имя останется неизменным, что позволяет узлам кластера БД надежно находить друг друга.

    Жизненный цикл базы данных в Kubernetes (Синтез)

    Соберем все концепции воедино на примере развертывания кластера MongoDB из 3 узлов ().

  • Инициализация: Инженер применяет манифесты: StorageClass (AWS EBS), Headless Service (mongo-svc) и StatefulSet (mongo) с volumeClaimTemplates.
  • Шаг : Контроллер создает PVC data-mongo-0. StorageClass динамически запрашивает у AWS диск на 50 Гб. Создается PV. CSI-драйвер монтирует его к узлу. Запускается Pod mongo-0.
  • Ожидание готовности: StatefulSet не переходит к следующему шагу, пока mongo-0 не перейдет в состояние Ready. Это критично для баз данных, где первый узел часто становится лидером (Primary).
  • Шаги и : Строго последовательно создаются mongo-1 (со своим диском data-mongo-1) и mongo-2 (с диском data-mongo-2).
  • Синхронизация: Внутри контейнеров процесс MongoDB использует DNS-имена mongo-0.mongo-svc, mongo-1.mongo-svc для сборки в единый кластер репликации.
  • Авария: Узел с mongo-1 выходит из строя (Kernel Panic).
  • Самовосстановление:
  • - Контроллер замечает отсутствие mongo-1. - Создается новый Pod mongo-1 на здоровом узле. - Планировщик видит, что Pod требует PVC data-mongo-1. - CSI-драйвер выполняет Detach диска от мертвого узла и Attach к новому. - Pod mongo-1 стартует, читает свои старые данные, подключается к mongo-0.mongo-svc и докачивает дельту изменений, произошедших за время его отсутствия.

    Таким образом, Kubernetes обеспечивает надежное управление состоянием, комбинируя независимость дисков (PV/PVC), предсказуемость идентификаторов (StatefulSet) и стабильную сетевую адресацию (Headless Service). В следующей главе мы рассмотрим, как передавать в эти поды конфигурационные файлы и пароли, не пересобирая сами контейнеры.

    7. Конфигурирование приложений: использование ConfigMaps и Secrets без перезагрузки образов

    Конфигурирование приложений: использование ConfigMaps и Secrets без перезагрузки образов

    Изменение пароля от базы данных или уровня логирования с INFO на DEBUG не должно требовать пересборки Docker-образа весом в гигабайт. Зашивание конфигурации внутрь контейнера нарушает методологию Twelve-Factor App и превращает управление инфраструктурой в кошмар: для каждого окружения (Dev, Stage, Prod) приходится поддерживать свой уникальный образ. Kubernetes решает эту проблему элегантно, полностью отрывая жизненный цикл конфигурации от жизненного цикла самого приложения.

    Разделение кода и конфигурации: анатомия ConfigMap

    ConfigMap — это API-объект, предназначенный для хранения неконфиденциальных данных в формате «ключ-значение». Он позволяет вынести конфигурационные файлы, аргументы командной строки и переменные окружения за пределы спецификации пода.

    > ConfigMap не является заменой базе данных. Объем данных, который можно сохранить в одном объекте, ограничен мегабайтом — это жесткий лимит etcd для любого отдельного ресурса в кластере.

    Существует два принципиально разных способа доставить данные из ConfigMap внутрь контейнера. Выбор между ними определяет, сможет ли приложение реагировать на изменения конфигурации на лету.

    | Способ доставки | Механизм | Обновление при изменении ConfigMap | Применение | | :--- | :--- | :--- | :--- | | Переменные окружения (Environment Variables) | Kubelet считывает значения из etcd в момент старта контейнера и передает их процессу. | Нет. Требуется полный перезапуск пода (Pod Restart). | Статичные настройки: URL базы данных, порты, флаги запуска. | | Монтирование тома (Volume Mounts) | Kubelet создает файлы на файловой системе рабочего узла и пробрасывает их внутрь контейнера. | Да. Kubelet периодически синхронизирует файлы. | Сложные конфигурации: nginx.conf, application.yaml, правила маршрутизации. |

    Секреты и иллюзия безопасности

    Если ConfigMap используется для открытых данных, то для паролей, токенов и ключей SSH применяется Secret. На первый взгляд, манифест Secret выглядит почти идентично, но архитектурно он имеет критические отличия, которые часто становятся предметом обсуждения на технических интервью.

    Главная ловушка заключается в формате хранения данных:

    Значение cGFzc3dvcmQxMjM= — это не шифрование, а обычная кодировка Base64 (в данном случае слово password123). По умолчанию Kubernetes хранит секреты в etcd в виде открытого (хоть и закодированного) текста.

    Чтобы Secret действительно стал безопасным, на уровне Control Plane включается Encryption at Rest (шифрование в покое). В этом случае API-сервер прозрачно шифрует данные перед записью в etcd и расшифровывает при чтении, используя ключи шифрования, заданные администратором кластера.

    Вторая линия защиты реализуется на рабочих узлах (Worker Nodes). Когда Secret монтируется в Pod как том, kubelet никогда не записывает его на физический диск узла. Вместо этого используется tmpfs — виртуальная файловая система, хранящаяся исключительно в оперативной памяти (RAM). Как только Pod удаляется, память освобождается, и секрет бесследно исчезает.

    Помимо стандартного типа Opaque (произвольные данные), Kubernetes предоставляет специализированные типы секретов:

  • kubernetes.io/tls — для хранения SSL-сертификатов (используется Ingress-контроллерами для терминации HTTPS).
  • kubernetes.io/dockerconfigjson — учетные данные для скачивания приватных Docker-образов (ImagePullSecrets).
  • kubernetes.io/service-account-token — токены для авторизации подов в Kubernetes API.
  • Под капотом: как kubelet обновляет файлы на лету

    Если мы смонтировали ConfigMap как файл (например, app-config.json) и обновили его через kubectl apply, как именно приложение узнает об изменениях? Это один из самых глубоких вопросов на понимание работы kubelet.

    Прямая перезапись файла, который в данный момент читается приложением, может привести к ошибкам чтения или частичной загрузке данных (состояние гонки). Kubernetes решает эту проблему через атомарную подмену символических ссылок (symlinks).

    Если зайти внутрь контейнера и посмотреть на смонтированную директорию, мы увидим следующую структуру:

  • Реальные данные лежат в скрытой директории с временной меткой (..2023_...).
  • Скрытая ссылка ..data указывает на эту директорию.
  • Видимый приложению файл app-config.json указывает на ..data/app-config.json.
  • Когда kubelet замечает изменение ConfigMap в etcd, он:

  • Создает новую директорию с новой временной меткой и записывает туда обновленный файл.
  • Атомарно переключает симлинк ..data на новую директорию.
  • Удаляет старую директорию.
  • Благодаря атомарности операции на уровне ядра Linux, приложение никогда не прочитает «наполовину записанный» файл.

    > Важно понимать: kubelet не отслеживает изменения мгновенно. Он использует механизм кэширования и периодической синхронизации (по умолчанию время синхронизации минута). Поэтому между обновлением ConfigMap и реальным обновлением файла в поде всегда есть задержка.

    Более того, само по себе обновление файла на диске не заставит приложение перечитать его. Приложение должно уметь отслеживать изменения файлов (например, через inotify в Linux) или реагировать на внешние сигналы (например, Nginx перезагружает конфиги при получении сигнала SIGHUP).

    Проблема производительности и Immutable-ресурсы

    В кластерах с тысячами подов постоянное наблюдение за изменениями ConfigMaps и Secrets создает колоссальную нагрузку на kube-apiserver. Каждый kubelet вынужден держать открытые соединения (watches) или регулярно опрашивать API для проверки обновлений.

    Если конфигурация приложения принципиально не меняется без перезапуска пода, используется концепция Immutable Resources (неизменяемых ресурсов). Добавив всего одно поле в манифест:

    Мы даем кластеру жесткое обещание: этот ConfigMap или Secret никогда не изменится. Получив такой флаг, kubelet полностью отключает фоновый опрос API-сервера для этого объекта. Это радикально снижает сетевой трафик и нагрузку на Control Plane, особенно при массовом масштабировании (например, при запуске тысяч реплик). Если конфигурацию все же нужно изменить, создается новый ConfigMap с новым именем, а в Deployment обновляется ссылка на него, что вызывает стандартный Rolling Update.

    Архитектурный пример: Nginx с динамическим конфигом и TLS

    Соберем все концепции воедино. Задача: развернуть Nginx, который использует кастомный файл конфигурации и обслуживает HTTPS-трафик, используя SSL-сертификат.

  • Secret для TLS-сертификата:
  • Создается объект типа kubernetes.io/tls, содержащий публичный сертификат и приватный ключ. При монтировании в Pod он окажется в оперативной памяти узла (tmpfs).

  • ConfigMap для конфигурации:
  • Создается объект с ключом nginx.conf. Внутри описываются правила маршрутизации.

  • Deployment:
  • В спецификации пода мы описываем два тома (Volumes): один ссылается на ConfigMap, второй — на Secret. Затем монтируем их в нужные директории контейнера (/etc/nginx/nginx.conf и /etc/ssl/certs/ соответственно).

    Если потребуется изменить правила маршрутизации, мы обновляем ConfigMap. Kubelet через некоторое время атомарно обновит симлинки в директории /etc/nginx/. Чтобы Nginx применил новые правила без обрыва текущих сетевых соединений (Zero Downtime), достаточно отправить в контейнер команду nginx -s reload. Контейнер не перезапускается, IP-адрес пода не меняется, а новая конфигурация вступает в силу.

    Таким образом, кластер обеспечивает безопасную, масштабируемую и динамическую доставку настроек, сохраняя сами образы контейнеров абсолютно неизменными и универсальными.

    8. Планирование нагрузки: алгоритмы работы Scheduler и управление ресурсами через Requests и Limits

    Планирование нагрузки: алгоритмы работы Scheduler и управление ресурсами через Requests и Limits

    Представьте кластер из трех узлов, каждый из которых обладает 32 ГБ оперативной памяти. Вы разворачиваете крошечный под с Nginx, который потребляет от силы 50 МБ, но он зависает в статусе Pending и никогда не запускается. В то же время на одном из узлов происходит сбой: утечка памяти в Java-приложении съедает всю доступную физическую память узла, «убивая» соседние критически важные сервисы. Оба этих сценария — следствие одного фундаментального факта: Kubernetes не планирует размещение подов на основе фактического потребления ресурсов. Вся система балансировки и защиты от перегрузок строится на строгой математике обещаний и жестких ограничений.

    Анатомия ресурсов: Requests и Limits

    Чтобы kube-scheduler мог принимать решения, а kubelet — защищать узел от перегрузки, каждому контейнеру в поде необходимо задать профиль потребления ресурсов. Он состоит из двух параметров: Requests (запросы) и Limits (лимиты).

    Requests — это гарантированный объем ресурсов, который кластер резервирует для контейнера. Если сумма Requests всех запущенных подов на узле достигает 100% его емкости, планировщик больше не отправит на этот узел ни одного пода, даже если фактически узлом используется лишь 10% CPU и памяти. Именно поэтому 50-мегабайтный Nginx из примера выше остался в Pending: на узлах закончились свободные резервы (Requests), а не физическая память.

    Limits — это жесткий потолок потребления. Если контейнер попытается превысить этот порог, в дело вмешается ядро Linux (через механизм cgroups), и последствия будут зависеть от типа ресурса.

    Kubernetes делит ресурсы на два принципиально разных класса:

  • Сжимаемые (Compressible) — процессорное время (CPU).
  • Несжимаемые (Incompressible) — оперативная память (RAM).
  • Математика CPU и троттлинг

    Процессорное время измеряется в миллиядрах (millicores, m). Одно физическое или виртуальное ядро узла равно . Если контейнеру выделено , это означает, что он имеет право на 25% времени одного ядра.

    Когда контейнер достигает своего CPU Limit, он не завершается с ошибкой. Вместо этого ядро Linux начинает применять троттлинг (CPU Throttling) через планировщик CFS (Completely Fair Scheduler).

    Выделяемое время рассчитывается по формуле:

    Где по умолчанию равен 100 мс ().

    Если вы установили лимит CPU в (0.5 ядра), контейнер получит ровно 50 мс процессорного времени в каждом 100-миллисекундном окне. Как только эти 50 мс исчерпаны, процесс замораживается ядром до начала следующего 100-миллисекундного цикла. Внешне это выглядит как резкая деградация производительности и рост времени ответа (latency) приложения, хотя сам под продолжает числиться в статусе Running.

    Несжимаемая память и OOMKilled

    В отличие от процессорного времени, оперативную память нельзя «поставить на паузу». Если контейнер пытается выделить память сверх установленного параметра Limits, операционная система немедленно завершает его процесс.

    В статусе пода появляется причина OOMKilled (Out Of Memory Killed), после чего kubelet перезапускает контейнер (если позволяет политика restartPolicy).

    Классы обслуживания (QoS): кто выживет при нехватке памяти?

    Установка Requests и Limits влияет не только на конкретный под, но и на его приоритет выживания в масштабах всего узла. Когда физическая память на рабочем узле подходит к концу (Node Memory Pressure), kubelet начинает принудительно выселять (Evict) поды, чтобы спасти узел от полного зависания.

    Очередность выселения определяется классом обслуживания (Quality of Service, QoS), который Kubernetes присваивает поду автоматически на основе соотношения его Requests и Limits. Kubelet транслирует этот класс в параметр ядра Linux oom_score_adj — чем он выше, тем больше шансов, что процесс убьет OOM Killer.

    | QoS Class | Условие присвоения | Значение oom_score_adj | Приоритет выселения | | :--- | :--- | :--- | :--- | | BestEffort | У пода вообще не указаны ни Requests, ни Limits. | 1000 | Первые в очереди. Убиваются при малейшей нехватке ресурсов на узле. | | Burstable | Requests меньше Limits, либо ресурсы указаны не для всех контейнеров в поде. | От 2 до 999 (зависит от % потребления) | Вторые в очереди. Убиваются, если BestEffort уже удалены, а памяти всё еще не хватает. | | Guaranteed | Requests строго равны Limits для CPU и RAM у всех контейнеров в поде. | -997 | Неприкасаемые. Выселяются только в крайнем случае, если системным демонам узла не хватает памяти. |

    Чтобы защитить критически важную базу данных от случайного выселения из-за соседнего прожорливого приложения, ей необходимо обеспечить класс Guaranteed, жестко приравняв Requests к Limits.

    Двухэтапный алгоритм kube-scheduler

    Теперь, когда у подов есть математически выраженные требования к ресурсам, в дело вступает kube-scheduler. Его задача — найти идеальный рабочий узел для каждого нового пода.

    Процесс планирования запускается, когда в etcd появляется объект Pod с пустым полем nodeName. Планировщик пропускает этот под через конвейер из двух основных фаз.

    Фаза 1: Фильтрация (Predicates)

    На этом этапе планировщик отсеивает узлы, которые физически не могут принять под. Это жесткие требования (Hard constraints).

    Ключевые проверки (предикаты):

  • PodFitsResources: Достаточно ли на узле нераспределенных резервов (Requests)? Если узлу доступно 8 ГБ RAM, а сумма Requests уже запущенных подов равна 7.8 ГБ, узел будет отброшен для пода с Request в 500 МБ.
  • PodFitsHostPorts: Если под требует открытия статического порта на узле (HostPort), свободен ли этот порт?
  • MatchNodeSelector: Соответствуют ли лейблы узла требованиям, указанным в nodeSelector пода?
  • Если после фильтрации не осталось ни одного подходящего узла, под переходит в состояние Pending и остается в нем до тех пор, пока ресурсы не освободятся или в кластер не добавят новый узел.

    Фаза 2: Оценка (Priorities)

    Все узлы, прошедшие фильтрацию, гарантированно могут запустить под. Теперь планировщику нужно выбрать лучший из них. Для этого каждому узлу начисляются баллы от 0 до 100 на основе набора функций оценки (Soft constraints).

    Примеры функций оценки:

  • LeastAllocated: Узлы с наименьшим процентом зарезервированных ресурсов получают больше баллов. Это обеспечивает равномерное размазывание нагрузки по кластеру.
  • ImageLocality: Если на узле уже кэширован Docker-образ, необходимый поду, этот узел получит бонусные баллы. Запуск произойдет быстрее, так как не нужно скачивать образ по сети.
  • NodeAffinityPriority: Начисление баллов за совпадение с желаемыми (preferred) правилами привязки.
  • Узел, набравший максимальную сумму баллов, становится победителем. Планировщик выполняет операцию Binding — отправляет API-серверу запрос на обновление поля nodeName в манифесте пода. С этого момента запуск пода становится зоной ответственности kubelet на выбранном узле.

    Управление гравитацией: Taints, Tolerations и Affinity

    Базовых алгоритмов планировщика хватает для простых кластеров, но в production-средах часто требуется ручное управление логикой размещения. Например, нужно изолировать узлы с дорогими GPU только для задач машинного обучения или разнести реплики базы данных по разным физическим стойкам для отказоустойчивости.

    Отталкивание подов: Taints и Tolerations

    Механизм Taints (пятна) позволяет узлам «отталкивать» поды. Если на узел повесить Taint, планировщик не посадит на него ни один под, кроме тех, у которых есть соответствующий Toleration (иммунитет).

    > Taints и Tolerations не притягивают поды к узлам. Они лишь дают поду право быть размещенным на узле с "пятном". Планировщик все равно может отправить этот под на любой другой обычный узел.

    Пример изоляции узла с GPU:

  • Администратор помечает узел: kubectl taint nodes gpu-node-1 hardware=gpu:NoSchedule
  • Обычные веб-серверы перестают планироваться на этот узел (работает предикат фильтрации).
  • В манифест пода с нейросетью добавляется иммунитет:
  • Притяжение подов: Node Affinity

    Если нужно заставить под гарантированно приземлиться на определенную группу узлов, используется Node Affinity (родство с узлом). В отличие от устаревшего nodeSelector, Affinity обладает выразительным синтаксисом с поддержкой логических операторов (In, NotIn, Exists) и разделением на жесткие и мягкие правила.

  • requiredDuringSchedulingIgnoredDuringExecution — жесткое требование (работает на фазе Фильтрации). Под не запустится, если нет подходящего узла.
  • preferredDuringSchedulingIgnoredDuringExecution — мягкое пожелание (работает на фазе Оценки). Планировщик начислит баллы подходящим узлам, но если их нет, разместит под где придется.
  • Топологическое распределение: Pod Affinity и Anti-Affinity

    Самый мощный инструмент планирования — правила взаимного расположения подов.

    Pod Affinity позволяет сказать: «Размести этот под кэша рядом с подом основного веб-приложения, чтобы минимизировать сетевую задержку». Pod Anti-Affinity решает обратную задачу. Если у вас есть Deployment ZooKeeper с тремя репликами, размещение их всех на одном физическом узле (или в одной зоне доступности) создает единую точку отказа.

    С помощью Anti-Affinity и ключа топологии (topologyKey) планировщику задается правило: не размещать поды с одинаковыми лейблами в пределах одного домена отказа.

    В этом примере предикат фильтрации проверит лейбл зоны доступности на узлах. Если в зоне us-east-1a уже есть под ZooKeeper, все остальные узлы этой зоны будут исключены из рассмотрения для следующих реплик.

    Связка из математически выверенных Requests/Limits и правил Affinity превращает кластер из хаотичного набора серверов в предсказуемую систему, где каждый процесс получает ровно столько ресурсов, сколько ему положено, и запускается именно там, где обеспечит максимальную отказоустойчивость.

    9. Безопасность кластера: модель RBAC, Service Accounts и разграничение прав доступа

    Безопасность кластера: модель RBAC, Service Accounts и разграничение прав доступа

    Представьте ситуацию: вы развернули безобидный контейнер с Nginx. Злоумышленник находит уязвимость в вашем веб-приложении, получает доступ к оболочке (shell) внутри контейнера и ради интереса выполняет команду kubectl delete namespaces --all. Внезапно весь ваш кластер исчезает. Как такое возможно? Исторически Kubernetes доверял внутреннему трафику, и даже сегодня небрежно настроенные права по умолчанию могут выдать обычному поду «ключи от королевства».

    В основе безопасности Kubernetes лежит параноидальная архитектура: API-сервер по умолчанию не доверяет никому. Любой запрос — будь то от администратора с ноутбука или от микросервиса из соседнего узла — должен доказать свою легитимность.

    Трехэтапный конвейер API-сервера

    Каждый HTTP-запрос к kube-apiserver проходит через строгий конвейер проверок, прежде чем состояние кластера будет изменено или прочитано.

  • Аутентификация (Authentication / AuthN): Отвечает на вопрос «Кто ты?». Система проверяет предоставленные токены или сертификаты и извлекает имя субъекта.
  • Авторизация (Authorization / AuthZ): Отвечает на вопрос «Имеешь ли ты право это делать?». Система сверяет запрошенное действие с матрицей прав доступа.
  • Admission Control: Отвечает на вопрос «Соответствует ли запрос глобальным политикам кластера?». Даже если у вас есть права на создание пода, плагины Admission Control (которые мы разбирали при изучении жизненного цикла пода) могут заблокировать запрос, если, например, образ контейнера загружается из недоверенного реестра.
  • В этой статье мы сосредоточимся на первых двух этапах: как Kubernetes идентифицирует субъектов и как распределяет между ними права.

    Идентификация: Люди против Машин

    В Kubernetes существует фундаментальное разделение на два типа субъектов, обращающихся к API: обычные пользователи и сервисные аккаунты.

    | Характеристика | User Accounts (Люди) | Service Accounts (Машины) | | :--- | :--- | :--- | | Где хранятся | Вне кластера (Identity Provider, LDAP, OIDC, сертификаты). В Kubernetes нет объекта User. | Внутри кластера (как объекты API ServiceAccount). | | Область видимости | Глобальная (пользователь alice уникален для всего кластера). | Ограничена неймспейсом (существуют в конкретном пространстве имен). | | Жизненный цикл | Управляется внешним каталогом (например, Active Directory). | Управляется Kubernetes (создаются и удаляются вместе с приложениями). |

    Поскольку API-сервер не имеет собственной базы данных пользователей, он делегирует проверку людей внешним системам. Но для внутренних процессов — подов, контроллеров и операторов — Kubernetes использует ServiceAccounts. Если при создании пода вы не указали конкретный сервисный аккаунт, Kubernetes автоматически назначит ему аккаунт с именем default в его неймспейсе.

    Математика авторизации: модель RBAC

    После успешной аутентификации в дело вступает подсистема авторизации. Стандартом де-факто в Kubernetes является RBAC (Role-Based Access Control — управление доступом на основе ролей).

    Главный принцип RBAC в Kubernetes — строгая аддитивность (запрет по умолчанию). В системе физически не существует правил типа «Запретить» (Deny). Если действие не разрешено явно, оно запрещено.

    Логику принятия решения API-сервером можно описать следующей формулой:

    Где:

  • — итоговое решение ( — запрос пропущен, — возвращается ошибка 403 Forbidden).
  • (логическое ИЛИ) — математическое выражение аддитивности. Система перебирает все привязанные к субъекту правила . Если хотя бы одно правило () дает совпадение, доступ предоставляется.
  • — идентифицированный пользователь или ServiceAccount.
  • — запрашиваемое действие (например, get, list, create, delete).
  • — целевой объект API (например, pods, secrets, deployments).
  • Четыре столпа RBAC

    Чтобы связать субъекта с правами, RBAC использует четыре типа объектов, разделенных по области видимости,.

  • Role: Набор разрешений (правил), действующий строго внутри одного пространства имен.
  • ClusterRole: Набор разрешений, действующий глобально на весь кластер. Необходим для доступа к ресурсам, не имеющим неймспейсов (например, Nodes или PersistentVolumes), или для создания универсальных политик.
  • RoleBinding: Привязывает Role к субъекту внутри конкретного неймспейса.
  • ClusterRoleBinding: Привязывает ClusterRole к субъекту на уровне всего кластера.
  • > Ловушка на техническом интервью: > Интервьюер может спросить: «Можно ли связать ClusterRole с субъектом, используя RoleBinding?» > > Да, это один из самых мощных паттернов в Kubernetes! Встроенная ClusterRole под названием admin содержит сотни правил. Вместо того чтобы дублировать её в каждый неймспейс как обычную Role, вы создаете RoleBinding в неймспейсе dev, ссылаясь на эту глобальную ClusterRole. В результате субъект получает права администратора, но исключительно в границах неймспейса dev.

    Как поды доказывают свою личность

    Мы выяснили, что поды используют ServiceAccounts, а права выдаются через RBAC. Но как технически процесс внутри контейнера аутентифицируется в API-сервере?

    Когда kubelet запускает под, он обращается к API-серверу за токеном для назначенного ServiceAccount. Этот токен (в формате JWT) монтируется внутрь файловой системы контейнера как проекционный том (Projected Volume).

    По умолчанию токен всегда лежит по одному и тому же пути: /var/run/secrets/kubernetes.io/serviceaccount/token

    Любой официальный клиент Kubernetes (например, библиотека на Python или Go) при запуске внутри кластера автоматически ищет этот файл и использует его для подписи HTTP-запросов к API-серверу.

    И здесь кроется главная уязвимость из нашего первого примера. По умолчанию Kubernetes монтирует этот токен в каждый создаваемый под,. Если вашему Nginx нужно просто отдавать статические HTML-страницы, ему абсолютно не нужно общаться с API-сервером кластера. Но токен всё равно лежит в файловой системе, ожидая, пока злоумышленник им воспользуется.

    Чтобы закрыть эту дыру, необходимо использовать директиву automountServiceAccountToken в манифесте пода или самого ServiceAccount:

    Установка этого флага в false предотвращает кражу учетных данных при компрометации контейнера, реализуя принцип наименьших привилегий,.

    Security Context: защита на уровне узла

    RBAC защищает API-сервер Kubernetes. Но что, если злоумышленник не пытается удалить ресурсы через API, а использует уязвимость в ядре Linux, чтобы вырваться из изоляции контейнера и получить root-доступ к самому рабочему узлу (Worker Node)?

    Для защиты от таких атак используется SecurityContext — набор настроек, которые kubelet передает среде выполнения (Container Runtime) перед стартом процесса. Это последний рубеж обороны.

    Если RBAC определяет, что контейнер может делать в кластере, то SecurityContext определяет, что контейнер может делать на хост-машине.

    Ключевые параметры безопасного контекста:

  • runAsNonRoot: true и runAsUser: 1000 — гарантируют, что процесс внутри контейнера не будет запущен от имени суперпользователя (root, UID 0).
  • allowPrivilegeEscalation: false — запрещает процессу получать больше прав, чем у его родителя (блокирует работу флагов SUID в Linux).
  • capabilities: drop: ["ALL"] — отзывает все привилегии ядра Linux (Capabilities), оставляя процесс абсолютно бесправным на уровне операционной системы.
  • Комбинация отключенного автоматического монтирования токенов (защита API), строгого RBAC (ограничение радиуса поражения) и жесткого Security Context (защита узла) превращает кластер из открытой площадки в неприступную крепость.