Migration: transfer data and run apps
This commit is contained in:
@@ -8,6 +8,245 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Шаг 13 — приложения подняты на target, cutover завершён (2026-05-23, выполнено)
|
||||||
|
|
||||||
|
После rsync'а (Шаг 12) — финальный прогон ансибла без `--skip-tags`,
|
||||||
|
поэтапно по приложениям. К ~16:30 DNS уже указывал на target (Шаг
|
||||||
|
переключения 15:45 + TTL 20 мин, пропагация подтверждена в 16:20),
|
||||||
|
так что Caddy при старте сразу пошёл за LE-сертификатами без задержек.
|
||||||
|
|
||||||
|
Прогоны делал поштучно через `inv pl -- <app>` (после Шага
|
||||||
|
переключения `HOSTS_FILE = "timeweb.yml"` в `tasks.py`), не всем
|
||||||
|
сразу — чтобы видеть каждый плейбук чисто.
|
||||||
|
|
||||||
|
### Что подтверждено работающим в браузере
|
||||||
|
|
||||||
|
- `vakhrushev.me` — homepage отдаёт страницу.
|
||||||
|
- `auth.vakhrushev.me` — Authelia, логин работает.
|
||||||
|
- `matrix.vakhrushev.me` — Tuwunel поднялся, Element подключается.
|
||||||
|
- `git.vakhrushev.me` — Gitea, репозитории и issue tracker на месте.
|
||||||
|
- `outline.vakhrushev.me` — документы видны.
|
||||||
|
- `gramps.vakhrushev.me` — генеалогическое дерево открывается.
|
||||||
|
- `wakapi.vakhrushev.me` — статистика времени видна.
|
||||||
|
- `status.vakhrushev.me` — Netdata собирает и рисует метрики.
|
||||||
|
|
||||||
|
Точечно зашёл в outline / gramps / wakapi / gitea — данные на месте,
|
||||||
|
ничего не потерялось при rsync'е.
|
||||||
|
|
||||||
|
### Отложенные на «потом по ходу дела» проверки
|
||||||
|
|
||||||
|
- `miniflux`, `memos`, `remembos`, `wanderer`, `calibre`, `rssbridge`,
|
||||||
|
`dozzle`, `goaccess` — открыть и убедиться, что отдают свои данные.
|
||||||
|
- **SMTP-test** — reset-password из gitea/authelia. Проверит, что
|
||||||
|
Postbox после разблокировки в панели Timeweb принимает наши письма.
|
||||||
|
- **Backup-cron в 1:00** — самый поздний smoke-тест системы. Покажет,
|
||||||
|
что `backup-all.py` отработал на target, restic пишет в S3 с новым
|
||||||
|
`host_name`, apprise шлёт уведомление.
|
||||||
|
- `docker pull cr.yandex/...` руками — повторная проверка
|
||||||
|
OAuth-аутентификации.
|
||||||
|
|
||||||
|
### Отклонения от плана сегодня
|
||||||
|
|
||||||
|
1. **VPS пересоздан в СПб** (Шаг 8) — первая выдача попала на
|
||||||
|
гипервизор с битой сетью.
|
||||||
|
2. **Docker Hub rate limit** на pull'е netdata — anonymous лимит
|
||||||
|
подсети Timeweb уже выбран соседями. Лечится ручным
|
||||||
|
`sudo docker login` на target (через free-аккаунт + PAT).
|
||||||
|
**Backlog:** добавить `community.docker.docker_login` для
|
||||||
|
`docker.io` в `playbook-docker.yml`, по аналогии с cr.yandex (Шаг
|
||||||
|
3). Креды в vault как `dockerhub_username` / `dockerhub_token`.
|
||||||
|
3. **Postbox SMTP не доступен извне YC** — оказалось, что в плане
|
||||||
|
(`timeweb.md:81`) предпосылка «Postbox доступен извне YC по тем же
|
||||||
|
credentials» неверна. Yandex Cloud Postbox дропает SMTP от не-YC
|
||||||
|
источников; 443 при этом отвечает. Дополнительно Timeweb по
|
||||||
|
умолчанию **сам** блокирует egress SMTP (25/465/587) — toggle в
|
||||||
|
панели Timeweb снимает блок, после чего Postbox отвечает баннером.
|
||||||
|
Authelia в exit-loop'е поднялась после рестарта. Запись в auto-
|
||||||
|
memory `project_timeweb_smtp_block.md` — пригодится при следующих
|
||||||
|
миграциях.
|
||||||
|
4. **Bug ordering в `playbook-goaccess.yml`** (см. Шаг 9, фикс
|
||||||
|
зашит) — латентный bug, проявившийся только на чистой машине.
|
||||||
|
|
||||||
|
### Что осталось до полной заморозки
|
||||||
|
|
||||||
|
По плану (`timeweb.md:464-473`):
|
||||||
|
|
||||||
|
- **≥ 24 часа** держим источник в выключенном состоянии (docker уже
|
||||||
|
остановлен, daemon отключён через `disable`), как горячее запасное.
|
||||||
|
- Если за сутки ничего не всплыло — выключить VM в YC.
|
||||||
|
- Подождать ещё неделю-две — на всякий случай.
|
||||||
|
- Удалить VM и связанные compute-ресурсы. **S3-бакет с
|
||||||
|
restic-бэкапами и Container Registry — оставляем**, они продолжают
|
||||||
|
использоваться.
|
||||||
|
- Удалить `production.yml`, переименовать `timeweb.yml` →
|
||||||
|
`production.yml`, откатить `HOSTS_FILE = "production.yml"` в
|
||||||
|
`tasks.py`. Закоммитить.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 12 — rsync данных с источника на target (2026-05-23, выполнено)
|
||||||
|
|
||||||
|
Перенос `/mnt/applications/` на YC → `/srv/applications/` на Timeweb
|
||||||
|
после заморозки источника (Шаг 11). Это финальный канал переноса
|
||||||
|
данных — основной для всех приложений, единственный для `caddyproxy`,
|
||||||
|
`remembos`, `transcriber` (у которых нет backup-механизма, см. Шаг 7b).
|
||||||
|
|
||||||
|
### Пилотный прогон на remembos
|
||||||
|
|
||||||
|
Прежде чем гнать всё дерево, проверил рецепт на самом маленьком
|
||||||
|
приложении (~35 КБ всего):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -E rsync -aAX --info=progress2 --delete --rsync-path="sudo rsync" \
|
||||||
|
-e "ssh -o StrictHostKeyChecking=accept-new" \
|
||||||
|
major@158.160.46.255:/mnt/applications/remembos/ \
|
||||||
|
/srv/applications/remembos/
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка после прогона:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo ls -la /srv/applications/remembos/
|
||||||
|
drwxr-x--- 4 remembos remembos 4096 Apr 30 13:22 .
|
||||||
|
drwxr-x--- 2 remembos remembos 4096 Feb 12 17:22 config
|
||||||
|
drwxr-x--- 2 remembos remembos 4096 May 23 12:41 data
|
||||||
|
-rw-r----- 1 remembos remembos 494 Apr 30 13:22 docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Owner отрисован именами (`remembos:remembos`, не numeric `1103:1103`)
|
||||||
|
— значит на обеих сторонах ансибл создал юзера с одним и тем же uid,
|
||||||
|
mapping сошёлся. Mode (750) и mtime сохранены.
|
||||||
|
|
||||||
|
### Засада с agent-forwarding'ом под sudo
|
||||||
|
|
||||||
|
Первая попытка упала с `Permission denied (publickey)`. Причина:
|
||||||
|
rsync запускается через `sudo` на target, а sudo по дефолту чистит
|
||||||
|
`SSH_AUTH_SOCK` из env (`Defaults env_reset` в /etc/sudoers) — ssh
|
||||||
|
внутри sudo не видит проброшенный agent, пытается парольную
|
||||||
|
аутентификацию, проваливается.
|
||||||
|
|
||||||
|
Лечится разрешением sudo проносить именно эту переменную:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo 'Defaults env_keep += "SSH_AUTH_SOCK"' | sudo tee -a /etc/sudoers.d/major
|
||||||
|
sudo visudo -cf /etc/sudoers.d/major
|
||||||
|
```
|
||||||
|
|
||||||
|
Безопасно: сокет агента принадлежит `major`, root к нему имеет доступ
|
||||||
|
по определению; мы просто говорим sudo не вычищать переменную с путём
|
||||||
|
к нему. После этого `sudo -E rsync …` отрабатывает.
|
||||||
|
|
||||||
|
### Полный прогон по всем приложениям
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -E rsync -aAX --info=progress2 --delete --exclude='lost+found' \
|
||||||
|
--rsync-path="sudo rsync" \
|
||||||
|
-e "ssh -o StrictHostKeyChecking=accept-new" \
|
||||||
|
major@158.160.46.255:/mnt/applications/ \
|
||||||
|
/srv/applications/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Что делает каждый флаг
|
||||||
|
|
||||||
|
- **`sudo -E`** — локальный rsync на target запускается под root
|
||||||
|
(нужно, чтобы писать файлы с любым owner'ом / mode); `-E` сохраняет
|
||||||
|
env, в первую очередь `SSH_AUTH_SOCK` для agent forwarding.
|
||||||
|
- **`-a`** (`--archive`) — собирательный флаг `-rlptgoD`: recursive +
|
||||||
|
symlinks как symlinks + permissions + times + group + owner +
|
||||||
|
special files. Базовое «копировать всё как есть».
|
||||||
|
- **`-A`** — сохранить POSIX ACL.
|
||||||
|
- **`-X`** — сохранить extended attributes (xattrs), включая
|
||||||
|
security-атрибуты типа capabilities или SELinux-меток.
|
||||||
|
- **`--info=progress2`** — совокупный прогресс по всему transfer'у,
|
||||||
|
а не per-file (для больших деревьев читабельнее).
|
||||||
|
- **`--delete`** — стереть на target всё, чего нет на источнике.
|
||||||
|
Безопасно в нашем случае: после rsync'а прогоняем ансибл, он
|
||||||
|
перерендерит конфиги и пересоздаст любые отсутствующие структурные
|
||||||
|
каталоги. Стирается, по сути, только содержимое, отрендеренное
|
||||||
|
плейбуком на Шаге 9 без `run-app`.
|
||||||
|
- **`--exclude='lost+found'`** — на YC `/mnt/applications/` это mount
|
||||||
|
point внешнего диска, в его корне может лежать системный
|
||||||
|
`lost+found`. Нам он не нужен и на target такого монтирования
|
||||||
|
больше нет (`mount_external_storage: false`).
|
||||||
|
- **`--rsync-path="sudo rsync"`** — критично: на удалённой стороне
|
||||||
|
(источнике) rsync запускается через sudo. Иначе он стартует под
|
||||||
|
`major`, у которого нет прав читать чужие `/mnt/applications/<app>/`
|
||||||
|
(mode 750, owner — приложение). У `major` на источнике NOPASSWD
|
||||||
|
sudo, так что sudo прокатывает молча.
|
||||||
|
- **`-e "ssh -o StrictHostKeyChecking=accept-new"`** — кастомная
|
||||||
|
команда транспорта. По умолчанию rsync запускает чистый `ssh`; мы
|
||||||
|
добавляем флаг для автопринятия host key источника (на target
|
||||||
|
`known_hosts` ещё пустой).
|
||||||
|
- **`major@158.160.46.255:/mnt/applications/`** — источник. Trailing
|
||||||
|
slash важен: «копировать содержимое каталога», а не сам каталог.
|
||||||
|
Без слэша получили бы `/srv/applications/applications/...`.
|
||||||
|
- **`/srv/applications/`** — назначение. Trailing slash для
|
||||||
|
симметрии — содержимое кладётся в существующий каталог,
|
||||||
|
созданный ансиблом на Шаге 9.
|
||||||
|
|
||||||
|
### Результат
|
||||||
|
|
||||||
|
```
|
||||||
|
22,613,081,829 99% 7.11MB/s 0:50:34 (xfr#21837, to-chk=0/31024)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Объём — ~22.6 ГБ, файлов — 31 024.
|
||||||
|
- Длительность — 50 минут 34 секунды, средняя скорость ~7 МБ/с
|
||||||
|
(предсказуемо для YC↔Timeweb).
|
||||||
|
- `du -s` после прогона: источник 22 088 224 КБ, target 22 164 172 КБ
|
||||||
|
— разница ~76 МБ (0.34%). Это не рассинхрон данных, а разница в
|
||||||
|
аллокации блоков ФС и метаданных между источником и target (разные
|
||||||
|
inode-таблицы, journal, group descriptors). Содержимое файлов
|
||||||
|
совпадает — rsync'у на это указали checksum'ы, errors не было.
|
||||||
|
|
||||||
|
Окно даунтайма с момента стопа docker'а (Шаг 11) до конца rsync'а —
|
||||||
|
около часа. С учётом параллельно запущенного DNS-переключения
|
||||||
|
(Шаг между 11 и 12, 15:45) к моменту запуска приложений на target
|
||||||
|
пропагация уже прошла (16:20).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 11 — источник заморожен (docker + cron остановлены) (2026-05-23, выполнено)
|
||||||
|
|
||||||
|
Сразу после финального бэкапа (Шаг 10) — отключил docker и cron на
|
||||||
|
источнике, чтобы зафиксировать состояние данных перед rsync'ом и
|
||||||
|
исключить случайные записи в `/mnt/applications/` во время переноса.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl stop docker.service docker.socket
|
||||||
|
sudo systemctl disable docker.service docker.socket
|
||||||
|
sudo systemctl stop cron
|
||||||
|
```
|
||||||
|
|
||||||
|
`disable` — страховка от автостарта docker'а при возможной
|
||||||
|
перезагрузке источника (если вернёмся для отката или проверки).
|
||||||
|
`cron stop` — чтобы ночной `backup-all.py` не запустился впустую без
|
||||||
|
работающего daemon'а.
|
||||||
|
|
||||||
|
С этого момента источник «мёртв» для пользователей — окно даунтайма
|
||||||
|
открыто. Следующий шаг — переключить DNS и параллельно гнать rsync.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 10 — финальный бэкап на источнике (2026-05-23, выполнено)
|
||||||
|
|
||||||
|
Прогнал `backup-all.py` на источнике, пока docker ещё жив (он нужен
|
||||||
|
для `pg_dump` и других in-container backup-команд внутри
|
||||||
|
`backup.sh`-скриптов отдельных приложений).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo /usr/local/sbin/backup-all.py 2>&1 | tee /tmp/final-backup.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Свежий restic-снапшот в `yandex_cloud_s3` зафиксирован — страховочный
|
||||||
|
канал на случай, если rsync пойдёт криво (для приложений с
|
||||||
|
`backup.sh` можно будет восстановить из S3; для `caddyproxy`,
|
||||||
|
`remembos`, `transcriber` страховки нет, для них только rsync).
|
||||||
|
|
||||||
|
После прогона можно гасить docker без риска потерять backup-окно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Шаг 9 — раскатана база и приложения без запуска (2026-05-23, выполнено)
|
## Шаг 9 — раскатана база и приложения без запуска (2026-05-23, выполнено)
|
||||||
|
|
||||||
На свежей Timeweb-машине прогнаны два плейбука без даунтайма источника
|
На свежей Timeweb-машине прогнаны два плейбука без даунтайма источника
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ from invoke.context import Context
|
|||||||
from invoke.exceptions import Exit
|
from invoke.exceptions import Exit
|
||||||
from invoke.tasks import task
|
from invoke.tasks import task
|
||||||
|
|
||||||
HOSTS_FILE = "production.yml"
|
HOSTS_FILE = "timeweb.yml"
|
||||||
VARS_FILE = "vars/vars.yml"
|
VARS_FILE = "vars/vars.yml"
|
||||||
AUTHELIA_DOCKER = "docker run --rm -v $PWD:/data authelia/authelia:4.39.4 authelia"
|
AUTHELIA_DOCKER = "docker run --rm -v $PWD:/data authelia/authelia:4.39.4 authelia"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user