Настройка
Справочник jobs.yaml
Каждое поле файла конфигурации: что принимает и что делает, если его не написать.
Здесь описано текущее, упрощённое устройство. Что убрала июльская чистка 2026 года (Vaultwarden/
vw://, rclonemode: plain,webdav; добавлена доставка restic) и как переписать старую конфигурацию — в SIMPLIFICATION.md.
Unruin настраивается одним файлом YAML (по традиции jobs.yaml), который держат
в git. Неизвестные ключи отвергаются при загрузке, поэтому опечатка падает громко.
unruin validate <файл> проверяет всё: ссылки, параметры источников, расписания,
правила хранения и наличие доступов. Добавьте --preflight, чтобы прогнать и
живые проверки — разрешить каждый секрет, соединиться по SSH с каждым сервером и
убедиться, что каждое хранилище достижимо. Тогда опечатка в имени kr:// или
лежащий хост упадут сейчас, а не в три часа ночи.
Секреты в файл не пишут никогда. В нём ссылки, разрешаемые в момент запуска:
kr://<имя>[#<поле>]— собственное шифрованное хранилище Unruin (по умолчанию). Ниже подробнее.env://<ПЕРЕМЕННАЯ>— переменная окружения (там живёт офлайновый секрет аварийного конверта).file:///путь/к/секрету— содержимое файла (секрет Docker или k8s, примонтированный файлом, либо ключ SSH для опытов).
Встроенное хранилище секретов (kr://) — способ по умолчанию
У Unruin есть своё хранилище на AES-256-GCM в каталоге состояния (secrets.kr),
которое открывает UNRUIN_MASTER_PASSPHRASE (PBKDF2-SHA256). В jobs.yaml лежит
только ссылка kr://имя — зашифрованные значения живут в состоянии, а не в git.
Вести его можно из командной строки (значения читаются со стандартного ввода, а не
из аргументов) или вставлять в поле панели, работающее только на запись:
export UNRUIN_MASTER_PASSPHRASE='длинная фраза'
printf %s 'ключ-хранилища' | unruin secret set restic/minio # пароль, без перевода строки
unruin secret set ssh/vps-prod-1 < ~/.ssh/id_ed25519 # ключ, дословно, с переводом строки
unruin secret ls # только имена, никогда значения
unruin secret rm ssh/old-host
Дальше ссылаетесь: ssh_key: "kr://ssh/vps-prod-1",
repo_password: "kr://restic/minio".
Имя kr:// может нести необязательное #поле, чтобы один логический доступ
держал несколько значений — например, два ключа хранилища S3 под одним именем:
printf %s 'AKIA…' | unruin secret set minio/primary#access_key
printf %s 'wJalr…' | unruin secret set minio/primary#secret_key
и ссылки kr://minio/primary#access_key и kr://minio/primary#secret_key.
Фраза в окружении открывает хранилище при каждом перезапуске сама — ни сессии,
которую надо обновлять, ни внешнего процесса. Размен: кто может прочитать
окружение, тот может расшифровать хранилище. Плашка ⚠ в панели загорается, если
фразы нет или она неверная. Секрет аварийного конверта держите на env://, а не
здесь, чтобы он пережил потерю и Unruin, и этого хранилища.
Убрано:
vw://(Vaultwarden черезbw) вырезан целиком во время чистки — ниBW_SESSION, ни Node/Bitwarden CLI в образе. Конфигурация, где осталсяvw://, не загрузится, а сообщение приведёт в SIMPLIFICATION.md; замените каждыйvw://папка/запись#поленаkr://папка/запись#поле— грамматика та же.
Верхний уровень
servers: { <имя>: <сервер> }
rotation: <хранение> # общее значение; задания накладываются поверх
destinations: { <имя>: <хранилище> }
jobs: [ <задание>, ... ]
servers
host_key_policy: tofu # общее: tofu (доверие при первой встрече) | strict
servers:
vps-prod-1:
host: 10.0.0.11 # обязательно
port: 22 # по умолчанию 22
user: root # обязательно
ssh_key: "kr://ssh/vps-prod-1" # обязательно (ссылка на закрытый ключ)
host_key: "" # можно закрепить ключ хоста; иначе он запомнится при первом соединении
host_key_policy: strict # можно переопределить общее правило для сервера
Правило про ключ хоста
host_key_policy управляет StrictHostKeyChecking в SSH:
tofu(по умолчанию) — принять новый: первое соединение доверяет ключу хоста и записывает его вknown_hosts, все дальнейшие сверяются с ним.strict— ключ хоста обязан уже быть вknown_hosts; незнакомый хост — жёсткий отказ. Сначала засейтеunruin validate --preflight <cfg>(он соединяется в режиме доверия и записывает ключ), сверьте отпечатки по другому каналу, потом переключайтесь на strict.
Задаётся глобально и переопределяется по серверу. known_hosts лежит в каталоге
состояния (см. окружение), поэтому закреплённые ключи переживают перезагрузку. И
каталог состояния по умолчанию находится в пользовательском конфиге, а не в
/tmp: чистка временных файлов не должна тихо заново доверить всему парку.
rotation (сколько хранить)
Ложится на restic forget --prune. Задаётся глобально и/или в задании; правила
задания накладываются на общие — каждый заданный ключ побеждает, остальные
наследуются.
rotation:
keep_last: 3
keep_hourly: 24
keep_daily: 7
keep_weekly: 4
keep_monthly: 12
keep_yearly: 3
keep_within: "14d" # в формате длительности restic
Пустые правила хранения значат «хранить вечно»: validate предупреждает, а
prune отказывается работать — удалять всё он не станет.
prune_schedule (уборка сама)
По умолчанию правила хранения применяются, только когда вы зовёте unruin prune.
Задайте prune_schedule (выражение cron), и serve будет применять их сам,
привилегированным доступом, — чтобы без присмотра хранилища не росли бесконечно:
prune_schedule: "30 4 * * *" # общее: каждый день в 04:30
jobs:
- name: app-db-nightly
# ...
prune_schedule: "0 5 * * 0" # у задания своё: раз в неделю
Задание без правил хранения убирать нельзя (validate отвергает
prune_schedule без rotation). Уборка по расписанию встаёт в очередь с
копированием в то же хранилище по метке занятости и никогда не снимает метку
уборки сама.
run_timeout (защита от зависания)
Запуск, перешагнувший свой предел по времени, обрывается и записывается
неудачным: зависшая цель (замерший pg_dump, мёртвое, но открытое соединение
SSH) не должна держать задание вечно.
run_timeout: "3h" # общее
jobs:
- name: big-files
# ...
timeout: "8h" # у задания своё; "0" выключает
Не задали — применяется щедрое встроенное значение, шесть часов. Вместе с живостью SSH это и есть подпорка надёжности для работы без присмотра.
notifications
Куда приходят исходы запусков. В первой версии один канал — вебхук, который
работает с ntfy, Slack, Discord и произвольной точкой приёма: в теле есть поле
сообщения, которое ждёт каждый из них (text, content, message), плюс
структурные job, status, detail, time.
notifications:
webhook:
url: "kr://notify/webhook" # ссылка на секрет — в адресе часто есть ключ
Какие исходы отправлять, решает notify.on у задания (по умолчанию
[failed, partial]). Если канал не настроен вовсе, validate предупреждает, что
исходы никуда не придут.
destinations
Каждое хранилище — хранилище restic. Чтобы копия уходила больше чем в одно
место, перечислите у задания несколько (ниже); остальные наполняются
параллельными restic copy, и дедупликация сохраняется.
destinations:
minio-primary:
mode: restic # можно не писать: restic — единственный режим и значение по умолчанию
backend: # типы бэкендов ниже
type: s3
endpoint: https://minio.example.com
bucket: backups
access: "kr://minio/primary" # доступ к бэкенду
prune_access: "kr://minio/prune" # необязательный привилегированный доступ (только уборка, только у оркестратора)
repo_password: "kr://restic/minio-primary" # ключ шифрования хранилища
- Каждое хранилище — хранилище restic. Первое хранилище в задании основное
(источник копируется в него), остальные наполняются
restic copy. modeпо умолчаниюrestic, его можно не писать. Другого режима нет; оставшийсяmode: plainunruin validateотвергает.accessне нужен бэкендам, которым не нужны доступы (например,local).prune_accessнеобязателен: одинaccessделает всё, включая уборку. Задавайте его только ради разделения — когда у целей доступ только на дозапись, аforget/pruneходят отдельным привилегированным ключом, который живёт только у оркестратора и на цель не уезжает. Не задан — уборка идёт черезaccess.- Для S3 направьте
access(иprune_access) на один доступ с двумя полями —access_keyиsecret_key, — напримерkr://minio/primary#access_keyиkr://minio/primary#secret_keyпод одним именемkr://minio/primary.
Нужна вторая копия на другой площадке? Заведите второе хранилище restic и
укажите его в задании после основного — Unruin разошлёт туда параллельным
restic copy.
Типы бэкендов
type |
поля | адрес в restic |
|---|---|---|
local |
path |
<path> |
s3 |
endpoint, bucket |
s3:<endpoint>/<bucket> |
sftp |
host, path, user?, port? |
sftp:[user@]<host>:<path> |
rest |
url |
rest:<url> |
У хранилища sftp есть ещё необязательная ссылка ssh_key: — рядом с
repo_password, а не внутри backend. Бэкенд sftp в restic запускает свой
собственный ssh; ssh_key даёт ему ключ и закрепляет ключ хоста (доверие при
первой встрече, запись в каталоге состояния), чтобы он авторизовался и проверил
хост, а не падал с «Host key verification failed». Обычно это тот же ключ, который
вы дали серверу; в панели при выборе уже заведённой машины для хранилища «Другой
компьютер» её ключ подставляется сам. Без него restic сваливается на окружающий
ssh контейнера — без ключа и без known_hosts, и это самая частая причина сбоев
хранилищ по sftp.
jobs
jobs:
- name: app-db-nightly # обязательно, уникально
server: vps-prod-1 # должно ссылаться на сервер
source: { type: postgres, db: app }
dest: # [0] — основная копия; остальные — restic copy
- { name: minio-primary, required: true }
- { name: offsite-sftp, required: false } # по возможности
schedule: "0 3 * * *" # cron из пяти полей; пусто — только вручную
on_missed: skip # skip (по умолчанию) | run (догонять после простоя?)
on_overlap: skip # skip (по умолчанию) | queue (предыдущий запуск ещё идёт?)
rotation: { keep_daily: 14 } # накладывается на общее по ключам
verify: true # по умолчанию true: контрольное восстановление после копии
notify: { on: [failed, partial] } # по умолчанию [failed, partial]
prune_schedule: "0 5 * * 0" # можно переопределить общее расписание уборки
timeout: "8h" # можно переопределить общий run_timeout ("0" выключает)
repo_password: "env://BREAKGLASS_RESTIC_PW" # можно переопределить у задания (аварийный конверт)
Типы источников
type |
класс | обязательные параметры | что делает |
|---|---|---|---|
postgres |
поток | db |
pg_dump <db> (согласованный за счёт MVCC) |
mysql |
поток | db |
mysqldump --single-transaction <db> (обязательно) |
command |
поток | command |
любая команда, пишущая в stdout |
files |
путь | path |
restic на цели по этой папке |
docker_volume |
путь | volume |
снять именованный docker-том (через вспомогательный контейнер) |
Источники-потоки льются по SSH в restic backup --stdin на оркестраторе.
Источники-пути запускают restic на цели. Дампы не сжимаются заранее: restic
сам нарежет, дедуплицирует и сожмёт.
Целям источников-путей ставить не нужно ничего: если restic на цели нет, Unruin доставит туда свой статический бинарник в
~/.cache/unruin/resticпо SSH (с проверкой архитектуры; когда restic уже есть — незаметно). Отказаться —UNRUIN_NO_RESTIC_PUSH=1, отправить конкретный бинарник —UNRUIN_RESTIC_PUSH_BIN. Пользователю SSH по-прежнему нужно право читать копируемый путь — см. SERVER-SETUP.md.
Окружение
| переменная | зачем |
|---|---|
UNRUIN_MASTER_PASSPHRASE |
открывает встроенное шифрованное хранилище (kr://) |
UNRUIN_STATE_DIR |
история запусков в SQLite, ключи SSH, known_hosts и secrets.kr (по умолчанию $XDG_CONFIG_HOME/unruin/state, постоянный каталог — не /tmp) |
UNRUIN_RESTIC_BIN |
какой restic запускает оркестратор (по умолчанию restic из PATH) |
UNRUIN_NO_RESTIC_PUSH |
1 — не доставлять restic на цели источников-путей |
UNRUIN_RESTIC_PUSH_BIN |
какой бинарник restic отправлять на цели вместо собственного |
UNRUIN_TICK |
шаг планировщика у serve (по умолчанию 1m) |
UNRUIN_LISTEN |
адрес панели и API (по умолчанию 127.0.0.1:8080; на адресе не-loopback без ключа serve стартовать откажется) |
UNRUIN_API_TOKEN |
если задан, требуется как Authorization: Bearer <token>; нужен, чтобы выставить панель за пределы loopback |
UNRUIN_READONLY |
запрещает любые правки конфигурации (создание, изменение, удаление заданий) независимо от адреса |
UNRUIN_INSECURE |
осознанный отказ от проверки: адрес не-loopback без ключа |
то, на что ссылается env:// |
любые офлайновые секреты (например, BREAKGLASS_RESTIC_PW) |