Migration: add draft for timeweb migration
This commit is contained in:
@@ -0,0 +1,526 @@
|
|||||||
|
# Миграция сервера в Timeweb
|
||||||
|
|
||||||
|
## Контекст и цели
|
||||||
|
|
||||||
|
Сервер `rivendell-v2` переезжает с виртуальной машины в Yandex Cloud
|
||||||
|
(`158.160.46.255`) на VPS в Timeweb.
|
||||||
|
|
||||||
|
### Причины переезда
|
||||||
|
|
||||||
|
1. **Высокая стоимость.** Тариф в Yandex Cloud обходится в ≈ 2 900 ₽/мес
|
||||||
|
за конфигурацию, которая в Timeweb стоит ≈ 2 000 ₽/мес и при этом
|
||||||
|
мощнее по всем параметрам (см. сравнение ниже).
|
||||||
|
2. **Упор в потолок RAM.** Текущий сервер уже использует ≈ 80 %
|
||||||
|
доступной памяти на штатной нагрузке (см.
|
||||||
|
`project_server_specs`). Любой всплеск (миграции БД, индексация
|
||||||
|
в Outline, бэкап с restic) — и приложения начинают конкурировать
|
||||||
|
за память, появляются OOM-риски. Дальше расти на этом тарифе
|
||||||
|
некуда без значительного увеличения цены.
|
||||||
|
3. **Медленные диски.** Из-за высокой стоимости в YC приходится
|
||||||
|
использовать дешёвый HDD-том вместо SSD/NVMe — это заметно
|
||||||
|
снижает отзывчивость приложений (особенно Gitea, Outline,
|
||||||
|
тёплый старт контейнеров, рестики check/forget). На Timeweb за
|
||||||
|
меньшие деньги получаем NVMe.
|
||||||
|
|
||||||
|
Переезд решает все три проблемы одновременно: дешевле, больше
|
||||||
|
RAM, быстрее диск.
|
||||||
|
|
||||||
|
### Сравнение тарифов
|
||||||
|
|
||||||
|
| Параметр | Yandex Cloud | Timeweb Cloud VPS |
|
||||||
|
| -------------- | ----------------------------------------- | ------------------ |
|
||||||
|
| CPU | Intel Cascade Lake, vCPU 2, гарантия 50 % | 4 × 3.3 ГГц |
|
||||||
|
| RAM | 4 ГБ | 8 ГБ |
|
||||||
|
| Диск | 120 ГБ HDD | 80 ГБ NVMe |
|
||||||
|
| Публичный IP | да | да |
|
||||||
|
| **Цена/месяц** | **2 887 ₽** | **1 980 ₽** |
|
||||||
|
|
||||||
|
Итого: **−907 ₽/мес (≈ −31 %)**, при этом **×2 RAM** (закрывает
|
||||||
|
причину 2), **×2 ядер**, гарантия CPU 100 % вместо 50 %,
|
||||||
|
**NVMe вместо HDD** (закрывает причину 3). Минус — диск меньше
|
||||||
|
(80 ГБ против 120 ГБ HDD), что и стало основанием для фазы 2 с
|
||||||
|
подключением второго «холодного» диска под крупные данные.
|
||||||
|
|
||||||
|
Переезжает **только compute** (VM с приложениями). Остальные сервисы
|
||||||
|
Yandex Cloud остаются на месте и продолжают использоваться с новой
|
||||||
|
машины:
|
||||||
|
|
||||||
|
- **Container Registry** — `cr.yandex/crplfk0168i4o8kd7ade` для образов
|
||||||
|
`homepage-nginx` и `transcriber`.
|
||||||
|
- **Object Storage (S3)** — restic-репозиторий `yandex_cloud_s3`.
|
||||||
|
- **Postbox SMTP** — `postbox.cloud.yandex.net` (gitea, gramps, wakapi,
|
||||||
|
outline, authelia, apprise).
|
||||||
|
- **Yandex 360 / DNS-зона** `vakhrushev.me` — там же управляются записи
|
||||||
|
и почтовый домен.
|
||||||
|
|
||||||
|
Параметры даунтайма — мягкие, это личная машина. Стратегия — «cold
|
||||||
|
cutover»: остановить сервисы на источнике, раскатать ansible на
|
||||||
|
target без запуска приложений, перенести данные с сохранением
|
||||||
|
uid/gid, запустить сервисы на target, переключить DNS.
|
||||||
|
|
||||||
|
Конфигурация target — Cloud VPS Timeweb с одним диском **80 ГБ** на
|
||||||
|
первой фазе. Позднее (отдельной фазой) будет подключён второй
|
||||||
|
«медленный» диск под крупные данные (`calibre`, бэкапы, возможно
|
||||||
|
`outline`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Инвентаризация YC-зависимостей в коде
|
||||||
|
|
||||||
|
| Компонент | Где | Что делать при переезде |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `production.yml` | `ansible_host: 158.160.46.255`, `ansible_user: major` | Заменить на новый IP/пользователя Timeweb |
|
||||||
|
| `files/yandex-docker-registry-auth.sh` | Логин в `cr.yandex` через **YC metadata service** (`169.254.169.254`) | **Не работает вне YC.** Перейти на static OAuth-token / IAM-token (новый скрипт + секрет в vault) |
|
||||||
|
| `playbook-system.yml` (mount-storage) | UUID `3942bffd-…` монтируется в `/mnt/applications` | Фаза 1: отключить mount или сделать UUID переменной vault. Фаза 2 (после подключения медленного диска): включить заново с новым UUID |
|
||||||
|
| `files/backups/config.template.toml` | `[storage.yandex_cloud_s3]` + `AWS_*` ключи | **Не меняем.** Тот же бакет/ключи продолжают работать. Меняется только `host_name` (для подписи снапшотов и нотификаций) — он уже шаблонится |
|
||||||
|
| SMTP (`postbox_host/port/user/pass`) | gitea, gramps, wakapi, outline, authelia, apprise | **Не меняем.** Postbox SMTP доступен извне YC по тем же credentials |
|
||||||
|
| `files/backups/rclone.template.conf` (`pr86keedav`) | WebDAV-копия restic — внешний сервис | **Не меняем** |
|
||||||
|
| Caddy `tls anwinged@ya.ru` | ACME | Не меняется, ACME перевыпустит сертификаты после смены IP |
|
||||||
|
|
||||||
|
Никаких других hardcoded YC-эндпоинтов в плейбуках / шаблонах нет —
|
||||||
|
SSH, ufw, fail2ban, docker, eget, restic, Caddy полностью переносимы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UID / GID — критично для rsync
|
||||||
|
|
||||||
|
UID/GID каждого приложения зафиксированы в плейбуках и в
|
||||||
|
`vars/homepage.yml` / `vars/transcriber.yml`. Роль `owner` создаёт
|
||||||
|
группы и пользователей **с явно указанными gid/uid**
|
||||||
|
(`roles/owner/tasks/main.yml`). Это значит:
|
||||||
|
|
||||||
|
- Если на новой машине **сначала** раскатать все плейбуки (без
|
||||||
|
запуска приложений), пользователи получатся с теми же uid/gid.
|
||||||
|
- Тогда `rsync -aAX` (с сохранением owner) корректно ляжет на target.
|
||||||
|
- Дополнительный maping uid не нужен.
|
||||||
|
|
||||||
|
Список приложений с uid/gid (для сверки и для документации):
|
||||||
|
|
||||||
|
```
|
||||||
|
caddyproxy 1010 / 1011
|
||||||
|
authelia 1011 / 1012
|
||||||
|
netdata 1012 / 1013
|
||||||
|
miniflux 1013 / 1014
|
||||||
|
rssbridge 1014 / 1015
|
||||||
|
wakapi 1015 / 1016
|
||||||
|
dozzle 1016 / 1017
|
||||||
|
transcriber 1017 / 1018
|
||||||
|
wanderer 1018 / 1019
|
||||||
|
memos 1019 / 1020
|
||||||
|
gitea 1005 / 1006
|
||||||
|
outline 1007 / 1008
|
||||||
|
homepage 1008 / 1009
|
||||||
|
gramps 1009 / 1010
|
||||||
|
calibre 1102 / 1102
|
||||||
|
remembos 1103 / 1103
|
||||||
|
apprise 1104 / 1104
|
||||||
|
tuwunel 1105 / 1105
|
||||||
|
goaccess 1106 / 1106
|
||||||
|
```
|
||||||
|
|
||||||
|
(Возможные пересечения uid одного приложения и gid другого
|
||||||
|
существуют, но Linux держит их в разных пространствах имён — не
|
||||||
|
страшно.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Подготовка кода проекта
|
||||||
|
|
||||||
|
Делается **до** аренды Timeweb-машины, отдельным PR (или сериями
|
||||||
|
коммитов на отдельной ветке). Цель — чтобы тот же ansible
|
||||||
|
работал и на источнике, и на target без условных хаков.
|
||||||
|
|
||||||
|
### 1. Заменить YC-specific docker registry auth
|
||||||
|
|
||||||
|
`files/yandex-docker-registry-auth.sh` сейчас использует metadata
|
||||||
|
service (`169.254.169.254`). Это работает только внутри YC VM,
|
||||||
|
поэтому на Timeweb его надо заменить.
|
||||||
|
|
||||||
|
**Решение — OAuth-token Яндекса.** Простой и достаточный для
|
||||||
|
домашнего сервера механизм:
|
||||||
|
|
||||||
|
1. Получить OAuth-token в кабинете Яндекса:
|
||||||
|
<https://oauth.yandex.ru/authorize?response_type=token&client_id=1a6990aa636648e9b2ef855fa7bec2fb>
|
||||||
|
(стандартный client_id для `yc` CLI, токен с правом доступа к
|
||||||
|
Container Registry).
|
||||||
|
2. Положить в `vars/secrets.yml` как `yc_oauth_token` (vault).
|
||||||
|
3. Переписать `files/yandex-docker-registry-auth.sh` как шаблон
|
||||||
|
(`.template.sh`) и рендерить через `ansible.builtin.template`
|
||||||
|
вместо `script:`. Скрипт сводится к:
|
||||||
|
```sh
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
echo "{{ yc_oauth_token }}" | \
|
||||||
|
docker login --username oauth --password-stdin cr.yandex
|
||||||
|
```
|
||||||
|
Альтернатива — не рендерить, а передавать токен в скрипт
|
||||||
|
аргументом или через переменную окружения, чтобы не светить его
|
||||||
|
в системе.
|
||||||
|
4. В `playbook-homepage.yml` и `playbook-transcriber.yml` поменять
|
||||||
|
`ansible.builtin.script:` на `ansible.builtin.template:` +
|
||||||
|
`ansible.builtin.command:` (либо использовать модуль
|
||||||
|
`community.docker.docker_login` напрямую с `username: oauth`,
|
||||||
|
`password: "{{ yc_oauth_token }}"` — это самый чистый вариант,
|
||||||
|
тогда отдельный скрипт вообще не нужен).
|
||||||
|
5. То же самое — для локальных push-плейбуков
|
||||||
|
`playbook-homepage-registry.yml` и
|
||||||
|
`playbook-transcriber-registry.yml`.
|
||||||
|
|
||||||
|
Рекомендую вариант с `community.docker.docker_login` — это убирает
|
||||||
|
shell-скрипт целиком и сильно проще.
|
||||||
|
|
||||||
|
Минусы OAuth-token: токен живёт долго и даёт доступ ко всему
|
||||||
|
аккаунту Яндекса. Для личного сервера приемлемо; если позже
|
||||||
|
захочется минимизировать blast radius — заменить на IAM-key
|
||||||
|
сервисного аккаунта (отдельная итерация после миграции).
|
||||||
|
|
||||||
|
Затронутые места: `files/yandex-docker-registry-auth.sh` (удалить
|
||||||
|
или переписать), `playbook-homepage.yml`, `playbook-transcriber.yml`,
|
||||||
|
`playbook-homepage-registry.yml`, `playbook-transcriber-registry.yml`,
|
||||||
|
`vars/secrets.yml` (новый ключ `yc_oauth_token`).
|
||||||
|
|
||||||
|
### 2. Сделать опциональным монтирование внешнего диска
|
||||||
|
|
||||||
|
Сейчас `playbook-system.yml` жёстко монтирует UUID `3942bffd-…` в
|
||||||
|
`/mnt/applications`. На Timeweb этого диска нет.
|
||||||
|
|
||||||
|
Минимальная правка — вытащить UUID в переменную (`storage_uuid`) и
|
||||||
|
обернуть mount-задачу `when: storage_uuid is defined`. В
|
||||||
|
`vars/secrets.yml` или `vars/vars.yml` для текущего сервера задать
|
||||||
|
UUID, для Timeweb (фаза 1) — не задавать. На фазе 2 (когда придёт
|
||||||
|
медленный диск) — задать новый UUID.
|
||||||
|
|
||||||
|
Альтернатива: вынести параметры в инвентарь
|
||||||
|
(`production.yml` → `host_vars/server.yml`).
|
||||||
|
|
||||||
|
При этом сама директория `/mnt/applications` должна создаваться в
|
||||||
|
любом случае — playbook уже это делает, надо лишь убедиться, что
|
||||||
|
задача «Create directory for mount» не зависит от mount-задачи.
|
||||||
|
|
||||||
|
### 3. Параметризовать инвентарь
|
||||||
|
|
||||||
|
На время перехода — **два отдельных файла**: текущий
|
||||||
|
`production.yml` остаётся как есть, рядом появляется новый
|
||||||
|
`timeweb.yml` с настройками Timeweb-машины. Все ansible-команды
|
||||||
|
во время миграции явно указывают `-i timeweb.yml`. После того, как
|
||||||
|
переезд закончен и старая машина выключена — `production.yml`
|
||||||
|
просто удаляется, `timeweb.yml` переименовывается в
|
||||||
|
`production.yml`.
|
||||||
|
|
||||||
|
`tasks.py` использует `yq` для извлечения `ansible_host` / `ansible_user`
|
||||||
|
из инвентаря (`_yq(".ungrouped.hosts.server…")`) — путь к файлу
|
||||||
|
зашит константой `HOSTS_FILE = "production.yml"`. Варианты:
|
||||||
|
|
||||||
|
- На время миграции временно поменять `HOSTS_FILE = "timeweb.yml"`
|
||||||
|
в локальном коммите (или через env override), потом откатить — после
|
||||||
|
переименования всё снова работает.
|
||||||
|
- Принять, что `inv ssh / zj / btop / login` работают только с
|
||||||
|
активным сервером (тем, что в `production.yml`), а к старой
|
||||||
|
машине во время миграции ходим напрямую через `ssh
|
||||||
|
major@158.160.46.255`.
|
||||||
|
|
||||||
|
Первый вариант чище. Достаточно одной строчки правки.
|
||||||
|
|
||||||
|
### 4. Прочее
|
||||||
|
|
||||||
|
- `README.md` — обновить инструкцию по DNS и упомянуть Timeweb.
|
||||||
|
- Удалить (или пометить deprecated) yandex-метаданные в комментариях
|
||||||
|
`yandex-docker-registry-auth.sh`.
|
||||||
|
- Проверить, что у всех application-плейбуков задача с
|
||||||
|
`community.docker.docker_compose_v2: state: present` помечена
|
||||||
|
тегом `run-app` — это позволит раскатывать `--skip-tags run-app`
|
||||||
|
для подготовки target без запуска контейнеров. Сейчас тег `run-app`
|
||||||
|
есть в большинстве плейбуков, но надо пройтись и убедиться, что
|
||||||
|
он покрывает **все** контейнеры (включая calibre, dozzle,
|
||||||
|
remembos, transcriber, tuwunel, wanderer, memos).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Подготовка target-машины
|
||||||
|
|
||||||
|
1. Заказать Cloud VPS в Timeweb:
|
||||||
|
- Ubuntu LTS (та же мажорная версия, что и сейчас — упростит
|
||||||
|
совместимость пакетов).
|
||||||
|
- 4 GB RAM (текущий лимит ≈ 3.8 GiB, см. `project_server_specs`),
|
||||||
|
можно взять чуть с запасом — 4–6 GB, иначе netdata + tuwunel +
|
||||||
|
outline начнут давить.
|
||||||
|
- 2 vCPU.
|
||||||
|
- SSD 80 ГБ.
|
||||||
|
- Снять/настроить firewall провайдера (или отключить, т.к. у нас
|
||||||
|
свой ufw).
|
||||||
|
|
||||||
|
2. Создать пользователя с правами sudo (аналог `major`), залить
|
||||||
|
свой SSH-ключ.
|
||||||
|
|
||||||
|
3. Добавить хост в инвентарь как `server` (или временный
|
||||||
|
`timeweb`), убедиться, что `ansible -m ping` отвечает.
|
||||||
|
|
||||||
|
4. Снизить TTL DNS-записей в Yandex 360 до 60–300 секунд **за
|
||||||
|
~24–48 часов** до cutover.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cutover (план дня X)
|
||||||
|
|
||||||
|
Предусловия: код выкатан, target-машина пингуется по ansible, TTL
|
||||||
|
DNS снижены.
|
||||||
|
|
||||||
|
### Шаг 1. Финальный бэкап на источнике
|
||||||
|
|
||||||
|
```bash
|
||||||
|
inv ssh
|
||||||
|
sudo /usr/local/sbin/backup-all.py 2>&1 | tee /tmp/final-backup.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Убедиться, что в логе все приложения отработали успешно и в S3
|
||||||
|
появился свежий restic-snapshot (на случай отката или потери
|
||||||
|
данных при rsync).
|
||||||
|
|
||||||
|
### Шаг 2. Остановить все приложения на источнике
|
||||||
|
|
||||||
|
Аккуратно остановить контейнеры каждого приложения (через
|
||||||
|
`docker compose down` от соответствующего пользователя или одним
|
||||||
|
проходом):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
inv ssh
|
||||||
|
for user in caddyproxy authelia netdata miniflux rssbridge wakapi \
|
||||||
|
dozzle transcriber wanderer memos gitea outline homepage gramps \
|
||||||
|
calibre remembos apprise tuwunel goaccess; do
|
||||||
|
sudo -iu "$user" bash -c "cd /mnt/applications/$user && docker compose down"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
(Можно завести вспомогательный плейбук `playbook-shutdown-all.yml`,
|
||||||
|
если такое будет часто.)
|
||||||
|
|
||||||
|
Проверить `docker ps`, что пусто. Снять флаги cron на бэкап (чтобы
|
||||||
|
финальный backup не побежал во время миграции):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl stop cron
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 3. Раскатать инфраструктуру на target БЕЗ запуска приложений
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) системная база
|
||||||
|
uv run ansible-playbook -i timeweb.yml --diff playbook-all-setup.yml
|
||||||
|
|
||||||
|
# 2) приложения (создаём пользователей, каталоги, конфиги,
|
||||||
|
# но НЕ запускаем контейнеры)
|
||||||
|
uv run ansible-playbook -i timeweb.yml --diff \
|
||||||
|
--skip-tags run-app \
|
||||||
|
playbook-all-applications.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Цель — после этого на target есть:
|
||||||
|
|
||||||
|
- Корректные uid/gid для всех приложений.
|
||||||
|
- Каталоги `/mnt/applications/<app>/{data,config,backups}`.
|
||||||
|
- Шаблоны `docker-compose.yml` и application-конфиги — отрендерены
|
||||||
|
и лежат на месте.
|
||||||
|
- Docker и сети созданы.
|
||||||
|
- ufw настроен, fail2ban работает.
|
||||||
|
|
||||||
|
### Шаг 4. Перенос данных
|
||||||
|
|
||||||
|
Два варианта.
|
||||||
|
|
||||||
|
**Вариант A — rsync напрямую (быстрее).** С target-машины тянем
|
||||||
|
данные со старой:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo rsync -aAX --info=progress2 --delete \
|
||||||
|
--exclude='lost+found' \
|
||||||
|
major@158.160.46.255:/mnt/applications/ \
|
||||||
|
/mnt/applications/
|
||||||
|
```
|
||||||
|
|
||||||
|
`-aAX` сохраняет ACL/xattrs и uid/gid (численные значения).
|
||||||
|
|
||||||
|
Каждое приложение можно тянуть отдельно — удобнее наблюдать
|
||||||
|
прогресс и можно частично пересинхронизировать в случае ошибок.
|
||||||
|
|
||||||
|
**Вариант B — restore из restic.** Если по сети источник недоступен
|
||||||
|
(например, IP уже закрыли) или хочется проверить, что бэкапы вообще
|
||||||
|
рабочие — восстанавливаемся из YC S3:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo /usr/local/sbin/restic-shell.sh
|
||||||
|
restic restore latest --target /mnt/applications --path /mnt/applications
|
||||||
|
```
|
||||||
|
|
||||||
|
Рекомендую **A с фолбэком на B**: rsync быстрее и точнее (с
|
||||||
|
точностью до секунды), restic держим как страховку.
|
||||||
|
|
||||||
|
### Шаг 5. Запуск приложений на target
|
||||||
|
|
||||||
|
Раскатываем application-плейбуки ещё раз — теперь без `--skip-tags`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ansible-playbook -i timeweb.yml --diff \
|
||||||
|
playbook-all-applications.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот же запуск проверит идемпотентность шаблонов (не должно быть
|
||||||
|
diff'ов кроме docker-up).
|
||||||
|
|
||||||
|
После старта — проверить:
|
||||||
|
|
||||||
|
- `docker ps` — все контейнеры в healthy.
|
||||||
|
- Локально (по IP) `curl http://<target-ip>` — Caddy отвечает (на
|
||||||
|
редирект, т.к. сертификаты ещё не выпущены под этим IP).
|
||||||
|
- Логи Caddy — выпуск сертификатов запустится после смены DNS, не
|
||||||
|
раньше. Это нормально.
|
||||||
|
|
||||||
|
### Шаг 6. Переключение DNS
|
||||||
|
|
||||||
|
В Yandex 360 admin (`admin.yandex.ru/domains/vakhrushev.me`)
|
||||||
|
поменять A-записи для всех subdomain'ов на новый IP. Перечень
|
||||||
|
поддоменов (из `Caddyfile.template`):
|
||||||
|
|
||||||
|
```
|
||||||
|
vakhrushev.me (apex)
|
||||||
|
matrix.vakhrushev.me
|
||||||
|
auth.vakhrushev.me
|
||||||
|
status.vakhrushev.me
|
||||||
|
git.vakhrushev.me
|
||||||
|
outline.vakhrushev.me
|
||||||
|
gramps.vakhrushev.me
|
||||||
|
miniflux.vakhrushev.me
|
||||||
|
wakapi.vakhrushev.me
|
||||||
|
wanderer.vakhrushev.me
|
||||||
|
memos.vakhrushev.me
|
||||||
|
remembos.vakhrushev.me
|
||||||
|
calibre.vakhrushev.me
|
||||||
|
wanderbase.vakhrushev.me
|
||||||
|
rssbridge.vakhrushev.me
|
||||||
|
dozzle.vakhrushev.me
|
||||||
|
goaccess.vakhrushev.me
|
||||||
|
```
|
||||||
|
|
||||||
|
После смены — подождать пока TTL разойдётся, проверить через
|
||||||
|
`dig +short <hostname>` с независимой машины.
|
||||||
|
|
||||||
|
Caddy сам пойдёт за сертификатами Let's Encrypt — следить за его
|
||||||
|
логами (`docker logs caddyproxy_app -f`).
|
||||||
|
|
||||||
|
### Шаг 7. Проверка после cutover
|
||||||
|
|
||||||
|
Чеклист (примерно по приоритету):
|
||||||
|
|
||||||
|
- [ ] `vakhrushev.me` отвечает 200, отдаёт homepage.
|
||||||
|
- [ ] `auth.vakhrushev.me` — Authelia, можно залогиниться.
|
||||||
|
- [ ] `git.vakhrushev.me` — Gitea, репозитории на месте, ssh-доступ
|
||||||
|
(порт 2222 в ufw уже открыт).
|
||||||
|
- [ ] `outline.vakhrushev.me` — открывается, документы на месте.
|
||||||
|
- [ ] `matrix.vakhrushev.me` — Tuwunel/Element подключается;
|
||||||
|
federation проверяется через
|
||||||
|
<https://federationtester.matrix.org/>.
|
||||||
|
- [ ] `miniflux.vakhrushev.me`, `wakapi.vakhrushev.me`,
|
||||||
|
`memos.vakhrushev.me`, `gramps.vakhrushev.me`,
|
||||||
|
`remembos.vakhrushev.me`, `wanderer.vakhrushev.me`,
|
||||||
|
`calibre.vakhrushev.me`, `rssbridge.vakhrushev.me`,
|
||||||
|
`dozzle.vakhrushev.me`, `goaccess.vakhrushev.me` —
|
||||||
|
открываются, данные на месте.
|
||||||
|
- [ ] Netdata `status.vakhrushev.me` — собирает метрики.
|
||||||
|
- [ ] Backup-cron — следующий запуск (1:00) проходит успешно,
|
||||||
|
приходит уведомление в apprise.
|
||||||
|
- [ ] SMTP — отправить тестовое письмо из gitea/authelia (триггер
|
||||||
|
reset password).
|
||||||
|
- [ ] Container Registry — `docker pull cr.yandex/...` на новой
|
||||||
|
машине проходит (это значит, что наша новая аутентификация
|
||||||
|
через OAuth/IAM работает).
|
||||||
|
|
||||||
|
### Шаг 8. Заморозка источника
|
||||||
|
|
||||||
|
Когда всё подтверждено стабильным (≥ 24 часа):
|
||||||
|
|
||||||
|
- Остановить и выключить старую VM в YC.
|
||||||
|
- Подождать неделю-две на случай отката.
|
||||||
|
- Удалить VM и связанные ресурсы (только compute! S3-бакет с
|
||||||
|
restic-бэкапами и Container Registry **остаются**).
|
||||||
|
- Удалить `production.yml`, переименовать `timeweb.yml` →
|
||||||
|
`production.yml`, откатить временную правку `HOSTS_FILE` в
|
||||||
|
`tasks.py` (теперь снова `production.yml`). Закоммитить.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 2: подключение медленного диска
|
||||||
|
|
||||||
|
После того как Timeweb-сервер стабилен:
|
||||||
|
|
||||||
|
1. Заказать дополнительный «холодный» диск в Timeweb, прицепить
|
||||||
|
к VPS.
|
||||||
|
2. Узнать UUID нового устройства (`lsblk -f`).
|
||||||
|
3. Решить, куда монтировать — варианты:
|
||||||
|
- Сохранить текущую схему (`/mnt/applications` на медленном
|
||||||
|
диске целиком). Минус: всё IO приложений уходит на медленный
|
||||||
|
диск.
|
||||||
|
- **Лучше:** оставить `/mnt/applications` на быстром SSD,
|
||||||
|
медленный смонтировать как `/mnt/cold` и под calibre/большие
|
||||||
|
бэкапы делать bind-mount или поменять `data_dir` у нужных
|
||||||
|
приложений.
|
||||||
|
4. Восстановить в `playbook-system.yml` mount-задачу с новым
|
||||||
|
UUID (через переменную, заведённую на фазе 1).
|
||||||
|
5. Прогнать `inv pl -- system` с тегом `mount-storage`.
|
||||||
|
6. Переехать на холодный диск только большие данные. Для calibre
|
||||||
|
это означает остановить контейнер, `rsync` библиотеки книг,
|
||||||
|
поправить `data_dir` в `vars`, запустить.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что НЕ менять во время миграции
|
||||||
|
|
||||||
|
Чтобы не накапливать изменения в одном переезде:
|
||||||
|
|
||||||
|
- Версии docker-образов всех приложений — те же, что в источнике.
|
||||||
|
- Конфиги приложений — без правок.
|
||||||
|
- Restic snapshot policy.
|
||||||
|
- Apprise/notification каналы.
|
||||||
|
|
||||||
|
Любые улучшения (healthchecks из `docs/drafts/alerts.md`,
|
||||||
|
gitea runner и т.п.) — отдельным циклом после миграции.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Откат
|
||||||
|
|
||||||
|
Если на target что-то критично сломалось:
|
||||||
|
|
||||||
|
1. DNS возвращаем обратно на старый IP.
|
||||||
|
2. Старая VM в YC жива и заглушена → стартуем её, поднимаем
|
||||||
|
сервисы (`docker compose up -d` под каждым пользователем).
|
||||||
|
3. Изучаем, в чём дело на target, лечим, повторяем cutover.
|
||||||
|
|
||||||
|
Поэтому шаг «Заморозка источника» отделён от «удаления» — у нас
|
||||||
|
есть «горячее запасное» как минимум на пару дней.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Открытые вопросы
|
||||||
|
|
||||||
|
На текущей итерации — нет, все ключевые развилки закрыты:
|
||||||
|
|
||||||
|
- ~~Auth для cr.yandex~~ → OAuth-token Яндекса (`yc_oauth_token` в
|
||||||
|
vault, `community.docker.docker_login` в плейбуках).
|
||||||
|
- ~~Инвентарь~~ → два отдельных файла, после cutover `timeweb.yml`
|
||||||
|
переименовывается в `production.yml`.
|
||||||
|
- ~~Регион/TZ Timeweb~~ → совпадает с текущим.
|
||||||
|
- ~~IP-whitelist в конфигах~~ → отсутствует, смена IP безопасна.
|
||||||
|
- ~~Объём данных vs 80 ГБ~~ → 22 ГБ всего, из них calibre 16 ГБ;
|
||||||
|
с запасом влезает в фазе 1, второй диск не на критическом пути.
|
||||||
|
|
||||||
|
Возможные вопросы по ходу реализации (выяснятся в процессе):
|
||||||
|
|
||||||
|
- Конкретная процедура получения OAuth-token Яндекса (через
|
||||||
|
`oauth.yandex.ru` или через `yc` CLI).
|
||||||
|
- Поведение Caddy при первом выпуске сертификатов после смены DNS —
|
||||||
|
убедиться, что rate-limit Let's Encrypt не упрётся (≈ 17
|
||||||
|
поддоменов выпускаются сразу, лимит LE — 50 сертификатов в неделю
|
||||||
|
на registered domain, запас есть).
|
||||||
|
- Federation Matrix после смены IP — обычно достаточно того, что
|
||||||
|
apex `vakhrushev.me` отдаёт `.well-known/matrix/server`, но
|
||||||
|
стоит проверить через `federationtester.matrix.org` сразу после
|
||||||
|
cutover.
|
||||||
Reference in New Issue
Block a user