Migration: application directory as parameter

This commit is contained in:
2026-05-22 20:45:52 +03:00
parent fe024b3b12
commit 1ce168655d
6 changed files with 129 additions and 7 deletions
+97
View File
@@ -8,6 +8,103 @@
--- ---
## Шаг 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, выполнено) ## Шаг 4 — условное монтирование внешнего диска (2026-05-22, выполнено)
Задача `Mount external storages` в `playbook-system.yml` теперь Задача `Mount external storages` в `playbook-system.yml` теперь
+2 -1
View File
@@ -4,6 +4,7 @@
vars_files: vars_files:
- vars/secrets.yml - vars/secrets.yml
- vars/vars.yml
vars: vars:
user_name: "<put-name-here>" user_name: "<put-name-here>"
@@ -27,7 +28,7 @@
- name: "Remove application dir" - name: "Remove application dir"
ansible.builtin.file: ansible.builtin.file:
path: "/mnt/applications/{{ user_name }}" path: "{{ (application_dir, user_name) | path_join }}"
state: absent state: absent
- name: "Remove home dir" - name: "Remove home dir"
+4 -3
View File
@@ -4,6 +4,7 @@
vars_files: vars_files:
- vars/secrets.yml - vars/secrets.yml
- vars/vars.yml
vars: vars:
apt_packages: apt_packages:
@@ -40,9 +41,9 @@
group: root group: root
mode: "0755" mode: "0755"
- name: 'Create directory for mount' - name: 'Create directory for applications'
ansible.builtin.file: ansible.builtin.file:
path: '/mnt/applications' path: '{{ application_dir }}'
state: 'directory' state: 'directory'
mode: '0755' mode: '0755'
tags: tags:
@@ -50,7 +51,7 @@
- name: 'Mount external storages' - name: 'Mount external storages'
ansible.posix.mount: ansible.posix.mount:
path: '/mnt/applications' path: '{{ application_dir }}'
src: 'UUID=3942bffd-8328-4536-8e88-07926fb17d17' src: 'UUID=3942bffd-8328-4536-8e88-07926fb17d17'
fstype: ext4 fstype: ext4
state: mounted state: mounted
+1
View File
@@ -5,4 +5,5 @@ ungrouped:
ansible_host: "158.160.46.255" ansible_host: "158.160.46.255"
ansible_user: "major" ansible_user: "major"
ansible_become: true ansible_become: true
application_dir: "/mnt/applications"
mount_external_storage: true mount_external_storage: true
+22 -1
View File
@@ -10,6 +10,7 @@ from invoke.exceptions import Exit
from invoke.tasks import task from invoke.tasks import task
HOSTS_FILE = "production.yml" HOSTS_FILE = "production.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"
@@ -28,6 +29,25 @@ def _remote_host() -> str:
return _yq(".ungrouped.hosts.server.ansible_host") return _yq(".ungrouped.hosts.server.ansible_host")
def _application_dir() -> str:
"""Чтение application_dir: сначала из inventory (override), затем из vars/vars.yml."""
inv_value = _yq('.ungrouped.hosts.server.application_dir // ""')
if inv_value:
return inv_value
result = subprocess.run(
["yq", ".application_dir", VARS_FILE],
capture_output=True,
text=True,
check=True,
)
value = result.stdout.strip()
if not value or value == "null":
raise Exit(
f"application_dir не определён ни в inventory, ни в {VARS_FILE}", code=1
)
return value
def _rest_args() -> list[str]: def _rest_args() -> list[str]:
"""Возвращает аргументы после '--' из sys.argv""" """Возвращает аргументы после '--' из sys.argv"""
try: try:
@@ -85,8 +105,9 @@ def login_as_app(ctx: Context, app: str) -> None:
"""SSH и переключиться на пользователя приложения: inv login gitea""" """SSH и переключиться на пользователя приложения: inv login gitea"""
# sudo -i: login shell, -u: от имени пользователя # sudo -i: login shell, -u: от имени пользователя
# bash -i: интерактивный режим (job control), -l: login (читает профиль) # bash -i: интерактивный режим (job control), -l: login (читает профиль)
app_dir = f"{_application_dir()}/{app}"
subprocess.run( subprocess.run(
f"""ssh {_remote_user()}@{_remote_host()} -t 'sudo -iu {app} bash -c "cd /mnt/applications/{app} && exec bash -il"'""", f"""ssh {_remote_user()}@{_remote_host()} -t 'sudo -iu {app} bash -c "cd {app_dir} && exec bash -il"'""",
shell=True, shell=True,
) )
+3 -2
View File
@@ -7,8 +7,9 @@ primary_user_gid: 1001
# Directory for all user binaries and scripts # Directory for all user binaries and scripts
bin_prefix: "/usr/local/bin" bin_prefix: "/usr/local/bin"
# External disk for application data # Root directory for application data. Override in inventory if host
application_dir: "/mnt/applications" # uses a different path (e.g. external disk mounted elsewhere).
application_dir: "/srv/applications"
apprise_external_port: 8000 apprise_external_port: 8000
apprise_external_url: "http://127.0.0.1:{{ apprise_external_port }}" apprise_external_url: "http://127.0.0.1:{{ apprise_external_port }}"