Files
pet-project-server/docs/drafts/timeweb-migration-log.md
T

292 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Журнал миграции в Timeweb
Хронология фактического переезда. План и архитектурные решения —
в [timeweb.md](timeweb.md). Здесь только то, что реально сделано,
с датами.
Новые записи — сверху.
---
## Шаг 7 — `run-app` тег + унификация registry (2026-05-22, выполнено)
По итогам аудита подготовительных задач выявлены и закрыты две
несостыковки:
### 7a. Пропущенный `run-app` тег в remembos
В `playbook-remembos.yml:73` была задача
`Restart docker compose services if config changed but not
docker-compose.yml` (условный рестарт через `state: restarted`,
триггер — изменение `config.toml` без изменения `docker-compose.yml`),
у неё не было тега `run-app`. На cutover'е при
`--skip-tags run-app` основной запуск пропустился бы (правильно), а
эта условная задача всё равно сработала бы (потому что её `when:`
истинно при первом деплое — конфиг создаётся), попыталась бы
рестартануть несуществующий compose-стек и упала. Тег добавлен.
### 7b. Унификация `registry_url` в docker_login
`playbook-homepage.yml` и `playbook-transcriber.yml` использовали
хардкод `registry_url: "cr.yandex"`, а `playbook-remembos.yml`
`'{{ yc_container_registry }}'` из vault. Привёл к одному виду:
теперь во всех трёх — `"{{ yc_container_registry }}"` из vault.
`docker_registry_prefix` в `vars/homepage.yml` и `vars/transcriber.yml`
не трогал — там полный image-prefix вида `cr.yandex/<org-id>`,
это отдельная концепция (есть отдельный vault-var
`yc_container_registry_repository`, используемый в
`files/remembos/docker-compose.template.yml`). Если позже захочется
унифицировать целиком — это отдельная итерация.
### Аудит бэкапов: gap'ы по `caddyproxy`, `remembos`, `transcriber`
Эти три приложения имеют состояние в `data_dir`, но не имеют ни
`backup.template.sh`, ни ansible-генерируемого `backup-targets`.
Для миграции это закрывается через **rsync** на cutover'е — данные
переносятся напрямую, без зависимости от restic-снапшотов:
- `caddyproxy/data/` — TLS-сертификаты Let's Encrypt (важно, чтобы
не упереться в rate-limit LE при перевыпуске ~17 сертов).
- `remembos/data/` — user data (memos-токен, telegram tokens).
- `transcriber/data/` — пользовательские транскрипции.
Это означает: на этапе rsync (шаг 4 cutover'а в плане) **нельзя**
полагаться только на restic-restore — для этих трёх апов rsync —
единственный канал. Для остальных приложений (которые имеют
`backup.sh` или `backup-targets`) можно при необходимости использовать
restic как фолбэк, но rsync всё равно остаётся основным методом.
Долгосрочно — добавить им backup-механизм отдельной итерацией после
миграции. Сейчас это сверх сферы.
---
## Шаг 6 — `vars/vars.yml` загружается во всех плейбуках (2026-05-22, выполнено)
Сегодняшний коммит `8378f0e` («Migration: expose some public vars»)
вынес общие переменные (`application_dir`, `host_name`, `primary_user`,
`primary_user_uid`, `primary_user_gid`, `bin_prefix`,
`apprise_external_port`, `apprise_external_url`, `caddy_logs_dir`) из
vault в `vars/vars.yml`. Но большая часть плейбуков загружала только
`vars/secrets.yml` — на текущем сервере они работали лишь потому, что
inventory дублирует `application_dir` как override. На чистом
Timeweb-инвентаре без override они бы упали с undefined.
Прошёлся по всем плейбукам, добавил `- vars/vars.yml` сразу после
`- vars/secrets.yml`:
```
playbook-authelia.yml playbook-netdata.yml
playbook-calibre.yml playbook-outline.yml
playbook-docker.yml playbook-remembos.yml
playbook-dozzle.yml playbook-rssbridge.yml
playbook-eget.yml playbook-transcriber.yml
playbook-gitea.yml playbook-transcriber-registry.yml
playbook-gramps.yml playbook-tuwunel.yml
playbook-homepage.yml playbook-ufw.yml
playbook-homepage-registry.yml playbook-upgrade.yml
playbook-memos.yml playbook-wakapi.yml
playbook-miniflux.yml playbook-wanderer.yml
```
(21 файл — все «обычные» плейбуки, которые ещё не подключали vars.yml.)
Aggregator'ы `playbook-all-applications.yml` и `playbook-all-setup.yml`
не трогал — у них нет собственных `vars_files`, они используют
`import_playbook`, каждый импортируемый плейбук уже сам подключает
`vars.yml`.
`yamllint` чист. Идемпотентность проверить отдельным прогоном.
Проверить прогоном `inv pl -- all-applications` (или хотя бы
`inv pl -- gitea outline miniflux`) на текущем сервере — diff
ожидается пустой.
---
## Шаг 5 — переезд default application_dir на /srv (2026-05-22, выполнено)
`/mnt` по FHS — место для точек монтирования внешних дисков; на
системном диске Timeweb (фаза 1) это семантически неверно. Поменяли
дефолт на `/srv/applications` (FHS: «data for services provided by
this system»), для текущего YC-сервера сделали override в инвентаре.
Изменения:
- `vars/vars.yml``application_dir: "/srv/applications"`
(комментарий обновлён).
- `production.yml`у хоста `server` добавлен override
`application_dir: "/mnt/applications"`.
- `playbook-system.yml` — добавлен `vars/vars.yml` в `vars_files`,
захардкоженный `/mnt/applications` в задачах
`Create directory for mount` и `Mount external storages` заменён
на `{{ application_dir }}`.
- `playbook-remove-user-and-app.yml` — то же самое (`vars/vars.yml`
в `vars_files` + `{{ (application_dir, user_name) | path_join }}`).
- `tasks.py` — новый helper `_application_dir()` читает значение
сначала из inventory (override), затем из `vars/vars.yml`. `login_as_app`
больше не содержит `/mnt/applications`.
Что остаётся хардкодом — только `/mnt/applications` в `production.yml`
как override, и это правильно.
На Timeweb-инвентаре (когда появится) можно либо не задавать
`application_dir` вовсе (применится дефолт `/srv/applications`), либо
задать явно — для читаемости.
Проверить прогоном `inv pl -- system` на текущем сервере (Yandex
Cloud) — ничего не должно поменяться, потому что inventory override
возвращает `/mnt/applications` и mount всё ещё включён. Diff ожидается
пустой.
### Восстановление restic-снапшотов после смены путей
Старые снапшоты записаны с путями `/mnt/applications/<app>`. На
Timeweb данные должны лежать в `/srv/applications/<app>`. У restic
нет встроенного «remap path» при restore, поэтому делается в два
шага: восстановить во временный каталог, затем `rsync` на новое
место с сохранением uid/gid (приложения уже созданы playbook'ом с
теми же uid/gid, см. шаг про подготовку target).
Пример — восстановить gitea на Timeweb-машине:
```bash
sudo /usr/local/sbin/restic-shell.sh
# Распакуем нужную поддиректорию во временный каталог
restic restore latest \
--target /tmp/restic-restore \
--include /mnt/applications/gitea
# Перенесём данные на новый путь, сохранив владельца/группу/ACL/xattr
sudo rsync -aAX --info=progress2 \
/tmp/restic-restore/mnt/applications/gitea/ \
/srv/applications/gitea/
sudo rm -rf /tmp/restic-restore
```
Несколько приложений за один проход:
```bash
restic restore latest \
--target /tmp/restic-restore \
--include /mnt/applications/gitea \
--include /mnt/applications/outline \
--include /mnt/applications/miniflux
for app in gitea outline miniflux; do
sudo rsync -aAX --info=progress2 \
"/tmp/restic-restore/mnt/applications/$app/" \
"/srv/applications/$app/"
done
sudo rm -rf /tmp/restic-restore
```
Альтернатива через `restic mount` (если не хочется промежуточной
копии — данные мапятся как FUSE-FS):
```bash
sudo mkdir -p /mnt/restic-snapshots
restic mount /mnt/restic-snapshots &
sudo rsync -aAX \
/mnt/restic-snapshots/snapshots/latest/mnt/applications/gitea/ \
/srv/applications/gitea/
sudo fusermount -u /mnt/restic-snapshots
```
После переезда новые снапшоты будут записываться уже с путями
`/srv/applications/<app>` — никаких трюков для текущих бэкапов не
нужно.
---
## Шаг 4 — условное монтирование внешнего диска (2026-05-22, выполнено)
Задача `Mount external storages` в `playbook-system.yml` теперь
выполняется только при включённом флаге `mount_external_storage`
(default `false`). Сам UUID диска оставлен захардкоженным в
плейбуке — параметризовать не стали, потому что для Timeweb (фаза 1)
монтирование вообще не нужно, а для фазы 2 пока неизвестно, какой
UUID получится у второго диска.
Изменения:
- `playbook-system.yml` — у задачи mount добавлен
`when: mount_external_storage | default(false) | bool`.
- `production.yml` (инвентарь YC) — у хоста `server` добавлен
`mount_external_storage: true`, чтобы текущее поведение
сохранилось.
В будущем `timeweb.yml` просто не будет задавать эту переменную —
mount пропустится, `/mnt/applications` останется обычной директорией
на системном диске.
На фазе 2 (подключение медленного диска в Timeweb) UUID в
`playbook-system.yml` придётся поменять и включить флаг — это
осознанный шаг, не автоматизировано.
Проверено прогоном `inv pl -- system` на текущем сервере (Yandex
Cloud) — задача mount по-прежнему выполняется, `/mnt/applications`
смонтирован, изменений нет.
---
## Шаг 3 — переключение auth на cr.yandex (2026-05-22, выполнено)
Заменена аутентификация в Yandex Container Registry с YC-metadata
service на OAuth-token из vault.
Изменения:
- `files/yandex-docker-registry-auth.sh`**удалён**.
- `playbook-homepage.yml` — задача `ansible.builtin.script:
yandex-docker-registry-auth.sh` заменена на
`community.docker.docker_login` с `username: oauth`, `password:
"{{ yc_oauth_token }}"`.
- `playbook-transcriber.yml` — то же самое.
Локальные push-плейбуки (`playbook-homepage-registry.yml`,
`playbook-transcriber-registry.yml`) не трогал — там нет auth-задачи
в принципе, локальный docker аутентифицируется вручную
(`yc container registry configure-docker` или `docker login`).
Если позже захочется унифицировать — можно добавить тот же
`docker_login` с `delegate_to: 127.0.0.1`.
Проверено прогоном `inv pl -- homepage` и `inv pl -- transcriber` на
текущем сервере (Yandex Cloud) — ошибок нет, контейнеры работают.
Значит и на Timeweb заработает (единственная разница — исходящий IP,
а OAuth-токен в YC принимается извне).
---
## Шаг 2 — OAuth-token для cr.yandex (2026-05-22, выполнено)
В `vars/secrets.yml` добавлена (или обновлена) переменная
`yc_oauth_token` со свежим OAuth-токеном Яндекса. Токен будет
использоваться для логина в `cr.yandex` с новой машины Timeweb
(вместо текущего скрипта `files/yandex-docker-registry-auth.sh`,
который завязан на YC metadata service `169.254.169.254` и
работает только внутри YC).
Сам код переключения на `community.docker.docker_login` пока не
вносится — это следующая итерация. Сейчас токен просто положен в
vault, чтобы не делать этого в день cutover'а под прессом.
---
## Шаг 1 — снижение TTL DNS (2026-05-22, выполнено)
В админке Yandex 360 для зоны `vakhrushev.me` уменьшен TTL
A-записей с **21 600 с (6 ч)** до **1 200 с (20 мин)**. Это даёт
запас по времени на распространение изменений после смены IP в
день cutover'а — старые кэширующие резолверы перестанут отдавать
старый адрес максимум через 20 минут (вместо 6 часов).
Делается **заранее**, потому что само снижение TTL тоже
распространяется по кэшам по правилам старого TTL — то есть после
правки нужно подождать ≥ 6 часов, чтобы новое значение TTL само
успело прижиться. Раньше cutover'а нужно сделать с большим
запасом — день в день не сработает.