Развертывание отказоустойчивого кластера Kubernetes на Debian 12: от теории к практике

Комплексный курс по ручному созданию HA-кластера Kubernetes в среде VMware vCenter. Программа сочетает глубокую теоретическую подготовку к сертификации CKA с пошаговым руководством по настройке инфраструктуры, балансировки и сетевого взаимодействия.

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

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

В любой распределенной системе отказ оборудования — это не вероятность, а вопрос времени. Серверы теряют питание, сетевые коммутаторы выходят из строя, а дисковые массивы повреждают данные. Если кластер Kubernetes управляется единственным сервером (мастер-нодой), выход этого сервера из строя означает полную потерю контроля над всей инфраструктурой: приложения продолжат работать, но вы не сможете их обновить, масштабировать или перезапустить упавшие контейнеры. Чтобы система выдерживала аппаратные сбои без деградации управления, архитектуру изначально проектируют как отказоустойчивую (High Availability, HA).

Кубернетес логически разделен на две плоскости: Control Plane (плоскость управления) и Data Plane (плоскость данных, или воркер-ноды). Воркер-ноды выполняют полезную нагрузку — запускают контейнеры. Control Plane принимает решения: где запустить контейнер, как маршрутизировать к нему трафик и что делать, если воркер-нода перестала отвечать.

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

Топология отказоустойчивости

Чтобы Control Plane стал отказоустойчивым, мы должны запустить его компоненты в нескольких экземплярах на разных физических или виртуальных машинах. Однако просто скопировать процессы недостаточно. Разные компоненты Kubernetes требуют разных подходов к обеспечению отказоустойчивости. Одни могут работать в режиме Active-Active (когда все копии обрабатывают запросы одновременно), другие — только в режиме Active-Passive (когда работает один, а остальные ждут его падения), а третьи требуют строгих математических алгоритмов для достижения консенсуса.

!Архитектура отказоустойчивого кластера Kubernetes

В нашей целевой архитектуре мы будем использовать внешний балансировщик нагрузки (HAProxy + Keepalived) и три независимые мастер-ноды. На каждой мастер-ноде будет запущен полный набор компонентов Control Plane. Давайте разберем каждый из них.

etcd: Хранилище состояния и источник истины

Сердце любого кластера Kubernetes — это etcd. Это распределенное, надежное хранилище пар «ключ-значение». Абсолютно всё состояние кластера хранится здесь: конфигурации, секреты, информация о том, какие поды должны работать, какие работают по факту, и на каких узлах они находятся. Если данные в etcd будут потеряны, кластер перестанет существовать как единое целое. Ни один другой компонент Kubernetes не хранит состояние (они stateless), все они опираются на etcd.

Механизм отказоустойчивости: Алгоритм Raft и Кворум

Поскольку etcd хранит состояние, его экземпляры не могут просто записывать данные независимо друг от друга — это приведет к рассинхронизации (split-brain). Чтобы несколько узлов etcd работали как единое целое, они используют алгоритм консенсуса Raft.

Raft гарантирует, что все узлы согласны с текущим состоянием данных. В каждый момент времени среди узлов etcd выбирается один Лидер (Leader), а остальные становятся Ведомыми (Followers). Все запросы на изменение данных (запись) всегда проходят через Лидера. Лидер получает запрос, отправляет его Ведомым и ждет подтверждения. Запись считается успешной только тогда, когда большинство узлов кластера подтвердят сохранение данных на диск.

Это «большинство» называется кворумом. Кворум — это минимальное количество узлов etcd, которые должны быть доступны для того, чтобы кластер мог принимать решения и записывать данные. Размер кворума вычисляется по формуле:

Где — это размер кворума, — общее количество узлов в кластере etcd, а оператор означает округление вниз до целого числа.

Рассмотрим примеры, чтобы понять, почему в нашей архитектуре именно 3 мастер-ноды:

  • Если : Кворум . Кластер работает, но при падении единственного узла кворум теряется. Отказоустойчивости нет.
  • Если : Кворум . Если упадет хотя бы один узел, останется только 1 рабочий, что меньше кворума (2). Кластер перейдет в режим «только чтение», запись станет невозможна. Два узла хуже одного, так как вероятность отказа удваивается, а отказоустойчивость не появляется.
  • Если : Кворум . Кластер из трех узлов может пережить падение одного узла (останется 2, что равно кворуму). Это минимальная отказоустойчивая конфигурация.
  • Если : Кворум . Кластер из четырех узлов может пережить падение... только одного узла (останется 3). Если упадут два, останется 2, что меньше кворума (3).
  • Именно поэтому etcd всегда разворачивают нечетным числом узлов (3, 5, 7). Четное количество не добавляет отказоустойчивости по сравнению с предыдущим нечетным числом, но увеличивает сетевые задержки на синхронизацию. В нашем проекте мы используем 3 мастер-ноды, что позволяет нам безболезненно пережить отказ любого одного сервера ESXi или виртуальной машины.

    !Процесс репликации данных и выбора лидера в etcd

    kube-apiserver: Центральный коммуникационный узел

    kube-apiserver — это фасад Control Plane. Это единственный компонент, который имеет прямой доступ к etcd. Все остальные компоненты (как управляющие, так и воркер-ноды), а также внешние пользователи (через утилиту kubectl) общаются с кластером исключительно через REST API, предоставляемый kube-apiserver.

    Когда вы выполняете команду kubectl apply -f pod.yaml, запрос поступает в API-сервер. Сервер выполняет три этапа:

  • Аутентификация и Авторизация: Проверяет, кто вы и имеете ли право создавать поды.
  • Admission Control (Контроль допуска): Пропускает запрос через цепочку плагинов, которые могут изменить запрос (например, автоматически добавить лимиты памяти) или отклонить его (например, если используется запрещенный образ контейнера).
  • Сохранение: Если проверки пройдены, API-сервер конвертирует запрос во внутренний формат и записывает желаемое состояние в etcd.
  • Механизм отказоустойчивости: Active-Active и Балансировка нагрузки

    В отличие от etcd, kube-apiserver абсолютно не хранит в себе никакого состояния (stateless). Вся память вынесена в etcd. Это архитектурное решение делает масштабирование API-сервера тривиальной задачей: мы можем запустить любое количество экземпляров kube-apiserver, и все они могут обрабатывать запросы одновременно (Active-Active).

    Однако возникает проблема маршрутизации: если у нас есть три сервера labs-k8s-m01, labs-k8s-m02 и labs-k8s-m03, куда именно должны отправлять запросы воркер-ноды и администраторы? Если жестко прописать IP-адрес первого мастера, то при его падении кластер станет недоступен, даже если два других мастера работают отлично.

    Решением является внешний балансировщик нагрузки. В нашей топологии это сервер labs-k8s-balancer (IP: 10.3.60.50). На нем работает прокси-сервер HAProxy, который принимает весь трафик на порт 6443 и распределяет его (например, по алгоритму Round Robin) между тремя нашими мастер-нодами.

    Все компоненты кластера настраиваются на обращение к одному виртуальному IP-адресу (VIP) балансировщика. Если один kube-apiserver выходит из строя, HAProxy замечает это (через health-check) и перестает отправлять на него трафик, перераспределяя нагрузку на оставшиеся два. В enterprise-средах сам балансировщик тоже дублируется с помощью протокола VRRP (Keepalived), чтобы избежать единой точки отказа на уровне проксирования. В нашем проекте мы закладываем Keepalived на одном узле как архитектурный паттерн, который легко масштабируется добавлением второго LB-узла.

    kube-scheduler: Диспетчер ресурсов

    Когда API-сервер сохраняет в etcd информацию о том, что нужно создать новый Pod, этот Pod изначально не привязан ни к одной воркер-ноде. Его статус — Pending. Здесь в игру вступает kube-scheduler.

    Планировщик непрерывно наблюдает за API-сервером на предмет появления подов без назначенной ноды. Как только такой Pod обнаруживается, kube-scheduler запускает сложный процесс принятия решений:

  • Фильтрация (Filtering): Отсеиваются ноды, которые физически не могут принять Pod. Например, ноде не хватает свободной оперативной памяти, или на ней установлены метки (Taints), запрещающие размещение этого пода.
  • Оценка (Scoring): Оставшиеся подходящие ноды ранжируются. Планировщик оценивает, где Pod будет работать оптимальнее (например, предпочитает ноды, где уже есть нужные образы контейнеров, или старается распределить нагрузку равномерно).
  • Связывание (Binding): Планировщик выбирает ноду с наивысшим баллом и отправляет API-серверу запрос на привязку (Bind) пода к этой ноде.
  • Механизм отказоустойчивости: Active-Passive (Leader Election)

    Что произойдет, если мы запустим три экземпляра kube-scheduler и позволим им работать одновременно? Возникнет состояние гонки (race condition). Два планировщика могут одновременно увидеть новый Pod, независимо друг от друга провести вычисления и попытаться назначить его на разные ноды.

    Чтобы избежать хаоса, kube-scheduler (как и следующий компонент, Controller Manager) работает в режиме Active-Passive. Используется механизм выбора лидера (Leader Election).

    На практике это работает так: все три экземпляра планировщика запускаются, но первым делом они пытаются создать (или захватить) специальный объект блокировки (Lease) в etcd через API-сервер. Тот экземпляр, чей запрос дойдет первым, становится Лидером. Он начинает выполнять свою работу — планировать поды. Два других экземпляра переходят в режим ожидания.

    Лидер обязан регулярно обновлять объект Lease (отправлять heartbeat), подтверждая, что он жив. Если сервер с активным планировщиком падает, обновление Lease прекращается. Срок действия блокировки истекает, и оставшиеся в живых экземпляры немедленно пытаются захватить Lease. Новый победитель становится Лидером и продолжает работу с того места, где остановился предыдущий.

    kube-controller-manager: Автоматика и циклы согласования

    Kubernetes работает по декларативному принципу: вы описываете желаемое состояние (Desired State), а система сама приводит к нему текущее фактическое состояние (Actual State). За это приведение отвечает kube-controller-manager.

    Это единый бинарный файл, внутри которого крутятся десятки независимых циклов управления (Control Loops) — контроллеров. Примеры контроллеров:

  • Node Controller: Следит за состоянием воркер-нод. Если нода перестает отвечать (kubelet не шлет heartbeat), контроллер помечает ее как недоступную и инициирует эвакуацию подов.
  • ReplicaSet Controller: Следит за тем, чтобы количество запущенных копий пода всегда совпадало с указанным в конфигурации. Если вы запросили 3 реплики веб-сервера, а одна упала, контроллер замечает расхождение (3 желаемых, 2 фактических) и просит API-сервер создать еще один Pod.
  • Endpoints Controller: Связывает сервисы (Services) с подами, обновляя списки IP-адресов, куда должен направляться трафик.
  • Как и планировщик, kube-controller-manager использует механизм Leader Election. В каждый момент времени в кластере работает только один активный Controller Manager. Если бы их было несколько, они бы дублировали реакции на одни и те же события, создавая лишние поды или отправляя конфликтующие команды.

    Плоскость данных (Data Plane): Краткий обзор

    Хотя фокус этой статьи — отказоустойчивость Control Plane, для полноты картины необходимо понимать, с чем взаимодействуют мастера. На серверах labs-k8s-w01 и labs-k8s-w02 будут работать следующие компоненты:

  • kubelet: Агент Kubernetes на каждом узле. Он слушает инструкции от API-сервера (какие поды должны здесь работать) и управляет локальным Container Runtime (в нашем случае это будет Containerd), чтобы запустить или остановить контейнеры. Kubelet также регулярно рапортует мастеру о состоянии ноды.
  • kube-proxy: Сетевой компонент, который реализует концепцию Service в Kubernetes. Он управляет правилами маршрутизации (обычно через iptables или IPVS в ядре Linux), чтобы сетевой трафик корректно доходил до нужных подов, даже если они постоянно уничтожаются и создаются заново с новыми IP-адресами.
  • Container Runtime: Программное обеспечение (Containerd), которое непосредственно распаковывает образы и запускает процессы в изолированных пространствах имен (namespaces) ядра Linux.
  • Сценарии отказов: Как архитектура спасает кластер

    Чтобы закрепить понимание, смоделируем несколько аварийных ситуаций в нашей будущей инфраструктуре (1 LB, 3 Master, 2 Worker) и проследим за реакцией системы.

    Сценарий 1: Полный выход из строя мастер-ноды m01 Допустим, на сервере labs-k8s-m01 сгорела материнская плата.

  • etcd: В кластере остается 2 узла из 3. Кворум () сохраняется. База данных продолжает принимать операции чтения и записи. Если m01 был Лидером Raft, оставшиеся узлы за несколько миллисекунд выберут нового Лидера.
  • kube-apiserver: Балансировщик labs-k8s-balancer видит, что порт 6443 на m01 не отвечает, и исключает его из пула. Трафик от kubectl и воркеров бесшовно направляется на m02 и m03.
  • kube-scheduler и kube-controller-manager: Если m01 держал блокировку Лидера (Lease), он перестает ее обновлять. Через короткий таймаут компоненты на m02 или m03 перехватывают лидерство.
  • Итог: Кластер продолжает работать в штатном режиме. Администратор может спокойно развернуть новую виртуальную машину взамен сгоревшей и ввести её в кластер.

    Сценарий 2: Сетевое разделение (Split-Brain) Представим, что сетевой коммутатор настроен неверно, и мастер-нода m03 теряет связь с m01 и m02, но продолжает «видеть» воркер-ноды. Это самая опасная ситуация для распределенных систем. Узел m03 считает, что m01 и m02 упали. Он пытается стать Лидером etcd. Однако, чтобы стать Лидером, ему нужны голоса большинства (кворум = 2). Будучи изолированным, он голосует сам за себя (1 голос) и не может собрать кворум. В итоге etcd на m03 переходит в состояние ожидания и отказывается принимать любые изменения. Тем временем m01 и m02 прекрасно видят друг друга. У них есть 2 голоса из 3 — кворум собран. Они продолжают управлять кластером. Итог: Алгоритм Raft математически предотвращает ситуацию, когда две части кластера начинают принимать независимые решения, что привело бы к необратимому повреждению данных.

    Понимание этих механизмов критически важно для администратора. Разворачивая кластер с помощью kubeadm в следующих главах, мы будем не просто вводить команды по шаблону, а осознанно настраивать сертификаты для общения между тремя etcd, указывать IP-адрес балансировщика для kube-apiserver и проверять статусы захвата лидерства у контроллеров. Архитектура Kubernetes сложна, но эта сложность — плата за возможность системы самостоятельно восстанавливаться после тяжелых инфраструктурных сбоев.

    10. Тестирование отказоустойчивости, верификация компонентов и базовые операции управления кластером

    Тестирование отказоустойчивости, верификация компонентов и базовые операции управления кластером

    Зеленые статусы Ready в консоли сразу после установки кластера — это лишь снимок идеального состояния системы в вакууме. Настоящая архитектура высокой доступности (High Availability) доказывает свою состоятельность только в момент аварии. Серверы теряют питание, сетевые коммутаторы зависают, а дисковые массивы деградируют. Задача администратора Kubernetes — не предотвратить эти сбои, а убедиться, что механизмы самовосстановления кластера отрабатывают их без деградации пользовательского сервиса.

    Верификация базового состояния кластера

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

    Первый шаг — проверка регистрации узлов в API-сервере. Команда kubectl get nodes -o wide должна отобразить все пять серверов (три мастера и два воркера). Статус Ready означает, что локальный процесс kubelet на каждом узле успешно прошел аутентификацию, запустил необходимые CNI-плагины и регулярно (каждые 10 секунд) отправляет API-серверу сигналы heartbeat (сердцебиение), подтверждая свою готовность принимать рабочую нагрузку.

    Второй шаг — инспекция системных подов в пространстве имен kube-system. Выполнение kubectl get pods -n kube-system -o wide показывает внутреннюю механику Control Plane. Здесь критически важно убедиться в следующем:

  • Поды kube-apiserver, kube-controller-manager и kube-scheduler запущены в трех экземплярах (по одному на каждый узел m01, m02, m03).
  • Поды сетевого плагина kube-flannel-ds работают на всех пяти узлах кластера (это DaemonSet, обеспечивающий Overlay-сеть).
  • Поды coredns находятся в статусе Running. Если они остаются в Pending или ContainerCreating, это стопроцентный маркер проблем с сетью (CNI) или нехватки ресурсов.
  • Третий, самый глубокий шаг — проверка консенсуса etcd. Поскольку etcd хранит абсолютно все состояние кластера, его деградация фатальна. Проверка выполняется напрямую через утилиту etcdctl, обращаясь к локальным портам базы данных:

    В выводе этой команды необходимо обратить внимание на колонку IS LEADER. Только один узел должен иметь значение true. Колонки RAFT TERM (эпоха голосования) и RAFT INDEX (номер последней транзакции) должны быть практически идентичны на всех трех узлах, что подтверждает успешную репликацию данных.

    Подготовка тестовой нагрузки

    Для наглядного тестирования отказоустойчивости системных компонентов нам потребуется пользовательская нагрузка, распределенная по Data Plane. Создадим Deployment веб-сервера Nginx с шестью репликами, чтобы планировщик гарантированно распределил их между двумя нашими воркер-нодами (labs-k8s-w01 и labs-k8s-w02).

    Чтобы сымитировать внешний доступ, опубликуем этот Deployment через сервис типа NodePort:

    Выполнив kubectl get pods -l app=ha-test-nginx -o wide, мы увидим, что поды равномерно размазаны по узлам w01 и w02. Сервис NodePort откроет случайный порт в диапазоне 30000-32767 на всех узлах кластера. Теперь, обратившись по IP-адресу любого узла (например, curl http://10.3.60.54:<NodePort>), мы получим ответ от Nginx. Кластер готов к разрушительным тестам.

    Тестирование отказа Control Plane: потеря мастер-ноды

    Самый критичный сценарий — внезапный выход из строя узла, на котором запущены компоненты управления. Сымитируем жесткое отключение питания (Hard Power Off) виртуальной машины labs-k8s-m01 через интерфейс VCenter.

    Сразу после отключения узла попробуем выполнить любую команду, например, kubectl get nodes. Команда отрабатывает мгновенно и без ошибок. Почему это происходит, если мы только что «убили» узел, к которому изначально подключались при настройке?

    Секрет кроется в архитектуре балансировки. Наша утилита kubectl (как и процессы kubelet на воркерах) обращается не к физическому IP-адресу 10.3.60.51, а к виртуальному адресу (VIP) 10.3.60.50 на балансировщике labs-k8s-balancer.

    !Маршрутизация API-запросов при отказе мастера

    Как только m01 выключается, HAProxy на балансировщике не получает ответ на health-check по порту 6443. Согласно параметрам fall и rise, HAProxy помечает бэкенд m01 как мертвый и мгновенно перестает отправлять на него новые TCP-сессии. Все новые запросы от kubectl прозрачно маршрутизируются на оставшиеся в живых m02 и m03.

    Внутри самого кластера происходят следующие процессы:

  • API-сервер: Поскольку он является stateless-компонентом (не хранит состояние), потеря одного экземпляра просто уменьшает общую пропускную способность Control Plane, но не ломает логику.
  • etcd: Кластер базы данных фиксирует потерю одного участника. Из трех узлов в строю остаются два. Кворум для принятия решений сохраняется. База данных продолжает принимать операции записи.
  • Controller Manager и Scheduler: Если лидером (Active) в механизме Leader Election был именно m01, срок действия его блокировки (Lease) в etcd истекает (обычно через 15 секунд). Узлы m02 и m03 замечают это и инициируют новые выборы. Один из них захватывает лидерство и продолжает управлять кластером.
  • В выводе kubectl get nodes узел m01 перейдет в статус NotReady. Однако пользовательская нагрузка (наш Nginx) на воркерах продолжит работать без единого разрыва связи, так как Data Plane автономен от Control Plane в плане обработки сетевого трафика.

    После включения питания m01 узел автоматически загрузит ОС, запустит Containerd и kubelet. Kubelet прочитает манифесты статических подов из /etc/kubernetes/manifests/ и запустит API, etcd и контроллеры. Узел etcd свяжется с лидером, скачает недостающие логи транзакций (догонит RAFT INDEX) и вернется в кворум. Кластер восстановится без ручного вмешательства.

    Тестирование отказа Data Plane: эвакуация подов (Pod Eviction)

    Теперь проверим, как кластер защищает пользовательские приложения. Выполним жесткое отключение питания воркер-ноды labs-k8s-w01. На этом узле работали три из шести наших подов Nginx.

    Если мы сразу выполним kubectl get pods -o wide, то увидим неожиданную картину: поды на упавшем w01 все еще числятся в статусе Running. Kubernetes не реагирует мгновенно. Это сделано намеренно для защиты от кратковременных сетевых сбоев (моргание сети).

    Механика реакции кластера строго регламентирована тайм-аутами:

  • Потеря Heartbeat: Kubelet на w01 перестает отправлять обновления статуса.
  • Grace Period: Node Controller (часть kube-controller-manager) ждет в течение времени, заданного параметром node-monitor-grace-period (по умолчанию 40 секунд).
  • Смена статуса: По истечении 40 секунд Node Controller меняет статус узла w01 на NotReady.
  • Ожидание эвакуации: Начинается отсчет критического параметра pod-eviction-timeout (по умолчанию 5 минут). Кластер дает узлу шанс вернуться, чтобы не провоцировать массовое и ресурсоемкое пересоздание контейнеров по всей сети.
  • !Процесс эвакуации подов при отказе узла

    Ровно через 5 минут после потери связи Node Controller принимает решение об эвакуации. Важно понимать фундаментальную концепцию Kubernetes: поды никогда не мигрируют и не перемещаются. Под — это смертная сущность.

    Controller Manager помечает старые поды на w01 статусом Terminating (они останутся в этом статусе в базе данных, пока узел не вернется и не подтвердит их физическое удаление). Одновременно с этим ReplicaSet контроллер замечает, что живых подов стало 3 вместо требуемых 6. Он запрашивает создание трех новых подов. Scheduler видит, что единственный доступный узел — это w02, и назначает новые поды туда. В итоге все 6 экземпляров Nginx оказываются запущенными на labs-k8s-w02.

    Когда мы включим w01, его kubelet свяжется с API, увидит, что старые поды помечены на удаление, уничтожит их контейнеры и сообщит API об успешной очистке. Статусы Terminating исчезнут из консоли. При этом новые поды останутся на w02 — Kubernetes не занимается автоматической обратной балансировкой (rebalancing) запущенных подов, чтобы не прерывать работу приложений без необходимости.

    Безопасный вывод узла из эксплуатации: Cordon и Drain

    Жесткое отключение питания — это авария. В реальной эксплуатации администраторам регулярно требуется выводить узлы на техническое обслуживание (обновление ядра ОС, замена памяти, патчи безопасности). Делать это путем выдергивания кабеля непрофессионально, так как это приводит к 5-минутному простою части нагрузки (до срабатывания eviction timeout) и обрыву активных клиентских соединений.

    Для штатного обслуживания используются операции cordon и drain.

    Блокировка планирования (Cordon)

    Команда kubectl cordon labs-k8s-w02 переводит узел в режим изоляции. Под капотом Kubernetes просто вешает на объект Node специальный Taint (метку отторжения): node.kubernetes.io/unschedulable:NoSchedule.

    > Taint NoSchedule работает как запрещающий знак для планировщика. Уже запущенные на узле поды продолжают спокойно работать, но ни один новый под на этот сервер назначен не будет.

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

    Эвакуация нагрузки (Drain)

    Чтобы полностью освободить узел для перезагрузки, применяется команда drain. Она автоматически делает cordon (если он не был сделан ранее), а затем начинает аккуратно удалять поды с узла.

    Флаги здесь критически важны:

  • --ignore-daemonsets: Поды типа DaemonSet (например, Flannel или kube-proxy) привязаны к конкретному узлу. Их невозможно «перенести». Если не указать этот флаг, drain выдаст ошибку и остановится, защищая системные компоненты.
  • --delete-emptydir-data: Если поды используют временные локальные тома emptyDir, при удалении пода эти данные будут стерты навсегда. Kubernetes требует от администратора явного подтверждения (через этот флаг), что потеря локального кэша допустима.
  • В отличие от жесткого падения, drain инициирует процесс Graceful Shutdown (мягкой остановки). Процессу внутри контейнера (PID 1) отправляется сигнал SIGTERM. Приложению дается время (по умолчанию 30 секунд, регулируется параметром terminationGracePeriodSeconds в манифесте пода) на корректное закрытие соединений с базами данных, сохранение состояния и завершение обработки текущих HTTP-запросов. Если через 30 секунд процесс все еще жив, ядро ОС убивает его сигналом SIGKILL.

    Еще один мощный механизм защиты при выполнении drain — это PodDisruptionBudget (PDB). Это специальный объект Kubernetes, который позволяет владельцам приложений сказать кластеру: «Во время любых административных работ гарантируй, что минимум 4 реплики моего приложения всегда доступны». Если администратор запустит drain, и удаление подов нарушит лимит PDB (например, останется только 3 реплики), команда drain заблокируется и будет ждать, пока новые реплики не поднимутся на других узлах. Это исключает человеческий фактор при массовом обслуживании кластера.

    После завершения технических работ на сервере (например, перезагрузки после обновления ядра), узел необходимо вернуть в строй:

    Команда снимает Taint NoSchedule. Узел снова готов принимать нагрузку, и при следующем масштабировании приложений планировщик начнет отправлять на него новые поды.

    Архитектура Kubernetes построена на принципе Eventual Consistency (согласованность в конечном счете). Кластер не паникует при потере узлов. Контроллеры методично сравнивают текущее состояние инфраструктуры с декларативным манифестом, который хранится в etcd, и предпринимают математически выверенные шаги для устранения разницы, будь то перевыборы лидера, маршрутизация трафика через балансировщик или мягкая эвакуация подов. Понимание этих тайм-аутов и механизмов — граница, отделяющая начинающего пользователя от инженера, готового к production-инцидентам.

    2. Подготовка виртуальных машин в VCenter ESXi: спецификации ресурсов и сетевая топология проекта

    Около 70% проблем с производительностью и стабильностью Kubernetes-кластеров, развёрнутых on-premise, закладываются задолго до выполнения команды kubeadm init. Кластер оркестрации — это надстройка, которая слепо доверяет инфраструктурному слою. Если виртуальная машина мастера испытывает микрозадержки при записи на диск, алгоритм Raft в etcd начнёт терять лидера, что приведёт к каскадному отказу Control Plane, даже если процессоры загружены всего на 10%. В виртуализированной среде VMware vSphere (ESXi) абстракция железа создаёт дополнительные риски: неверно выбранный тип виртуального сетевого адаптера или контроллера диска способен незаметно «душить» сетевой плагин кластера.

    Архитектура вычислительных ресурсов: сайзинг узлов

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

    Master-узлы (Control Plane)

    В нашей топологии запланировано три мастер-узла: labs-k8s-m01, labs-k8s-m02, labs-k8s-m03. Их главная задача — управление состоянием кластера, а не выполнение пользовательских нагрузок.

    Сердцем Control Plane является база данных etcd. Это key-value хранилище, которое требует максимальной скорости последовательной записи. По спецификациям etcd, время синхронизации данных на диск (fsync) не должно превышать 50 миллисекунд. Если гипервизор не может обеспечить такую скорость, etcd начинает отправлять предупреждения о медленной записи, а при длительных задержках узлы начинают считать лидера мёртвым, запуская процесс перевыборов (Leader Election).

    !Влияние задержки диска на стабильность etcd

    Минимальные требования для мастер-узла в тестовой среде или для подготовки к CKA:

  • CPU: 2 vCPU. Меньше нельзя — компоненты Control Plane (API-сервер, контроллеры, планировщик) и etcd будут конкурировать за процессорное время, вызывая таймауты.
  • RAM: 4 ГБ. API-сервер кэширует множество объектов в памяти для быстрого ответа на запросы.
  • Disk: 30–50 ГБ.
  • Для продакшн-сред эти значения обычно удваиваются (4 vCPU, 8 ГБ RAM), так как с ростом количества подов и сервисов в кластере кратно возрастает нагрузка на kube-apiserver.

    Worker-узлы (Data Plane)

    Узлы labs-k8s-w01 и labs-k8s-w02 предназначены для запуска контейнеров с приложениями. Их сайзинг полностью зависит от профиля нагрузки. Однако важно понимать концепцию Allocatable Resources (доступных ресурсов).

    Если вы выделяете воркеру 4 ГБ оперативной памяти, Kubernetes не сможет отдать все 4 ГБ вашим подам. Часть ресурсов резервируется для операционной системы (OS System Daemon) и самого агента kubelet (Kube-Reserved). Если не учитывать этот налог на инфраструктуру, можно столкнуться с ситуацией, когда поды не планируются на узел из-за нехватки памяти, хотя утилита htop внутри ОС показывает свободные ресурсы.

    Базовые параметры для наших воркеров:

  • CPU: 2–4 vCPU.
  • RAM: 4–8 ГБ.
  • Disk: 50–100 ГБ. Контейнеры, их логи и образы (images) хранятся в /var/lib/containerd, поэтому дисковое пространство здесь расходуется быстрее всего.
  • Балансировщик нагрузки

    Узел labs-k8s-balancer выполняет роль единой точки входа для Control Plane. На нём будут работать только легковесные сервисы: HAProxy (проксирование TCP-трафика на порт 6443 мастер-узлов) и Keepalived (управление виртуальным IP-адресом).
  • CPU: 1–2 vCPU.
  • RAM: 1–2 ГБ.
  • Disk: 20 ГБ.
  • Оптимизация виртуального оборудования в vCenter

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

    Дисковая подсистема: PVSCSI и Thick Provisioning

    По умолчанию vCenter может предложить контроллер LSI Logic Parallel или LSI Logic SAS. Для Kubernetes (особенно для дисков, на которых работает etcd) необходимо выбирать VMware Paravirtual SCSI (PVSCSI).

    PVSCSI — это контроллер, осведомлённый о том, что он работает в виртуальной среде. Он переносит часть задач по обработке очередей ввода-вывода с процессора гостевой ОС на гипервизор, что значительно снижает потребление CPU при высоких показателях IOPS (операций ввода-вывода в секунду) и увеличивает глубину очереди.

    Формат диска также имеет значение. Существует три типа выделения места:

  • Thin Provisioning (тонкий диск) — место на физическом хранилище выделяется по мере записи. При первой записи в новый блок гипервизор тратит время на его обнуление. Это создаёт микрозадержки.
  • Thick Provisioning Lazy Zeroed — место резервируется сразу, но блоки обнуляются при первой записи.
  • Thick Provisioning Eager Zeroed — место резервируется, и все блоки немедленно заполняются нулями при создании диска.
  • Для узлов Control Plane настоятельно рекомендуется использовать Thick Provisioning Eager Zeroed. Это исключает любые задержки (penalty) на выделение и обнуление блоков со стороны системы хранения данных (СХД) во время работы кластера.

    Сетевые адаптеры: VMXNET3

    Никогда не используйте адаптер E1000 для узлов Kubernetes. E1000 — это программная эмуляция физической сетевой карты Intel 82545EM. Гипервизору приходится переводить каждую команду гостевой ОС в инструкции для эмулируемого железа, что вызывает огромный оверхед.

    VMXNET3 — это паравиртуализованный сетевой адаптер, созданный специально для виртуальных сред. Он не эмулирует физическое устройство, а напрямую взаимодействует с сетевым стеком гипервизора через кольцевые буферы в памяти. VMXNET3 поддерживает разгрузку контрольных сумм (checksum offloading), TCP segmentation offloading (TSO) и обеспечивает пропускную способность до 10 Гбит/с при минимальной нагрузке на CPU. Сетевой плагин Flannel, который мы будем использовать, инкапсулирует трафик (создаёт оверлейную сеть), что само по себе требует процессорного времени, поэтому экономия ресурсов на уровне драйвера интерфейса абсолютно необходима.

    Сетевая топология проекта

    Все узлы кластера должны находиться в одном широковещательном домене (L2-сети), чтобы балансировщик Keepalived мог корректно использовать протокол VRRP для передачи виртуального IP-адреса (VIP), а сетевой плагин мог маршрутизировать трафик без сложных настроек BGP.

    Для узлов Kubernetes критически важно использовать статические IP-адреса. Если IP-адрес мастер-узла изменится после перезагрузки, сертификаты API-сервера, выданные для старого IP, станут недействительными, и кластер будет разрушен.

    !Сетевая топология отказоустойчивого кластера

    Зафиксируем топологию, которая будет использоваться на протяжении всего курса:

    | Имя хоста (Hostname) | Роль | IP-адрес | Комментарий | | :--- | :--- | :--- | :--- | | labs-k8s-balancer | Load Balancer | 10.3.60.50 | Хостит HAProxy и Keepalived | | labs-k8s-vip | Virtual IP (VIP) | 10.3.60.100 | Плавающий IP, смотрит на balancer | | labs-k8s-m01 | Control Plane 1 | 10.3.60.51 | Инициализатор кластера | | labs-k8s-m02 | Control Plane 2 | 10.3.60.52 | Реплика etcd / API | | labs-k8s-m03 | Control Plane 3 | 10.3.60.53 | Реплика etcd / API | | labs-k8s-w01 | Worker Node 1 | 10.3.60.54 | Вычислительный узел | | labs-k8s-w02 | Worker Node 2 | 10.3.60.55 | Вычислительный узел |

    Все машины используют единый шлюз по умолчанию (Gateway) и DNS-сервер, настроенные в вашей корпоративной сети.

    Правила размещения (DRS Anti-Affinity)

    Отказоустойчивость кластера на уровне программного обеспечения (Kubernetes) не имеет смысла, если она не подкреплена отказоустойчивостью на уровне аппаратного обеспечения.

    Предположим, у вас есть кластер VMware vSphere, состоящий из трёх физических серверов (ESXi-хостов). Если гипервизор случайным образом разместит все три виртуальные машины мастер-узлов (m01, m02, m03) на одном физическом сервере ESXi, то выход из строя этого физического сервера приведёт к одновременной потере всех трёх мастеров. Кворум etcd будет безвозвратно утерян, и кластер Kubernetes перестанет функционировать.

    Чтобы этого избежать, в vCenter необходимо настроить правила DRS Anti-Affinity (правила разделения виртуальных машин). Эти правила принудительно запрещают гипервизору запускать указанные виртуальные машины на одном физическом хосте.

  • Создаётся правило типа «Separate Virtual Machines».
  • В него добавляются labs-k8s-m01, labs-k8s-m02, labs-k8s-m03.
  • При миграции vMotion или перезагрузках DRS (Distributed Resource Scheduler) будет гарантировать, что мастера всегда распределены по разным физическим серверам (Failure Domains).
  • Подготовка эталонного образа ОС (Golden Image)

    Устанавливать Debian 12 вручную на шесть виртуальных машин — нерациональная трата времени, чреватая ошибками конфигурации. Правильный подход — создать одну виртуальную машину, настроить её базовые параметры, превратить в шаблон (Template) в vCenter и клонировать остальные машины из неё.

    Базовая установка Debian 12 Bookworm

    При установке ОС с ISO-образа выбирается минимальная конфигурация. Графический интерфейс (GNOME/KDE) строго запрещён — он потребляет оперативную память и увеличивает площадь атаки. Из пакетов достаточно выбрать только «Standard system utilities» и «SSH server».

    Сразу после установки необходимо установить пакет open-vm-tools. Это открытая реализация VMware Tools, которая позволяет гипервизору корректно выключать гостевую ОС (Graceful shutdown), синхронизировать время с хостом ESXi и получать данные об IP-адресах машины.

    Проблема клонирования: Machine-ID

    Это самая частая и неочевидная ловушка при развёртывании через клонирование. В современных дистрибутивах Linux на базе systemd каждая система имеет уникальный идентификатор, хранящийся в файле /etc/machine-id.

    Этот идентификатор используется множеством служб. Например, DHCP-клиент использует его для формирования DUID (DHCP Unique Identifier), чтобы получать один и тот же IP-адрес. Что ещё важнее, сетевые плагины Kubernetes (например, Calico или Flannel) и сам kubelet могут использовать производные от этого ID для идентификации узла в кластере.

    Если вы просто склонируете виртуальную машину, все клоны будут иметь одинаковый /etc/machine-id. В результате:

  • Воркер-узлы могут перезаписывать статусы друг друга в API-сервере, так как кластер будет считать их одним и тем же физическим узлом.
  • Возникнут конфликты при выдаче IP-адресов, если используется DHCP.
  • Перед тем как выключить эталонную машину и конвертировать её в шаблон, необходимо очистить этот файл: truncate -s 0 /etc/machine-id При следующей загрузке (уже клонированной машины) systemd обнаружит пустой файл и сгенерирует новый, уникальный идентификатор. Также рекомендуется удалить ключи SSH-сервера (rm -f /etc/ssh/ssh_host_*), чтобы при загрузке клона сгенерировались новые уникальные криптографические ключи.

    Отключение Swap

    Исторически Kubernetes требовал полного отключения файла подкачки (swap) на уровне операционной системы. Причина кроется в механизмах работы планировщика (kube-scheduler). Планировщик распределяет поды по узлам на основе запросов (requests) и лимитов (limits) памяти. Если ОС начнёт сбрасывать страницы памяти контейнеров на медленный жесткий диск (в swap), производительность приложения непредсказуемо деградирует. Kubernetes не сможет гарантировать Quality of Service (QoS) для подов.

    Начиная с версии Kubernetes 1.28, поддержка swap перешла в стадию Beta, и её можно включить через специальные флаги kubelet. Однако для сертификации CKA и в классических on-premise инсталляциях золотым стандартом остаётся полное отключение подкачки.

    В эталонном образе Debian 12 необходимо отключить swap до создания шаблона:

  • Отключить текущий swap в оперативной памяти: swapoff -a
  • Удалить или закомментировать строку монтирования swap-раздела в файле /etc/fstab, чтобы он не включился после перезагрузки.
  • Подготовленный таким образом эталонный образ Debian 12 с настроенными паравиртуализованными драйверами (PVSCSI, VMXNET3), очищенными идентификаторами и отключенным swap станет надежным фундаментом. Процесс клонирования шести виртуальных машин из этого шаблона в vCenter займет несколько минут, после чего инфраструктура будет полностью готова к тонкой настройке ядра Linux и установке балансировщика.

    3. Базовая настройка ОС Debian Bookworm: оптимизация ядра, управление модулями и сетевыми параметрами

    Базовая настройка ОС Debian Bookworm: оптимизация ядра, управление модулями и сетевыми параметрами

    Чистое ядро Linux «из коробки» понятия не имеет, что такое контейнер, под или Service в Kubernetes. Для операционной системы контейнер — это просто изолированный процесс, а виртуальная сеть кластера — набор непонятных пакетов, которые по правилам безопасности по умолчанию должны быть отброшены. Чтобы превратить стандартный сервер на базе Debian 12 в полноценный узел кластера, способный маршрутизировать сотни тысяч пакетов в секунду и мгновенно запускать легковесные образы, необходимо изменить поведение ядра. Этот процесс называется bootstrapping (начальная подготовка) узла, и любая ошибка на этом этапе приведет к плавающим сетевым сбоям, которые крайне сложно отладить в работающем кластере.

    Все описанные ниже настройки должны быть применены абсолютно идентично на всех узлах нашей топологии: от labs-k8s-m01 до labs-k8s-w02.

    Подготовка файловой системы: модуль OverlayFS

    Контейнеры не копируют файловую систему операционной системы при каждом запуске — это было бы слишком медленно и требовало бы терабайты дискового пространства. Вместо этого используется механизм слоистых файловых систем. В Kubernetes стандартом де-факто является драйвер overlay, реализующий файловую систему OverlayFS.

    OverlayFS объединяет несколько независимых директорий на хосте так, что процесс внутри контейнера видит их как единое целое. Механизм опирается на три сущности:

  • LowerDir (Нижний слой) — директория, доступная только для чтения. Здесь лежат файлы базового образа (например, чистый Debian или Alpine). Если запустить десять подов с Nginx, они все будут читать один и тот же LowerDir с диска узла, экономя оперативную память и место.
  • UpperDir (Верхний слой) — директория, доступная для записи. Она уникальна для каждого запущенного контейнера. Если контейнер изменяет файл из базового образа, OverlayFS прозрачно копирует этот файл из LowerDir в UpperDir, и контейнер модифицирует уже копию. Этот принцип называется Copy-on-Write (CoW).
  • Merged (Объединенное представление) — виртуальная точка монтирования, которую фактически видит приложение внутри контейнера.
  • !Структура слоев OverlayFS

    Чтобы Containerd (среда выполнения контейнеров, которую мы установим позже) мог создавать такие структуры, ядро должно иметь загруженный модуль overlay.

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

    Команда modprobe overlay загружает его в текущей сессии без перезагрузки.

    Интеграция виртуальных мостов и сетевого экрана: модуль br_netfilter

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

    Каждый под получает свой собственный виртуальный сетевой интерфейс. На уровне хоста эти интерфейсы подключаются к виртуальному коммутатору — мосту (bridge), например cni0. Когда пакет выходит из пода, он попадает на этот мост.

    По умолчанию в Linux трафик, проходящий через мост (L2 уровень модели OSI), не передается на обработку в Netfilter (встроенный в ядро межсетевой экран, которым управляет утилита iptables). Ядро считает, что раз пакет просто перекладывается из одного порта коммутатора в другой, применять к нему правила маршрутизации L3 не нужно.

    Для Kubernetes это поведение фатально. Компонент kube-proxy, работающий на каждом узле, реализует концепцию Service (ClusterIP) именно через правила iptables. Если под обращается к базе данных по виртуальному IP-адресу сервиса, этот пакет выходит на мост. Если мост не передаст пакет в iptables, правило подмены адреса (DNAT) не сработает, и пакет будет отброшен, так как виртуального IP физически не существует в сети. Модуль br_netfilter заставляет ядро передавать весь bridged-трафик на инспекцию в iptables.

    Тонкая настройка сетевого стека (sysctl)

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

    Конфигурация задается в файле /etc/sysctl.d/k8s.conf:

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

    Активация инспекции мостового трафика

    Параметры net.bridge.bridge-nf-call-iptables = 1 и net.bridge.bridge-nf-call-ip6tables = 1 являются прямым продолжением работы модуля br_netfilter. Единица означает жесткое требование: любой L2-кадр, проходящий через программный мост Linux, содержащий внутри IPv4 или IPv6 пакет, должен быть отправлен в цепочки iptables.

    !Маршрутизация пакета через мост и Netfilter

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

    Разрешение пересылки пакетов (IP Forwarding)

    Параметр net.ipv4.ip_forward = 1 превращает сервер из конечного узла сети в маршрутизатор.

    Рассмотрим конкретный пример из нашей топологии. Допустим, на узле labs-k8s-w01 (IP 10.3.60.54) работает под Frontend с внутренним адресом . Ему нужно отправить запрос к поду Backend, который находится на узле labs-k8s-w02 (IP 10.3.60.55) и имеет адрес .

    Пакет формируется на w01 с адресом назначения . Он доходит до сетевого стека узла labs-k8s-w02 через туннель Flannel. Когда ядро узла w02 получает пакет, оно смотрит на адрес назначения () и сравнивает его со своим физическим адресом (). Поскольку адреса не совпадают, стандартное поведение Linux — отбросить чужой пакет (Drop).

    Установка ip_forward = 1 говорит ядру: «Если ты получил пакет, предназначенный не тебе, но ты знаешь маршрут к адресату (в данном случае — виртуальный интерфейс пода Backend), перешли его дальше». Без этого параметра меж-узловое взаимодействие подов невозможно.

    Применение настроек выполняется командой:

    Она зачитывает все конфигурационные файлы из /etc/sysctl.d/ и применяет их к работающему ядру.

    Синхронизация времени и криптография

    В распределенных системах точное время — это не вопрос удобства чтения логов, а фундамент работоспособности. В Kubernetes от времени зависят два критических механизма: консенсус базы данных etcd и валидация TLS-сертификатов.

    Все компоненты Control Plane общаются между собой и с воркерами исключительно по зашифрованным каналам (mTLS). При генерации сертификата в него зашиваются временные метки Not Before (недействителен до) и Not After (недействителен после). Если время на узле labs-k8s-w01 отстает от времени на labs-k8s-m01 на 5 минут, а срок жизни временного сертификата для какого-либо внутреннего процесса составляет те же 5 минут, узел w01 сочтет сертификат мастера просроченным (или еще не вступившим в силу). Соединение будет разорвано, и узел перейдет в статус NotReady.

    База данных etcd, использующая алгоритм Raft, опирается на таймауты (heartbeats) для поддержания лидерства. Жесткая рассинхронизация времени может спровоцировать ложные выборы лидера и деградацию производительности Control Plane.

    В Debian 12 по умолчанию используется легковесный демон systemd-timesyncd. Для гарантии его работы необходимо убедиться, что NTP-синхронизация включена:

    Проверка статуса timedatectl status должна показывать System clock synchronized: yes. В изолированных контурах VCenter (без доступа в интернет) потребуется указать внутренний NTP-сервер компании в файле /etc/systemd/timesyncd.conf.

    Отключение Swap на уровне ОС

    Ранее был разобран фундаментальный конфликт между механизмом подкачки (swap) и планировщиком Kubernetes (kube-scheduler), который должен гарантировать подам заявленные ресурсы (Quality of Service). На практике отключение swap в Debian 12 требует двух шагов.

    Первый шаг — немедленное отключение для текущей сессии:

    Второй шаг — предотвращение включения после перезагрузки. Для этого необходимо закомментировать строку, содержащую слово swap, в таблице файловых систем /etc/fstab. Если этого не сделать, после плановой перезагрузки узла kubelet обнаружит активный swap и аварийно завершит свою работу, оставив узел в нерабочем состоянии.

    Управление межсетевым экраном хоста

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

    Компонент kube-proxy динамически управляет правилами iptables. При создании, удалении или масштабировании сервисов он непрерывно добавляет и удаляет сотни правил. Если параллельно с ним работает UFW, возникает состояние гонки (race condition). UFW может перезаписать цепочки, созданные kube-proxy, или заблокировать динамически выделенные порты NodePort (диапазон 30000-32767).

    Для корректной работы кластера рекомендуется полностью отключить UFW (если он был установлен) и очистить существующие правила:

    Безопасность узлов в кластере обеспечивается не локальным файрволом ОС, а аппаратными межсетевыми экранами на уровне инфраструктуры VCenter (ограничивающими доступ к подсетям 10.3.60.0/24 извне) и сетевыми политиками самого Kubernetes (NetworkPolicies), которые реализуются через CNI плагин Flannel.

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

    4. Настройка балансировщика нагрузки: реализация отказоустойчивого входа через Keepalived и HAProxy

    Настройка балансировщика нагрузки: реализация отказоустойчивого входа через Keepalived и HAProxy

    Представьте, что рабочий узел (worker node), на котором крутятся критичные для бизнеса контейнеры, должен обновить свой статус. Он отправляет сетевой запрос к мастер-ноде. Но за секунду до этого на мастере сгорела материнская плата или произошел сбой ядра. Если конфигурация рабочего узла жестко привязана к IP-адресу этого конкретного мастера, узел окажется «осиротевшим». Он не сможет получать новые задачи и сообщать о своем состоянии, несмотря на то, что в кластере есть еще два абсолютно здоровых мастера, готовых принять запрос.

    Чтобы избежать такой ситуации, компоненты Kubernetes (kubelet на рабочих узлах, kube-proxy, а также внешние администраторы с утилитой kubectl) никогда не должны обращаться к конкретным серверам Control Plane напрямую. Им нужна стабильная, неизменная точка входа — фасад, который скроет за собой реальное количество мастер-нод и их текущее состояние. Эту задачу решает связка из балансировщика нагрузки (HAProxy) и менеджера отказоустойчивых IP-адресов (Keepalived).

    Архитектура единой точки входа: VIP и балансировка

    В нашей топологии выделен один сервер labs-k8s-balancer с физическим адресом 10.3.60.50. Однако мы не будем настраивать кластер Kubernetes на использование этого адреса. Вместо этого мы введем концепцию VIP (Virtual IP) — виртуального IP-адреса, который не привязан жестко к сетевой карте на аппаратном уровне, а управляется программно.

    Для нашего проекта мы выделим адрес 10.3.60.100 в качестве VIP. Именно этот адрес будет указан при инициализации кластера, и именно к нему будут обращаться все рабочие узлы.

    Возникает закономерный вопрос: если у нас всего один сервер-балансировщик, зачем усложнять схему виртуальным адресом и Keepalived? Ответ кроется в проектировании архитектуры под будущий рост (future-proofing). Привязав кластер к аппаратному IP .50, мы создадим единую точку отказа (SPOF — Single Point of Failure), которую невозможно устранить без перенастройки всего кластера (перевыпуска сертификатов и изменения конфигурации всех узлов). Используя VIP .100 с самого начала, мы оставляем возможность в любой момент развернуть второй сервер-балансировщик. Keepalived просто начнет делить этот VIP между двумя серверами, а сам кластер Kubernetes даже не заметит изменений в инфраструктуре.

    HAProxy: прозрачная маршрутизация на 4 уровне модели OSI

    В веб-разработке балансировщики часто работают на 7-м (прикладном) уровне модели OSI — они анализируют HTTP-заголовки, читают URL и могут маршрутизировать запросы на основе их содержимого. Для Kubernetes API такой подход губителен.

    Компоненты кластера общаются с kube-apiserver с использованием mTLS (Mutual TLS) — двусторонней аутентификации, где и сервер, и клиент предъявляют друг другу криптографические сертификаты. Если балансировщик попытается работать на L7, ему придется расшифровать трафик (TLS Termination), прочитать его, а затем зашифровать снова. Это сломает цепочку доверия: kube-apiserver увидит сертификат балансировщика, а не реального клиента (например, kubelet), и отклонит запрос.

    Поэтому HAProxy должен работать строго на 4-м (транспортном) уровне. Он принимает TCP-пакеты и пересылает их на мастер-ноды байт в байт, не пытаясь заглянуть внутрь зашифрованного туннеля. Это называется TCP Passthrough.

    !Схема маршрутизации L4 через HAProxy

    Конфигурация HAProxy и активные проверки здоровья

    На сервере labs-k8s-balancer установим HAProxy и разберем его конфигурацию, которая располагается в /etc/haproxy/haproxy.cfg.

    Мы создадим два логических блока: frontend (то, что слушает входящие запросы) и backend (пул серверов, куда эти запросы отправляются).

    Ключевые параметры, требующие глубокого понимания:

  • mode tcp: Директива, которая переключает HAProxy с дефолтного HTTP-режима в режим L4. Без нее балансировщик попытается распарсить трафик и разорвет соединения.
  • bind 10.3.60.100:6443: HAProxy будет слушать порт 6443 (стандартный порт apiserver) только на виртуальном адресе VIP.
  • balance roundrobin: Алгоритм распределения нагрузки. TCP-соединения будут распределяться по кругу: первый запрос на m01, второй на m02, третий на m03. Для долгоживущих TCP-сессий (какими являются watch-запросы в Kubernetes) это оптимальный выбор, обеспечивающий равномерную загрузку мастеров.
  • option tcp-check: Указывает HAProxy активно проверять доступность порта 6443 на мастер-нодах.
  • check fall 3 rise 2: Параметры агрессивности проверок. Если мастер-нода не ответит на 3 проверки подряд (fall 3), HAProxy пометит ее как мертвую и перестанет отправлять на нее трафик. Как только нода успешно ответит на 2 проверки подряд (rise 2), она вернется в пул. Разница между значениями fall и rise предотвращает ситуацию «флаппинга» (flapping), когда нестабильная нода каждую секунду то добавляется, то удаляется из пула.
  • Keepalived и протокол VRRP: удержание виртуального IP

    HAProxy отлично справляется с распределением трафика, но он сам по себе является обычным процессом в ОС. Если сервер labs-k8s-balancer выключится, VIP 10.3.60.100 исчезнет из сети. Чтобы управлять жизненным циклом VIP, используется демон Keepalived, реализующий протокол VRRP (Virtual Router Redundancy Protocol).

    VRRP был создан для маршрутизаторов, но отлично прижился в Linux. Его суть: несколько серверов объединяются в виртуальный маршрутизатор (Virtual Router ID, VRID). Один из них становится Мастером (Master) и берет на себя VIP, остальные переходят в режим Ожидания (Backup).

    Мастер непрерывно рассылает multicast-пакеты (анонсы) в локальную сеть, сообщая: «Я жив, VIP у меня». Если Backup-узлы перестают получать эти пакеты в течение определенного времени, они инициируют выборы нового Мастера.

    Время, через которое резервный узел поймет, что основной упал, и заберет VIP себе, рассчитывается по стандарту VRRP:

    Где:

  • — время до признания текущего Мастера мертвым (в секундах).
  • — интервал рассылки анонсов (обычно 1 секунда).
  • — приоритет данного резервного узла (число от 1 до 255).
  • Дробная часть формулы (называемая skew time) гарантирует, что если в сети несколько резервных узлов, тот, у кого приоритет выше, получит меньшее время ожидания и первым захватит VIP, исключая конфликт (split-brain) на уровне балансировщиков.

    !Симуляция перехвата VIP при отказе узла

    Мониторинг локального процесса

    Keepalived должен следить не только за соседями по сети, но и за локальным процессом HAProxy. Если сервер физически работает, сетевая карта активна, но сам демон HAProxy завис или упал, сервер будет продолжать рассылать VRRP-анонсы. Трафик будет идти на этот сервер, но обрабатывать его будет некому. Это сценарий «черной дыры».

    Для предотвращения этого в Keepalived используется механизм vrrp_script — скрипт, который регулярно проверяет состояние нужного процесса. Если скрипт возвращает ошибку, Keepalived добровольно снижает свой приоритет или переходит в состояние FAULT, сбрасывая с себя VIP.

    Конфигурация Keepalived

    Создадим файл /etc/keepalived/keepalived.conf на сервере labs-k8s-balancer:

    Разбор критичных параметров:

  • script "killall -0 haproxy": Команда killall -0 не убивает процесс, а лишь проверяет, имеет ли система право отправить ему сигнал (то есть существует ли процесс). Это самый легковесный и надежный способ проверить, жив ли HAProxy, не порождая тяжелых bash-скриптов.
  • interval 2: Проверка выполняется каждые 2 секунды.
  • weight -20: Если процесс HAProxy не найден, приоритет этого узла в VRRP будет снижен на 20 единиц (станет 80 вместо 100). В нашей конфигурации с одним узлом это пока ни на что не повлияет, но при добавлении второго балансировщика с приоритетом 90, это заставит VIP переехать на здоровый узел.
  • state MASTER: Начальное состояние узла.
  • interface ens192: Имя сетевого интерфейса (в Debian 12 на VMware VMXNET3 оно обычно выглядит так, но перед настройкой обязательно сверьтесь с выводом команды ip a).
  • virtual_router_id 51: Уникальный идентификатор группы VRRP. Должен быть одинаковым на всех балансировщиках нашего кластера и уникальным в пределах L2-сети.
  • virtual_ipaddress { 10.3.60.100/24 }: Тот самый VIP, который Keepalived поднимет на интерфейсе ens192.
  • Практическая реализация на Debian 12: проблема Non-local Bind

    Прежде чем запускать настроенные сервисы, мы столкнемся с фундаментальной защитой ядра Linux.

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

    Но в динамичной среде VRRP это становится проблемой. При перезагрузке сервера HAProxy может стартовать на миллисекунду раньше, чем Keepalived успеет провести выборы и назначить VIP 10.3.60.100 на интерфейс. HAProxy попытается выполнить bind 10.3.60.100:6443, получит от ядра ошибку Cannot bind to IP address и завершится с фатальным сбоем (Crash Loop).

    Чтобы разрешить эту гонку состояний (Race Condition), мы должны явно указать ядру Linux разрешить привязку к нелокальным адресам.

    На сервере labs-k8s-balancer открываем файл системных параметров: nano /etc/sysctl.d/99-kubernetes-cri.conf (можно добавить в тот же файл, который мы создавали на этапе базовой настройки ОС) и добавляем строку:

    Применяем изменения без перезагрузки: sysctl --system

    Только после этого параметра связка Keepalived + HAProxy становится по-настоящему отказоустойчивой и независимой от порядка запуска системных служб.

    Запуск и верификация работы

    После того как конфигурационные файлы созданы, а ядро настроено, мы перезапускаем службы и добавляем их в автозагрузку:

    Как проверить, что наша архитектура входа работает, если сам кластер Kubernetes (и kube-apiserver) еще даже не установлен на мастер-нодах?

    Шаг 1: Проверка наличия VIP на интерфейсе. Выполняем команду ip a show ens192 (или ваше имя интерфейса) на сервере labs-k8s-balancer. В выводе, помимо основного адреса 10.3.60.50, вы должны увидеть строку: inet 10.3.60.100/24 scope global secondary ens192 Это означает, что Keepalived успешно стартовал, объявил себя Мастером и поднял виртуальный адрес.

    Шаг 2: Проверка прослушивания порта. Выполняем ss -tlnp | grep 6443. Вы должны увидеть, что процесс haproxy слушает адрес 10.3.60.100:6443. Это подтверждает, что параметр ip_nonlocal_bind сработал корректно.

    Шаг 3: Проверка маршрутизации (логи HAProxy). Если мы сейчас попытаемся подключиться к VIP (например, с рабочей станции выполним nc -vz 10.3.60.100 6443), соединение будет установлено (HAProxy ответит), но сразу же разорвано.

    Почему? Потому что HAProxy принял запрос на frontend, попытался передать его в backend, но алгоритм активной проверки (check fall 3) уже пометил узлы m01, m02 и m03 как недоступные (ведь Kubernetes там еще нет, и порт 6443 закрыт).

    Это абсолютно нормальное и ожидаемое поведение на данном этапе. Вы можете заглянуть в системный лог: journalctl -u haproxy | grep "Server kubernetes-master-nodes" Там будут записи вида Server ... is DOWN, reason: Layer4 connection problem.

    Наш фасад полностью готов. Балансировщик находится в боевом дежурстве, удерживает VIP и непрерывно опрашивает мастер-ноды. Как только на следующем этапе мы инициализируем kube-apiserver на узле m01, HAProxy мгновенно обнаружит открытый порт, пометит сервер как UP и начнет прозрачно проксировать к нему весь трафик от кластера.

    5. Установка среды выполнения контейнеров Containerd и системных утилит Kubernetes

    Установка среды выполнения контейнеров Containerd и системных утилит Kubernetes

    Kubernetes сам по себе не умеет запускать контейнеры. Будучи оркестратором, он лишь принимает декларативные манифесты и вычисляет, на каком узле должна оказаться рабочая нагрузка. Фактическое создание изолированных процессов, загрузка образов по сети и монтирование файловых систем делегируется специализированному программному обеспечению — среде выполнения контейнеров (Container Runtime). На каждом узле нашего кластера, будь то мастер или воркер, должна быть установлена и настроена такая среда, а также системные агенты, которые будут ею управлять.

    Эволюция сред выполнения и стандарт CRI

    Исторически Kubernetes был жестко привязан к Docker. Код для управления демоном Docker находился прямо внутри исходного кода Kubernetes (так называемый dockershim). Это создавало огромные проблемы: релизные циклы двух независимых проектов не совпадали, а монолитная архитектура Docker, включающая сборщик образов, утилиты для работы с томами и собственную сеть, была избыточна для оркестратора, которому нужно было только запускать контейнеры.

    Чтобы разорвать эту зависимость, был разработан Container Runtime Interface (CRI) — стандартизированный gRPC API. Теперь Kubernetes через свой локальный агент (kubelet) отправляет gRPC-запросы, а любая среда выполнения, реализующая этот интерфейс, их обрабатывает.

    В современных инсталляциях стандартом де-факто стал Containerd. Это легковесный, стабильный демон, который изначально был частью Docker, но затем был выделен в независимый проект под эгидой CNCF.

    !Схема взаимодействия Kubelet, Containerd и OCI-runtime

    Архитектура запуска контейнера через Containerd выглядит как многослойный пирог:

  • Kubelet вызывает CRI-плагин внутри Containerd, передавая спецификацию пода.
  • Containerd скачивает образ из реестра и подготавливает корневую файловую систему (используя OverlayFS).
  • Containerd не запускает контейнер напрямую. Он порождает процесс containerd-shim.
  • containerd-shim вызывает низкоуровневую утилиту runc (соответствующую стандарту OCI — Open Container Initiative).
  • runc обращается к ядру Linux, создает пространства имен (namespaces) и контрольные группы (cgroups), запускает процесс приложения и немедленно завершает свою работу.
  • Процесс приложения остается дочерним по отношению к containerd-shim.
  • Наличие containerd-shim — критический элемент отказоустойчивости. Благодаря этой прослойке демон Containerd можно перезапускать или обновлять прямо на лету, не убивая при этом запущенные контейнеры. Shim-процесс перехватывает стандартные потоки ввода-вывода (stdout/stderr) контейнера и поддерживает его жизненный цикл автономно от главного демона.

    Управление ресурсами: конфликт Cgroup-драйверов

    Контрольные группы (cgroups) — это механизм ядра Linux, который ограничивает потребление ресурсов (CPU, память, I/O) для процессов. В Debian 12 по умолчанию используется современная реализация cgroup v2.

    Управлять иерархией cgroups можно двумя способами (через два разных драйвера):

  • cgroupfs: прямое создание директорий и файлов в /sys/fs/cgroup/.
  • systemd: делегирование управления подсистеме инициализации systemd, которая централизованно распределяет ресурсы через юниты (slices и scopes).
  • Поскольку Debian 12 использует systemd в качестве системы инициализации (PID 1), ядро ОС уже управляет ресурсами через systemd. Если мы оставим Containerd и kubelet с настройками по умолчанию (они часто используют cgroupfs), возникнет архитектурный конфликт: два независимых менеджера будут пытаться управлять одними и теми же ресурсами.

    В условиях высоких нагрузок это приводит к рассинхронизации учета памяти. Узел может начать непредсказуемо убивать поды (OOM Killer) или перейти в статус NotReady, так как systemd и kubelet будут иметь разные представления о том, сколько памяти реально доступно. Чтобы кластер был стабильным, необходимо жестко перевести Containerd и kubelet на использование драйвера systemd.

    Практическая установка Containerd на Debian 12

    Все описанные ниже действия должны быть выполнены абсолютно на всех узлах будущей инфраструктуры: labs-k8s-m01, m02, m03, labs-k8s-w01 и w02.

    Сначала установим необходимые пакеты для работы с HTTPS-репозиториями:

    Containerd предоставляется из официального репозитория Docker. Добавим GPG-ключ и сам репозиторий:

    После обновления индексов устанавливаем пакет:

    Конфигурация config.toml и Pause-контейнер

    По умолчанию containerd.io устанавливается с минимальной конфигурацией, которая не подходит для Kubernetes (в ней может быть отключен CRI-плагин). Нам нужно сгенерировать полный конфигурационный файл и внести в него изменения.

    Открыв файл /etc/containerd/config.toml, необходимо найти секцию, отвечающую за опции runc, и включить драйвер systemd. Путь к этому параметру в TOML-файле выглядит так: [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]

    В этой секции нужно изменить значение SystemdCgroup с false на true.

    Кроме того, в этом же конфигурационном файле определяется параметр sandbox_image. По умолчанию он указывает на образ вроде registry.k8s.io/pause:3.9.

    Pause-контейнер (или инфраструктурный контейнер) — это фундаментальная концепция пода в Kubernetes. Под — это не один процесс, а группа контейнеров, разделяющих общие пространства имен (сеть, IPC). Чтобы это работало, Containerd сначала запускает микроскопический Pause-контейнер. Его единственная задача — запросить у ядра сетевое пространство имен, получить IP-адрес и затем уйти в бесконечный сон (вызвать системный вызов pause()). Все остальные контейнеры приложения (рабочие и init-контейнеры) затем присоединяются к пространству имен, которое удерживает этот Pause-контейнер. Если sandbox_image недоступен из-за сетевых ограничений, ни один под на узле не запустится.

    Применяем настройки перезапуском демона:

    Установка системных утилит Kubernetes: kubeadm, kubelet и kubectl

    Следующим шагом является установка базового набора инструментов Kubernetes. Исторически пакеты размещались на серверах Google (apt.kubernetes.io), но с 2023 года проект переехал на инфраструктуру, управляемую сообществом — pkgs.k8s.io.

    Репозитории теперь разделены по минорным версиям. Это значит, что для установки Kubernetes версии 1.30 мы подключаем репозиторий именно для v1.30. Это защищает кластер от случайного мажорного обновления при выполнении apt-get upgrade.

    Добавляем ключ и репозиторий для версии 1.30:

    Теперь устанавливаем три ключевых компонента:

    Роли компонентов

    Kubeadm — это утилита жизненного цикла кластера. Она не работает как фоновый демон. Вы запускаете ее только в момент инициализации кластера (kubeadm init), добавления новых узлов (kubeadm join) или обновления сертификатов. Она генерирует ключи, создает статические манифесты для Control Plane и настраивает базовую конфигурацию.

    Kubectl — клиент командной строки для взаимодействия с kube-apiserver. Он не обязателен на воркер-нодах, но его традиционно ставят везде для удобства отладки.

    Kubelet — это «капитан» узла. В отличие от компонентов Control Plane (apiserver, scheduler), которые будут запущены как контейнеры, kubelet работает как обычный системный процесс (systemd-сервис). Его задача — слушать команды от apiserver по сети, транслировать их в gRPC-вызовы для Containerd и следить за тем, чтобы статус запущенных подов соответствовал ожидаемому.

    Блокировка версий (Version Skew Policy)

    Сразу после установки необходимо заблокировать версии этих пакетов:

    Если проигнорировать этот шаг, автоматические обновления безопасности Debian или ручной запуск apt-get upgrade могут обновить kubelet на воркер-ноде до версии, превышающей версию Control Plane.

    Kubernetes имеет строгую политику перекоса версий (Version Skew Policy). Компоненты Control Plane должны обновляться первыми. Kubelet может отставать от kube-apiserver максимум на три минорные версии (например, apiserver v1.30, а kubelet v1.27), но никогда не должен быть новее apiserver. Если kubelet на воркере обновится до v1.31, а мастера останутся на v1.30, узел перестанет корректно взаимодействовать с кластером, так как новый kubelet может использовать API-вызовы, о которых старый apiserver еще не знает. Команда apt-mark hold гарантирует, что обновление Kubernetes будет происходить только осознанно и под вашим контролем.

    CrashLoop Kubelet до инициализации

    Если после установки проверить статус службы kubelet (systemctl status kubelet), вы увидите, что она находится в состоянии перезапуска (CrashLoop) и генерирует ошибки в системный журнал.

    Это абсолютно штатное поведение. Kubelet при запуске ищет конфигурационный файл /var/lib/kubelet/config.yaml и сертификаты для аутентификации в кластере. Поскольку мы еще не запускали kubeadm init (на мастерах) или kubeadm join (на воркерах), этих файлов не существует. Kubelet будет циклично падать и перезапускаться каждые несколько секунд, ожидая, пока администратор не предоставит ему необходимые данные для подключения к Control Plane.

    На данном этапе все пять узлов нашей инфраструктуры (3 мастера и 2 воркера) имеют идентичную базовую конфигурацию: настроенную ОС, отключенный swap, установленный Containerd с драйвером systemd и ожидающий конфигурации kubelet. Фундамент готов, и следующим шагом станет превращение первого узла labs-k8s-m01 в полноценный центр управления кластером.

    6. Инициализация первой мастер-ноды: конфигурация Kubeadm и создание эталонного состояния кластера

    Инициализация первой мастер-ноды: конфигурация Kubeadm и создание эталонного состояния кластера

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

    Декларативный подход к конфигурации кластера

    Утилиту kubeadm init можно запустить, передав ей длинную строку флагов командной строки. Однако в профессиональной среде и на сертификационных экзаменах вроде CKA (Certified Kubernetes Administrator) стандартом де-факто является использование декларативного конфигурационного файла в формате YAML.

    Этот подход решает три критические задачи:

  • Воспроизводимость и версионирование. Файл конфигурации можно сохранить в Git-репозитории.
  • Доступ к расширенным параметрам. Не все настройки Kubernetes API можно передать через флаги kubeadm. Некоторые специфические параметры планировщика или контроллеров доступны только через конфигурационный файл.
  • Управление отказоустойчивостью. Для HA-кластера (High Availability) требуется точная настройка конечных точек и сертификатов, которую крайне сложно безошибочно описать в одной строке.
  • Для нашего кластера, где балансировщик нагрузки имеет адрес 10.3.60.50, а виртуальный IP-адрес (VIP) Keepalived настроен на 10.3.60.100, мы создадим файл kubeadm-config.yaml на первой мастер-ноде (labs-k8s-m01).

    Конфигурация состоит из нескольких логических блоков, разделенных ---. Основные из них — InitConfiguration (настройки конкретного узла, на котором запускается команда) и ClusterConfiguration (глобальные настройки всего кластера).

    Ключевым параметром в ClusterConfiguration является controlPlaneEndpoint. Он указывает на адрес, через который все компоненты (воркеры, администраторы, внешние системы) будут обращаться к API-серверу. В отказоустойчивом кластере здесь жестко прописывается VIP-адрес балансировщика и порт HAProxy: 10.3.60.100:6443. Если указать здесь физический IP первой мастер-ноды, кластер навсегда останется привязан к ней, и при ее падении управление будет потеряно, даже если остальные мастера работают.

    Второй критический параметр — certSANs (Subject Alternative Names). Когда kubeadm генерирует TLS-сертификат для kube-apiserver, он по умолчанию включает туда локальный IP-адрес узла и внутренние DNS-имена кластера. Если администратор попытается подключиться к кластеру через VIP-адрес 10.3.60.100, клиент kubectl получит сертификат, в котором этого адреса нет, и выдаст фатальную ошибку x509: certificate is valid for 10.3.60.51, not 10.3.60.100. Массив certSANs принудительно добавляет в сертификат все IP-адреса и DNS-имена, через которые возможен доступ к API.

    Третий важный элемент — podSubnet. Сетевые плагины (CNI), такие как Flannel, требуют строго определенной подсети, из которой будут выдаваться IP-адреса подам. Для Flannel историческим стандартом является 10.244.0.0/16. Если не указать этот параметр при инициализации, CNI-плагин не сможет корректно настроить маршрутизацию.

    Анатомия загрузки: фазы инициализации

    Когда администратор запускает инициализацию, kubeadm выполняет строго определенную последовательность действий (фаз).

    !Пошаговая визуализация фаз инициализации kubeadm init

    Фаза Preflight (Предлетные проверки) Система проверяет готовность узла. Доступен ли сокет Containerd? Отключен ли swap? Открыт ли порт 6443? Совпадают ли версии kubelet и kubeadm? Если хотя бы одна проверка не пройдена, процесс прерывается до внесения изменений.

    Фаза Certs (Генерация инфраструктуры открытых ключей) Kubernetes использует взаимную TLS-аутентификацию (mTLS) для всех внутренних коммуникаций. На этом этапе в директории /etc/kubernetes/pki создается корневой центр сертификации (CA) кластера, а также CA для etcd и агрегации фронтенда. Затем генерируются сертификаты для API-сервера, клиентов etcd, менеджера контроллеров и планировщика. Каждый компонент получает свой уникальный криптографический паспорт.

    Фаза Kubeconfig (Создание файлов конфигурации) Генерируются файлы конфигурации в директории /etc/kubernetes/. Это admin.conf (для суперпользователя), kubelet.conf, controller-manager.conf и scheduler.conf. Внутрь этих файлов встраиваются только что сгенерированные клиентские сертификаты. Именно через эти файлы системные компоненты понимают, как авторизоваться на API-сервере.

    Статические поды: решение парадокса курицы и яйца

    На следующем этапе инициализации возникает архитектурный парадокс. Kubernetes управляет контейнерами (подами). Компоненты Control Plane (etcd, apiserver, scheduler, controller-manager) сами должны работать как поды. Но чтобы запустить под, нужно отправить запрос в API-сервер, который передаст его планировщику, а тот назначит под на узел. Как запустить API-сервер, если API-сервер еще не работает?

    Для решения этой проблемы используется механизм статических подов (Static Pods).

    !Взаимодействие kubelet с файловой системой и Containerd при запуске статических подов

    Статический под — это под, которым управляет не Control Plane кластера, а непосредственно локальный демон kubelet на конкретном сервере.

    При старте kubeadm генерирует YAML-манифесты для etcd и компонентов Control Plane и сохраняет их в директорию /etc/kubernetes/manifests/. Служба kubelet, запущенная через systemd (мы обсуждали это в предыдущей главе), непрерывно сканирует эту директорию. Как только она обнаруживает новые файлы, она напрямую обращается к Containerd через CRI (Container Runtime Interface) и приказывает запустить контейнеры, полностью игнорируя отсутствие API-сервера.

    Как только статические поды запущены, кластер фактически оживает:

  • Поднимается база данных etcd.
  • Запускается kube-apiserver, подключаясь к etcd.
  • Запускаются kube-scheduler и kube-controller-manager, которые начинают слушать API-сервер.
  • Завершение инициализации: токены, RBAC и системные аддоны

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

    Система создает Bootstrap Token. Это временный криптографический секрет (обычно валидный 24 часа), который будет использоваться другими узлами (мастерами и воркерами) для безопасного присоединения к кластеру. Токен позволяет новому узлу доказать API-серверу, что он имеет право запросить полноценный клиентский сертификат (процесс TLS Bootstrapping).

    Далее kubeadm загружает конфигурацию кластера в ConfigMap с именем kubeadm-config в пространстве имен kube-system. Это позволяет новым мастер-нодам при присоединении скачать эталонную конфигурацию, а не требовать от администратора ручного копирования YAML-файла на каждый сервер.

    В самом конце разворачиваются системные аддоны: CoreDNS (для внутреннего разрешения имен сервисов) и kube-proxy (для маршрутизации трафика сервисов на узлах). В отличие от компонентов Control Plane, они запускаются не как статические поды, а как стандартные ресурсы Kubernetes (Deployment и DaemonSet), отправленные через API.

    Практическая реализация на labs-k8s-m01

    Соберем все знания воедино и подготовим файл конфигурации на узле labs-k8s-m01. Создадим файл kubeadm-config.yaml:

    Обратите внимание на блок apiServer.certSANs. Мы внесли туда VIP-адрес (10.3.60.100), физический IP балансировщика (10.3.60.50) и DNS-имена всех будущих мастер-нод. Это гарантирует, что при любом векторе обращения к API-серверу TLS-соединение будет признано доверенным.

    Теперь запускаем процесс инициализации. Для отказоустойчивого кластера команда содержит один критически важный флаг:

    Флаг --upload-certs решает фундаментальную проблему дистрибуции ключей. Сертификаты и ключи CA (Certificate Authority), созданные на фазе Certs, физически лежат в /etc/kubernetes/pki на сервере m01. Чтобы узлы m02 и m03 могли стать полноценными мастерами, им нужны копии этих закрытых ключей.

    В старых версиях Kubernetes администратору приходилось вручную копировать папку pki по SSH с первой ноды на остальные. Флаг --upload-certs автоматизирует это: kubeadm шифрует все сертификаты случайным ключом и загружает их в секрет kubeadm-certs внутри самого кластера. Секрет автоматически удалится через 2 часа из соображений безопасности. В выводе команды администратор получит ключ дешифровки, который нужно будет передать узлам m02 и m03 при их присоединении.

    После успешного выполнения команды (которое занимает 2-4 минуты), консоль выдаст инструкции по настройке доступа и готовые команды kubeadm join для воркеров и мастеров.

    Верификация: чтение состояния кластера

    Чтобы начать управлять кластером, утилите kubectl нужен файл kubeconfig, содержащий клиентский сертификат администратора. kubeadm создал его в /etc/kubernetes/admin.conf.

    Настроим окружение для пользователя root:

    Теперь проверим состояние узлов:

    Вывод покажет один узел labs-k8s-m01, но его статус будет NotReady. Это абсолютно нормальное и ожидаемое поведение на данном этапе. Узел не перейдет в статус Ready, пока на нем не заработает сетевой плагин (CNI). Kubelet сообщает API-серверу: «Я работаю, контейнеры запускать могу, но сеть для них настроить не умею, жду инструкций».

    Проверим состояние системных подов:

    В выводе мы увидим, что поды etcd, kube-apiserver, kube-controller-manager и kube-scheduler находятся в статусе Running. Это те самые статические поды, которые kubelet запустил из директории /etc/kubernetes/manifests/.

    Поды kube-proxy также будут в статусе Running. А вот поды coredns будут "висеть" в статусе Pending.

    Статус Pending означает, что планировщик не может найти подходящий узел для запуска пода. Почему он не может разместить CoreDNS на единственной доступной мастер-ноде? Причина снова в отсутствии сети. CoreDNS — это обычный (не статический) под, которому требуется IP-адрес из диапазона podSubnet. Пока CNI не установлен, кластер не умеет выдавать IP-адреса подам, и планировщик блокирует их запуск, защищая систему от создания неработоспособных контейнеров.

    На данном этапе мы получили полностью функционирующий, но пока изолированный Control Plane. База данных etcd состоит из одного узла, балансировщик нагрузки HAProxy уже направляет трафик на порт 6443 сервера m01, а кластер ожидает развертывания оверлейной сети, чтобы позволить узлам обмениваться трафиком на уровне подов.

    7. Установка и настройка сетевого плагина Flannel: организация Overlay-сети и маршрутизации трафика

    Установка и настройка сетевого плагина Flannel: организация Overlay-сети и маршрутизации трафика

    Сразу после успешного выполнения kubeadm init на первой мастер-ноде кластер находится в парадоксальном состоянии. Если выполнить команду kubectl get nodes, узел labs-k8s-m01 будет иметь статус NotReady. Если проверить системные поды командой kubectl get pods -n kube-system, окажется, что критически важный компонент coredns завис в состоянии Pending, хотя API-сервер, планировщик и контроллер-менеджер успешно работают. Причина этого состояния кроется в фундаментальном архитектурном решении Kubernetes: он не умеет самостоятельно маршрутизировать трафик между контейнерами. Кластер ожидает установки сетевого плагина.

    Стандарт CNI и сетевая модель Kubernetes

    Kubernetes делегирует все задачи по созданию сетевых интерфейсов, назначению IP-адресов и настройке таблиц маршрутизации сторонним решениям, которые реализуют спецификацию CNI (Container Network Interface). CNI — это стандартизированный API, позволяющий среде выполнения контейнеров (в нашем случае Containerd) вызывать внешний бинарный файл при создании или удалении пода.

    Сетевая модель Kubernetes предъявляет к любому CNI-плагину три жестких требования:

  • Любой под может связываться с любым другим подом на любом узле без использования трансляции сетевых адресов (NAT).
  • Любой узел может связываться со всеми подами на этом узле (и на других узлах) без NAT.
  • IP-адрес, который под видит внутри себя, в точности совпадает с IP-адресом, который видят другие поды при обращении к нему.
  • В нашей инфраструктуре на базе VCenter узлы labs-k8s-m01 (10.3.60.51) и labs-k8s-m02 (10.3.60.52) находятся в одной физической подсети L2. Однако поды будут получать адреса из совершенно другого диапазона, который мы задали при инициализации кластера — 10.244.0.0/16. Физические коммутаторы и маршрутизаторы ничего не знают об этой подсети. Если под с узла m01 попытается отправить пакет поду на узле m02, физическая сеть просто отбросит его, так как не найдет маршрута к 10.244.x.x.

    Для решения этой проблемы сетевые плагины строят Overlay-сеть — виртуальную логическую сеть, развернутую поверх существующей физической инфраструктуры. В качестве инструмента реализации Overlay-сети мы используем Flannel, настроенный на работу с протоколом VXLAN.

    Архитектура Flannel и магия инкапсуляции VXLAN

    Протокол VXLAN (Virtual eXtensible Local Area Network) решает задачу передачи L2-трафика (Ethernet-кадров) через L3-сеть (IP-сеть). Он делает это с помощью механизма инкапсуляции.

    Когда пакет покидает пределы узла, ядро Linux перехватывает его и упаковывает целиком (вместе с исходными IP-адресами подов) внутрь нового UDP-пакета. В качестве адресов источника и назначения во внешнем пакете указываются реальные физические IP-адреса узлов кластера (10.3.60.x). Физическая сеть видит обычный UDP-трафик между двумя серверами и успешно его доставляет. На принимающем узле внешний пакет отбрасывается (декапсулируется), и оригинальный пакет передается целевому поду.

    !WHAT: infographic diagram showing VXLAN packet encapsulation structure, horizontal layout. Show nested layers. Outer layers (left to right): Outer MAC, Outer IP, UDP Header, VXLAN Header. Inner layers (encapsulated payload): Inner MAC, Inner IP, TCP/UDP Payload. LABELS: "Outer Ethernet", "Outer IP", "UDP (8472)", "VXLAN (50 bytes)", "Original Pod Packet". AVOID: complex 3D perspective, hardware routers, human figures.

    Поскольку инкапсуляция добавляет дополнительные заголовки (внешний Ethernet, внешний IP, внешний UDP и заголовок VXLAN), размер полезной нагрузки, которую можно передать в одном кадре, уменьшается. Стандартный размер максимального блока данных (MTU) в сети Ethernet равен 1500 байт. Заголовки VXLAN занимают 50 байт. Таким образом, MTU внутри виртуальной сети Flannel вычисляется как байт. Если не учесть это снижение и попытаться отправить из пода пакет размером 1500 байт, он будет фрагментирован, что приведет к резкому падению производительности сети. Flannel автоматически настраивает MTU 1450 на сетевых интерфейсах подов.

    Для работы VXLAN на каждом узле создается специальный виртуальный интерфейс — VTEP (VXLAN Tunnel Endpoint). Во Flannel он всегда носит имя flannel.1. Именно этот интерфейс занимается упаковкой и распаковкой трафика на уровне ядра ОС.

    Распределение подсетей (Subnet Allocation)

    Диапазон 10.244.0.0/16 содержит более 65 тысяч адресов. Чтобы избежать конфликтов, когда два пода на разных узлах получают одинаковый IP, Flannel жестко закрепляет за каждым узлом непересекающийся блок адресов.

    По умолчанию Flannel выделяет каждому узлу подсеть с префиксом /24 (256 адресов, из которых для подов доступно 254).

  • Узел labs-k8s-m01 может получить блок 10.244.0.0/24.
  • Узел labs-k8s-m02 получит блок 10.244.1.0/24.
  • Узел labs-k8s-w01 получит блок 10.244.2.0/24.
  • Информация о том, какому узлу принадлежит какая подсеть, сохраняется в базе данных etcd (через Kubernetes API). На каждом узле кластера запускается агент flanneld в виде DaemonSet. Этот агент непрерывно отслеживает API-сервер. Как только в кластере появляется новый узел, flanneld узнает о выделенной ему подсети и автоматически обновляет локальные таблицы маршрутизации и базу пересылки (FDB — Forwarding Data Base) интерфейса flannel.1.

    Установка Flannel и согласование конфигурации

    Установка сетевого плагина выполняется путем применения YAML-манифеста. Однако перед этим критически важно убедиться, что конфигурация Flannel совпадает с параметрами, переданными в kubeadm init.

    В главе об инициализации мастера мы указали в kubeadm-config.yaml параметр podSubnet: "10.244.0.0/16". В стандартном манифесте Flannel (в секции ConfigMap kube-flannel-cfg) сеть по умолчанию задана как 10.244.0.0/16. Если бы при инициализации кластера был выбран другой диапазон (например, 10.200.0.0/16), манифест Flannel пришлось бы скачать, отредактировать вручную и только потом применять. В нашем случае параметры совпадают, поэтому можно применить манифест напрямую.

    Команда установки на узле labs-k8s-m01:

    При выполнении этой команды в кластере создается ряд объектов:

  • ServiceAccount, ClusterRole и ClusterRoleBinding: предоставляют агенту flanneld права на чтение информации об узлах и запись аннотаций в Kubernetes API.
  • ConfigMap: содержит конфигурацию сети (Network) и тип бэкенда (Backend: vxlan).
  • DaemonSet: гарантирует, что на каждом узле кластера (и на мастерах, и на воркерах) будет запущен ровно один под с агентом flanneld.
  • Как только под kube-flannel-ds запускается на узле labs-k8s-m01, он считывает конфигурацию, создает интерфейс flannel.1, генерирует файл конфигурации CNI в директории /etc/cni/net.d/10-flannel.conflist и сообщает процессу kubelet, что сеть готова.

    Пошаговый путь пакета от пода к поду

    Чтобы по-настоящему понимать работу сетевой подсистемы Kubernetes, необходимо проследить путь пакета на самом низком уровне. Рассмотрим ситуацию, когда кластер уже полностью развернут. На узле labs-k8s-m01 работает Под А с IP-адресом 10.244.0.5. На узле labs-k8s-m02 работает Под B с IP-адресом 10.244.1.6. Под А выполняет HTTP-запрос к Поду B.

    !Маршрутизация пакета от пода к поду через интерфейс flannel.1

  • Внутри Пода А (10.244.0.5): Пакет формируется с адресом назначения 10.244.1.6. Сетевой стек пода проверяет свою локальную таблицу маршрутизации. Поскольку адрес назначения находится за пределами локальной подсети пода, пакет отправляется на шлюз по умолчанию. Шлюзом для всех подов на узле выступает виртуальный мост cni0 (с адресом 10.244.0.1).
  • На уровне ядра узла m01: Пакет выходит из пода через пару виртуальных интерфейсов veth и попадает на мост cni0 в корневом сетевом пространстве имен (root network namespace) узла.
  • Маршрутизация узла m01: Ядро Debian анализирует пакет. В таблице маршрутизации узла есть запись, созданная демоном flanneld: все пакеты, предназначенные для сети 10.244.1.0/24, должны быть направлены на устройство flannel.1.
  • Инкапсуляция на интерфейсе flannel.1: Пакет попадает на VTEP-интерфейс. Драйвер VXLAN в ядре ОС обращается к своей таблице FDB. Он ищет ответ на вопрос: «Какой физический IP-адрес соответствует узлу, обслуживающему подсеть 10.244.1.0/24?». База данных отвечает: 10.3.60.52. Ядро упаковывает исходный пакет в UDP-дейтаграмму. Внешний IP-источник — 10.3.60.51, внешний IP-назначения — 10.3.60.52, порт назначения — 8472 (стандартный порт VXLAN во Flannel).
  • Физическая сеть: Сформированный UDP-пакет отправляется через физический интерфейс eth0 узла m01 в коммутатор VCenter, который доставляет его на интерфейс eth0 узла m02.
  • Декапсуляция на узле m02: Ядро узла m02 принимает UDP-пакет на порт 8472. Драйвер VXLAN распознает его, распаковывает и извлекает оригинальный пакет (от 10.244.0.5 к 10.244.1.6).
  • Доставка Поду B: Распакованный пакет передается в таблицу маршрутизации узла m02. Ядро видит, что сеть 10.244.1.0/24 подключена к локальному мосту cni0. Пакет перенаправляется на cni0, а оттуда через veth-интерфейс попадает внутрь сетевого пространства имен Пода B.
  • Этот сложный процесс происходит за доли миллисекунды и абсолютно прозрачен для самих контейнеров. Поды "думают", что находятся в одной большой плоской сети.

    Верификация работоспособности сети

    После применения манифеста Flannel состояние кластера стремительно меняется. Чтобы убедиться, что Overlay-сеть развернута корректно, необходимо провести несколько проверок.

    Во-первых, узел должен перейти в рабочее состояние:

    Статус labs-k8s-m01 изменится с NotReady на Ready. Это происходит потому, что служба kubelet регулярно проверяет наличие конфигурации в /etc/cni/net.d/. Как только Flannel создает там файл, kubelet проводит инициализацию сети и отправляет API-серверу сигнал NetworkReady=True.

    Во-вторых, стагнация системных компонентов должна прекратиться:

    Под coredns, который ранее находился в состоянии Pending, перейдет в статус Running. Планировщик Kubernetes отказывался назначать DNS-сервер на узел без рабочей сети, так как поду CoreDNS требуется IP-адрес для обслуживания запросов. С появлением CNI-плагина под получает адрес из пула 10.244.0.0/24 и успешно запускается.

    В-третьих, изменения можно увидеть на уровне самой операционной системы Debian. Если выполнить команду ip a, в списке интерфейсов появятся два новых устройства. Интерфейс flannel.1 (VTEP) будет иметь MAC-адрес и IPv4-адрес, представляющий данный узел в Overlay-сети. Интерфейс cni0 (виртуальный мост) получит адрес 10.244.0.1 — он будет выступать шлюзом для всех подов на узле labs-k8s-m01.

    Наконец, команда ip route покажет, как ядро Linux планирует обрабатывать трафик. В выводе появится маршрут для локальной подсети подов, направленный на мост cni0. По мере добавления в кластер новых узлов (m02, m03, w01, w02), демон flanneld будет динамически добавлять в эту таблицу новые маршруты, направляя трафик для чужих подсетей на интерфейс flannel.1.

    Установка сетевого плагина завершает формирование базового фундамента Control Plane на первом узле. Кластер обрел способность маршрутизировать трафик, выдавать IP-адреса и разрешать DNS-имена. Локальная база etcd работает, API-сервер доступен через балансировщик нагрузки Keepalived по VIP-адресу. Система полностью готова к масштабированию и присоединению остальных мастер-нод для формирования полноценного отказоустойчивого кворума.

    8. Масштабирование Control Plane: репликация etcd и объединение мастеров в кворум

    Масштабирование Control Plane: репликация etcd и объединение мастеров в кворум

    Прямо сейчас наш кластер представляет собой иллюзию надежности. Узел labs-k8s-m01 успешно инициализирован, CNI-плагин Flannel маршрутизирует трафик, а балансировщик нагрузки HAProxy готов распределять запросы. Однако вся база данных состояния кластера, все криптографические ключи и компоненты управления физически находятся на одном сервере. Если питание labs-k8s-m01 отключится в эту секунду, кластер перестанет существовать как управляемая сущность. Чтобы превратить одиночный сервер в полноценную отказоустойчивую систему (High Availability), необходимо клонировать вычислительные компоненты и, что гораздо сложнее, синхронизировать распределенное хранилище состояния.

    Анатомия команды присоединения мастер-узла

    Добавление рабочего узла (воркера) в Kubernetes — тривиальная задача: узел получает сертификат, запускает kubelet и начинает слушать команды от API-сервера. Добавление узла Control Plane (labs-k8s-m02 и labs-k8s-m03) — это фундаментально иной процесс. Новый мастер должен не просто подчиняться командам, он должен стать равноправным участником управления, получить доступ к корневым сертификатам кластера и интегрироваться в базу данных etcd.

    Для этого используется расширенная версия команды присоединения, сгенерированная при инициализации первого узла:

    kubeadm join 10.3.60.100:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash> --control-plane --certificate-key <key>

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

  • Флаг --control-plane дает инструкцию утилите kubeadm не просто настроить kubelet, но и сгенерировать манифесты статических подов для kube-apiserver, kube-controller-manager, kube-scheduler и etcd.
  • Флаг --certificate-key содержит симметричный ключ шифрования.
  • Механика передачи сертификатов требует особого внимания. Корневой центр сертификации (CA) кластера, ключи для подписи сервисных аккаунтов (SA) и CA для etcd были сгенерированы на labs-k8s-m01. Чтобы labs-k8s-m02 мог выполнять функции мастера, он должен обладать точными копиями этих закрытых ключей. Передавать их по сети в открытом виде или копировать через SSH вручную — нарушение стандартов безопасности.

    Вместо этого при выполнении kubeadm init с флагом --upload-certs (который мы использовали ранее) первый мастер зашифровал все критичные ключи симметричным ключом и поместил их в специальный объект Secret в пространстве имен kube-system. Этот Secret имеет короткий срок жизни (по умолчанию 2 часа). Когда labs-k8s-m02 выполняет команду join, он аутентифицируется по токену, скачивает этот зашифрованный Secret и расшифровывает его локально с помощью --certificate-key. Таким образом, корневые секреты безопасно мигрируют на новый узел.

    !Топология отказоустойчивого Control Plane

    Фазы интеграции нового мастера

    Процесс, запускаемый на labs-k8s-m02, строго детерминирован и состоит из нескольких фаз, сбой на любой из которых прерывает установку.

    Сначала происходит загрузка и расшифровка корневых сертификатов (CA). Получив их, kubeadm не копирует сертификаты API-сервера с первой ноды, а генерирует новые локальные сертификаты (Server и Peer), подписанные скачанным CA. Это критически важное различие: корневые сертификаты на всех мастерах идентичны, но сертификат самого kube-apiserver на labs-k8s-m02 уникален — он содержит IP-адрес 10.3.60.52, тогда как сертификат на m01 содержит 10.3.60.51. Обеспечивается это благодаря тому, что оба подписаны одним центром сертификации, которому доверяют все компоненты кластера.

    Следующий и самый сложный этап — расширение кластера etcd. База данных etcd не допускает появления новых узлов «из ниоткуда». Если просто запустить контейнер etcd на m02 и указать ему подключаться к m01, первый узел отвергнет соединение. Защита алгоритма Raft требует, чтобы кластер сначала административно одобрил добавление нового участника.

    Поэтому kubeadm действует в два шага:

  • Выступает в роли клиента и отправляет API-запрос к существующему кластеру etcd (на m01): «Зарегистрируй нового участника с адресом https://10.3.60.52:2380».
  • Только после успешной регистрации kubeadm генерирует манифест статического пода etcd для m02, инструктируя его запуститься в режиме initial-cluster-state=existing (подключение к существующему кластеру, а не создание нового).
  • !Процесс добавления узла в etcd и синхронизация логов

    Когда etcd на m02 стартует, его база данных абсолютно пуста. Он связывается с лидером (в данный момент это m01) и запрашивает снимок состояния. Лидер приостанавливает очистку старых логов и передает полный дамп базы данных по сети. После применения дампа m02 начинает получать инкрементальные обновления (Raft logs), пока полностью не догонит лидера. Только после этого m02 получает право голоса в кластере.

    Опасность промежуточного состояния: кластер из двух узлов

    Подключение labs-k8s-m02 переводит наш кластер в крайне специфическое состояние. Теперь у нас два узла Control Plane. Интуитивно может показаться, что надежность системы возросла вдвое. Математика алгоритма консенсуса утверждает обратное: отказоустойчивость кластера в этот момент равна нулю.

    Кворум — это строгое большинство узлов, необходимое для принятия любых решений (записи новых данных, выбора лидера). Формула кворума: , где — общее количество зарегистрированных узлов.

    Рассмотрим динамику состояний:

  • Исходное состояние (m01): . Кворум составляет . Кластер работает, но выход из строя одного узла означает полную потерю работоспособности. Допустимое количество отказов: .
  • Промежуточное состояние (m01 + m02): . Кворум составляет . Для работы кластера etcd требуются оба узла.
  • Если в состоянии из двух узлов сервер m02 выйдет из строя, оставшийся m01 увидит, что в сети доступен только 1 узел из 2 зарегистрированных. Так как 1 меньше требуемого кворума (2), m01 немедленно снимет с себя полномочия лидера и переведет базу данных в режим «только чтение» для защиты от split-brain. API-сервер Kubernetes перестанет принимать любые запросы на изменение состояния (невозможно создать под, обновить Deployment или удалить сервис).

    Именно поэтому архитектура из двух мастер-узлов в Kubernetes является антипаттерном. Она не только не добавляет отказоустойчивости, но и удваивает вероятность отказа всей системы: аппаратный сбой любого из двух серверов приведет к остановке Control Plane.

    Восстановление ключей и присоединение узлов на практике

    Токены и ключи шифрования, сгенерированные при инициализации, имеют ограниченный срок действия в целях безопасности. Токен для присоединения живет 24 часа, а ключ расшифровки сертификатов (--certificate-key) — всего 2 часа. Если с момента настройки m01 прошло больше времени, оригинальная команда join вернет ошибку аутентификации или расшифровки.

    В реальной практике развертывания администратору часто приходится генерировать эти данные заново. На узле labs-k8s-m01 необходимо выполнить две команды:

    Создание нового токена и вывод базовой команды join: kubeadm token create --print-join-command

    Повторная загрузка сертификатов в Secret и генерация нового ключа шифрования: kubeadm init phase upload-certs --upload-certs

    Получив свежие данные, мы конструируем итоговую команду и выполняем ее на сервере labs-k8s-m02: kubeadm join 10.3.60.100:6443 --token <новый_токен> --discovery-token-ca-cert-hash sha256:<хеш> --control-plane --certificate-key <новый_ключ>

    Процесс занимает около минуты. В логах консоли будет видно, как kubeadm скачивает сертификаты, генерирует kubeconfig для компонентов и ожидает запуска статических подов. Как только kubelet на m02 прочитает манифесты из /etc/kubernetes/manifests/ и запустит контейнеры, узел присоединится к кластеру.

    Сразу после успешного завершения команды на m02, необходимо без промедления выполнить абсолютно ту же самую команду на сервере labs-k8s-m03.

    Добавление третьего узла переводит кластер в целевое состояние. Теперь . Кворум составляет . В этой конфигурации кластер может пережить отказ одного любого узла (3-1=2, что равно кворуму). Если m03 сгорит, оставшиеся m01 и m02 сохранят кворум, выберут нового лидера etcd (если им был m03) и продолжат обслуживать запросы на запись. Отказоустойчивость достигнута.

    Верификация распределенной системы

    После присоединения всех трех мастеров необходимо убедиться, что система работает как единое целое, а не как три независимых сервера. Проверка статуса узлов через kubectl get nodes покажет наличие labs-k8s-m01, m02 и m03 в статусе Ready (благодаря уже установленному CNI Flannel). Однако этот статус отражает лишь состояние kubelet и сетевого плагина. Он не гарантирует, что база данных etcd успешно реплицируется.

    Для глубокой проверки здоровья распределенного хранилища используется утилита etcdctl. Поскольку etcd работает внутри статического пода, проще всего выполнить команду через kubectl exec, обратившись к контейнеру на первом мастере. При этом необходимо передать утилите клиентские сертификаты для аутентификации:

    В выводе этой команды критически важны три метрики для каждого из трех эндпоинтов:

  • IS_LEADER: Только один узел должен иметь значение true. Остальные — false. Это подтверждает, что выборы лидера прошли успешно и split-brain отсутствует.
  • RAFT_TERM: Номер текущей эпохи голосования. Должен быть абсолютно одинаковым на всех трех узлах. Разные значения термина указывают на то, что кластер распался на изолированные сегменты.
  • RAFT_INDEX: Номер последней зафиксированной транзакции. Эти значения должны быть идентичными или различаться на 1-2 единицы (если в кластер прямо сейчас идет запись). Значительное отставание индекса на одном из узлов говорит о проблемах с сетью или дисковой подсистемой — узел не успевает применять реплицируемые логи.
  • Вторая линия проверки — работоспособность балансировщика. На текущий момент kube-apiserver запущен на всех трех серверах. HAProxy на узле labs-k8s-balancer (10.3.60.50) регулярно отправляет TCP-проверки на порт 6443 каждого мастера. Если мы остановим службу kubelet на m01 и принудительно убьем контейнер API-сервера, HAProxy обнаружит недоступность порта и исключит m01 из пула балансировки.

    При этом для конечного пользователя или рабочего узла, обращающегося к кластеру по виртуальному адресу VIP (10.3.60.100), ничего не изменится. Запрос будет прозрачно перенаправлен на m02 или m03. Оставшиеся мастера обратятся к локальным экземплярам etcd, которые, благодаря сохранению кворума (2 из 3), корректно обработают транзакцию. Механизм Leader Election внутри kube-controller-manager и kube-scheduler заметит отсутствие лидера (если им был m01), дождется истечения срока аренды блокировки (Lease) в etcd и назначит новым активным контроллером экземпляр на m02 или m03. Логика отказоустойчивости замыкается, обеспечивая непрерывность управления даже при потере физического сервера.

    9. Подключение воркер-нод к кластеру: механизмы аутентификации и регистрации вычислительных ресурсов

    Когда новый вычислительный сервер (воркер-нода) пытается присоединиться к кластеру Kubernetes, для Control Plane он представляет собой абсолютно недоверенный объект — просто IP-адрес в сети, отправляющий сетевые запросы. У этого сервера пока нет ни криптографической идентичности, ни прав на запуск контейнеров, ни записей в базе данных etcd. Процесс превращения «голой» ОС Debian с установленным бинарным файлом kubelet в полноценный узел Data Plane — это строгая последовательность взаимных проверок, генерации сертификатов и регистрации аппаратных ресурсов.

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

    Архитектура доверия: Discovery и защита от MITM-атак

    Первая проблема, которую решает kubeadm при добавлении узла, — это проблема курицы и яйца в криптографии. Воркер-нода должна отправить секретный токен на API-сервер, чтобы пройти аутентификацию. Но как воркер-нода может быть уверена, что по адресу 10.3.60.100:6443 (наш VIP балансировщика) действительно находится легитимный API-сервер кластера, а не скомпрометированный хост, перехвативший IP-адрес (атака Man-in-the-Middle)?

    Для этого в команде kubeadm join используется параметр --discovery-token-ca-cert-hash.

    Когда мы выполняем команду присоединения на узлах labs-k8s-w01 и labs-k8s-w02, процесс начинается с фазы Discovery (обнаружения). Kubelet обращается к API-серверу по незащищенному каналу и запрашивает публичный сертификат удостоверяющего центра (CA) кластера. Получив этот сертификат, kubelet локально вычисляет его SHA-256 хеш и сравнивает с тем, что был передан в параметре --discovery-token-ca-cert-hash.

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

    Механизм TLS Bootstrapping

    Убедившись в подлинности API-сервера, kubelet должен получить собственный клиентский сертификат. Вручную генерировать сертификаты для сотен воркер-нод нецелесообразно, поэтому Kubernetes использует механизм автоматической выдачи — TLS Bootstrapping.

    !Процесс TLS Bootstrapping

    Процесс состоит из нескольких этапов, скрытых под капотом kubeadm:

  • Аутентификация по токену. Kubelet использует временный Bootstrap Token (например, abcdef.0123456789abcdef) для первичной авторизации. Этот токен привязан к системной группе system:bootstrappers:kubeadm:default-node-token.
  • Генерация ключей. Kubelet локально на воркер-ноде генерирует приватный RSA-ключ. Этот ключ никогда не покидает пределы узла (хранится в /var/lib/kubelet/pki/).
  • Запрос на подпись (CSR). Kubelet формирует Certificate Signing Request (CSR) и отправляет его API-серверу. В запросе он просит выдать ему сертификат, где в поле Subject (CN) будет указано имя узла (например, system:node:labs-k8s-w01), а в поле Organization (O) — группа system:nodes.
  • Автоматическое одобрение. Внутри kube-controller-manager на мастер-нодах работает специальный контроллер — csrsigning. Он видит входящий CSR от пользователя из группы system:bootstrappers, проверяет его корректность и автоматически подписывает его корневым ключом кластера.
  • Получение сертификата. Kubelet скачивает подписанный сертификат (сохраняется как kubelet-client-current.pem), отбрасывает временный токен и перезапускает свое соединение с API-сервером уже с использованием полноценного mTLS.
  • С этого момента узел labs-k8s-w01 обладает уникальной криптографической идентичностью.

    Изоляция узлов: NodeRestriction

    Получение сертификата с CN system:node:labs-k8s-w01 — это не просто формальность. В Kubernetes работает Admission Controller под названием NodeRestriction.

    Когда kubelet с узла w01 пытается обновить свой статус или запросить секреты для запущенных на нем подов, API-сервер проверяет его сертификат. NodeRestriction гарантирует, что узел w01 может изменять только свой собственный объект Node в etcd и может читать секреты/конфигмапы только тех подов, которые запланированы именно на него. Если скомпрометированный kubelet на w01 попытается прочитать секрет пода, работающего на w02, API-сервер отклонит запрос. Это фундаментальный механизм защиты Data Plane.

    Регистрация ресурсов: Capacity и Allocatable

    После успешной аутентификации kubelet формирует объект Node и отправляет его в API-сервер. В этот момент кластер узнает о существовании новых вычислительных мощностей. Но сколько именно ресурсов доступно для запуска пользовательских контейнеров?

    Kubelet сканирует аппаратное обеспечение сервера (читает /proc/cpuinfo, /proc/meminfo в Debian) и вычисляет два ключевых показателя: Capacity (общая физическая емкость) и Allocatable (доступно для выделения).

    !Структура распределения ресурсов узла

    Формула расчета доступных ресурсов выглядит следующим образом:

    Где:

  • (Allocatable) — ресурсы, которые планировщик Kubernetes может раздать пользовательским подам.
  • (Capacity) — полный физический объем ресурсов узла (например, 16 ГБ RAM).
  • (System Reserved) — ресурсы, зарезервированные для системных демонов ОС (sshd, systemd, journald).
  • (Kube Reserved) — ресурсы, зарезервированные для компонентов самого Kubernetes (kubelet, container runtime).
  • (Eviction Threshold) — неснижаемый остаток памяти, при достижении которого kubelet начинает экстренно убивать поды (OOM), чтобы спасти операционную систему от зависания (обычно 100 МБ).
  • Если на воркер-ноде labs-k8s-w01 установлено 16 ГБ оперативной памяти, планировщик никогда не позволит запустить на ней поды, суммарный Request которых равен 16 ГБ. Kubelet через механизм cgroups жестко ограничит контейнеры объемом Allocatable. Если пользовательский под попытается превысить этот лимит, ядро Linux (через OOM Killer) завершит процесс контейнера, защищая системные демоны Debian и сам kubelet от нехватки памяти.

    Практическое присоединение узлов

    Для инициализации процесса на узлах labs-k8s-w01 и labs-k8s-w02 используется команда, сгенерированная при инициализации первой мастер-ноды. Если токен истек (по умолчанию он живет 24 часа), на любой мастер-ноде можно сгенерировать новый командой kubeadm token create --print-join-command.

    Команда для воркера выглядит так:

    Обратите внимание на точку входа: 10.3.60.100:6443. Это виртуальный IP-адрес (VIP), управляемый Keepalived на балансировщике. Воркер-нода не знает о существовании конкретных мастеров m01, m02 или m03. Она обращается к абстрактному фасаду. Если в момент присоединения воркера мастер m01 выйдет из строя, HAProxy на балансировщике мгновенно перенаправит TCP-трафик на m02, и процесс TLS Bootstrapping завершится без ошибок.

    После выполнения команды на воркере, kubelet, который до этого находился в состоянии CrashLoop (ожидая конфигурации), находит сгенерированный файл /etc/kubernetes/kubelet.conf, читает оттуда сертификаты и успешно запускается как systemd-сервис.

    На мастер-ноде выполнение команды kubectl get nodes покажет новые узлы. Изначально они могут появиться в статусе NotReady. Это нормальное поведение: узел зарегистрировал свои ресурсы, но на нем еще не настроена сетевая маршрутизация для подов.

    Развертывание инфраструктурных DaemonSet

    Как только объект Node появляется в etcd, в дело вступают контроллеры Kubernetes, управляющие объектами типа DaemonSet. DaemonSet гарантирует, что определенный под будет запущен на каждом узле кластера (или на узлах, соответствующих определенным меткам).

    На новые воркер-ноды немедленно планируются два критически важных компонента:

  • Flannel (CNI)
  • Kube-proxy
  • Интеграция в Overlay-сеть

    Под Flannel запускается на labs-k8s-w01 и получает от API-сервера информацию о выделенной этому узлу подсети (например, 10.244.3.0/24). Демон flanneld создает на воркере виртуальный интерфейс flannel.1 (VTEP) и настраивает правила маршрутизации в таблице ядра Linux. Теперь ядро Debian знает, что пакеты, адресованные в подсеть 10.244.4.0/24 (которая выделена узлу w02), нужно инкапсулировать в UDP-пакеты и отправлять на физический IP 10.3.60.55.

    Только после успешной настройки интерфейса CNI-плагином, kubelet меняет статус узла с NotReady на Ready. С этого момента узел официально готов принимать пользовательскую нагрузку.

    Маршрутизация сервисов: роль Kube-proxy

    Пока Flannel обеспечивает связность «под-под» (L3 маршрутизация), kube-proxy решает другую задачу — маршрутизацию к объектам Service. Service в Kubernetes — это абстрактный, не существующий в реальности IP-адрес (ClusterIP), который балансирует трафик между несколькими подами.

    Когда kube-proxy запускается на новом воркере, он подключается к API-серверу и подписывается на изменения объектов Service и Endpoints. Если в кластере есть сервис с IP 10.96.0.10, за которым стоят три пода, kube-proxy использует подсистему netfilter ядра Debian (именно для этого мы ранее загружали модуль br_netfilter и включали bridge-nf-call-iptables).

    Он создает цепочки правил в iptables. Логика этих правил следующая: «Если какой-либо локальный процесс или под пытается отправить TCP-пакет на адрес 10.96.0.10, перехвати этот пакет на уровне ядра, измени адрес назначения (DNAT) на IP-адрес одного из трех реальных подов и отправь его дальше». Выбор конкретного пода осуществляется случайным образом через модуль statistic в iptables.

    Таким образом, kube-proxy превращает каждый воркер-узел в распределенный L4-балансировщик. Любой под на labs-k8s-w01 может обратиться к сервису, и ядро локальной ОС само решит, куда физически направить пакет, не обращаясь к центральному маршрутизатору.

    Завершение формирования кластера

    Подключение воркер-нод замыкает архитектурный контур кластера. Control Plane (узлы m01, m02, m03), защищенный кворумом etcd и балансировщиком, теперь имеет в своем распоряжении вычислительный пул (Data Plane в виде w01 и w02).

    Каждый узел прошел криптографическую проверку, получил сертификат, ограниченный политикой NodeRestriction, вычислил свои безопасные лимиты (Allocatable) и настроил локальные правила маршрутизации через flannel и kube-proxy. Кластер перешел из состояния набора разрозненных виртуальных машин в единую распределенную систему, готовую к развертыванию отказоустойчивых приложений.