Привет! Меня зовут Николай Митрофанов, я технический руководитель команды Deckhouse Core, которая занимается поддержкой взаимодействия между нашей платформой и самим Kubernetes.
Не секрет, что управление жизненным циклом узлов в «ванильном» Kubernetes имеет свои трудности. В статье я расскажу, как мы переосмыслили и автоматизировали этот процесс в Deckhouse Kubernetes Platform — от заказа узла у облачного провайдера до его удаления с корректным drain и пересозданием ресурсов.
Вы узнаете, какие компоненты за что отвечают, зачем нам столько контроллеров, как мы закрываем сценарии для облака и для «железа» и почему всё это не должно пугать администратора. Материал будет полезен инженерам, которые управляют кластерами в production, думают о масштабировании и хотят избавить свою платформу от хрупкости и ручного труда.

«Ванильный» Kubernetes и проблемы, которые он не решает
«Ванильный» Kubernetes предоставляет некоторый набор инструментов для управления узлом кластера. Один из ключевых — это kubeadm. Он позволяет развернуть кластер и ввести в него узел. Но при этом не решает ряд задач.
Например, в облачных сетапах перед созданием кластера нужно подготовить окружение — и kubeadm этого не делает. Он никак не взаимодействует с API облачного провайдера. В итоге приходится вручную создавать виртуалки и настраивать их.
Также kubeadm не решает задачу подготовки самого узла. Прежде чем запускать kubeadm, нужно установить containerd и настроить его в виде container runtime, установить kubelet и включить необходимые модули ядра Linux.
Кроме того, в kubeadm нет декларативного описания узлов или групп узлов. Это значит, что нужно поддерживать набор скриптов или плейбуков для их развёртывания и обновления. Без декларативности сложно отслеживать историю изменений и использовать GitOps-подходы к управлению кластером.
Если вы пробовали развёртывать кластеры вручную, тогда, скорее всего, уже сталкивались с этими ограничениями.
Развёртывание и первый узел: с чего всё начинается
Теперь перейдём к другим проблемам, с которыми мы сталкиваемся в самом начале работы с кластером. Как вообще развернуть инфраструктуру — особенно в облаке? Прежде чем поднять кластер, нужно как минимум подготовить базовые ресурсы: сети, файрволы, всё то, что требуется для работы узлов.
Кроме того, часто нужны узлы, которые не будут удаляться автоматически, как это бывает в динамически масштабируемых группах. Например, master-узлы — они должны быть стабильными и жить как можно дольше. И здесь возникает классический вопрос: как развернуть самый первый мастер, когда в системе ещё нет ничего? Идеальный сценарий — это YAML-файл, который скармливаешь утилите, и всё происходит само.
Чтобы приблизиться к этому сценарию, мы сделали утилиту dhctl. В облачном сетапе она требует предварительной конфигурации: нужно задать параметры подключения для облачного провайдера и описать, какие узлы и с какими характеристиками будут заказаны в облаке для группы узлов под control-plane-нагрузки. Это реализуется с помощью ключа masterNodeGroup.
Также здесь можно указать количество узлов в группе с помощью ключа replicas — например, чтобы развернуть HA control plane для кластера в процессе установки. Кроме того, вы можете определить дополнительные группы узлов для определённых типов нагрузок, которым требуются стабильные узлы. Например, фронтенд-узлы со стабильным внешним IP или какой-нибудь gateway, которому необходим стабильный IP. Для этого используется ключ nodeGroups.
kind: VCDClusterConfiguration
layout: Standard
masterNodeGroup:
instanceClass:
sizingPolicy: c4m8
template: ubuntu-24.04
replicas: 1
nodeGroups:
- instanceClass:
sizingPolicy: c2m4
template: debian-12
name: front
replicas: 1
provider: {}Дальше запускается dhctl bootstrap:
dhctl bootstrap
--ssh-agent-private-keys id.rsa
--config conf.yamlВнутри он использует Terraform — сейчас мы постепенно переходим на OpenTofu. У нас подготовлен набор конфигураций, которые собираются на лету из шаблонов и исходной конфигурации. Сначала Terraform создаёт базовую инфраструктуру — ту самую «обвязку» под кластер. Следующим шагом он, опять же через dhctl, развёртывает первый master-узел.

Потом нужно накатить минимум компонентов, чтобы запустить кластер. Для этого dhctl подключается к узлу по SSH, копирует туда bashible — наш фреймворк для настройки узла (чуть позже о нём поговорим).
После запуска bashible у нас уже есть минимальный control plane для кластера: etcd, kube-api-server, kube-scheduler и kube-controller-manager. Теперь утилита может задеплоить в кластер deckhouse-controller и саму конфигурацию кластера.
Deckhouse-controller, в свою очередь, развёртывает и остальные необходимые компоненты для работы с узлами: CAPI, CAPI-провайдер, bashible-api-server и автоскейлер. О каждом из них я ещё расскажу. Контроллер также развёртывает CNI, DNS и компоненты облачного провайдера (в рамках статьи мы оставим их «за скобками»).
Как Deckhouse развёртывает узлы и масштабирует кластер
Итак, у нас есть кластер и первый узел. Теперь нужно как-то развёртывать остальные нагрузки. Под разные типы нагрузок может требоваться разное железо. Например, если у вас есть AI-задачи, то, скорее всего, потребуется узел с видеокартой. Кроме того, для отказоустойчивости нужно несколько узлов под одну и ту же нагрузку. А если нагрузка нестабильная, важно уметь быстро масштабироваться — в обе стороны.
Для этого Deckhouse реализует собственный интерфейс с помощью CustomResourceDefinition: NodeGroup и XXXInstanceClass. XXX в названии второго зависит от облачного провайдера — добавляется префикс, например VCDInstanceClass.
NodeGroup — это описание группы узлов: имя, количество реплик, лейблы.
apiVersion: deckhouse.io/v1
kind: NodeGroup
metadata:
name: worker
spec:
cloudInstances:
classReference:
kind: VCDInstanceClass
name: worker
maxPerZone: 3
minPerZone: 1
nodeTemplate:
labels:
node-role/worker: ""
nodeType: CloudEphemeralInstanceClass описывает физические характеристики самих узлов. Например, с помощью sizingPolicy можно задать, что нам нужен узел с четырьмя CPU и восемью гигабайтами памяти.
apiVersion: deckhouse.io/v1
kind: VCDInstanceClass
metadata:
name: worker
spec:
rootDiskSizeGb: 40
sizingPolicy: c4m8
storageProfile: store
template: ubuntu-22.04-dkpРазрабатывать собственные контроллеры для управления эфемерными узлами смысла нет, особенно если есть готовые решения. В Deckhouse Kubernetes Platform «под капотом» работают два контроллера: MCM (Machine Controller Manager) и CAPI (Cluster API). MCM считается устаревшим, и мы постепенно переходим на CAPI. В статье будем говорить только о нём.
После того как вы описали NodeGroup и InstanceClass, Deckhouse подписывается на эти ресурсы и создаёт MachineDeployment и MachineTemplate — это ресурсы CAPI. Также подготавливается секрет с cloud-init-конфигом, в ко��ором лежат bootstrap-токен и bootstrap-скрипт (о них позже).

Дальше CAPI-менеджер получает эти ресурсы и создаёт новые — Machine и MachineSet. Затем в дело вступает CAPI-провайдер — отдельный контроллер, написанный под конкретный облачный провайдер. Он получает Machine, достаёт cloud-init-скрипт из секрета, идёт в облако, создаёт виртуалку и передаёт туда скрипт через поля метаданных. Дальше на узле срабатывает cloud-init — и начинается бутстрап, о котором мы расскажем чуть позже.

Теперь кейс с масштабированием. Допустим, вы хотите изменить тип машины — меняете sizingPolicy в InstanceClass. Deckhouse-controller видит изменения, пересоздаёт MachineTemplate и MachineDeployment, заново готовит секрет c cloud-init-конфигом. CAPI Manager удаляет старый MachineSet, дрейнит узлы (важно!), удаляет их из API-сервера. А CAPI-провайдер, увидев, что машины больше нет, удаляет её и из облака. Затем создаётся новый MachineSet, и весь процесс бутстрапа повторяется.
Что делать, если у вас не облако, а свой ЦОД
Перейдём к ещё одному типичному сценарию. Допустим, у вас корпоративный дата-центр без API для развёртывания виртуалок. Может быть, у вас просто стоит ферма физических серверов — никакого облачного провайдера. Но при этом хочется управлять узлами так же удобно, как в облаке.
Для таких сценариев мы сделали собственный инструмент — CAPS. Это инфраструктурный провайдер для Cluster API, который позволяет Deckhouse управлять статичными узлами.
Чтобы CAPS заработал, нужно описать два ресурса:
SSHCredentials— способ подключения к узлам;StaticInstances— список конкретных машин, на которые можно подключаться, и их параметры.
apiVersion: deckhouse.io/v1alpha2
kind: SSHCredentials
metadata:
name: credentials
spec:
privateSSHKey: LS0tLS1CRUdJ
sshPort: 22
user: ubuntuapiVersion: deckhouse.io/v1alpha2
kind: StaticInstance
metadata:
labels:
role: gpu
name: gpu-0
spec:
address: 192.168.199.33
credentialsRef:
kind: SSHCredentials
name: credentialsЗатем каждый StaticInstance связываем с NodeGroup:
apiVersion: deckhouse.io/v1
kind: NodeGroup
metadata:
name: gpu
spec:
nodeType: Static
staticInstances:
count: 1
labelSelector:
matchLabels:
role: gpuДальше работает вся та же машинерия, как и с облаком: CAPS получает Machine, выбирает свободные (в состоянии Pending) StaticInstance, берёт bootstrap-скрипт из секрета, который подготовил Deckhouse-controller, по SSH загружает его и запускает.

Масштабирование вниз тоже поддерживается: вы просто меняете значение поля count, например, с 1 на 0. CAPS выбирает нужный Instance, запускает на нём скрипт очистки (который мы заранее туда кладём) и возвращает связанный с узлом StaticInstance в Pending.
Вот так с помощью CAPS можно управлять узлами даже без облака — в своём собственном дата-центре.
Добавляем узлы в кластер и обновляем их
Просто получить узлы недостаточно — их нужно ещё и забустрапить, а потом периодически обновлять. С этим тоже есть свои сложности.
Во-первых, каждая NodeGroup имеет свои настройки, например параметры kubelet или дополнительные конфигурации. Иногда на узлах нужно установить какие-то пакеты или обновить ядро. При этом наш фреймворк настройки узлов довольно объёмный, а у облаков — жёсткие лимиты на размер метаданных узла, через который передаётся скрипт. Например, для AWS это 16 КБ. Поэтому в cloud-init мы передаём только минимальный скрипт (порядка пары килобайт), который через curl скачивает основной bootstrap с мастера.
К тому же скрипты и настройки могут меняться. Чтобы доставлять актуальный набор инструкций на узел, мы сделали компонент bashible-api-server.
Deckhouse-controller для каждой NodeGroup формирует отдельный рендеринг-контекст. Скрипты создаются по шаблонам на Go (gotpl). Контекст передаётся в bashible-api-server через Secret. Также bashible-api-server подписан на ресурсы NodeGroupConfiguration — с его помощью можно подключать дополнительные скрипты для конкретной NodeGroup.
После рендеринга скриптов bashible-api-server открывает эндпоинты для их получения.
В случае облачных узлов начальный скрипт (bootstrap.sh) передаётся через cloud-init.
В случае статических узлов — через CAPS.

На стороне узла всё происходит так:
Cloud-init настраивает сеть.
Затем запускается минимальный инициализирующий скрипт, который запрашивает bootstrap.sh у API-сервера.
API-сервер в свою очередь обращается в bashible-api-server и получает нужный скрипт.
bootstrap.sh запускается и скачивает основной скрипт — bashible.sh.
bashible.sh получает бандл со всеми нужными скриптами (мы его так и называем — Bundle) и запускает их.
После выполнения сохраняется чек-сумма, по которой дальше можно определять, изменился ли состав скриптов.

Зачем нужна чек-сумма? Мы не хотим запускать бандл каждый раз — это тяжело. Поэтому bashible.sh, который запускается через systemd-таймер, сначала сверяет актуальную чек-сумму с той, что была раньше. Если они не совпадают, значит, есть обновление.
Но перед выполнением обновления bashible.sh ждёт апрува. Он запрашивается через аннотацию на узле. Deckhouse-сontroller выставляет специальную аннотацию на ресурс Node, когда видит, что предыдущий узел из NodeGroup уже обновился и можно переходить к следующему. Это позволяет обновлять узлы по очереди, не выводя сразу весь пул из строя в случае ошибок.
Так работают инициализация и обновление узлов в Deckhouse Kubernetes Platform. Безопасно, пошагово и под контролем.
Итоги
Тема управления жизненным циклом узлов большая — и, наверное, чтобы по-настоящему погрузиться в детали, потребуется ещё пара статей. Но подвести промежуточные итоги можно уже сейчас.
Чтобы управлять узлами в Kubernetes, нужен довольно большой набор компонентов. У каждого — своя конфигурация. В Deckhouse Kubernetes Platform мы стараемся скрыть эту сложность за удобными интерфейсами: NodeGroup, InstanceClass, NodeGroupConfiguration, dhctl и другими. Вам не нужно вручную развёртывать Machine Controller Manager/Cluster API или возиться с его настройками.
Процесс настройки узла автоматизирован. У нас уже готов полный набор скриптов, которые отвечают за развёртывание и поддержку жизненного цикла узлов. Всё работает с минимальным участием пользователя.
Конечно, в скриптах могут быть ошибки — это нормально. Мы стараемся писать их идемпотентно и закладываем логику откатов: если скрипт падает, bashible может перезапустить процесс до 10 раз. Это нужно, чтобы при исправлении ошибки узел автоматически получил обновлённый набор инструкций и заново прошёл этап настройки. Также Deckhouse предоставляет механизм для ручного подтверждения изменения узлов, если обновление может затронуть пользовательские нагрузки на узле.
А главное — всё работает безопасно, без простоев и с минимальными рисками. Управлять нагрузкой и обновлениями узлов с Deckhouse можно без боли.
P. S.
Читайте также в нашем блоге:
