Продакшн-кластер Proxmox как код
ИТ

Продакшн-кластер Proxmox как код

Обычно кластер виртуализации собирают руками: ставят каждую ноду с флешки, кликают по мастеру, настраивают сеть в веб-интерфейсе, правят конфиги, пока не заработает. Для демо этого достаточно. Проблема в том, что остаётся после: работающий кластер, точное состояние которого не хранится нигде, кроме чьей-то головы; его нельзя воспроизвести, откатить или проверить. Когда нода умирает в три часа ночи, «кажется, я настраивал вот так» — это не план восстановления.

Мой кластер Proxmox из трёх нод устроен наоборот: он описан кодом от самого железа. Один git clone и набор плейбуков Ansible воспроизводят всю систему целиком, включая то, что большинство оставляет ручными сносками: скоростной интерконнект, автопереключение, бэкапы, удалённый доступ. Это не игрушка. Система несёт реальную нагрузку, переживает смерть ноды без единого нажатия клавиши, и каждый её кусок записан. Ниже описано, как работает каждый слой: не просто что это, а как оно построено и почему именно так, и почему это важно тому, кто платит за результат.

Кластер Proxmox, который восстанавливается из git clone

Продакшн-кластер Proxmox как код: три ноды, соединённые Thunderbolt-мешем

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

Всё — это роль: слой Ansible

Сердце всей системы — единый репозиторий Ansible, и его структура и есть архитектура. Ничто в кластере не настраивается заходом по SSH и набором команд; всё настраивается описанием желаемого конечного состояния кодом, а Ansible приводит реальность в соответствие с ним. Основную работу тянут три идеи.

Инвентарь и переменные отделяют «что» от «где». Инвентарь перечисляет ноды; общекластерные настройки лежат в групповых переменных; всё специфичное для одной ноды — в её собственных переменных. Одна и та же роль ведёт себя правильно на каждой ноде, потому что различающиеся факты — это данные, а не код. Добавить ноду, описать её в инвентаре, прогнать плейбук — и она сходится к точной копии остальных, без единой набранной руками команды.

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

  • Харденинг хостов: политика SSH, файрвол, пользователи, базовый уровень безопасности, которому обязана соответствовать каждая нода.
  • Репозитории и пакеты: точные источники и версии ПО, чтобы каждая нода несла идентичный известный стек, а не «то, что было актуально в день установки».
  • Раздача сертификатов: настоящие TLS-сертификаты, разложенные на все внутренние сервисы, обновляемые и переразвёртываемые кодом, чтобы ничто не отдавало браузеру самоподписанный сертификат с предупреждением.
  • Мониторинг: проверки здоровья и метрики, поднятые одинаково на каждой ноде, чтобы кластер сам отчитывался о себе.
  • Thunderbolt-меш: интерконнект, описанный ниже, целиком собираемый отдельной ролью.

Плейбуки компонуют роли по смыслу, а теги делают прогоны точечными. Плейбуки сгруппированы так, как вы на самом деле думаете о системе: инфраструктура (кластер, сеть, хранилище), обслуживание (раскладка сертификатов, очистка), мониторинг и сервисы. Теги позволяют прогнать ровно один срез (скажем, «перенастроить только автопереключение на ноде хранилища»), не трогая остального. А поскольку каждая роль идемпотентна, повторный прогон плейбука безопасен: второй раз он сообщает ноль изменений, потому что реальность уже совпадает с кодом. Именно это свойство и делает «прогони автоматизацию заново» надёжным шагом восстановления, а не лотереей.

Под инструментами лежит правило, которое превращает кучу плейбуков в дисциплину: любое ручное исправление обязано стать кодом, прежде чем задача считается закрытой. Цикл всегда один: сформулировать гипотезу, проверить её по документации и по реальности, применить, проверить фактический эффект и затем свернуть рабочее решение в роль. Конкретный пример: однажды я решил, что прокси уже применил настоящий сертификат, потому что его API об этом отрапортовал. Проверка сырым TLS-рукопожатием показала, что он всё ещё отдаёт самоподписанный; API соврал. Исправление (перевыпустить правильным путём и перезагрузить) тут же ушло в плейбук, чтобы этот класс ошибки больше не мог тихо повториться. Исправление, живущее лишь в истории терминала, — не закончено; исправление в роли — да.

Почему три ноды, а не две

Отказоустойчивости нужно большинство, чтобы принимать решения безопасно. Две ноды на это не способны: когда связь между ними рвётся, каждая считает вторую мёртвой и обе пытаются взять управление. Это split-brain, и он рушит данные. Третья нода — арбитр: кластер действует, только когда согласно большинство, поэтому победитель всегда однозначен.

В моей сборке третья нода — это лёгкий witness (свидетель), который заодно работает сервером бэкапов. Она держит голос и держит резервные копии, не будучи полноценной вычислительной нодой. Нечётное число нод, чёткое большинство, никакого split-brain, и никакого лишнего железа, притворяющегося третьей рабочей лошадью. Маленькое проектное решение, покупающее реальную устойчивость почти даром.

Thunderbolt-меш вместо 10-гигабитного свитча

Ноды кластера должны общаться друг с другом быстро и с низкой задержкой: для согласования, для живой миграции виртуалки с ноды перед обслуживанием, для реплицируемого хранилища. Учебниковый ответ — свитч на 10 или 25 гигабит: дорого, прожорливо и ещё одна коробка, которая может отказать. Вместо этого я соединил ноды напрямую через Thunderbolt (тот самый порт, куда обычно втыкают монитор) в виде меша «точка-точка». Thunderbolt 4 даёт около 20 Гбит/с на линк, Thunderbolt 5 — примерно вдвое больше, ценой одного кабеля.

Заставить видеопорт вести себя как надёжная кластерная сеть — вот где большинство обзоров заканчивается, а у меня продолжается. Это заняло настоящую работу, и вся она живёт в одной роли Ansible:

  • Стабильные имена интерфейсов. Thunderbolt-порты поднимаются с непредсказуемыми именами, поэтому роль закрепляет их через systemd link-файлы. Кластерная сеть, у которой интерфейсы переименовываются на каждой перезагрузке, — это сеть, которой нельзя доверять; здесь они детерминированы.
  • Динамическая маршрутизация по мешу. Линки работают под настоящим протоколом маршрутизации: OpenFabric из семейства IS-IS, через FRRouting. Поскольку меш маршрутизируется, а не бриджуется, при обрыве пути трафик перестраивается сам, а не кластер теряет конечность. Это тот же класс протокола, что крутится внутри операторских сетей, уменьшенный до трёх машин.
  • Ядро настроено под скорость. Дефолты написаны под офисную сеть на 1 гигабит и задушат 20-гигабитный линк. Роль поднимает сокет-буферы до сотен мегабайт, переключает контроль перегрузки на BBR, а дисциплину очереди — на честную (fair-queue), и увеличивает бэклог, — чтобы линк реально отдавал свою полосу, а не спотыкался о настройки из другой эпохи.
  • Сознательный потолок 5 Гбит/с. На полном насыщении Thunderbolt-линк изредка ресетился, а стабильные 5 Гбит/с лучше рваных 20. Traffic shaping ограничивает каждый интерфейс намеренно. Этого компромисса не видно в спецификации; его находишь, только гоняя систему под постоянной нагрузкой и глядя, что реально происходит.

Есть и особенность современной платформы, о которой стоит сказать, потому что это ровно то, что съедает вечер, если не знать. Слой сердцебиения кластера ждёт, что все ноды объявляют одинаковое число сетевых колец (rings). У witness-ноды Thunderbolt нет, поэтому ей выдаётся безобидный адрес-заглушка, чтобы уравнять счётчики. Одна строка конфига, оформленная ролью, и часы запутанных ошибок кворума просто больше не случаются: ни у меня, ни у того, кто унаследует репозиторий.

Получается фабрика из двух путей: обычная сеть как основной путь и Thunderbolt-меш как быстрый независимый второй путь для кластерного трафика. Поверх неё, по хостам и машинам, которые двигают реальные данные, я гоняю jumbo frames (пакет размером 9000 байт вместо стандартных 1500) для измеримо более высокой пропускной способности на файловых шарах и миграциях, агрегированный поверх пары сетевых карт, чтобы один сдохший кабель не ронял хост. Jumbo frames работают, только если весь путь согласен на больший размер, поэтому связка проверяется из конца в конец зондом максимального размера с запретом фрагментации, а не принимается на веру. Снова: проверка вместо предположения.

Автопереключение, которое реально работает

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

Механизм — стандартный протокол резервирования маршрутизатора (VRRP), которым управляет keepalived. Один контейнер настроен с более высоким приоритетом и обычно владеет адресом; второй стоит в резерве с более низким. Владелец примерно раз в секунду объявляет «я жив». Вот что происходит, когда он замолкает:

  • Секунды 0–3: основной гаснет от пропадания питания, сбоя сети или краха. Резерв пропускает несколько сердцебиений и выжидает таймаут, а не реагирует на единичный сбой.
  • Около 3-й секунды: таймаут истекает, резерв повышает себя, поднимает плавающий адрес на своём интерфейсе и рассылает gratuitous ARP, фактически кричит сети «этот адрес теперь здесь», чтобы свитчи мгновенно обновили таблицы.
  • Секунды 5–8: клиентские сессии переподключаются к тому же адресу, который теперь обслуживает выжившая нода, и продолжают работу.

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

Бэкапы, которые спят между запусками

Бэкапы чего-то стоят, только если они идут без вас, надёжно восстанавливаются и не разоряют на содержании. Это подсистема, которой я тихо горжусь больше всего, потому что она решает реальное противоречие: ёмкое холодное хранилище дёшево купить, но прожорливо, если оно крутится сутками ради задачи на девяносто секунд. Поэтому диски бэкапа (USB-подключённый дисковый бокс на witness-ноде) между окнами физически обесточены и простаивают на примерно 4 ваттах вместо ~27, которые тянут в раскрутке. За год, круглые сутки, эта разница выливается в реальные деньги и заметно больший ресурс дисков.

Фокус в том, что «обесточены» должно быть полностью автоматически и полностью безопасно, в обе стороны. Когда срабатывает расписание, специальный менеджер проходит весь жизненный цикл:

  • Пробуждение. Подать питание на бокс на уровне USB, импортировать пулы хранилища по их стабильным аппаратным идентификаторам и вывести датастор бэкапа в онлайн.
  • Бэкап. Найти все виртуалки и контейнеры по кластеру, снять снапшот каждого и стримом залить в дедуплицирующий сервер бэкапов, проверив результат. Вся система, около восемнадцати машин, управляется примерно за девяносто секунд, потому что дедупликация двигает только изменившиеся блоки.
  • Ротация. Применить схему «дед-отец-сын» (скользящее окно свежих, суточных, недельных и месячных копий), чтобы история была глубокой, но не росла бесконечно.
  • Сон. Развернуть каждый шаг ровно назад: датастор офлайн, пулы чисто экспортированы, питание USB снято. Диски раскручиваются вниз и возвращаются к ~4 ваттам.
  • Отчёт. Отправить сводку в чат-канал, чтобы тишину не приняли за успех: бэкап, о котором вам не сообщили, — это бэкап, которого у вас нет.

Два проектных решения делают это надёжным, а не хрупким. Во-первых, менеджер модульный: управление питанием, импорт/экспорт пулов, состояние датастора и очистка блокировок — отдельные компоненты с единственной ответственностью, каждый тестируется сам по себе. Когда что-то ведёт себя не так, есть ровно одно место, куда смотреть. Во-вторых, каждый шаг ждёт реального условия, а не фиксированного таймера: он идёт дальше, когда пул действительно импортирован, а датастор действительно онлайн, а не после оптимистичной паузы в десять секунд. Фиксированные таймауты — это то, как скрипты бэкапа тихо падают в три часа ночи; динамические проверки — то, как они продолжают работать, когда диск просыпается медленно.

Удалённый доступ без единого открытого порта

Ничто в кластере не выставлено в интернет напрямую. На роутере не проброшен ни один порт, так что той входящей поверхности атаки, которую большинство домашних и небольших офисных сетей тихо оставляет открытой, здесь просто нет. И при этом каждый сервис доступен откуда угодно и безопасно. Эта комбинация складывается из четырёх кусочков Cloudflare Zero Trust:

  • Исходящий туннель. Лёгкий агент внутри сети набирает наружу к краю Cloudflare и держит соединение открытым (Cloudflare Tunnel). Поскольку соединение исходит изнутри, входящего сканировать, стучаться по портам или брутфорсить нечего. Публичные имена сопоставляются с внутренними сервисами через ingress-правила, а там, где сервис говорит по HTTPS со своим внутренним сертификатом, туннелю велено ему доверять, так что шифрование сохраняется из конца в конец без предупреждения в браузере.
  • Личность на входе. Каждый сервис стоит за политикой доступа (Cloudflare Access): вы проходите аутентификацию у настоящего провайдера идентификации прежде, чем вообще доберётесь до страницы входа самого сервиса. Неаутентифицированный гость не получает пароль, который можно подбирать; он не получает ничего.
  • Клиент приватной сети для остального. То, что не является сайтами, доступно через зашифрованный клиент (WARP), который выводит авторизованное устройство в приватную сеть, вообще не публикуя эти сервисы наружу.
  • Split-horizon DNS. Одно и то же имя резолвится в быстрый локальный путь, когда вы дома, и в туннель, когда вы снаружи, через DNS-перезаписи. Одна закладка работает везде, и внутри сети вас никогда не выкидывает в интернет и обратно, чтобы дотянуться до коробки в двух шагах.

Какие сервисы опубликованы и как — это тоже данные: каждый несёт флаг в небольшом реестре, говорящий, локальный он или доступен через туннель, и этот флаг управляет и дашбордом, и конфигурацией доступа. Итог: «дотянуться откуда угодно» без «дотянуться может кто угодно»; удобство и уязвимость расцеплены, а именно этот компромисс большинство настроек проваливает в сторону уязвимости.

Один реестр описывает каждый сервис

Нить, связывающая сервисы вместе, — единственный файл: реестр, перечисляющий каждый сервис кластера — его имя, где он живёт, к какой группе относится и должен ли быть доступен только в локальной сети или опубликован через туннель. Это описание — единый источник истины, и Ansible разворачивает его во всё, что ниже по течению и должно об этом знать:

  • reverse proxy получает маршрутизацию и настройку TLS;
  • дашборд получает свои плитки, сгруппированные и с иконками;
  • слой zero-trust получает ingress-правила и политики доступа;
  • мониторинг получает список того, за чем следить.

Добавить сервис в одном месте, прогнать один плейбук, и он появляется везде разом: за прокси, зашифрованный, на дашборде, под мониторингом и, если попросили, доступный снаружи, без единого дважды сделанного шага и без шанса, что прокси и дашборд разойдутся во мнении о том, что существует. Дрейф конфигурации — та медленная гниль, когда реальность и документация тихо расходятся, — здесь не может даже начаться, потому что описание ровно одно, а всё остальное из него генерируется.

Сертификаты, которые обновляют себя сами

Ничто в кластере не отдаёт самоподписанный сертификат с предупреждением браузера, и ни один сертификат никогда не обновляется руками. Reverse proxy (Nginx Proxy Manager) терминирует TLS для каждого опубликованного сервиса настоящими сертификатами Let’s Encrypt, выпускаемыми и обновляемыми автоматически по протоколу ACME задолго до истечения. Ежегодной пожарной тревоги «сертификат протух, и всё легло» просто не существует.

Шифрование не заканчивается у парадной двери. Отдельная роль Ansible раскладывает настоящие сертификаты на сервисы за прокси, так что даже трафик между сервисами внутри кластера как следует зашифрован, а не доверяет самоподписанному сертификату в надежде, что никто не заметит предупреждения. Всё это описано кодом и управляется из того же реестра, поэтому новый сервис корректен по HTTPS в момент добавления, а не после того, как кто-то вспомнит починить замочек.

Одна панель и сигнал, когда он нужен

Кластер, который не видно, — это кластер, которому нельзя доверять, поэтому он построен так, чтобы сам о себе сообщать. Это делают три слоя, и, как и всё остальное, они генерируются, а не поддерживаются руками:

  • Один дашборд. Единая self-hosted панель (Homepage) показывает каждый сервис с одного взгляда, сгруппированный по функции, и каждая плитка генерируется из реестра, поэтому дашборд всегда полон и никогда не показывает то, чего уже нет, и не упускает то, что есть. Это единственная закладка, отвечающая на вопрос «что запущено и живо ли оно?».
  • Мониторинг доступности. Отдельный монитор (Uptime Kuma) непрерывно опрашивает каждый сервис и хранит историю: не только «жив ли он сейчас», но «как часто он не был жив», а это и есть та цифра, которая реально говорит, нужно ли вмешаться.
  • Сигналы, которые до вас доходят. Система шлёт в Telegram: каждый прогон бэкапа отчитывается о результате, а мониторинг подаёт голос в ту же секунду, как сервис перестаёт отвечать. Вы узнаёте, что дисковый бокс не проснулся или сервис упал, из сообщения на телефоне, а не от пользователя, который пожаловался неделю спустя. Тишина сконструирована так, чтобы означать «здоров», а всё остальное доходит до вас немедленно.

Что ломалось и чему научило

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

  • Ночь, когда диски не заснули. Бэкап упал на середине и оставил бокс включённым на час (27 ватт впустую) потому что шаг «усыпить диски» так и не выполнился: скрипт умер, не дойдя до него. Лечение — гарантированный обработчик очистки, который срабатывает, как бы скрипт ни завершился, успехом или сбоем. Теперь сбой безопасен, а не дорог: диски всегда оказываются спящими.
  • Тихие полу-импорты. Пулы бэкапа импортировались ненадёжно, пока к ним не начали обращаться по стабильным аппаратным идентификаторам вместо того имени устройства, что ядро выдало на этой загрузке. До правки прогон мог тихо идти по половине дисков; после неё импорты детерминированы. Разница между бэкапом и ложным ощущением бэкапа часто вот такая маленькая.
  • Смерть от кодировки. Скрипт обслуживания ломался при копировании между машинами из-за невидимых различий кодировки и переводов строк, тот самый баг, что съедает полдня и ничему не учит, пока его не назовёшь. Теперь развёртывание передаёт файлы способом, который нельзя испортить в пути, и каждый файл проверяется по прибытии.

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

Стек

  • Proxmox VE: гипервизор и слой кластеризации.
  • Ansible: каждый хост, сервис и настройка как идемпотентный версионируемый код.
  • FRRouting с OpenFabric: динамическая маршрутизация по Thunderbolt-мешу.
  • keepalived (VRRP): плавающий IP и автопереключение.
  • ZFS и Syncthing: целостность хранилища и живая репликация датасета.
  • Proxmox Backup Server: дедуплицированные проверяемые бэкапы.
  • Cloudflare Zero Trust (Tunnel + Access + WARP): удалённый доступ без открытых портов.
  • Nginx Proxy Manager и Let’s Encrypt: reverse proxy и автоматически обновляемый TLS.
  • Homepage, Uptime Kuma и Telegram: дашборд, мониторинг доступности и сигналы.

Готовые компоненты, открытые там, где это важно, ничего проприетарного, что нельзя заменить. Тот же паттерн масштабируется с трёх мини-ПК на полке до стойки серверов без смены формы, потому что работу делает паттерн, а не детали.

Почему это важно тому, кто за это платит

Кластер оценивают по трём вопросам. Переживёт ли он смерть ноды без присмотра? Сможет ли им управлять кто-то, кроме того, кто его собрал? И стоит ли он в эксплуатации столько, сколько должен? Кластер Proxmox, собранный как код, отвечает на все три, и каждый ответ бизнесовый, а не технический.

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

Три маленькие коробки на краю сети или боевая инфраструктура в дата-центре, метод один и тот же: описать систему кодом, проверить каждый слой, автоматизировать каждое исправление. И платите вы, по сути, за метод, а не за железо.

Частые вопросы

Источники и инструменты


Нужна консультация?

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

Оцените статью