6 мин чтения#infrastructure#ansible#proxmox#migration

Перевёз csylabs на Selectel за ночь — но история не про скорость

Вчера вечером я начал миграцию, которую ещё неделю назад собирался растянуть на пять дней. Закончил её в 7:50 утра. Заодно сделал примерно всё, что планировал не делать.

Главное в этой истории не скорость. Главное — что миграция получилась production-grade. Mail/DKIM/PTR работают, headscale-mesh переехал координатором на новый IP, бэкапы лежат в другом городе по приватной сети 9 мс, ansible-инвентарь переименован и приведён к канонической форме. То, что я в плане собирался держать на staged-миграции с bake-окнами по 24 часа между волнами, схлопнулось в одну ночь — но без потери ни одного production-свойства.

Это история про то, как substrate за два года делает миграции скучными, а production-grade — почти бесплатным побочным эффектом.

Откуда пришёл и куда

csylabs и мои персональные проекты последние пару лет жили у Servers.ru. Провайдер давал хорошее железо в Москве по разумным деньгам, всё работало как часы. В конце 2024-го Servers.ru приобрёл Selectel.

Технически ничего не изменилось: Servers.ru как работал, так и продолжает работать, и Selectel как работал, так и работает. Но вся платформа Servers.ru постепенно переезжает в биллинг-панель Selectel, а инфраструктура остаётся та же. Один владелец, два бренда, два контракта.

В апреле 2026-го у меня сошлись две вещи. Во-первых, мой Servers.ru-хост упёрся в потолок: 64 GB памяти на 70% утилизации, NVMe нет (только SATA SSD), бэкапы в текущей схеме лежат на самом хосте и копируются на домашний Synology — для боевого контура это работает, но это не настоящий disaster recovery с географическим разнесением. Во-вторых, я зашёл в прямой контракт с Selectel и увидел, что за те же деньги можно получить:

→ Тот же CPU (Xeon E-2388G), но 128 GB RAM вместо 64 GB
→ NVMe вместо SATA SSD, 4× больше места под боевые виртуалки
→ Выделенный PBS-хост в Питере с 4 TB архивного хранилища
→ Приватная сеть Москва ↔ Питер на 9 мс — Geographic DR из коробки
→ Тот же месячный бюджет

Что я при этом теряю: 2×10 Gbit аплинки (на Selectel — 1 Gbit) и Dell-iDRAC удалённую консоль (на Selectel-хосте мне iDRAC не отдают, всё через PVE-консоль). Для текущей нагрузки и моего рабочего pattern'а этого достаточно — но честно: это сознательный downgrade в одной части ради апгрейда в другой. Не выдаю это за чистый win.

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

План был — пять дней. Реальность — одна ночь.

Изначальный план был чистый blue/green: восстановить все 12 сервисов на Selectel параллельно, оставить их dormant, переключать DNS по одной службе с bake-периодом в 24 часа между волнами.

К вечеру 28-го был готов Phase 2: все 9 виртуалок и 3 LXC восстановлены на Selectel, IP переставлены, onboot=0. План был — идти спать и мигрировать утром по одной службе за раз.

Вместо этого в час ночи я решил, что окно — это не утро, а сейчас. Логика была простая: параллельная работа на двух хостах создаёт больше операционного шума, чем экономит. Один cutover вместо двух — и один откат, если что-то ляжет, а не сценарий «угадай, какая половина живая».

Решение спорное. По итогу — оправдалось, но это решение «по итогу», а не «по риску».

50 минут активной работы

Последовательность была такая:

→ Ротация Cloudflare API-токена. Один я давно засветил в одном Caddyfile, второй — через ansible-vault view несколькими днями раньше. Второй случай теперь личное правило: токен из vault'а смотрим только через grep по имени, никогда через view.
→ Аудит Caddy на трёх хостах. Нашёл 17 csylabs-vhost'ов на платформе, 2 на caddy-VM, 2 на видео-узле.
→ Подготовка Cloudflare-плейбука с новыми IP, TTL=60. Список retired-записей собрал отдельно.
→ Свежие delta-бэкапы 5 активных служб через PBS. Дедупликация дала восстановление за минуту на сервис — цифры воспроизводятся каждый раз, но привыкнуть всё равно не получается.
→ Destroy + restore поверх Selectel-копий. Платформа на 500 GB ехала 19 минут, остальные — секунды.
→ IP-реконфиг через qm set / pct set --net0.
→ Stop источника + start назначения. Все 5 сервисов в окне 30 секунд.
→ DNS-плейбук.

К 7:50 все мейлы шли через новый mail-узел, видео-стрим лежал на новом IP, headscale координировал mesh с Selectel, и кофе кончился.

Что напоролось — три действительно интересных штуки

Vault-путь. В configure_cloudflare_dns.yml сборка переменных тянулась из ../group_vars/all/vault.yml — playbook-level vault, который никто никогда не обновлял. О каноническом vault'е в инвентаре никто не помнит, и токен я ротировал именно в него. Первый прогон провалился: 30+ записей вернулись с 403. Cloudflare за 403 рейт-лимитит, и второй прогон уже бил в 429 — приятный детектив на ровном месте. Симптом простой: DNS не обновляется. Причина — три уровня vault-резолюции, два из которых работали правильно. Чинится одной строкой, ловится — час.

cloudflare_dns плодит дубликаты. Когда модуль с state=present встречает запись с тем же именем, но другим значением, он не обновляет — он добавляет вторую. Теперь у каждого имени было две A-записи: старая 88.212.x и новая 155.212.x, и Cloudflare round-robin'ил между ними непредсказуемо. Ломалось не сразу и не везде — поэтому я заметил уже после того, как «всё работало». Решение — короткий скрипт на голом CF API, который собрал все записи зоны, отфильтровал по старому диапазону и удалил по ID. 38 записей. Удалил, замолчал, поехал дальше.

Dual-IP на первом boot LXC. После pct restore контейнер первый раз поднялся с обоими IP на eth0 — старый и новый одновременно. systemd-networkd в конфиге показывал только новый, kernel — оба. Лечение оказалось тривиальным: pct stop && pct start сбрасывает kernel-state. Но об этом надо знать заранее, иначе в первый момент кажется, что что-то страшное.

Что на этом построено

Selectel-кластер сейчас выглядит так:

pve-selectel-mow-01 — 12 сервисов
pbs-selectel-spb-01 — backup в другом городе
→ Приватная связка через Selectel Global Router
→ ansible-инвентарь inventories/selectel/, hosts.yml в одной канонической форме
→ Cloudflare DNS управляется одним playbook с inventories/selectel/group_vars/all/vault.yml как SSOT
→ Stalwart на новой машине, DKIM/SPF/DMARC на месте, PTR настроен
→ headscale переехал, координатор mesh.csylabs.com отвечает с нового IP
→ MistServer для видео-вертикали жив

Servers.ru-хост остался как кандидат на отключение. Bake-период — пара дней, чтобы ловить отложенные DNS-кэши и неучтённые скрипты, которые ещё ходят на старые IP. После этого — заявка в техподдержку на отключение и расторжение договора.

Что я не делал и почему

Не обновлял kernel. Proxmox 7.0 уже доступен как opt-in beta для PVE 9, в нём интересные вещи (Zen 6 и Nova Lake, EXT4 быстрее на параллельных direct-I/O, page-cache reclaim 75% быстрее) — но мои Selectel-хосты на Xeon E-классе не получают от этого ничего, что оправдало бы риск во время bake-периода. Default 7.0 для PVE 9.2 / PBS 4.2 ожидается в конце Q2. Подожду.

Не обновлял ansible-роли «по дороге». Цель миграции — миграция. Caddyfile-cleanup, удаление инлайн-токенов и пара пыльных vhost'ов — отдельный коммит на следующей неделе.

Не запускал highlights-узел и openvidu-узел на новой площадке. Они в spec'е, но текущая нагрузка их не требует. Восстановлены, dormant — в случае нужды поднимаются за минуту.

Дисциплинарный итог

Мне нравится не сам факт «переехал за ночь». Мне нравится то, что инструменты — PBS-дедупликация, ansible-инвентари, Cloudflare playbook — собрались в комбинацию, в которой 50 минут реального cutover-времени стало возможно. Большая часть работы — не та ночь. Это два года substrate'а: Proxmox-привычки, Ansible-роли, MikroTik-/sing-box-маршруты, привычка хранить всё в vault'е.

Когда такие штуки лежат, миграция за ночь становится скучной задачей, а не подвигом. Хочется именно скучных миграций. Подвиги — это симптом отсутствия substrate'а.

Читать по теме