Архитектура Kubernetes и продвинутая автоматизация с Helm

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

1. Архитектура Kubernetes: глубокое погружение во взаимодействие компонентов Control Plane и Worker Nodes

Архитектура Kubernetes: глубокое погружение во взаимодействие компонентов Control Plane и Worker Nodes

Когда вы выполняете команду kubectl apply -f deployment.yaml, в кластере запускается сложнейшая цепочка событий, напоминающая работу слаженного оркестра. Но задумывались ли вы, почему Kubernetes называют «декларативным»? В отличие от императивных систем, где вы приказываете «создай виртуальную машину», в Kubernetes вы описываете желаемое состояние: «я хочу, чтобы всегда работало три копии этого приложения». Магия системы заключается в том, как компоненты Control Plane и Worker Nodes договариваются между собой, чтобы это желание стало реальностью, даже если прямо сейчас в дата-центре горит стойка с серверами.

Философия управления: Декларативность и Control Loop

Фундамент Kubernetes — это концепция петли управления (Control Loop). Это бесконечный цикл, который выполняет три действия:

  • Наблюдение (Observe): чтение текущего состояния объектов.
  • Анализ (Diff): сравнение текущего состояния с желаемым, записанным в манифесте.
  • Действие (Act): выполнение операций для устранения расхождений.
  • Эта логика пронизывает всю архитектуру. Если вы удалите Pod вручную, система не «обидится», она просто увидит, что в базе данных числится три реплики, а по факту работают две. Контроллер немедленно отдаст команду на создание новой. Чтобы понять, как это работает на уровне «железа» и системных вызовов, нужно разделить кластер на две логические части: мозг (Control Plane) и руки (Worker Nodes).

    Control Plane: Мозговой центр кластера

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

    kube-apiserver: Единственная точка истины

    API Server — это «лицо» Control Plane и единственный компонент, который напрямую общается с хранилищем данных. Все остальные участники — будь то системный контроллер или администратор с kubectl — обязаны идти через него.

    Работа API Server строится на REST-интерфейсе. Когда запрос поступает в систему, он проходит через строгий конвейер: * Аутентификация: проверка, кто пришел (сертификаты, токены). * Авторизация: проверка прав (RBAC — Role-Based Access Control). Имеет ли этот пользователь право создавать Pod в этом пространстве имен? * Admission Control: специальные плагины, которые могут модифицировать или отклонить запрос. Например, ResourceQuota проверит, не превысили ли вы лимит по памяти в данном проекте.

    Важнейшая особенность API Server — его «глупость» в хорошем смысле. Он не знает, как запускать контейнеры. Он умеет только валидировать данные, сохранять их и уведомлять подписчиков об изменениях через механизм Watch.

    etcd: Память системы

    Все состояние кластера хранится в etcd — высокодоступном распределенном хранилище типа «ключ-значение». Если API Server — это интерфейс, то etcd — это жесткий диск Kubernetes.

    Здесь используется алгоритм консенсуса Raft. Это означает, что для записи данных необходимо подтверждение от большинства узлов (кворума). Если у вас три узла etcd и два из них вышли из строя, кластер перейдет в режим «только чтение», так как кворум () не будет достигнут.

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

    kube-scheduler: Мастер размещения

    Когда в системе появляется новый Pod без указанного узла (nodeName пуст), за дело берется Scheduler. Его задача — найти наиболее подходящую ноду для запуска. Процесс выбора делится на два этапа:

  • Фильтрация (Predicates): отсекаются узлы, которые физически не могут принять Pod. Причины: нехватка RAM/CPU, несовпадение nodeSelector, наличие Taints (запретов на размещение), которые Pod не может игнорировать.
  • Ранжирование (Priorities): среди оставшихся узлов выбирается лучший. Алгоритм учитывает множество факторов: равномерность распределения нагрузки, наличие уже запущенных образов контейнера на узле (чтобы не тратить время на скачивание), близость к другим связанным Pod'ам.
  • Результат работы планировщика — запись в API Server о том, что Pod "X" назначен на ноду "Y". Сам планировщик ничего не запускает — он просто ставит «штамп» в документах.

    kube-controller-manager: Вечный страж

    Это процесс, в котором запущены десятки различных контроллеров. Каждый из них следит за своим типом ресурсов. * Node Controller: следит за состоянием узлов. Если нода перестает отвечать, он помечает её как недоступную. * Replication Controller: обеспечивает работу нужного количества реплик Pod. * Endpoints Controller: связывает сервисы (Services) и конкретные Pod'ы, заполняя списки IP-адресов.

    Контроллеры работают по принципу «Edge-driven» и «Level-driven». Они реагируют на события (изменение объекта), но также периодически сканируют состояние, чтобы убедиться, что ничего не было упущено.

    Worker Nodes: Исполнители на местах

    Worker Nodes — это серверы (виртуальные или физические), где фактически работают ваши контейнеры. Здесь также есть свои ключевые компоненты.

    kubelet: Капитан корабля на ноде

    kubelet — это агент, который запускается на каждом узле. Если API Server — это штаб, то kubelet — это командир на передовой. Он получает от API Server список Pod'ов, которые должны быть запущены на его узле (через тот самый механизм Watch).

    Основные обязанности kubelet: * Взаимодействие с Container Runtime для запуска и остановки контейнеров. * Мониторинг состояния контейнеров и отправка отчетов (Liveness/Readiness status) обратно в API Server. * Монтирование томов (Volumes) и управление секретами. * Очистка узла от неиспользуемых образов и завершенных контейнеров.

    Интересный нюанс: kubelet может работать и без API Server. Если положить манифест в специальную локальную директорию на диске узла, kubelet запустит так называемые Static Pods. Именно так часто запускаются сами компоненты Control Plane в инсталляциях типа kubeadm.

    kube-proxy: Сетевой регулировщик

    kube-proxy отвечает за сетевую связность. Он реализует концепцию Service — абстракцию, которая дает стабильный IP-адрес группе динамических Pod'ов. В большинстве современных систем kube-proxy работает в режиме IPVS или iptables. Он не является прокси-сервером в классическом понимании (через который проходит весь трафик), а скорее «программистом» сетевого стека Linux. Он прописывает правила форвардинга: «если пакет идет на IP сервиса 10.96.0.10, перенаправь его на IP одного из живых Pod'ов».

    Container Runtime: Двигатель

    Kubernetes не умеет запускать контейнеры сам. Ему нужен посредник. Раньше это был Docker, но сейчас стандартом является использование среды, поддерживающей CRI (Container Runtime Interface), например containerd или CRI-O. Среда исполнения отвечает за: * Скачивание образов из реестра. * Создание изолированного окружения (Namespaces и Cgroups в Linux). * Запуск процесса внутри этого окружения.

    Анатомия взаимодействия: Путь одного Pod

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

  • Запрос: Пользователь вводит kubectl apply -f my-app.yaml. Утилита kubectl парсит YAML, проверяет его на базовые ошибки и отправляет HTTP POST запрос в kube-apiserver.
  • Валидация и запись: API Server аутентифицирует пользователя, проверяет права и записывает объект Deployment в etcd. На этом этапе Pod'ы еще не созданы.
  • Работа Deployment Controller: Этот контроллер (внутри kube-controller-manager) видит новый объект Deployment. Его логика говорит: «Для этого Deployment мне нужно создать объект ReplicaSet». Он отправляет запрос в API Server на создание ReplicaSet.
  • Работа ReplicaSet Controller: Видит новый ReplicaSet и понимает: «Мне нужно создать 3 объекта Pod». Он отправляет 3 запроса в API Server. Теперь в etcd есть записи о трех Pod, но у них есть одна проблема — в поле nodeName пусто. Они находятся в статусе Pending.
  • Планирование: kube-scheduler замечает Pod'ы без назначенных узлов. Он анализирует доступные ресурсы на всех Worker Nodes, выбирает лучшие и обновляет объекты Pod в API Server, записывая туда имена выбранных узлов.
  • Исполнение: kubelet на конкретном Worker Node видит (через Watch), что ему «назначили» новый Pod.
  • * Он обращается к CRI, чтобы скачать образ. * Он настраивает сеть (через CNI — Container Network Interface). * Он запускает контейнеры.
  • Отчетность: kubelet сообщает API Server, что Pod успешно запущен. Статус меняется на Running.
  • Доступность: kube-proxy на всех узлах видит, что появились новые эндпоинты для сервиса, и обновляет правила маршрутизации трафика, чтобы пользователи могли достучаться до приложения.
  • Нюансы отказоустойчивости Control Plane

    В промышленной эксплуатации (Production) Control Plane никогда не состоит из одного узла. Типичная схема — 3 или 5 мастеров.

    | Компонент | Механизм отказоустойчивости | | :--- | :--- | | etcd | Кластеризация по протоколу Raft. Требуется нечетное количество узлов. | | kube-apiserver | Stateless-компонент. Можно ставить сколько угодно экземпляров за внешним Load Balancer. | | Scheduler & Controller Manager | Используют механизм Lease (аренда). В один момент времени активен только один экземпляр (Leader Election), остальные находятся в режиме ожидания. |

    Если «умрет» активный Controller Manager, один из запасных заметит исчезновение записи об аренде в API Server и возьмет управление на себя. Это гарантирует, что в кластере не возникнет ситуации «двоевластия», когда два планировщика пытаются отправить один и тот же Pod на разные узлы.

    Граничные случаи: Что будет, если...?

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

    Сценарий 1: API Server недоступен. Если «голова» отключена, текущие Pod'ы продолжат работать на узлах. kubelet не будет их убивать. Однако вы не сможете вносить изменения: деплоить новые версии, масштабировать или удалять ресурсы. Если в этот момент упадет какой-то контейнер, система не сможет его пересоздать на другом узле, так как некому отдать команду.

    Сценарий 2: etcd переполнен или тормозит. Поскольку все операции проходят через etcd, его задержки (latency) напрямую влияют на скорость работы кластера. Если диск под etcd медленный, API Server будет долго отвечать на запросы, что может привести к тайм-аутам в контроллерах. В худшем случае кластер «развалится», так как компоненты не смогут подтвердить свое состояние.

    Сценарий 3: Разрыв сети между Master и Worker. Если узел теряет связь с Control Plane, Node Controller ждет определенное время (по умолчанию 5 минут, настраивается через pod-eviction-timeout). Если связь не восстанавливается, Control Plane считает узел мертвым и начинает процесс «эвакуации» — создания копий всех Pod'ов этого узла на других доступных нодах. При этом старый kubelet, если он жив, может продолжать крутить контейнеры в изоляции, пока не восстановится связь и он не получит команду на удаление.

    Специфика взаимодействия через gRPC и HTTP/2

    Внутренние коммуникации Kubernetes оптимизированы. API Server использует протокол HTTP/2, что позволяет держать открытыми длительные соединения для механизма Watch. Это гораздо эффективнее, чем если бы тысячи агентов каждую секунду опрашивали сервер (Polling).

    Взаимодействие между kubelet и Container Runtime (CRI), а также сетевыми плагинами (CNI) и плагинами хранилищ (CSI) происходит через gRPC по Unix-сокетам. Это минимизирует накладные расходы на передачу данных внутри самой операционной системы узла.

    Иерархия объектов и владение (Owner References)

    Архитектура Kubernetes опирается на строгую иерархию. Deployment «владеет» ReplicaSet, а ReplicaSet «владеет» Pod'ами. В метаданных каждого дочернего объекта есть поле ownerReferences. Это критически важно для процесса очистки (Garbage Collection). Когда вы удаляете Deployment, каскадный контроллер находит все связанные ReplicaSet и Pod'ы и удаляет их. Без этого механизма кластер быстро заполнился бы «сиротами» — ресурсами, которые работают, но никем не управляются.

    Резюмируя устройство системы

    Kubernetes — это не просто оркестратор, это распределенная база данных состояния с набором интеллектуальных агентов, которые стремятся привести реальность к этому состоянию.

    * Control Plane — это хранилище (etcd), шлюз (API Server) и логика принятия решений (Scheduler, Controllers). * Worker Nodes — это вычислительные мощности, где kubelet следит за порядком, а kube-proxy плетет сеть.

    Понимание этого взаимодействия позволяет не только успешно дебажить проблемы (например, почему Pod завис в Pending — смотрим Scheduler; почему не обновляется конфиг — проверяем kubelet), но и проектировать по-настоящему отказоустойчивые системы, учитывая ограничения и возможности каждого компонента. В следующих главах мы перейдем от теории архитектуры к практике управления ресурсами, где эти знания станут фундаментом для настройки Requests и Limits.

    2. Жизненный цикл Pod и тонкая настройка ресурсов контейнеров: Requests и Limits

    Жизненный цикл Pod и тонкая настройка ресурсов контейнеров: Requests и Limits

    Представьте ситуацию: вы запускаете критически важное приложение в кластере Kubernetes, оно прекрасно проходит все тесты, но в момент пиковой нагрузки внезапно исчезает. В логах пусто, а статус пода лаконично сообщает: OOMKilled. При этом соседние, менее важные сервисы продолжают работать как ни в чем не бывало. Этот парадокс — прямое следствие непонимания того, как Kubernetes управляет «здоровьем» минимальной единицы развертывания и как он распределяет скудные ресурсы физических узлов между конкурирующими процессами. Правильная настройка лимитов — это не просто заполнение полей в YAML-файле, а стратегия выживания вашей системы в условиях дефицита памяти и процессорного времени.

    Анатомия состояний: от создания до терминации

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

    Когда манифест попадает в etcd через API Server, Pod переходит в фазу Pending. На этом этапе происходит самое важное: планировщик (kube-scheduler) ищет подходящий узел. Если вы указали слишком высокие требования к ресурсам, Pod может остаться в этом состоянии навсегда. Как только узел выбран, управление передается kubelet. Он начинает скачивать образы (ImagePullBackOff — частая ошибка именно здесь) и запускать контейнеры.

    Фаза Running означает, что Pod привязан к узлу и хотя бы один контейнер запущен или находится в процессе запуска. Однако «Running» не гарантирует, что ваше приложение готово принимать трафик. Это лишь индикатор того, что процесс в контейнере существует.

    Завершение работы может быть штатным (Succeeded) или аварийным (Failed). Важно понимать механизм terminationGracePeriodSeconds. Когда вы удаляете Pod, Kubernetes отправляет процессу сигнал SIGTERM. У приложения есть стандартно 30 секунд, чтобы закрыть соединения с базой данных, сохранить состояние и корректно завершиться. Если оно не укладывается в срок, посылается SIGKILL.

    > Механизм Graceful Termination > > Игнорирование сигналов завершения в коде приложения часто приводит к потере данных или «битым» транзакциям. Всегда настраивайте обработку SIGTERM в вашем runtime (Node.js, Python, Go), чтобы гарантировать целостность системы.

    Экономика ресурсов: Requests vs Limits

    Kubernetes использует двухступенчатую модель управления ресурсами: запросы (Requests) и ограничения (Limits). Это разделение позволяет достичь баланса между гарантированной производительностью и эффективным использованием «простаивающего» железа.

    Requests: Гарантия и планирование

    Requests — это минимальное количество ресурсов, которое Kubernetes гарантирует предоставить контейнеру. Планировщик использует это значение как «входной билет» на узел. Если на узле осталось 512 МБ свободной оперативной памяти, а ваш Pod запрашивает 1 ГБ, он никогда не будет там запущен, даже если фактически узел сейчас почти пуст.

    Requests определяют Scheduling (планирование). Это обещание системы: «Я найду тебе место, где эти ресурсы точно будут».

    Limits: Жесткий потолок

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

    Limits определяют Enforcement (принуждение). Это механизм защиты кластера от «прожорливых» приложений.

    Процессорное время: Сжимаемый ресурс

    Управление CPU в Kubernetes основано на механизмах ядра Linux, в частности на CFS (Completely Fair Scheduler) и контрольных группах (cgroups). Процессор считается «сжимаемым» (compressible) ресурсом. Это означает, что если приложение пытается использовать больше CPU, чем разрешено, система не убивает его, а просто ограничивает (throttles) его вычислительную мощность.

    Единица измерения CPU в Kubernetes — это «миллиядра» ().

    Если вы указываете cpu: 500m, это эквивалентно 50% времени одного логического ядра процессора.

    Механизм Throttling

    Когда контейнер достигает своего cpu limit, CFS начинает ограничивать его доступ к циклам процессора. Для пользователя это выглядит как резкое увеличение задержек (latency) и падение производительности. Приложение не падает, но «тормозит».

    Опасность слишком низких лимитов CPU заключается в том, что в моменты запуска (startup) или сборки мусора (GC) приложению требуется кратковременный всплеск мощности. Если лимит жестко ограничивает этот всплеск, время запуска может увеличиться в десятки раз, что приведет к срабатыванию Liveness Probe и бесконечному циклу перезагрузок.

    Оперативная память: Несжимаемый ресурс

    В отличие от CPU, память — ресурс «несжимаемый» (incompressible). Если приложению нужно 2 ГБ памяти, а физически доступно меньше, вы не можете просто «замедлить» чтение из RAM. В этой ситуации в игру вступает OOM Killer (Out Of Memory Killer) — механизм ядра, который выбирает жертву для немедленного уничтожения ради спасения всей системы.

    Лимиты памяти и OOMKilled

    Если контейнер превышает свой memory limit, Kubernetes (через cgroups) немедленно убивает процесс в этом контейнере. Вы увидите статус OOMKilled. Если же контейнер находится в рамках своего лимита, но на самом узле закончилась свободная память (например, из-за системных процессов или фрагментации), Kubernetes начинает выселять (Eviction) поды, исходя из их приоритета и качества обслуживания (QoS).

    Пример расчета лимитов памяти: Допустим, у нас есть Java-приложение. Мы знаем, что Heap занимает 1 ГБ, а метаданные и стек — еще 500 МБ.

  • Requests: 1.5 ГБ (минимально необходимое для работы).
  • Limits: 2 ГБ (запас на случай пиковых нагрузок или утечек памяти).
  • Если мы установим Requests и Limits равными, мы получим самый высокий приоритет обслуживания, но лишимся гибкости.

    Классы качества обслуживания (QoS)

    Kubernetes автоматически присваивает каждому Поду один из трех классов QoS на основе того, как настроены его ресурсы. Это определяет порядок, в котором поды будут уничтожаться при нехватке ресурсов на узле.

    | Класс QoS | Условие присвоения | Поведение при дефиците | | :--- | :--- | :--- | | Guaranteed | Requests == Limits для всех ресурсов во всех контейнерах. | Самый стабильный. Удаляется последним. | | Burstable | Requests < Limits или установлен только один из параметров. | Может использовать свободные ресурсы узла, но подвержен риску выселения. | | BestEffort | Ресурсы не указаны вовсе. | «Низший сорт». Удаляется первым при малейшем давлении на ресурсы. |

    Использование BestEffort в продакшене — это огромный риск. Такие поды не имеют никаких гарантий и живут «на птичьих правах». Для критических баз данных или API-шлюзов рекомендуется использовать класс Guaranteed.

    Тонкая настройка: Стратегии выбора значений

    Как определить идеальные значения для конкретного приложения? Существует три основных подхода.

    1. Метод исторического анализа

    Самый надежный способ — запустить приложение в тестовой среде под нагрузкой, близкой к реальной, и собрать метрики через Prometheus.
  • Посмотрите на 95-й перцентиль потребления CPU и памяти.
  • Установите Requests на уровне среднего потребления.
  • Установите Limits на уровне или .
  • 2. Учет накладных расходов Runtime

    Разные языки программирования ведут себя по-разному:
  • Go: Очень эффективно работает с памятью, но требует внимания к количеству горутин.
  • Java (JVM): Требует четкой синхронизации лимитов контейнера и флагов JVM (например, -XX:MaxRAMPercentage). Если JVM не знает о лимитах контейнера, она может попытаться выделить больше памяти, чем разрешено cgroups, что приведет к мгновенному OOMKilled.
  • Node.js: Однопоточная модель означает, что указывать лимит CPU больше (1 ядро) для одного процесса часто бессмысленно, если вы не используете Worker Threads.
  • 3. Вертикальное автоскейлинг (VPA)

    Kubernetes предоставляет инструмент Vertical Pod Autoscaler. Он наблюдает за реальным потреблением и может автоматически корректировать Requests и Limits. Это особенно полезно для приложений с переменной нагрузкой, где сложно угадать точные цифры заранее.

    Граничные случаи и опасности

    Проблема "Overcommitment"

    Kubernetes позволяет сумме Limits всех подов на узле превышать физические ресурсы узла. Это называется оверкоммитментом. Это выгодно: вы можете запустить больше приложений на том же железе. Однако, если все приложения одновременно решат использовать свои лимиты на максимум, узел «упадет». Правило большого пальца: Сумма Requests никогда не должна превышать ресурсы узла, а сумма Limits не должна превышать их более чем в 1.5–2 раза для CPU (для памяти оверкоммитмент гораздо опаснее).

    Swap в Kubernetes

    Традиционно Kubernetes требовал отключения Swap на узлах. Это связано с тем, что планировщику крайне сложно предсказать производительность, когда часть памяти сброшена на диск. В последних версиях появилась экспериментальная поддержка Swap, но для большинства продакшн-систем стандартным советом остается: Swap должен быть выключен.

    Использование Init-контейнеров

    Init-контейнеры также потребляют ресурсы. При расчете суммарного запроса Пода планировщик берет максимальное значение:

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

    Практические рекомендации по написанию манифестов

    При составлении спецификации контейнера следуйте правилу «минимально достаточного запроса».

    В данном примере мы гарантируем приложению четверть ядра и полгигабайта памяти. При этом мы разрешаем ему «разгоняться» до целого ядра в моменты пиков. Если же потребление памяти превысит 1 ГБ, контейнер будет перезапущен. Такая конфигурация относится к классу Burstable, что является «золотым стандартом» для большинства микросервисов.

    Особое внимание уделите единицам измерения памяти. Используйте двоичные суффиксы (Mi, Gi), так как они соответствуют тому, как память видит операционная система (), в отличие от десятичных (M, G), где . Ошибка в этих буквах может привести к разнице в 7% объема, что критично при больших лимитах.

    Управление ресурсами — это непрерывный процесс. Невозможно один раз настроить лимиты и забыть о них. С изменением кода, обновлением библиотек или ростом базы данных профиль потребления будет меняться. Регулярный аудит ресурсов с помощью инструментов вроде kube-score или goldilocks поможет избежать деградации производительности и лишних затрат на облачную инфраструктуру.

    3. Контроллеры управления нагрузкой: специфика Deployment, StatefulSet и DaemonSet

    Контроллеры управления нагрузкой: специфика Deployment, StatefulSet и DaemonSet

    Почему в Kubernetes мы практически никогда не создаем объекты Pod напрямую? Ответ кроется в эфемерности контейнеров: если узел выйдет из строя, одиночный Pod погибнет навсегда. Чтобы обеспечить надежность, масштабируемость и автоматическое восстановление, Kubernetes использует абстракции более высокого уровня — контроллеры. Они реализуют концепцию «желаемого состояния» (Desired State), постоянно сравнивая реальность с манифестом и предпринимая действия для их синхронизации. Однако выбор конкретного типа контроллера определяет не только способ запуска приложения, но и его сетевую идентичность, работу с дисками и стратегию обновления.

    Декларативное управление и иерархия объектов

    Прежде чем разбирать различия между контроллерами, необходимо понять механизм их работы через систему меток (labels) и селекторов (selectors). Контроллер не «владеет» подами в строгом смысле прямой связи; он находит их в кластере по заданным критериям.

    Эта связь реализуется через spec.selector. Если вы вручную создадите Pod, метки которого совпадают с селектором существующего Deployment, контроллер обнаружит «лишнюю» копию и немедленно удалит её, чтобы привести количество реплик к заданному числу. Эта «отстраненность» контроллера от конкретных инстансов позволяет Kubernetes быть невероятно гибким: мы можем менять настройки контроллера, не прерывая работу подов, или перехватывать управление подами для отладки, просто временно изменив их метки.

    Deployment: стандарт для приложений без сохранения состояния

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

    Архитектурная цепочка

    Deployment не управляет подами напрямую. Он управляет объектами ReplicaSet.

  • Deployment определяет стратегию обновления и желаемое состояние.
  • ReplicaSet гарантирует, что в кластере запущено ровно копий подов.
  • Pod — конечная единица исполнения.
  • Эта трехуровневая структура необходима для реализации механизмов отката (rollback). Когда вы обновляете версию образа в Deployment, он создает новый ReplicaSet и начинает постепенно переносить нагрузку в него, сохраняя старый ReplicaSet (с нулевым количеством реплик) на случай, если что-то пойдет не так.

    Стратегии обновления и их математика

    Наиболее критичный параметр Deployment — spec.strategy.rollingUpdate. Он позволяет обновлять приложение без простоя (Zero Downtime). Здесь ключевую роль играют два параметра: maxSurge и maxUnavailable.

  • maxSurge: определяет, сколько дополнительных подов сверх желаемого количества может быть создано в процессе обновления. Может быть числом или процентом.
  • maxUnavailable: определяет максимальное количество подов, которые могут быть недоступны в процессе обновления.
  • Рассмотрим пример. У нас есть Deployment с replicas: 10, maxSurge: 25% и maxUnavailable: 25%.

  • Расчет maxSurge: . Округляется вверх до . В пике обновления в кластере может быть до подов.
  • Расчет maxUnavailable: . Округляется вниз до . В любой момент времени как минимум подов должны быть в статусе Running.
  • Если ваше приложение потребляет много ресурсов и кластер плотно забит, установка высокого maxSurge может привести к тому, что новые поды просто не поместятся на узлы (Pending), и обновление застрянет. Напротив, если ваше приложение долго инициализируется, слишком большой maxUnavailable может привести к деградации производительности, так как оставшиеся поды не справятся с трафиком.

    Тонкости Recreate

    Существует и альтернативная стратегия — type: Recreate. Она убивает все старые поды перед запуском новых. Это кажется неэффективным, но критически важно в двух случаях:

  • Приложение не поддерживает одновременную работу двух разных версий с одной базой данных (проблемы миграции схемы).
  • Приложение использует ReadWriteOnce (RWO) тома, которые не могут быть примонтированы к новому поду, пока старый не освободит их.
  • StatefulSet: когда порядок и имя имеют значение

    Если Deployment — это «стадо» (cattle), где гибель одной особи незаметна, то StatefulSet — это «домашние питомцы» (pets). Этот контроллер используется для баз данных (PostgreSQL, MongoDB), брокеров сообщений (Kafka, RabbitMQ) и распределенных систем (Elasticsearch).

    Уникальная идентичность

    В отличие от Deployment, где поды получают случайные суффиксы (например, web-7c54d59c-abc12), StatefulSet присваивает подам порядковые индексы, начиная с нуля: db-0, db-1, db-2. Эта идентичность сохраняется всегда. Если db-1 упадет и переедет на другой узел, он вернется с тем же именем. Это критично для кластерных приложений, где узлы должны знать адреса друг друга.

    Для обеспечения сетевой связности StatefulSet требует наличия Headless Service (сервис с clusterIP: None). Это позволяет DNS-серверу Kubernetes возвращать IP-адреса конкретных подов по их именам, а не один балансировщик.

    Работа с данными: VolumeClaimTemplates

    Главная проблема Deployment при работе с дисками заключается в том, что все реплики пытаются использовать один и тот же PersistentVolume. В StatefulSet реализован механизм volumeClaimTemplates. Когда создается под db-0, контроллер автоматически создает для него персональный PersistentVolumeClaim (PVC) с именем data-db-0. Для db-1 создается data-db-1. При перезапуске пода он всегда находит «свой» диск.

    > Важный нюанс: По умолчанию при удалении StatefulSet или уменьшении количества реплик, созданные PVC не удаляются. Это сделано для безопасности данных. Администратор должен удалять их вручную, если они больше не нужны.

    Порядок развертывания

    StatefulSet по умолчанию соблюдает строгую последовательность:

  • Под db-1 не начнет создаваться, пока db-0 не перейдет в состояние Running и Ready.
  • При удалении процесс идет в обратном порядке (LIFO — Last In, First Out).
  • Это гарантирует, что в распределенных системах сначала поднимется Master-узел (обычно индекс 0), а затем Slave-узлы. Если такая строгость не нужна и вы хотите ускорить запуск (например, для Cassandra), можно использовать podManagementPolicy: Parallel.

    DaemonSet: инфраструктурный слой кластера

    DaemonSet гарантирует, что на каждом узле (или на выбранном подмножестве узлов) запущена ровно одна копия пода. Если в кластер добавляется новый узел, DaemonSet автоматически разворачивает на нем под. Если узел удаляется — под не переносится на другие узлы, он просто исчезает.

    Типичные кейсы использования

  • Сбор логов: fluentd или logstash, которые должны читать файлы логов с локальной файловой системы каждого узла.
  • Мониторинг: node-exporter, собирающий метрики железа (CPU, RAM, Disk) конкретного сервера.
  • Сетевые плагины (CNI): calico или flannel, обеспечивающие связность между подами.
  • Хранилища: glusterfs или ceph агенты.
  • Механизм размещения и Taints/Tolerations

    В старых версиях Kubernetes DaemonSet управлялся собственным контроллером, но сейчас он использует стандартный kube-scheduler. Это важно, потому что DaemonSet должен учитывать «загрязнения» (Taints) узлов. Например, Master-узлы обычно имеют таинт node-role.kubernetes.io/master:NoSchedule. Чтобы ваш системный DaemonSet (например, мониторинг) запустился и на мастерах, в его манифесте необходимо прописать соответствующие Tolerations.

    Без этой настройки DaemonSet покроет только Worker-узлы.

    Сравнительный анализ стратегий и жизненных ситуаций

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

    | Характеристика | Deployment | StatefulSet | DaemonSet | | :--- | :--- | :--- | :--- | | Тип приложения | Stateless (Web, API) | Stateful (DB, MQ) | Infrastructure (Logs, Monitoring) | | Имена подов | Случайные (hash) | Детерминированные (index) | Случайные или по имени узла | | Хранилище | Общее (редко) или эфемерное | Индивидуальное для каждой реплики | Локальное (HostPath) | | Порядок запуска | Одновременно (Parallel) | Последовательно (Ordered) | Одновременно при появлении узла | | Сетевой доступ | Через Service (ClusterIP/LB) | Через Headless Service | HostNetwork (часто) или Service |

    Граничный случай: Очереди задач (Jobs)

    Иногда возникает соблазн запустить разовую задачу (миграция БД, генерация отчета) через Deployment с одной репликой. Это плохая практика. Если задача завершится успешно (Exit Code 0), Deployment решит, что под «упал» (так как он больше не запущен), и создаст новый. Для таких целей существуют контроллеры Job и CronJob, которые мы разберем в контексте автоматизации, но важно помнить: Deployment, StatefulSet и DaemonSet предназначены для постоянно запущенных процессов (Long-running services).

    Продвинутые аспекты управления: Rollback и History

    Все контроллеры поддерживают версионность. В Deployment параметр revisionHistoryLimit определяет, сколько старых ReplicaSet хранить. Это ваш страховочный трос.

    Команда kubectl rollout undo deployment/my-app позволяет мгновенно вернуться к предыдущей версии, если после деплоя мониторинг зафиксировал всплеск 500-х ошибок. Kubernetes просто поменяет количество реплик в старом (сохраненном) ReplicaSet с 0 до , а в текущем — до 0.

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

    Жизненный цикл обновления в деталях

    Рассмотрим детально, что происходит при обновлении Deployment. Это понимание критично для настройки алертов и CI/CD пайплайнов.

    Когда вы меняете поле .spec.template (например, обновляете тег образа с v1 на v2):

  • Deployment Controller замечает изменение и создает новый ReplicaSet (RS-v2).
  • RS-v2 начинает создавать первый под.
  • Одновременно Deployment Controller дает команду старому ReplicaSet (RS-v1) начать уменьшение количества подов, соблюдая лимит maxUnavailable.
  • Как только новый под из RS-v2 проходит проверку Readiness Probe (мы подробно разберем пробы в главе 7), он считается готовым принимать трафик.
  • Процесс повторяется, пока RS-v2 не достигнет целевого количества реплик, а RS-v1 не обнулится.
  • Если на шаге 4 новый под не проходит проверку (например, падает с CrashLoopBackOff), процесс обновления останавливается. Это предотвращает ситуацию, когда вы «выкашиваете» все рабочие старые поды, заменяя их нерабочими новыми. Именно эта связка контроллера и проб обеспечивает высокую доступность.

    Резюмируя логику выбора

    Выбор контроллера — это фундамент архитектуры вашего приложения в кластере.

    Если ваше приложение может масштабироваться горизонтально, не хранит данные локально и не требует специфического порядка запуска — ваш выбор Deployment. Это даст вам максимальную гибкость в обновлении и простоту управления.

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

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

    Понимание этих различий позволяет не только правильно описать YAML-манифест, но и предсказать поведение системы в критических ситуациях: при нехватке ресурсов, обновлении версии Kubernetes или аварии на физическом сервере. В следующих главах мы увидим, как эти контроллеры взаимодействуют с сетевым слоем и как Helm помогает управлять их конфигурациями в разных окружениях.

    4. Сетевая модель Kubernetes: реализация доступа через Services и Ingress-контроллеры

    Сетевая модель Kubernetes: реализация доступа через Services и Ingress-контроллеры

    Почему в Kubernetes нельзя просто обратиться к приложению по IP-адресу его пода? В динамической среде, где поды рождаются и умирают десятки раз в час, полагаться на эфемерный IP — верный путь к деградации системы. Представьте, что вы строите город, где у каждого дома каждое утро меняется почтовый адрес. Чтобы почта доходила до адресата, вам нужна не адресная книга, а интеллектуальная служба маршрутизации, которая знает, кто сейчас находится за конкретной дверью. В Kubernetes эту роль выполняет сетевая модель, основанная на абстракциях Service и Ingress.

    Фундаментальные принципы сетевой модели: IP-per-Pod

    Прежде чем разбирать механизмы доступа, необходимо зафиксировать базовую аксиому Kubernetes: каждый под имеет свой уникальный IP-адрес в пределах кластера. Эта концепция называется «IP-per-Pod». В отличие от традиционной модели Docker с использованием NAT (Network Address Translation) на уровне хоста, Kubernetes требует, чтобы:

  • Все поды могли связываться со всеми остальными подами без использования NAT.
  • Все узлы (Nodes) могли связываться со всеми подами (и наоборот) без NAT.
  • IP-адрес, который под видит сам у себя, совпадает с IP-адресом, который видят другие компоненты системы.
  • Эта модель значительно упрощает перенос приложений из физической среды в контейнеры, так как порты не нужно пробрасывать (mapping) на уровне хоста. Однако здесь кроется главная проблема: поды не вечны. При обновлении Deployment старые поды удаляются, а новые получают совершенно другие IP-адреса из пула CNI (Container Network Interface). Чтобы обеспечить стабильную точку входа, Kubernetes вводит объект Service.

    Service: абстракция стабильного сетевого адреса

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

    Механизм работы Endpoints и EndpointSlices

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

    В крупных кластерах с тысячами подов объект Endpoints становится слишком тяжелым, так как любое изменение одного пода требует пересылки всего списка всем узлам. Для решения этой проблемы были введены EndpointSlices — фрагментированные списки адресов, которые позволяют масштабировать сетевую инфраструктуру более эффективно.

    Типы сервисов и их назначение

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

  • ClusterIP (по умолчанию)
  • Экспонирует сервис на внутреннем IP-адресе кластера. Этот адрес доступен только внутри Kubernetes. Это идеальный выбор для взаимодействия между микросервисами (например, backend обращается к базе данных).

  • NodePort
  • Открывает определенный порт на каждом узле кластера. Трафик, приходящий на этот порт, перенаправляется на ClusterIP сервиса. Диапазон портов по умолчанию: . Хотя это простой способ выставить сервис наружу, он небезопасен и неудобен для промышленной эксплуатации из-за ограничений по портам.

  • LoadBalancer
  • Использует балансировщик нагрузки облачного провайдера (AWS ELB, GCP LB, Azure LB). Kubernetes автоматически создает облачный ресурс и настраивает проброс трафика к NodePort, а затем к подам.

  • ExternalName
  • Особый тип сервиса, который не имеет селекторов и не использует проксирование. Он просто возвращает CNAME-запись (например, my-db.external.com). Это полезно, когда приложению внутри кластера нужно обращаться к внешней базе данных по стабильному внутреннему имени.

    Магия kube-proxy и режимы реализации

    Как трафик, отправленный на виртуальный IP сервиса (ClusterIP), попадает в реальный под? За это отвечает kube-proxy — компонент, работающий на каждом узле. Существует три основных режима его работы:

    Режим Userspace

    Старый и неэффективный метод. kube-proxy открывает порт в пространстве пользователя, перехватывает трафик и сам перенаправляет его. Это вызывает огромные накладные расходы на переключение контекста между ядром и пользователем.

    Режим IPTables

    Самый распространенный режим. kube-proxy настраивает правила iptables в ядре Linux. Когда пакет идет на IP сервиса, ядро применяет правила DNAT (Destination Network Address Translation) и выбирает один из целевых подов (случайным образом). * Плюс: работает полностью в пространстве ядра. * Минус: при огромном количестве сервисов (более 5000) поиск по правилам iptables становится линейным и медленным ().

    Режим IPVS (IP Virtual Server)

    Режим для высоконагруженных систем. Использует транспортную балансировку нагрузки на уровне ядра Linux. * Производительность: использует хэш-таблицы для поиска, что обеспечивает сложность . * Гибкость: поддерживает различные алгоритмы балансировки (round-robin, least connection, shortest expected delay).

    Где — количество правил (пропорционально количеству сервисов). При разница в задержках (latency) становится критической.

    Service Discovery: как поды находят друг друга

    В Kubernetes реализовано два механизма обнаружения сервисов: переменные окружения и DNS.

    Переменные окружения

    При запуске пода kubelet добавляет в него переменные окружения для каждого активного сервиса. Например, для сервиса redis-master будут созданы переменные REDIS_MASTER_SERVICE_HOST и REDIS_MASTER_SERVICE_PORT. Проблема: под должен быть запущен после создания сервиса, иначе он не получит эти переменные.

    CoreDNS

    Это стандарт де-факто. В кластере работает системный под с DNS-сервером. Каждый сервис получает доменное имя вида: <service-name>.<namespace>.svc.cluster.local

    Если под находится в том же пространстве имен (namespace), он может обращаться к сервису просто по его имени. Это делает конфигурацию приложений независимой от конкретных IP-адресов.

    Ingress: управление доступом на уровне L7

    Если Service работает на уровне L4 (TCP/UDP), то Ingress — это уровень L7 (HTTP/HTTPS). Service типа LoadBalancer на каждый чих создает новый дорогой облачный балансировщик. Ingress позволяет использовать один балансировщик для сотен сервисов, разделяя трафик на основе путей (Paths) или доменных имен (Hosts).

    Анатомия Ingress-контроллера

    Объект Ingress в Kubernetes — это всего лишь набор правил (конфигурация). Чтобы эти правила заработали, в кластере должен быть запущен Ingress Controller. Самый популярный — ingress-nginx, но существуют также решения на базе HAProxy, Traefik, Kong и облачные реализации (ALB Ingress Controller).

    Контроллер работает следующим образом:

  • Постоянно опрашивает API-сервер на предмет новых объектов Ingress.
  • Генерирует конфигурационный файл (например, nginx.conf) на основе этих правил.
  • Перезагружает (или обновляет через API) прокси-сервер.
  • Направляет трафик напрямую в IP-адреса подов, минуя Service ClusterIP (для уменьшения количества сетевых прыжков), но используя Service для обнаружения этих адресов.
  • Примеры стратегий маршрутизации

    Маршрутизация по хостам (Host-based): Вы можете направить трафик с api.example.com на один сервис, а с app.example.com — на другой, используя один и тот же IP-адрес Ingress-контроллера.

    Маршрутизация по путям (Path-based): Трафик на example.com/orders уходит в сервис заказов, а на example.com/products — в сервис каталога.

    TLS-терминация

    Ingress берет на себя задачу расшифровки SSL-трафика. Сертификаты хранятся в объектах Secret. Это освобождает разработчиков от необходимости настраивать SSL внутри каждого контейнера приложения — приложение получает уже «чистый» HTTP-трафик.

    Сравнение Service и Ingress: когда что использовать?

    | Характеристика | Service (L4) | Ingress (L7) | | :--- | :--- | :--- | | Уровень OSI | Транспортный (TCP/UDP) | Прикладной (HTTP/HTTPS/gRPC) | | Балансировка | Простая (Round-Robin/Random) | Сложная (Cookie, Sticky Sessions, Header-based) | | Облачные затраты | Высокие (LB на каждый Service) | Низкие (один LB на много правил) | | SSL/TLS | Не поддерживает нативно | Поддерживает терминацию сертификатов | | Основные функции | Стабильный IP, внутренняя связь | Маршрутизация по URL, виртуальный хостинг |

    Глубокий разбор: Headless Services

    Иногда нам не нужна балансировка нагрузки. Например, в случае с базами данных (StatefulSets), где приложению нужно подключиться к конкретной реплике (Master) или собрать список всех узлов кластера (Discovery в Cassandra или ElasticSearch).

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

  • Kubernetes не выделяет виртуальный IP.
  • kube-proxy не создает правила трансляции.
  • DNS-запрос к такому сервису возвращает не один IP сервиса, а сразу список A-записей всех подов, входящих в селектор.
  • Это позволяет приложению самостоятельно решать, к какому поду подключаться, реализуя логику балансировки на стороне клиента.

    Проблемы и нюансы сетевого взаимодействия

    Эффект "Hairpin" (Петля)

    Иногда поду нужно обратиться к самому себе через внешний IP сервиса. Без специальной настройки сети пакет может «заблудиться». Современные CNI решают эту проблему, позволяя пакету выйти из пода и тут же вернуться обратно через правила трансляции.

    Сохранение IP-адреса клиента (Source IP)

    При использовании NodePort или LoadBalancer реальный IP клиента часто заменяется на IP узла из-за SNAT. Если приложению важно знать IP пользователя (например, для логирования или безопасности), в сервисе нужно установить параметр: externalTrafficPolicy: Local

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

    Network Policies: безопасность сети

    По умолчанию в Kubernetes разрешено всё: любой под может достучаться до любого другого пода. В крупных организациях это недопустимо. NetworkPolicy — это аналог Firewall внутри кластера. Они позволяют ограничить трафик на уровне L3/L4, разрешая, например, доступ к базе данных только из сегмента backend и запрещая из frontend. Важно: Network Policies работают только если ваш CNI плагин (Calico, Cilium, Weave) их поддерживает. Стандартный flannel политики игнорирует.

    Взаимодействие с внешним миром: Egress

    До сих пор мы говорили о входящем трафике (Ingress). Но что, если поду нужно выйти в интернет или обратиться к API внешней системы? Обычно это происходит через NAT шлюз (NAT Gateway) узла. Однако в высокобезопасных средах выходной трафик также контролируется. Можно использовать Egress правила в NetworkPolicy, чтобы разрешить поду обращаться только к конкретным внешним CIDR-блокам или DNS-именам (последнее требует продвинутых CNI, таких как Cilium).

    Сетевая модель Kubernetes — это слоеный пирог, где каждый уровень решает свою задачу: CNI обеспечивает связность, Service — стабильность, а Ingress — гибкость управления. Понимание того, как пакет проходит путь от браузера пользователя через Ingress-контроллер, правила iptables и до конкретного сетевого интерфейса пода eth0, является критически важным навыком для построения отказоустойчивых систем.

    5. Постоянное хранение данных: механизмы PersistentVolumes и динамическое выделение памяти

    Постоянное хранение данных: механизмы PersistentVolumes и динамическое выделение памяти

    Если вы удалите Pod с базой данных, запущенный через обычный Deployment с emptyDir, ваши данные исчезнут навсегда вместе с контейнером. В эфемерном мире Kubernetes, где узлы могут выходить из строя, а поды — переезжать с сервера на сервер, обеспечение сохранности данных становится нетривиальной задачей. Как подружить концепцию «неизменяемой инфраструктуры» с необходимостью хранить терабайты транзакций, логов или пользовательского контента? Ответ кроется в абстракции хранилищ, которая отделяет жизненный цикл данных от жизненного цикла вычислительных мощностей.

    Проблема эфемерности и эволюция томов

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

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

  • emptyDir: создается при назначении Pod на узел и живет ровно столько, сколько живет Pod на этом узле. При удалении пода данные стираются.
  • hostPath: монтирует файл или директорию с файловой системы узла в Pod. Это опасно с точки зрения безопасности и ломает переносимость — если Pod переедет на другой узел, он не найдет там своих данных.
  • Для полноценной работы с Statefull-приложениями (базы данных, очереди сообщений, файловые хранилища) Kubernetes ввел подсистему Persistent Volumes (PV). Её задача — сделать хранилище «внешним ресурсом», подобно тому как узел является внешним ресурсом процессора и памяти.

    Анатомия PersistentVolume и PersistentVolumeClaim

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

    PersistentVolume (PV)

    Это «физический» (или логический в облаке) кусок хранилища, который был предварительно настроен администратором или динамически создан облачным провайдером. PV не принадлежит конкретному пространству имен (Namespace) — это глобальный ресурс кластера.

    Основные характеристики PV:

  • Capacity: объем памяти (например, 5Gi).
  • Access Modes: режимы доступа (кто и как может читать/писать).
  • Reclaim Policy: что делать с данными после удаления запроса на хранение.
  • StorageClassName: привязка к конкретному профилю хранилища.
  • PersistentVolumeClaim (PVC)

    Это запрос пользователя на хранение. Если PV — это «диск», то PVC — это «заявка на диск». Разработчик описывает в PVC, сколько места ему нужно и с каким режимом доступа, не вдаваясь в подробности того, как именно это реализовано (на базе NFS, iSCSI или облачного диска AWS EBS).

    Процесс связывания (Binding) происходит автоматически: Control Plane ищет подходящий PV, который удовлетворяет требованиям PVC, и «бронирует» его. Как только связь установлена, PV становится эксклюзивно доступным для этого PVC.

    > Закон соответствия: > Один PVC всегда связывается ровно с одним PV. Отношение гарантирует, что данные не будут случайно перезаписаны другим приложением, если это не предусмотрено режимом доступа.

    Режимы доступа и политики утилизации

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

  • ReadWriteOnce (RWO): том может быть смонтирован на чтение и запись только одним узлом. Это типично для блочных хранилищ (AWS EBS, GCE Persistent Disk, Azure Disk). Если ваш Pod переедет на другой узел, том будет отмонтирован от старого и примонтирован к новому.
  • ReadOnlyMany (ROX): том может быть смонтирован многими узлами только для чтения. Полезно для статического контента или конфигурационных баз.
  • ReadWriteMany (RWX): том может быть смонтирован многими узлами на чтение и запись. Это требует использования сетевых файловых систем, таких как NFS, CephFS или Azure Files.
  • ReadWriteOncePod (RWOP): (появилось в последних версиях) ограничивает доступ только одним подом во всем кластере, а не узлом.
  • Когда работа с данными закончена и пользователь удаляет PVC, в силу вступает Reclaim Policy:

  • Retain: PV сохраняется. Данные на месте, но PV переходит в статус Released. Другой PVC не сможет его занять автоматически — администратор должен вручную очистить данные и пересоздать объект.
  • Delete: PV удаляется автоматически вместе с физическим ресурсом в облаке. Это стандарт для динамического выделения памяти.
  • Recycle: (устарело) простая очистка (rm -rf /thevolume/*), чтобы сделать PV доступным снова.
  • Динамическое выделение памяти: StorageClasses

    В больших кластерах ручное создание PV администратором под каждый чих разработчика — это путь к деградации процессов. Для автоматизации этого процесса используется объект StorageClass (SC).

    StorageClass определяет «профиль» хранилища. Например, у вас может быть класс fast (на SSD) и класс slow (на обычных HDD). Когда разработчик создает PVC и указывает в нем storageClassName: fast, Kubernetes вызывает специальный компонент — Provisioner.

    Как работает динамический цикл:

  • Пользователь создает PVC, указывая имя StorageClass.
  • Kubernetes видит запрос и находит соответствующий StorageClass.
  • Provisioner (внешний плагин или встроенный контроллер) обращается к API облака или СХД и создает реальный диск.
  • Provisioner автоматически создает объект PV в кластере с нужными параметрами.
  • PVC связывается с новым PV.
  • Это позволяет реализовать подход Infrastructure as Code (IaC) в полной мере: инфраструктура хранения создается «на лету» по требованию приложения.

    Глубокое погружение в Container Storage Interface (CSI)

    Раньше код для работы с хранилищами (AWS, GCP, vSphere) находился внутри основного бинарного файла Kubernetes (так называемые in-tree плагины). Это было неудобно: любое обновление драйвера требовало обновления всего кластера.

    Сегодня стандартом является CSI (Container Storage Interface). Это спецификация, которая позволяет вендорам хранилищ писать драйверы отдельно от Kubernetes. CSI-драйвер обычно состоит из нескольких компонентов:

  • CSI Controller: работает в Control Plane, отвечает за создание/удаление томов в облаке.
  • CSI Node: работает на каждом воркер-узле, отвечает за монтирование (mount/attach) тома к файловой системе узла.
  • Когда вы видите в манифесте provisioner: kubernetes.io/aws-ebs, это старый метод. Современный подход — provisioner: ebs.csi.aws.com.

    Жизненный цикл тома: Attach vs Mount

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

  • Attach (Прикрепление): происходит на уровне инфраструктуры. Облачный провайдер «подключает» диск к виртуальной машине (узлу). Если узел уже имеет максимально допустимое количество дисков, Attach завершится ошибкой.
  • Mount (Монтирование): происходит внутри операционной системы узла. Kubelet создает директорию и монтирует в неё устройство. Здесь могут возникнуть проблемы с файловыми системами или правами доступа.
  • Если Pod «завис» в состоянии ContainerCreating, первым делом нужно проверить события (kubectl describe pod):

  • Ошибка Multi-Attach означает, что диск типа RWO все еще прикреплен к другому узлу (возможно, старый Pod еще не завершился).
  • Ошибка MountVolume.SetUp failed часто указывает на проблемы с драйвером CSI или отсутствием необходимых утилит на узле (например, nfs-common).
  • Тонкая настройка: Расширение томов и Volume Snapshots

    Современные требования к данным включают возможность масштабирования и резервного копирования.

    Volume Expansion

    Kubernetes позволяет расширять уже созданные тома без потери данных. Для этого в StorageClass должен быть установлен флаг allowVolumeExpansion: true. Процесс выглядит так:
  • Пользователь редактирует PVC, увеличивая поле resources.requests.storage.
  • Kubernetes дает команду облаку расширить диск.
  • После расширения физического диска, файловая система внутри пода должна быть расширена. В зависимости от типа тома это может произойти автоматически при перезагрузке пода или «на горячую».
  • Volume Snapshots

    Для создания бэкапов используется связка объектов VolumeSnapshotClass, VolumeSnapshot и VolumeSnapshotContent. Это декларативный способ сказать облаку: «Сделай снимок этого диска прямо сейчас». Позже этот снимок можно использовать как dataSource для нового PVC, чтобы восстановить данные на определенный момент времени.

    Особенности работы со StatefulSet

    Как мы обсуждали в предыдущих главах, Deployment не подходит для баз данных, потому что все его реплики используют один и тот же шаблон пода. Если вы укажете PVC в шаблоне Deployment, все реплики попытаются подключиться к одному и тому же PV, что приведет к ошибке для блочных устройств (RWO).

    StatefulSet решает эту проблему через volumeClaimTemplates. Вместо того чтобы ссылаться на существующий PVC, StatefulSet описывает шаблон. Для каждой реплики (например, db-0, db-1) Kubernetes создаст персональный PVC (например, data-db-0, data-db-1).

    Это критически важно: при масштабировании StatefulSet вниз (scale down), PVC не удаляются автоматически. Это защитный механизм: данные важнее, чем жизненный цикл контроллера. Если вы позже масштабируете базу обратно вверх, новые поды «подцепят» свои старые диски по индексу.

    Безопасность и права доступа (fsGroup)

    Частая проблема: контейнер запускается от имени непривилегированного пользователя (например, UID 1000), а смонтированный том принадлежит root. В итоге приложение падает с Permission Denied.

    Для решения этой проблемы в securityContext пода используется параметр fsGroup:

    Когда Kubernetes монтирует том, он рекурсивно меняет владельца всех файлов на томе на GID 2000. Это позволяет приложению писать на диск. Однако будьте осторожны: на томах с миллионами мелких файлов этот процесс может занять очень много времени, и Pod будет висеть в ContainerCreating, пока chown не завершится. В новых версиях Kubernetes (1.23+) для этого есть параметр fsGroupChangePolicy: OnRootMismatch, который ускоряет процесс, проверяя только корневую директорию.

    Выбор файловой системы и производительность

    При работе с PV важно учитывать накладные расходы.

  • Блочные устройства (EBS, Azure Disk): обеспечивают максимальную производительность, сравнимую с локальными дисками. Идеально для СУБД (PostgreSQL, MongoDB).
  • Сетевые ФС (NFS, CephFS): имеют задержки (latency) из-за сетевого протокола. Не подходят для высоконагруженных баз данных, но незаменимы для общих хранилищ медиа-файлов или конфигураций.
  • Также стоит помнить о лимитах IOPS (операций ввода-вывода в секунду). В облаках производительность диска часто привязана к его объему. Иногда выгоднее заказать диск на 100 ГБ вместо 10 ГБ просто ради того, чтобы получить большую пропускную способность, даже если данные занимают мало места.

    Эфемерные тома (Generic Ephemeral Volumes)

    В версии 1.23 появилась интересная возможность — использовать PVC как эфемерное хранилище. Это называется Generic Ephemeral Volumes. В чем отличие от emptyDir? emptyDir всегда использует диск узла. Generic Ephemeral Volume позволяет вам использовать все возможности StorageClass (например, быстрые SSD или внешние СХД) для временных данных, которые будут удалены вместе с подом. Это полезно для CI/CD джоб, которым нужно много места и высокая скорость диска для сборки проекта, но данные после завершения не нужны.

    Резюме механизмов взаимодействия

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

    | Компонент | Ответственность | Уровень абстракции | | :--- | :--- | :--- | | StorageClass | Определение параметров создания (тип диска, зона, Provisioner) | Инфраструктурный шаблон | | PersistentVolume | Реальное хранилище в кластере (ресурс) | Глобальный (Cluster-wide) | | PersistentVolumeClaim | Запрос пользователя на выделение ресурсов | Namespace-scoped | | CSI Driver | Мост между API Kubernetes и API хранилища | Драйвер / Плагин |

    Понимание этой цепочки позволяет системно подходить к отладке. Если диск не создался — смотрим StorageClass и логи CSI-контроллера. Если диск создался, но Pod не стартует — проверяем AccessModes и события Kubelet на узле.

    Данные — это самая ценная часть системы. В Kubernetes управление ими превратилось из рутинной настройки серверов в гибкое управление объектами. Правильное использование PV, PVC и StorageClasses позволяет строить системы, которые не боятся падений узлов и легко масштабируются вместе с ростом бизнеса.

    6. Конфигурация и безопасность: управление секретами, ConfigMaps и модель доступа RBAC

    Конфигурация и безопасность: управление секретами, ConfigMaps и модель доступа RBAC

    Представьте, что вы строите современный цифровой банк. У вас сотни микросервисов, каждый из которых требует уникальных настроек: адреса баз данных, API-ключи платежных шлюзов, сертификаты для шифрования трафика и уровни логирования. Если вы «зашьете» эти данные внутрь Docker-образов, вы создадите кошмар для безопасности и лишитесь гибкости: любое изменение конфига потребует пересборки и перевыкладки всего стека. В Kubernetes решение этой проблемы строится на принципе отделения конфигурации от кода, но это лишь половина дела. Вторая половина — гарантировать, что к этим данным (и к самому API кластера) имеют доступ только те, кому это действительно необходимо.

    Разделение конфигурации и кода через ConfigMap

    Объект ConfigMap — это базовый кирпич в фундаменте двенадцатифакторных приложений (12-factor apps) внутри Kubernetes. Его задача — хранить нечувствительные данные в виде пар «ключ-значение» или целых конфигурационных файлов.

    Когда вы создаете ConfigMap, данные сохраняются в etcd. Однако ключевая ценность заключается в способах доставки этих данных в контейнер. Существует три основных метода:

  • Переменные окружения (Environment Variables). Самый простой способ, когда значения из ConfigMap пробрасываются в процесс приложения.
  • Аргументы командной строки. Использование данных для модификации флагов запуска (например, --log-level=$(LOG_LEVEL)).
  • Монтирование в виде файлов (Volumes). Kubernetes создает временную файловую систему (tmpfs) и проецирует ключи ConfigMap как файлы в указанную директорию.
  • Особое внимание стоит уделить механизму обновления. Если вы используете переменные окружения, то при изменении ConfigMap значение в работающем контейнере не изменится. Вам придется перезапустить Pod (например, через kubectl rollout restart), чтобы kubelet перечитал обновленные данные при создании нового процесса.

    В случае с монтированием томов ситуация интереснее. Kubelet периодически синхронизирует состояние смонтированных ConfigMap. Внутри директории создается сложная структура символических ссылок:

  • ..data указывает на актуальную директорию с данными (метка времени).
  • имя_файла указывает на ..data/имя_файла.
  • Когда данные обновляются, kubelet создает новую директорию, обновляет симлинк ..data, и атомарно подменяет пути. Приложения, которые умеют следить за изменениями файлов (через inotify), могут подхватывать новые настройки без перезагрузки. Однако стоит помнить: если вы монтируете ConfigMap через subPath, автоматическое обновление работать не будет. Это связано с тем, что subPath фиксирует конкретный инод файла в момент монтирования.

    Управление чувствительными данными: Kubernetes Secrets

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

    Главное техническое отличие на уровне манифеста — данные в Secret должны быть закодированы в base64. Важно понимать: base64 — это не шифрование, а кодирование. Это защита от случайного прочтения конфига через плечо, но не от злоумышленника с доступом к API.

    Kubernetes предлагает несколько типов секретов:

  • Opaque: произвольные данные (самый частый тип).
  • kubernetes.io/service-account-token: токены для аутентификации ServiceAccounts.
  • kubernetes.io/dockerconfigjson: учетные данные для скачивания образов из приватных реестров.
  • kubernetes.io/tls: пара из публичного сертификата и приватного ключа.
  • Безопасность секретов в стандартной поставке Kubernetes часто критикуют, и небезосновательно. По умолчанию данные в etcd хранятся в открытом виде. Чтобы превратить Kubernetes Secret в действительно безопасный инструмент, необходимо внедрить Encryption at Rest. Это механизм, при котором API-сервер шифрует данные перед записью в etcd с помощью ключа, заданного в конфигурации провайдера шифрования (EncryptionConfiguration).

    Еще один нюанс — потребление памяти. Секреты монтируются в tmpfs (оперативную память узла). Это предотвращает запись чувствительных данных на физический диск узла, где они могли бы остаться после удаления пода, но накладывает ограничения на объем: хранить тяжелые сертификаты или Java KeyStores мегабайтами в секретах — плохая практика.

    Проблема «курицы и яйца» и внешние хранилища

    Несмотря на наличие встроенных объектов, в крупных Enterprise-инсталляциях часто используют внешние системы, такие как HashiCorp Vault, AWS Secrets Manager или Azure Key Vault. Почему?

  • Централизация: управление секретами для нескольких кластеров в одном месте.
  • Аудит: детальные логи того, кто и когда запрашивал пароль от БД.
  • Динамические секреты: возможность генерировать временные доступы к БД, которые «протухают» через час.
  • Для интеграции используется Secrets Store CSI Driver. Он позволяет монтировать данные из внешних хранилищ напрямую в Pod как тома. Приложение даже не знает, что пароль пришел из Vault — оно просто читает файл /mnt/secrets/db-password.

    Модель доступа RBAC: кто, что и над чем

    Если ConfigMap и Secrets отвечают за то, что приложение знает, то RBAC (Role-Based Access Control) определяет, что пользователь или сервис может делать.

    RBAC в Kubernetes строится на четырех основных объектах, которые можно разделить на две оси: область действия (Namespace vs Cluster) и сущность (Описание прав vs Привязка прав).

    Роли и их область видимости

  • Role: Описывает набор разрешений внутри конкретного Namespace. Например: «разрешить чтение подов в неймспейсе dev».
  • ClusterRole: Описывает разрешения на уровне всего кластера. Она необходима для:
  • - Доступа к ресурсам, не привязанным к неймспейсам (Nodes, PersistentVolumes). - Доступа к ресурсам во всех неймспейсах сразу (например, для мониторинга). - Определения общих прав, которые потом будут «сужены» до неймспейса.

    Структура правила в Role всегда состоит из трех частей:

  • apiGroups: к какой группе относится ресурс (например, apps для Deployment или "" для базовых подов).
  • resources: над чем совершается действие (pods, services, configmaps).
  • verbs: само действие (get, list, watch, create, update, patch, delete).
  • Привязка прав (Bindings)

    Сама по себе роль — это просто список возможностей. Чтобы они заработали, их нужно привязать к субъекту (User, Group или ServiceAccount).

  • RoleBinding: Привязывает Role или ClusterRole к субъекту в рамках конкретного Namespace. Если вы привязываете ClusterRole через RoleBinding, субъект получит права из этой роли только внутри того неймспейса, где создан Binding. Это отличный способ переиспользовать стандартные роли (например, view или edit) без дублирования манифестов.
  • ClusterRoleBinding: Дает права, описанные в ClusterRole, на уровне всего кластера.
  • > Важное правило безопасности: > Применяйте принцип наименьших привилегий (POLP). Никогда не используйте ClusterRoleBinding там, где достаточно RoleBinding. Ошибка в конфигурации прав одного микросервиса может привести к компрометации всего кластера, если у него есть права cluster-admin.

    ServiceAccounts: личности для программ

    В Kubernetes пользователи (Users) — это внешние сущности, которыми кластер сам не управляет (они обычно приходят из внешних систем через сертификаты или OIDC). Но приложениям внутри подов тоже нужно общаться с API-сервером. Для этого существуют ServiceAccounts.

    Когда вы создаете Pod, Kubernetes автоматически назначает ему default ServiceAccount из того же неймспейса (если не указано иное). Внутри контейнера по пути /var/run/secrets/kubernetes.io/serviceaccount/ монтируется токен. Приложение использует этот токен для аутентификации в API.

    Однако default ServiceAccount по умолчанию не имеет прав. Если вашему приложению нужно, например, читать список других подов для самообнаружения (Service Discovery), вам нужно:

  • Создать свой ServiceAccount (например, my-app-sa).
  • Создать Role с разрешением list на ресурс pods.
  • Создать RoleBinding, связывающий my-app-sa и эту Role.
  • Указать serviceAccountName: my-app-sa в спецификации пода.
  • Одной из лучших практик безопасности является установка флага automountServiceAccountToken: false. Это предотвращает автоматическое монтирование токена в поды, которым не нужно общаться с API Kubernetes. Чем меньше «патронов» у потенциального взломщика, тем лучше.

    Практические аспекты настройки безопасности ресурсов

    При работе с ConfigMap и Secrets часто возникают ситуации, когда данные должны быть доступны только определенным процессам. Здесь на помощь приходит SecurityContext пода.

    Рассмотрим пример с монтированием секрета. По умолчанию файлы внутри тома имеют права 0644 и принадлежат пользователю root. Если ваше приложение запускается от имени не-привилегированного пользователя (например, uid: 1000), оно может столкнуться с Permission Denied.

    Для решения этой проблемы в pod.spec используется параметр fsGroup:

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

    Иммутабельные (неизменяемые) ресурсы

    Начиная с версии 1.21, Kubernetes поддерживает поле immutable: true для ConfigMap и Secrets. Это мощный инструмент оптимизации и безопасности.

  • Производительность: Kubelet перестает опрашивать API-сервер на предмет изменений этого ресурса, что существенно снижает нагрузку на Control Plane в больших кластерах.
  • Стабильность: Вы гарантируете, что конфигурация не изменится «на лету». Если нужно обновить конфиг — создайте новый объект с другим именем и обновите Deployment. Это реализует паттерн «Immutable Infrastructure» на уровне настроек.
  • Иерархия и приоритеты при работе с контекстами

    Часто возникает вопрос: что делать, если одно и то же значение определено и в ConfigMap, и в переменных окружения самого Docker-образа? В Kubernetes действует строгая иерархия:

  • Значения в манифесте Pod (env): имеют наивысший приоритет.
  • Значения из ConfigMap/Secret: подставляются в момент старта.
  • Значения из Dockerfile (ENV): являются базовыми и перекрываются всем, что указано выше.
  • Для управления сложными наборами переменных удобно использовать envFrom. Это позволяет загрузить все ключи из ConfigMap как переменные окружения одной командой:

    Это сокращает размер манифеста, но таит в себе опасность: если в ConfigMap добавят ключ, совпадающий с системной переменной (например, PATH или HOME), это может нарушить работу контейнера. Поэтому для критических приложений рекомендуется явное перечисление ключей через valueFrom.

    Аудит и отладка доступа

    Когда вы настраиваете RBAC, неизбежно возникают ошибки типа Forbidden. Для их отладки существует полезная команда kubectl auth can-i. Она позволяет проверить права, не выполняя само действие.

    Например, чтобы проверить, может ли ваш ServiceAccount удалять поды: kubectl auth can-i delete pods --as=system:serviceaccount:default:my-app-sa

    Если вы получаете yes, значит права настроены верно. Если no — нужно проверять цепочку Role -> RoleBinding.

    Также стоит помнить о встроенных ролях. Kubernetes поставляется с четырьмя стандартными ClusterRoles:

  • cluster-admin: полный доступ ко всему.
  • admin: полный доступ в рамках неймспейса (кроме квот).
  • edit: разрешение на изменение ресурсов (но не просмотр секретов или изменение ролей).
  • view: только чтение ресурсов (без секретов).
  • Использование этих ролей через RoleBinding — самый быстрый и безопасный способ делегировать доступ разработчикам или CI/CD системам.

    Замыкание мысли: баланс между удобством и защитой

    Управление конфигурациями и доступом в Kubernetes — это постоянный компромисс. С одной стороны, мы хотим максимальной автоматизации (ConfigMap с автоматическим обновлением, ServiceAccounts с широкими правами для операторов), с другой — жесткой изоляции.

    Ключ к успеху лежит в многослойной защите. Используйте ConfigMap для логики, Secrets для паролей, шифруйте etcd, ограничивайте права через RBAC и всегда следуйте правилу: если ресурсу не нужен доступ к API Kubernetes, у него не должно быть токена ServiceAccount. Такой подход превращает кластер из «прозрачного ящика», где всё доступно всем, в надежно сегментированную крепость, готовую к эксплуатации в самых агрессивных средах.