# Журнал миграции в 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/`, это отдельная концепция (есть отдельный 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/`. На Timeweb данные должны лежать в `/srv/applications/`. У 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/` — никаких трюков для текущих бэкапов не нужно. --- ## Шаг 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'а нужно сделать с большим запасом — день в день не сработает.