- docs/ai/: stable, repo-verified context - README, architecture, tech-stack, edge-cases, plan (confirmed active work only), migration-tofu (the blue-green OpenTofu migration runbook and per-service findings), legacy-warning, links. - AGENTS.md: slimmed to a working contract that points at docs/ai instead of restating it; CLAUDE.md is an adapter that @-includes it. - README.md, ansible/README.md, ansible/roles/README.md, roles/lxc_docker_host/README.md: bring wording in line with the current control plane (Makefile entry point, registry, tofu, memoir-bot gone). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uoq5AVK8mkBgg83Mq6o5V
789 lines
59 KiB
Markdown
789 lines
59 KiB
Markdown
# Переход provisioning LXC на OpenTofu
|
||
|
||
Пошаговый план миграции. Рассчитан на исполнителя, который этот разговор не
|
||
видел: всё нужное либо здесь, либо по ссылкам на файлы репозитория.
|
||
|
||
Стратегия — **blue-green**: боевые контейнеры не импортируются в Tofu и не
|
||
переконфигурируются на месте. Вместо этого рядом создаётся новый контейнер,
|
||
туда переносятся данные, затем переключается адрес, а старый контейнер
|
||
некоторое время стоит остановленным как откат.
|
||
|
||
---
|
||
|
||
## 0. Как начать сессию по этому плану
|
||
|
||
Открыть Claude Code в корне репозитория и дать примерно такой промпт,
|
||
подставив нужный сервис:
|
||
|
||
Работаем по docs/ai/migration-tofu.md — переход provisioning LXC на OpenTofu
|
||
по схеме blue-green. Прочитай его целиком, а также tofu/README.md и
|
||
AGENTS.md.
|
||
|
||
Делаем сервис №1 из раздела 5 (emergency-bot, CT 148). Идём строго по
|
||
процедуре раздела 4, по шагам, не забегая вперёд.
|
||
|
||
Правила:
|
||
- разрушающие шаги (pct stop, pct destroy, tofu-destroy, смена адреса)
|
||
выполняешь только после моего явного подтверждения;
|
||
- старый контейнер не удаляем, он остаётся откатом;
|
||
- make validate должен быть зелёным до и после;
|
||
- если реальность разошлась с планом — останавливайся и говори, не
|
||
придумывай обход.
|
||
|
||
Между сервисами сессию имеет смысл начинать заново: контекст одного переезда
|
||
следующему не нужен, а свежий контекст надёжнее.
|
||
|
||
---
|
||
|
||
## 1. Что уже сделано — не переделывать
|
||
|
||
- `tofu/` с провайдером `bpg/proxmox`, цели `make tofu-init | tofu-plan |
|
||
tofu-apply CONFIRM=1 | tofu-destroy CONFIRM=1`.
|
||
- Аутентификация: `root@pam` по паролю из корневого `.env`
|
||
(`PROXMOX_ROOT_USER`, `PROXMOX_ROOT_PASSWORD`). Выбор режима автоматический,
|
||
печатается в stderr. Подробности и матрица возможностей — `tofu/README.md`.
|
||
- Цели `tofu-*` сами поднимают SSH-туннель через `ru-vps`: провайдер ходит в
|
||
API по HTTPS и ProxyJump не умеет.
|
||
- Реестр `homelab_services` программно потребляют: `reverse-proxy.yml`,
|
||
`ru-vps-base.yml`, `pve-backup-jobs.yml` (списки VMID по `backup.job`),
|
||
`roles/backup_audit` (VMID по `monitoring.backup_audit_vmid`), `validate.yml`.
|
||
- `make validate` — read-only gate, сверяет реестр с `pct config` по hostname,
|
||
IP, cores, memory, swap. Падает при расхождении.
|
||
- `make lint` воспроизводит CI (линтеры из корня + syntax-check всех плейбуков).
|
||
|
||
### Что проверено на пилоте (VMID 199, снесён)
|
||
|
||
В режиме `root@pam` Tofu выставляет декларативно всё, что нужно этой
|
||
инфраструктуре:
|
||
|
||
dev0: deny-write=0,path=/dev/net/tun,uid=0,gid=0,mode=0660
|
||
features: fuse=1,keyctl=1,nesting=1
|
||
mp0: data:199/vm-199-disk-1.raw,mp=/opt/pilot-data,size=4G
|
||
|
||
Повторный `plan` даёт `No changes`. Следствия:
|
||
|
||
- правка `/etc/pve/lxc/<vmid>.conf` через `lineinfile` (девять контейнеров)
|
||
заменяется декларацией в Tofu, но КАКОЙ именно — зависит от устройства:
|
||
`/dev/fuse` — штатным флагом `features { fuse = true }`, `/dev/net/tun` —
|
||
блоком `device_passthrough`. Оба варианта видны в выводе пилота выше:
|
||
`features: fuse=1,...` и `dev0: ...path=/dev/net/tun`;
|
||
- шаг `pct set <vmid> --features nesting=1,keyctl=1` больше не нужен;
|
||
- `lifecycle { ignore_changes = [features] }` не нужен;
|
||
- apply одной фазой.
|
||
|
||
**Важно:** всё это работает ТОЛЬКО под `root@pam` по паролю. API-токен, даже
|
||
принадлежащий root, проверку не проходит — в Proxmox она буквальная
|
||
(`$authuser eq 'root@pam'`, `PVE/LXC.pm:1658`), а при токенной аутентификации
|
||
`$authuser` равен полному `user@realm!tokenname` (`PVE/HTTPServer.pm:86`).
|
||
|
||
### Ключевое упрощение
|
||
|
||
**Если при переезде сохранить IP, меняется только VMID.** А все потребители
|
||
VMID уже выводят его из реестра автоматически (backup jobs, backup audit).
|
||
Поэтому cutover — это правка одного поля `vmid` в `services.yml`, а не
|
||
синхронная правка шести файлов. Ради этого и делалась генерация.
|
||
|
||
---
|
||
|
||
## 2. Инварианты
|
||
|
||
1. **Последовательным обязан быть только cutover.** Инвариант изначально
|
||
звучал как «один сервис за раз»; 2026-09-02 он уточнён после параллельного
|
||
прогона шести сервисов. Разделение такое:
|
||
- **Параллелится безопасно:** подготовка (снятие эталона, описание в Tofu),
|
||
создание новых контейнеров (один пакетный `apply`, Tofu сам разводит
|
||
ресурсы) и конфигурация на временных адресах (шаг 4.3). Всё это не
|
||
трогает боевые контейнеры и полностью откатывается точечным
|
||
`tofu destroy` нужного ресурса.
|
||
- **Остаётся строго последовательным:** перенос данных (4.4), переключение
|
||
адреса (4.5) и правка реестра (4.6) — по одному сервису, с явным
|
||
подтверждением человека на каждый разрушающий шаг.
|
||
Причины, по которым `apply` физически не параллелится: состояние Tofu одно
|
||
и локальное, а цели `make tofu-*` поднимают SSH-туннель на фиксированный
|
||
порт 18006 с фиксированным control-сокетом. Два одновременных прогона
|
||
столкнутся.
|
||
2. **Старый контейнер останавливается, но не удаляется** — минимум неделю. Это
|
||
единственный быстрый откат.
|
||
3. `make validate` зелёный до и после каждого переезда.
|
||
4. Перед стартом каждого сервиса — свежий бэкап PBS этого VMID.
|
||
5. VMID и IP выведенных контейнеров не переиспользовать сразу: в PBS остаются
|
||
цепочки по VMID, в кэшах — адреса.
|
||
6. **CT 120 `pbs` не мигрировать.** Это цель бэкапов, на которую опирается план
|
||
отката всех остальных. Он `unmanaged` и таким остаётся.
|
||
7. Не менять одновременно provisioning и рантайм сервиса без необходимости.
|
||
Если переводишь `docker run` на `compose_service` — это отдельное
|
||
осознанное решение по конкретному сервису, а не часть переезда по умолчанию.
|
||
|
||
---
|
||
|
||
## 3. Предпосылки перед первым переездом
|
||
|
||
- [x] Вывод `memoir-bot` — live-шаги оператора выполнены 2026-09-02: deploy key
|
||
`SecondBrain` отозван, монитор в Uptime Kuma снят, `pct stop 142`
|
||
выполнен (подтверждено `pct list` на mini-pc). Это была репетиция
|
||
удаления старого контейнера. `pct destroy 142` сознательно отложен —
|
||
см. [`plan.md`](plan.md) и общий инвариант о паузе перед удалением.
|
||
- [x] `make validate` — зелёный (проверено 2026-09-02, 12 сервисов, drift нет).
|
||
- [x] `make backup-audit` — прогнан 2026-09-02, все хосты `ok`, без ошибок.
|
||
- [x] `tofu/terraform.tfstate` — решено оставить только локальным на время
|
||
переезда сервиса №1. Осознанный риск: state небольшой, при потере можно
|
||
re-import всё созданное заново. Постоянное решение (например, offsite
|
||
restic-профиль) — отдельная задача, не блокирует старт.
|
||
- [x] Свободное место проверено. На 2026-09-02: cloud-pc `data` — 816 ГиБ
|
||
свободно, mini-pc `local-lvm` — 116 ГиБ. Запаса хватает на любой сервис,
|
||
включая grimmory (64 ГиБ).
|
||
|
||
### Пул временных адресов и VMID
|
||
|
||
Свободны на 2026-09-02: IP `192.168.1.6-9, 11-19, 21, 22, 26, 33, 36-40`,
|
||
VMID `151+` и `199`. Временный адрес обязан лежать в `192.168.1.5-40` —
|
||
только этот диапазон ru-vps маршрутизирует в LAN через `tun0`, иначе новый
|
||
контейнер будет недоступен по ProxyJump.
|
||
|
||
Шаблон `local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst` присутствует на
|
||
обеих нодах — проверено.
|
||
|
||
---
|
||
|
||
## 4. Процедура переезда одного сервиса
|
||
|
||
Обозначения: `OLD` — текущий VMID, `NEW` — временный VMID, `IP` — боевой адрес,
|
||
`TMPIP` — временный адрес.
|
||
|
||
### 4.1 Подготовка
|
||
|
||
1. `make validate` и `make backup-audit` — оба зелёные.
|
||
2. Снять эталон: `ssh <node> sudo pct config OLD` — сохранить вывод. Это
|
||
источник правды для того, что нужно воспроизвести.
|
||
3. Запустить свежий бэкап: `ssh <node> sudo vzdump OLD --storage pbs --mode snapshot`.
|
||
- **Успех проверять наличием снапшота, а не кодом возврата.** Исторически
|
||
на 2026-09-02 client-side prune давал `missing Datastore.Modify|Datastore.Prune`,
|
||
из-за чего `vzdump` печатал `Backup of VM ... failed` и возвращал ненулевой
|
||
код после успешной выгрузки. Это объяснение для старых логов; сейчас
|
||
client-side prune из плейбуков убран, поэтому любой новый non-zero надо
|
||
считать проблемой. Проверять так:
|
||
`ssh cloud-pc 'sudo pvesm list pbs | grep ct/<OLD>'`. Подробности —
|
||
[`plan.md`](plan.md), раздел про prune.
|
||
- **Если у сервиса Docker с fuse-overlayfs, а rootfs на каталоговом
|
||
хранилище (`data`) — сначала остановить сервис.** Такой rootfs не
|
||
поддерживает снапшоты, vzdump уходит в режим `suspend` с rsync и падает
|
||
на `var/lib/docker/fuse-overlayfs/.../merged`: Permission denied.
|
||
Проверено на gitea 2026-09-02: с остановленным сервисом бэкап проходит.
|
||
У сервисов на `local-lvm` (vaultwarden) проблемы нет — там снапшот.
|
||
- **У сервиса с bind mount в бэкап попадает только rootfs.** vzdump пишет
|
||
`excluding bind mount point mpN (...) from backup (not a volume)`. До
|
||
переезда это касалось gitea: его репозитории и БД в PBS не попадали
|
||
вообще. После перехода на volume попадают.
|
||
|
||
### 4.2 Описание в Tofu
|
||
|
||
4. Завести ресурс в `tofu/` (например `tofu/services.tf`), воспроизведя
|
||
эталон: `vm_id = NEW`, hostname как у боевого, `TMPIP`, cores, memory, swap,
|
||
rootfs на том же datastore и того же размера, `features`, декларацию для
|
||
каждого устройства из `lxc.mount.entry`, `mount_point` для каждого `mpN`,
|
||
`startup.order`, `start_on_boot`, `unprivileged`, `console { type = "shell" }`
|
||
(эталон `cmode` — см. ниже).
|
||
- **`/dev/fuse` — это `features.fuse`, а НЕ `device_passthrough`.**
|
||
Ловушка: в эталоне `/dev/fuse` выглядит как пара сырых строк
|
||
(`lxc.cgroup2.devices.allow: c 10:229 rwm` + `lxc.mount.entry`), и её
|
||
хочется механически перевести в `device_passthrough`. Так делать не надо:
|
||
`device_passthrough` (`dev0:` в PVE 8.2+) предназначен для сырых
|
||
character-устройств вида `/dev/net/tun`, а у `/dev/fuse` есть штатный
|
||
флаг PVE, проверенный на пилоте. См. `tofu/pilot.tf.example:17-21`.
|
||
Следствие: `pct config` нового контейнера покажет `features: fuse=1,...`,
|
||
то есть будет текстуально отличаться от эталона при том же эффекте —
|
||
это ожидаемо, а не drift.
|
||
- **Bind mount заменять на volume.** Если у сервиса `mpN: /host/path,mp=...`
|
||
(сейчас так только у gitea), в новом контейнере это должен быть
|
||
`mount_point { volume = "<datastore>", size = "...", path = "..." }`.
|
||
Данные переносятся на шаге 4.4, а не монтированием того же каталога:
|
||
два контейнера, пишущие в один каталог, повредят данные.
|
||
- **`console { type = "shell" }` обязателен.** `roles/pve_lxc` всем
|
||
контейнерам ставит `cmode: shell`; провайдер отслеживает это через блок
|
||
`console`, и без явного объявления следующий `tofu-plan` предложит
|
||
откатить его на дефолт Proxmox `tty` — тот же класс drift, что и
|
||
`keyctl` в гибридной схеме. Подробности и история находки —
|
||
`tofu/README.md`.
|
||
5. `make tofu-plan` — убедиться, что план ровно `1 to add`.
|
||
6. `make tofu-apply CONFIRM=1`.
|
||
7. Сверить: `ssh <node> sudo pct config NEW` против эталона из шага 2. Отличаться
|
||
должны только `vmid` и адрес (плюс явно выписанные Proxmox-дефолты вида
|
||
`cpulimit`, `protection`, `template`, `tty` — эталон их просто не
|
||
показывал, реальное поведение то же самое). `cmode: shell` должен совпасть
|
||
с эталоном уже на этом шаге, если блок `console` объявлен в шаге 4 — правкой
|
||
постфактум через `pct set` не чинить, это создаёт drift (см. шаг 4 выше и
|
||
`tofu/README.md`).
|
||
|
||
### 4.3 Настройка
|
||
|
||
8. Добавить временную запись в `ansible/inventory/hosts.yml`: `<name>-new` с
|
||
`ansible_host: TMPIP` и `expected_lan_ip: TMPIP`, и такую же запись в
|
||
`ansible/ssh_config` (Host-блок + в агрегированные строки ProxyJump, root,
|
||
общие опции).
|
||
- Во время cutover использовалась временная группа `lxc_migration_new`;
|
||
она не входила в `servers`, поэтому незавершённый переезд не краснил
|
||
`make check` и `make status`.
|
||
- **Транспорт задавался только в `group_vars/lxc_migration_new/main.yml`, а
|
||
не блоком `vars:` в самом `hosts.yml`.** У Ansible inline-переменные
|
||
группы из inventory-файла имеют приоритет НИЖЕ, чем `group_vars/all/`,
|
||
поэтому `ansible_user: ansible` оттуда перебивает inline `root`, и
|
||
подключение падает с `Permission denied`. Каталог `group_vars/<группа>/`
|
||
— наоборот, выше `all`. Найдено 2026-09-02.
|
||
|
||
9. Прогнать конфигурационную часть существующего плейбука против нового
|
||
контейнера:
|
||
|
||
ansible-playbook playbooks/pve-<name>.yml \
|
||
-e pve_config_target=<name>-new --limit <name>-new
|
||
|
||
**Одного `--limit <name>-new` НЕДОСТАТОЧНО, и это не особенность отдельного
|
||
сервиса.** Ansible пересекает лимит с паттерном play, а не подменяет его.
|
||
Паттерн конфигурационного play — литеральное имя боевого хоста (`hosts:
|
||
gitea`), пересечение с `gitea-new` пусто, и play молча получает `hosts (0)`.
|
||
Проверено 2026-09-02 через `--list-hosts` на всех плейбуках сразу: ноль
|
||
хостов во ВСЕХ play, а не только в создающих. Прошлая сессия
|
||
(emergency-bot) сочла это особенностью того сервиса — на самом деле это
|
||
общее свойство.
|
||
|
||
Поэтому у конфигурационных play заведён переопределяемый таргет:
|
||
|
||
hosts: "{{ pve_config_target | default('<name>') }}"
|
||
|
||
Он есть в `pve-docker-test.yml`, `pve-gitea.yml`, `pve-vaultwarden.yml`,
|
||
`pve-grimmory.yml`, `gyro.yml` и `uptime-kuma.yml`. По умолчанию поведение
|
||
не меняется — без `-e` паттерн равен прежнему литералу.
|
||
|
||
`--limit` при этом всё равно обязателен: он отсекает play создания
|
||
контейнера и play-стражи, которые таргетят ноду (`cloud-pc`/`mini-pc`) или
|
||
`localhost`. Без него плейбук пойдёт делать `pct create` и править
|
||
`/etc/pve/lxc/<OLD>.conf` на боевом VMID.
|
||
|
||
**Перед каждым прогоном сверяться с `--list-hosts`.** Ожидание: у play
|
||
создания и стражей `hosts (0)`, у конфигурационного — ровно `hosts (1):
|
||
<name>-new`. Это и есть предохранитель, а не формальность.
|
||
|
||
Особые случаи:
|
||
- `monitoring` — конфигурации в `pve-monitoring.yml` нет вообще, там только
|
||
создание. Настраивает `playbooks/uptime-kuma.yml` (роль `uptime_kuma`).
|
||
`playbooks/monitoring.yml` (замороженный Prometheus) НЕ запускать.
|
||
- `gyro` — конфигурация в `playbooks/gyro.yml`, нужен пароль Ansible Vault
|
||
(`make gyro` / `--ask-vault-pass`). Роль читает `host_vars/gyro/`.
|
||
Во время cutover использовался временный host_vars shim; после перевода CT
|
||
156 на боевой IP он удалён.
|
||
|
||
10. Проверить, что сервис поднялся на `TMPIP` (его health-эндпоинт или порт).
|
||
У сервисов с данными новый контейнер на этом шаге работает на ПУСТОМ
|
||
состоянии — данные приезжают только на шаге 4.4. Пустой Gitea, пустой
|
||
Vaultwarden с экраном создания учётки, пустая Uptime Kuma, свежая схема
|
||
Flyway у grimmory — это ожидаемый результат шага 4.3, а не поломка.
|
||
Поднимать новую копию на боевых данных до остановки старой НЕЛЬЗЯ: две
|
||
живые копии над одной SQLite или одной MariaDB повредят состояние.
|
||
|
||
### 4.4 Перенос данных
|
||
|
||
11. Остановить сервис на СТАРОМ контейнере (`systemctl stop <unit>`), сам
|
||
контейнер пока не трогать.
|
||
12. Финальная синхронизация. Способ зависит от сервиса — см. раздел 5.
|
||
13. Запустить сервис на новом, проверить на `TMPIP`.
|
||
|
||
### 4.5 Переключение
|
||
|
||
14. `ssh <node> sudo pct stop OLD`.
|
||
15. В Tofu поменять адрес нового контейнера с `TMPIP` на `IP`, `make tofu-apply CONFIRM=1`.
|
||
16. Перезапустить контейнер, если адрес не подхватился на живую.
|
||
- **Если конфигурация сервиса содержит его собственный адрес — прогнать
|
||
плейбук заново сразу после смены адреса.** Из пакета 2026-09-02 это
|
||
касается только `grimmory` (`ports: <ip>:6060:6060` и правила
|
||
`GRIMMORY-FILTER`): его compose всё ещё содержит TMPIP, и сервис не
|
||
поднимется, пока файл не перегенерируют. Проверять просто:
|
||
`ss -ltn` на новом контейнере — если сокет висит на конкретном адресе,
|
||
а не на `0.0.0.0`/`*`, повторный прогон обязателен. У gitea,
|
||
vaultwarden и uptime-kuma сокеты wildcard, им это не нужно.
|
||
Прогон уже без `-e pve_config_target`: адрес стал боевым, и имя хоста
|
||
само резолвится в новый контейнер.
|
||
17. Health-check на боевом `IP`.
|
||
18. Если сервис публичный (`proxy` в реестре) — `make reverse-proxy` и проверить
|
||
домен снаружи. При сохранённом IP апстрим не меняется, но прогон подтвердит.
|
||
19. Обновить `~/.ssh/known_hosts` — host key нового контейнера другой:
|
||
`ssh-keygen -R <IP>` и `ssh-keygen -R <name>`. В `ssh_config` стоит
|
||
`StrictHostKeyChecking accept-new`, который принимает НОВЫЕ ключи, но не
|
||
изменившиеся, поэтому без чистки подключение будет отвергнуто.
|
||
- **Заодно закрыть ControlMaster: `ssh -F ansible/ssh_config -O exit
|
||
<host>`.** Мастер переживает cutover живым, но его туннель ведёт в уже
|
||
остановленный контейнер; новые сессии мультиплексируются поверх мёртвого
|
||
соединения и виснут с `Connection timed out during banner exchange`.
|
||
Симптом выглядит как сетевая проблема, хотя контейнер полностью доступен
|
||
с ноды. Разовый обход для диагностики — `-o ControlPath=none`.
|
||
Найдено 2026-09-02 на docker-test.
|
||
- У gitea SSH host-ключи git-over-ssh лежат в переносимых данных
|
||
(`<data>/ssh/ssh_host_*`), поэтому порт 2222 после переезда отдаёт ТОТ ЖЕ
|
||
ключ и `known_hosts` git-клиентов остаётся валидным. Менять нужно только
|
||
ключ sshd самого контейнера (порт 22).
|
||
- **Про gyro и gitea: ранее записанное предупреждение неверно, проверено
|
||
2026-09-02.** Утверждалось, что `roles/gyro` пинит host key
|
||
`[192.168.1.25]:2222` и после переезда gitea сломается. На самом деле
|
||
задача `Remove obsolete Gitea known host` стоит с `state: absent` — она
|
||
УДАЛЯЕТ устаревшую запись, а не создаёт её. Пинится
|
||
`gyro_git_known_hosts_name: github.com`, и `gyro_repo_url` —
|
||
`git@github.com:...`. Gyro клонирует с GitHub, к gitea отношения не
|
||
имеет. Никаких дополнительных действий после переезда gitea не нужно.
|
||
|
||
### 4.5a Offsite restic: профиль надо переустановить на новом контейнере
|
||
|
||
**Шаг, которого в плане не было. Найден 2026-09-02 на vaultwarden.**
|
||
|
||
Если offsite-профиль restic выполняется ВНУТРИ контейнера сервиса (а не на
|
||
ноде), то на новом контейнере его нет: там нет ни `restic`, ни `rclone`, ни
|
||
systemd-таймера. Бэкап тихо перестаёт выполняться, и `make backup-audit` этого
|
||
не ловит — он ставит только свой таймер аудита.
|
||
|
||
Проверять так:
|
||
|
||
ssh <name> 'command -v restic rclone; systemctl list-timers --all | grep restic'
|
||
|
||
Чинить так:
|
||
|
||
make offsite-restic EXTRA="--limit <name>"
|
||
|
||
Затем обязательно проверить всю цепочку, а не только наличие таймера:
|
||
|
||
ssh <name> 'systemctl start homelab-restic-offsite-<name>.service'
|
||
ssh <name> 'journalctl -u homelab-restic-offsite-<name>.service -n 30 --no-pager'
|
||
|
||
Успех выглядит как подключение к репозиторию с уже существующими снапшотами и
|
||
`Finished ... Deactivated successfully`.
|
||
|
||
Кого это касается:
|
||
|
||
- `vaultwarden` — профиль внутри LXC. Сделано и проверено 2026-09-02.
|
||
- `grimmory` — профиль тоже внутри LXC (`hosts: grimmory`). Понадобится то же.
|
||
- `gitea` — профиль выполняется на cloud-pc и указывает на `/opt/data/gitea`.
|
||
Там задача другая: после переезда на volume этот путь исчезает, профиль надо
|
||
переписать на выполнение внутри контейнера, по образцу vaultwarden.
|
||
|
||
### 4.6 Реестр и потребители
|
||
|
||
20. В `ansible/inventory/group_vars/all/services.yml` поменять `vmid: OLD` на
|
||
`vmid: NEW`. Если менялся `provisioner` — поправить и его на `tofu`.
|
||
21. Убрать временные записи `<name>-new` из `hosts.yml` и `ssh_config`.
|
||
22. Прогнать зависящие цели: `make backup-jobs` (списки VMID выведутся заново),
|
||
`make backup-audit` (перерендерит скрипт аудита с новым VMID).
|
||
23. `make validate` — должен быть зелёный.
|
||
24. `make status` — контейнер UP, юниты active.
|
||
|
||
### 4.7 Завершение
|
||
|
||
25. Оставить `OLD` остановленным минимум неделю.
|
||
26. Затем `pct destroy OLD` и решить судьбу его цепочки бэкапов в PBS.
|
||
27. Удалить из `playbooks/pve-<name>.yml` часть, создающую контейнер (первый play
|
||
с `pct create` / ролью `pve_lxc`, правками `/etc/pve/lxc/*.conf` и
|
||
`pct set --features`). Оставить конфигурационную часть.
|
||
|
||
---
|
||
|
||
## 5. Порядок сервисов и специфика
|
||
|
||
Порядок выбран по возрастанию риска. Не менять без причины.
|
||
|
||
| # | Сервис | VMID | Узел | Почему здесь |
|
||
|---|---|---|---|---|
|
||
| 1 | `emergency-bot` | ~~148~~ **151** | mini-pc | ПЕРЕЕХАЛ 2026-09-02. Полностью шаблонный, персистентных данных нет |
|
||
| 2 | `docker-test` | ~~145~~ **152** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02 |
|
||
| 3 | `gitea` | ~~141~~ **153** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02; bind mount → volume, данные впервые попали в PBS |
|
||
| 4 | `vaultwarden` | ~~140~~ **154** | mini-pc | ПЕРЕЕХАЛ 2026-09-02 |
|
||
| 5 | `monitoring` | ~~146~~ **155** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02; замороженный стек не переносился |
|
||
| 6 | `gyro` | ~~150~~ **156** | mini-pc | CUTOVER COMPLETED 2026-09-02; CT156 live on 192.168.1.35, CT150 held as rollback |
|
||
| 7 | `grimmory` | ~~149~~ **157** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02; MariaDB через dump/restore |
|
||
| 8 | `adguard` | ~~144~~ **158** | mini-pc | ПЕРЕЕХАЛ 2026-09-03; данные /opt/adguard, DNS-провал ~2 мин |
|
||
| 9 | `mihomo` | ~~143~~ **159** | mini-pc | ПЕРЕЕХАЛ 2026-09-03; первый боевой device_passthrough /dev/net/tun |
|
||
| 10 | `ovpn-mini` | ~~132~~ **160** | mini-pc | ПЕРЕЕХАЛ 2026-09-03; туннель-петля при cutover, см. 5.10 |
|
||
| — | `hermes-ai` | 147 | cloud-pc | заморожен и недостижим по SSH, см. раздел 7 |
|
||
| — | `pbs` | 120 | cloud-pc | не мигрировать |
|
||
|
||
**Все мигрируемые сервисы (1-10) переехали на OpenTofu.** Осталось: `hermes-ai`
|
||
(заморожен), `pbs` (не мигрируется). Пост-миграционная уборка раздела 6
|
||
(удаление `roles/pve_lxc`, стрип creation plays у 8 плейбуков, правки
|
||
`architecture.md`) — отдельная задача, ещё не сделана; частично разблокирована.
|
||
|
||
### Специфика по сервисам
|
||
|
||
**1. `emergency-bot`.** ПЕРЕЕХАЛ 2026-09-02: OLD 148 остановлен, NEW 151 боевой
|
||
на 192.168.1.32. Данных нет вообще: `roles/emergency_bot` разворачивает
|
||
`emergency_bot.py.j2`, `.env.j2` и юнит из шаблонов. Шаг 4.4 пропущен целиком.
|
||
Бэкапа у него нет (`backup: none`) и это не мешает — он воспроизводим из
|
||
репозитория. Нужны `EMERGENCY_*` в корневом `.env`. Хороший первый заход
|
||
именно потому, что ошибиться почти негде — тем не менее, реальность разошлась
|
||
с планом в двух местах, оба задокументированы там, где их искать:
|
||
|
||
- **`playbooks/pve-emergency-bot.yml` не содержит конфигурационной части.**
|
||
У этого сервиса она в отдельном `playbooks/emergency-access.yml`, причём
|
||
два из четырёх play там таргетят `mini-pc` и `ru-vps`, а не сам контейнер
|
||
(доверие reverse-SSH через `authorized_key` с `exclusive: true` — полная
|
||
замена ключа, не добавление). Шаг 4.3.9 в его буквальном виде
|
||
(`--limit <name>-new` на существующем плейбуке) для этого сервиса не
|
||
работает: `hosts:` там — жёсткие имена, не группа, и полный прогон против
|
||
`mini-pc`/`ru-vps` до cutover преждевременно переключил бы доверие. Кроме
|
||
того, `emergency_bot.py.j2` использует Telegram `getUpdates` (не webhook) —
|
||
запуск второй копии с тем же `EMERGENCY_BOT_TOKEN`, что и боевая, ловит
|
||
409 conflict. Решение для шага 4.3 при следующем сервисе с похожей
|
||
архитектурой: деплоить только play(-и), таргетящие сам новый контейнер,
|
||
через одноразовый playbook с `hosts: <name>-new` и заведомо нерабочим,
|
||
но валидным по формату токеном/credential — так проверяется факт деплоя
|
||
(юнит стартует, зависимости стоят, egress работает) без конфликта с боевым
|
||
инстансом. Полный прогон с реальными credentials — уже после cutover,
|
||
когда исходный playbook с жёстким `hosts: <name>` сам начинает резолвиться
|
||
в новый контейнер (адрес не менялся, значит `--limit` не нужен вообще).
|
||
- **`cmode` — провайдер отслеживает его через блок `console`, но по
|
||
умолчанию блок не объявлен.** Подробности и правильная декларация —
|
||
`tofu/README.md` и шаг 4.2 выше.
|
||
|
||
**2. `docker-test` (145).** Тоже без данных. Проверяет проброс `/dev/fuse`
|
||
через `features.fuse` на реальном сервисе (не через `device_passthrough` —
|
||
см. уточнение в шаге 4.2).
|
||
|
||
**3. `gitea` (141 -> 153).** Главный приз и самый содержательный шаг.
|
||
- Было: `mp0: /opt/data/gitea,mp=/opt/gitea/data` — bind mount каталога ноды.
|
||
**Стало и подтверждено на живом контейнере 2026-09-02:**
|
||
`mp0: data:153/vm-153-disk-1.raw,mp=/opt/gitea/data,size=32G` — независимый
|
||
volume. Внутри виден как отдельная ФС `/dev/loop10`, ext4, 32 ГиБ.
|
||
Ради этого миграция и затевалась.
|
||
- Данные: `/opt/data/gitea` на cloud-pc (2.3 ГБ) → внутрь нового контейнера.
|
||
Каталоги `git/` (репозитории), `gitea/` (в т.ч. SQLite `gitea/gitea.db`,
|
||
~37 МиБ) и `ssh/`. На ноде всё принадлежит `101000:101000` — это host-side
|
||
отображение uid 1000 внутри unprivileged LXC.
|
||
Последовательность составлена по итогам разведки, но НЕ исполнялась:
|
||
|
||
ssh gitea 'systemctl stop gitea' # OLD, адрес ещё боевой
|
||
ssh cloud-pc "sudo sqlite3 /opt/data/gitea/gitea/gitea.db \
|
||
'PRAGMA integrity_check;'" # ожидается ok
|
||
|
||
# tar по SSH: rsync не годится, его нет на свежем контейнере,
|
||
# а tar есть в любом Debian
|
||
ssh cloud-pc 'sudo tar -C /opt/data/gitea -cf - .' \
|
||
| ssh gitea-new 'tar -C /opt/gitea/data -xf -'
|
||
|
||
# volume создан с нуля, поэтому владельца выставить явно ИЗНУТРИ
|
||
# контейнера — так результат не зависит от offset idmap
|
||
ssh gitea-new 'chown -R 1000:1000 /opt/gitea/data'
|
||
ssh gitea-new "sqlite3 /opt/gitea/data/gitea/gitea.db 'PRAGMA integrity_check;'"
|
||
ssh gitea-new 'systemctl start gitea'
|
||
|
||
Копирование внутри одного узла упирается в скорость диска, не сети: порядка
|
||
1-3 минут на 2.3 ГБ. Общее окно простоя `git.ada-dev.ru` — 5-10 минут.
|
||
- Offsite restic-профиль `gitea` выполняется **на cloud-pc**, а не внутри LXC,
|
||
и указывает на `/opt/data/gitea`. После переезда на volume этот путь исчезнет
|
||
— профиль в `playbooks/offsite-restic-yadisk.yml` придётся переписать на
|
||
новый источник. **Не забыть**, иначе offsite-бэкап Gitea тихо перестанет
|
||
работать.
|
||
- ~~`roles/gyro` пинит SSH host key gitea — обновить.~~ НЕВЕРНО, снято
|
||
2026-09-02: там `state: absent` (удаление устаревшей записи), а пинится
|
||
github.com. Подробности — в шаге 4.5.19.
|
||
- Публичный домен `git.ada-dev.ru` — после cutover `make reverse-proxy`.
|
||
|
||
**4. `vaultwarden` (140 -> 154).** SQLite `/opt/vaultwarden/data/db.sqlite3`,
|
||
публичный домен `pass.ada-dev.ru`.
|
||
|
||
Разведка 2026-09-02: каталог данных 2.9 МБ; `db.sqlite3` с живыми `-wal`/`-shm`,
|
||
`icon_cache`, `rsa_key.pem`, `config.json`. Каталогов `attachments/` и `sends/`
|
||
нет, эти возможности не используются. **`config.json` содержит ADMIN_TOKEN и
|
||
SMTP**, Ansible ими не управляет; они переедут вместе с каталогом. Это
|
||
ожидаемо, но файл не должен попасть в репозиторий.
|
||
|
||
Offsite-профиль restic здесь выполняется ВНУТРИ контейнера
|
||
(`hosts: vaultwarden`, `offsite_source_path: /opt/vaultwarden/data`), а не на
|
||
ноде, и адрес при переезде не меняется, поэтому **правки профиля не требуется**
|
||
в отличие от gitea.
|
||
|
||
Оба контейнера на mini-pc, поэтому копирование идёт через `pct exec` без
|
||
промежуточного файла. Последовательность составлена по итогам разведки, но НЕ
|
||
исполнялась:
|
||
|
||
ssh mini-pc 'sudo pgrep -x vzdump' # ожидается пусто
|
||
ssh mini-pc 'sudo pct exec 140 -- systemctl stop vaultwarden'
|
||
|
||
# .backup сливает WAL в один файл и заодно проверяет источник
|
||
ssh mini-pc "sudo pct exec 140 -- sqlite3 -cmd 'PRAGMA busy_timeout=30000;' \
|
||
/opt/vaultwarden/data/db.sqlite3 \".backup '/opt/vaultwarden/data/db.sqlite3.migrate'\""
|
||
ssh mini-pc 'sudo pct exec 140 -- sqlite3 \
|
||
/opt/vaultwarden/data/db.sqlite3.migrate "PRAGMA integrity_check;"'
|
||
|
||
ssh mini-pc 'sudo sh -c "pct exec 140 -- tar --exclude=./db.sqlite3 \
|
||
--exclude=./db.sqlite3-wal --exclude=./db.sqlite3-shm \
|
||
-cf - -C /opt/vaultwarden/data . | pct exec 154 -- tar -xf - -C /opt/vaultwarden/data"'
|
||
ssh mini-pc 'sudo pct exec 154 -- mv /opt/vaultwarden/data/db.sqlite3.migrate \
|
||
/opt/vaultwarden/data/db.sqlite3'
|
||
ssh mini-pc 'sudo pct exec 154 -- sqlite3 /opt/vaultwarden/data/db.sqlite3 \
|
||
"PRAGMA integrity_check;"'
|
||
ssh mini-pc 'sudo pct exec 140 -- rm /opt/vaultwarden/data/db.sqlite3.migrate'
|
||
ssh mini-pc 'sudo pct exec 154 -- systemctl restart vaultwarden'
|
||
|
||
Объём крошечный, простой на данных порядка полуминуты: время съедают
|
||
stop/restart и проверки, а не копирование.
|
||
|
||
**5. `monitoring` (146 -> 155).** Замороженный Prometheus-стек в новый
|
||
контейнер не разворачивать.
|
||
|
||
**Данные Uptime Kuma лежат НЕ в `/opt/monitoring`**, как утверждалось раньше.
|
||
Разведка 2026-09-02: это два независимых каталога верхнего уровня.
|
||
|
||
| Путь | Размер | Что это |
|
||
|---|---|---|
|
||
| `/opt/uptime-kuma` | 27 МБ | активный сервис, переносить целиком |
|
||
| `/opt/uptime-kuma/data/kuma.db` | 25.6 МиБ | мониторы и уведомления |
|
||
| `/opt/monitoring` | 1.7 ГБ | замороженный стек, почти всё — TSDB Prometheus |
|
||
|
||
Переносить нужно только `/opt/uptime-kuma`. TSDB замороженного стека переносить
|
||
незачем: читать его в новом контейнере будет нечему.
|
||
|
||
Конфигурация нового контейнера — `playbooks/uptime-kuma.yml`. У
|
||
`pve-monitoring.yml` конфигурационной части нет вообще, только создание.
|
||
`playbooks/monitoring.yml` не запускать.
|
||
|
||
Последовательность составлена по итогам разведки, но НЕ исполнялась:
|
||
|
||
ssh cloud-pc 'sudo pct exec 146 -- sqlite3 /opt/uptime-kuma/data/kuma.db \
|
||
"PRAGMA wal_checkpoint(TRUNCATE);"'
|
||
ssh cloud-pc 'sudo pct exec 146 -- systemctl stop uptime-kuma'
|
||
ssh cloud-pc 'sudo pct exec 146 -- sqlite3 /opt/uptime-kuma/data/kuma.db \
|
||
"PRAGMA integrity_check;"'
|
||
ssh cloud-pc 'sudo sh -c "pct exec 146 -- tar -C /opt -cpf - uptime-kuma \
|
||
| pct exec 155 -- tar -C /opt -xpf -"'
|
||
ssh cloud-pc 'sudo pct exec 155 -- sqlite3 /opt/uptime-kuma/data/kuma.db \
|
||
"PRAGMA integrity_check;"'
|
||
ssh cloud-pc 'sudo pct exec 155 -- systemctl start uptime-kuma'
|
||
|
||
**Вторую копию нельзя поднимать на боевых мониторах до остановки первой:**
|
||
Uptime Kuma активно пробит сервисы и шлёт уведомления, две копии дадут
|
||
дубликаты алертов.
|
||
|
||
Уменьшение контейнера — отдельное решение, не часть переезда. Цифры для него,
|
||
снятые 2026-09-02: боевой CT 146 занимает 173 МБ RAM из 4096 и 4.7 ГБ из 24;
|
||
новый CT 155 без Prometheus-стека — 184 МБ RAM и 1.8 ГБ диска.
|
||
|
||
В контейнере нет `curl` — для ручных HTTP-проверок использовать `wget`.
|
||
|
||
**6. `gyro` (150 -> 156).** CUTOVER COMPLETED 2026-09-02. CT 156 живёт на
|
||
неизменном production IP `192.168.1.35`, provisioned by Tofu. CT 150
|
||
остановлен и удерживается как rollback минимум на неделю; его PBS chain не
|
||
удалялся. Файл фаервола `156.fw` уже копия `150.fw`, cluster firewall по-
|
||
прежнему disabled. Timer `gyro.timer` active, last oneshot succeeded.
|
||
Данных для переноса не было, и TMPIP-specific bind/re-run не требовались.
|
||
|
||
**7. `grimmory` (149 -> 157).** Самый болезненный, и единственный, где
|
||
конфигурационный play пришлось чинить.
|
||
|
||
**Найденный баг и его исправление (2026-09-02).** В конфигурационном play
|
||
боевой адрес был зашит в трёх местах, из-за чего play невозможно было
|
||
прогнать против любого другого контейнера:
|
||
|
||
- `ports: "192.168.1.34:6060:6060"` в compose — Docker не биндит чужой адрес и
|
||
роняет `grimmory.service` ещё до старта приложения;
|
||
- правила `GRIMMORY-FILTER` (`--ctorigdst 192.168.1.34`) — фильтровали бы не
|
||
тот адрес;
|
||
- URL задачи `Wait for Grimmory health endpoint`. **Это было опаснее всего:**
|
||
задача выполняется на самом целевом хосте, поэтому даже после починки
|
||
биндинга она уходила бы по сети в БОЕВОЙ grimmory, получала 200 и давала
|
||
ложно-положительный результат, ничего не проверив в новом контейнере.
|
||
|
||
Исправлено через `grimmory_bind_ip: "{{ expected_lan_ip }}"` — ту же идиому,
|
||
что использует `roles/uptime_kuma`. На боевом хосте переменная равна
|
||
192.168.1.34, поэтому рендер не изменился: `--check --diff` с `--limit
|
||
grimmory` даёт `changed=0`, включая обе задачи, которые пишут адрес в файлы.
|
||
|
||
**Следствие для шага 4.5, которого в плане не было.** Grimmory — единственный
|
||
из шести сервисов пакета, кто биндится на конкретный адрес (`ss -ltn` на новых
|
||
контейнерах: gitea `0.0.0.0:3000`, vaultwarden `0.0.0.0:80`, uptime-kuma
|
||
`*:3001`, grimmory `192.168.1.16:6060`). После смены адреса на боевой compose
|
||
всё ещё будет содержать TMPIP, и сервис не поднимется. Плейбук ОБЯЗАН быть
|
||
прогнан заново сразу после cutover — см. шаг 4.5.16.
|
||
|
||
**Состояние после конфигурации на TMPIP (проверено):** оба юнита active,
|
||
оба контейнера healthy, `health=200` на 192.168.1.16, прогон идемпотентен
|
||
(`changed=0`).
|
||
|
||
**Образы совпали с боевым по digest** — `grimmory/grimmory:v3.2.4@sha256:dfa7afdf…`
|
||
и `lscr.io/linuxserver/mariadb:11.4.8@sha256:91de7f70…`. Это требование
|
||
`legacy-warning.md`: code-only downgrade после Flyway-миграции запрещён.
|
||
|
||
**Flyway: схемы сошлись.** На боевом и на новом контейнере одинаково —
|
||
`MAX(version)=144`, 142 миграции, все успешны. То есть тот же образ на пустой
|
||
базе приходит ровно к боевой версии схемы, и restore дампа новых миграций не
|
||
вызовет. Холодный старт с нуля занимает около 5 минут — health-check ретраится,
|
||
это нормально.
|
||
|
||
**Перенос данных.** `/opt/grimmory` — 311 МБ: `books/` 139 МБ (библиотека),
|
||
`data/` 5.6 МБ (обложки), `mariadb/` 167 МБ. Файлы MariaDB копировать НЕЛЬЗЯ,
|
||
нужен dump/restore. Рабочий рецепт дампа уже есть в
|
||
`tasks/offsite-restic-profile.yml` (`mariadb-dump --single-transaction
|
||
--routines --events`). Последовательность составлена по итогам разведки, но НЕ
|
||
исполнялась:
|
||
|
||
# приложение стоп, MariaDB оставить живой
|
||
ssh grimmory 'cd /opt/grimmory && docker compose stop grimmory'
|
||
ssh grimmory 'set -a; . /opt/grimmory/.env; set +a; \
|
||
docker exec -e MYSQL_PWD="$DB_PASSWORD" grimmory-mariadb mariadb-dump \
|
||
--user=grimmory --single-transaction --routines --events \
|
||
--databases grimmory > /opt/grimmory/backup-staging/grimmory.sql'
|
||
|
||
# библиотека и обложки
|
||
ssh cloud-pc 'sudo sh -c "pct exec 149 -- tar -C /opt/grimmory -cpf - books data \
|
||
| pct exec 157 -- tar -C /opt/grimmory -xpf -"'
|
||
|
||
# дамп на новый и restore
|
||
ssh cloud-pc 'sudo sh -c "pct exec 149 -- cat /opt/grimmory/backup-staging/grimmory.sql \
|
||
| pct exec 157 -- tee /opt/grimmory/backup-staging/grimmory.sql >/dev/null"'
|
||
ssh grimmory-new 'cd /opt/grimmory && docker compose stop grimmory'
|
||
ssh grimmory-new 'set -a; . /opt/grimmory/.env; set +a; \
|
||
docker exec -i -e MYSQL_PWD="$DB_PASSWORD" grimmory-mariadb mariadb \
|
||
--user=grimmory grimmory < /opt/grimmory/backup-staging/grimmory.sql'
|
||
ssh grimmory-new 'systemctl restart grimmory'
|
||
|
||
**Дамп не содержит `--add-drop-database`.** На новом контейнере схема уже
|
||
создана Flyway с нуля, поэтому перед restore её нужно либо очистить, либо
|
||
добавить эту опцию в дамп. Отдельно стоит знать, что restore-путь в этом
|
||
репозитории НИКОГДА не проверялся: `edge-cases.md` отмечает, что аудит
|
||
проверяет непустоту дампа, но не импортирует его.
|
||
|
||
**Эталон для сверки после restore** (снят с боевого 2026-09-02):
|
||
`book 31`, `author 30`, `book_file 40`, `book_metadata 31`, `category 87`,
|
||
`reading_sessions 68`, `shelf 3`, `library 1`, `users 1`, `opds_user_v2 2`,
|
||
`koreader_user 1`, `flyway_schema_history 142`.
|
||
|
||
**После cutover — проверка OPDS на живой читалке, а не только curl'ом.**
|
||
Быстрая проверка заголовков:
|
||
|
||
curl -sS -D- -o /dev/null https://books.ada-dev.ru/api/v1/opds # atom+xml, без Content-Encoding
|
||
curl -sS -D- -o /dev/null https://books.ada-dev.ru/api/v1/opds/search.opds
|
||
curl -sS https://books.ada-dev.ru/api/v1/healthcheck
|
||
|
||
Затем в KOReader: добавить каталог, увидеть список полок, скачать книгу
|
||
целиком, проверить синхронизацию прогресса.
|
||
|
||
**8. `adguard` (144 → 158).** ПЕРЕЕХАЛ 2026-09-03. OLD 144 остановлен (откат
|
||
≥ неделя, до ~2026-09-10). Данные `/opt/adguard/{conf,work}` (~313 МБ,
|
||
`conf/AdGuardHome.yaml` несёт хэш пароля, DNS rewrites, upstream, клиентов)
|
||
перенесены целиком через `pct exec … tar` (оба контейнера на mini-pc).
|
||
|
||
- **Свежий AdGuard на пустом `conf/` не проходит health-гейт плейбука.** Он
|
||
уходит в setup-wizard: порт 3000 отдаёт 302, порт 80 — connection reset, а
|
||
`pve-adguard.yml` ждёт `http://127.0.0.1/` `[200,302]` и падает после 24
|
||
ретраев. Поэтому для adguard данные пред-заливаются ДО шага 4.3 (config play),
|
||
а не после. После пред-заливки прогон идемпотентен (`ok=13 changed=0`),
|
||
фильтрация подтверждена: `doubleclick.net → 0.0.0.0`.
|
||
- Ноды используют DNS роутера (192.168.1.1), контейнеры — 1.1.1.1 (из
|
||
`pct config` `nameserver`). Провал .28 задел только DHCP-клиентов LAN и
|
||
рабочую станцию (у неё fallback на роутер). Окно ~2 мин, ночью. Роутер не
|
||
трогали.
|
||
- Сокеты wildcard (`0.0.0.0`), повторный прогон после смены адреса не нужен.
|
||
|
||
**9. `mihomo` (143 → 159).** ПЕРЕЕХАЛ 2026-09-03. OLD 143 остановлен (откат
|
||
≥ неделя). Первый боевой сервис с **двумя механизмами проброса устройств**:
|
||
`/dev/fuse` через `features.fuse`, `/dev/net/tun` через блок
|
||
`device_passthrough` (`dev0: path=/dev/net/tun,mode=0660`) — оба под root@pam,
|
||
проверены здесь на живом сервисе (до этого `device_passthrough` был только на
|
||
пилоте).
|
||
|
||
- Данные `/opt/mihomo` (~80 КБ): `config/config.yaml`, `config/cache.db`,
|
||
`config/providers/main.yaml`. `config.yaml` содержит URL подписки прокси-
|
||
провайдера — секрет, копируется `pct exec`, в репозиторий не попадает.
|
||
Задача «Install default mihomo config if missing» идёт с `force: false`,
|
||
перенесённый конфиг не перетирается.
|
||
- Все сокеты wildcard (`0.0.0.0`) — повторный прогон после смены адреса не
|
||
нужен. Прокси реально проверен: `curl -x .27:7890 …/generate_204 → 204`.
|
||
- `ru-vps-mihomo-harden.yml` повторного прогона НЕ требует: он таргетит
|
||
`hosts: ru-vps`, работает со скриптом `/usr/local/sbin/ru-vps-mihomo-harden`
|
||
на ru-vps, адрес mihomo нигде в нём не зашит, и он за `CONFIRM`-гейтом.
|
||
- Зависимые (`bash_config_proxy_*` у hermes-ai, `emergency_telegram_proxy`,
|
||
`uptime_kuma_http_proxy`, правило gyro-фаервола `OUT ACCEPT 192.168.1.27:7890`,
|
||
`prometheus.yml.j2`) все ссылаются на .27 — адрес сохранён, правок не нужно.
|
||
|
||
**10. `ovpn-mini` (132 → 160).** ПЕРЕЕХАЛ 2026-09-03 из локальной сети. OLD 132
|
||
остановлен (откат ≥ неделя). `/dev/net/tun` через `device_passthrough`.
|
||
`features` — только `nesting` (как в эталоне). Конфигурации в `pve-ovpn-mini.yml`
|
||
нет — шлюз настраивает `openvpn-vps-mini.yml` (роль `openvpn_gateway`, группа
|
||
`vpn_openvpn`). Единственные данные — `/etc/openvpn/homelab/static.key`, общий
|
||
с ru-vps; LAN-адрес ovpn-mini ни в одном шаблоне роли не фигурирует
|
||
(masquerade по `-o eth0`), поэтому туннель не зависит от смены адреса.
|
||
|
||
- **Петля транспорта при cutover.** `-F ansible/ssh_config` до нод PVE идёт
|
||
ProxyJump через ru-vps, а ru-vps достаёт LAN ЧЕРЕЗ туннель, который
|
||
терминирует ovpn-mini. `make tofu-*` строит SSH-туннель к PVE API тем же
|
||
путём. `pct stop 132` кладёт туннель → `make tofu-apply` больше не достаёт
|
||
API. Разрыв: сначала поднять шлюз на НОВОМ контейнере ещё на TMPIP
|
||
(одноразовый плейбук `hosts: ru-vps` slurp ключа + `hosts: <name>-new` роль
|
||
`openvpn_gateway`), туннель встаёт с TMPIP-адреса → `make tofu-apply` снова
|
||
работает → сменить адрес на боевой → повторный `openvpn-vps-mini.yml`
|
||
(идемпотентен, `changed=0` на обоих концах). Прямой доступ к нодам во время
|
||
провала — `ssh -o ProxyJump=none <ansible-user>@192.168.1.{5,10}` из LAN.
|
||
- **`ssh_config`: у `ovpn-mini` теперь `ProxyJump none`** (в индивидуальном
|
||
Host-блоке, побеждает по «первое значение опции»). Путь через ru-vps
|
||
закольцовывался бы на его же туннель. Из LAN хост доступен напрямую; вне
|
||
LAN управление — консоль ноды (`pct exec`) или заранее поднятый туннель.
|
||
- **НЕ запускать OLD 132 как диагностику.** Его `pct config` всё ещё держит
|
||
`ip=192.168.1.23/24`; параллельный старт с CT 160 даёт конфликт .23 и роняет
|
||
туннель на 1-2 мин (проверено случайно 2026-09-03, восстановилось само).
|
||
- `make openvpn-check` — 7/7 ok. Кворум кластера не затронут (qdevice ходит
|
||
напрямую нода → ru-vps:5403, не через туннель).
|
||
- Косметика: `polkit.service` на CT 160 в `failed` (`status=217/USER` —
|
||
минимальный LXC-шаблон без нужного окружения polkit). На OpenVPN не влияет,
|
||
не чинилось.
|
||
|
||
---
|
||
|
||
## 6. После завершения всех переездов
|
||
|
||
- Удалить `roles/pve_lxc` — он станет не нужен.
|
||
- Убрать из `services.yml` поле `provisioner` со значениями `pct_ssh`/`pve_lxc`
|
||
либо заменить на `tofu`.
|
||
- Обновить `docs/ai/architecture.md`: раздел «Provisioning Flow» описывает два
|
||
пути через `pct` и API — оба исчезнут.
|
||
- Обновить `legacy-warning.md`: пункт про два provisioner-пути потеряет смысл.
|
||
- Расширить `validate.yml`: сейчас он сверяет hostname, IP, cores, memory, swap.
|
||
После миграции имеет смысл добавить features, устройства и mount points —
|
||
ровно те поля, которыми теперь управляет Tofu.
|
||
- Решить, нужен ли `roles/lxc_docker_host` — он до сих пор ни к чему не
|
||
подключён.
|
||
|
||
---
|
||
|
||
## 7. Известные проблемы вне миграции
|
||
|
||
Не блокируют переезд, но про них надо знать.
|
||
|
||
- **`hermes-ai` (147) недостижим по SSH.** Прозрачный прокси заворачивает в
|
||
`hermes-tun` всё, что не пришло с `lo`, включая ответные пакеты входящих
|
||
соединений (`ip rule` 9002). Ansible до хоста не достучится, управление —
|
||
только через `pct exec`. Сервис заморожен, `make check` показывает его DOWN
|
||
ожидаемо. Мигрировать не раньше, чем починится сеть.
|
||
- **`resticprofile-check@profile-default` на ru-vps падает еженедельно**:
|
||
профиль `default` без репозитория. Реальный `profile-services` работает.
|
||
Шум в секции FAILED отчёта `make status`.
|
||
- **Gitea runner** зарегистрирован и опрашивает Gitea, но метка `ru-vps` не
|
||
совпадает с `runs-on: ubuntu-latest` в workflow, поэтому CI не выполняется.
|
||
Раннер монтирует `/var/run/docker.sock` и `/opt/services` на запись — перед
|
||
включением CI это стоит пересмотреть.
|
||
- **`homelab_pve_egress_ip` динамический.** При смене домашнего адреса qdevice
|
||
замолчит. Видно в секции CLUSTER QUORUM отчёта `make status`.
|
||
- **`bootstrap-pve-api-token.yml` перезаписывает корневой `.env` целиком** —
|
||
вместе с `PROXMOX_ROOT_PASSWORD`, `MONITORING_*` и `EMERGENCY_*`. У задачи
|
||
есть `backup: true`, но восстанавливать придётся руками.
|
||
- Устаревшие правила UFW на ru-vps (`3128`, `1080`, `993`, `7892`) — за
|
||
переключателем `zt_cleanup_unrelated_stale_rules` в
|
||
`playbooks/ru-vps-zerotier-decommission.yml`, по умолчанию выключен.
|
||
|
||
---
|
||
|
||
## 8. Откат
|
||
|
||
**До шага 4.5.14** (пока старый контейнер работает): просто не переключаться.
|
||
Удалить новый контейнер `make tofu-destroy CONFIRM=1` или точечно.
|
||
|
||
**После переключения, но до `pct destroy`:** остановить новый, вернуть адрес
|
||
старому не нужно — он не менялся, `pct start OLD` возвращает всё как было.
|
||
Затем откатить `vmid` в `services.yml` и прогнать `make backup-jobs`,
|
||
`make backup-audit`, `make validate`.
|
||
|
||
**После `pct destroy OLD`:** только восстановление из PBS. Именно поэтому
|
||
шаг 4.1.3 (свежий бэкап) обязателен, а шаг 4.7.25 (неделя ожидания) не
|
||
сокращается.
|