diff options
| author | Ahmed Abdelhalim <[email protected]> | 2026-07-21 22:18:54 +0200 |
|---|---|---|
| committer | Ahmed Abdelhalim <[email protected]> | 2026-07-21 22:45:48 +0200 |
| commit | f63a92f908e9e79ed2249a0fdbe7b815084ce75c (patch) | |
| tree | b34058f6321db93244b23305eef54ac2a4c912f2 /roles/backup/README.md | |
| parent | 63e99d3e642eb8b8fdcba5cc9a083edfc55a8dfe (diff) | |
Add restore capabilities to backup role with --tags restore call
Co-Authored-By: Claude.ai
Diffstat (limited to 'roles/backup/README.md')
| -rw-r--r-- | roles/backup/README.md | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/roles/backup/README.md b/roles/backup/README.md new file mode 100644 index 00000000..88bdf7d4 --- /dev/null +++ b/roles/backup/README.md @@ -0,0 +1,125 @@ +# Ansible Role: backup + +This role backs up a set of paths to an S3-compatible restic repository, on a schedule, +and can restore a snapshot back on demand. + +## Role Variables + +### Identity + +- `backup_name` **Required for multiple jobs** unique name for this backup job — used + for the systemd unit/cron entry, script name, and as a sub-path in the restic + repository so multiple jobs can share one bucket (default: `"backup"`). +- `backup_user` / `backup_group` unprivileged system account the scheduled backup job + runs as (default: `"backup"` / `"backup"`). Restore does not use this account — see + Restore Process below. + +### What to back up + +- `backup_paths` **Required** list of paths to back up. + +### Schedule + +- `backup_systemd_schedule` systemd `OnCalendar` schedule (e.g. `daily`, `weekly`, + `*-*-* 02:00`) — used on systemd hosts (default: `"daily"`). +- `backup_cron_schedule` cron schedule (e.g. `0 0 * * *`) — used on OpenRC hosts, + since there's no systemd timer there (default: `"0 0 * * *"`). + +### Retention + +- `backup_retention_daily` number of daily snapshots to keep (default: `7`). +- `backup_retention_weekly` number of weekly snapshots to keep (default: `4`). +- `backup_retention_monthly` number of monthly snapshots to keep (default: `6`). + +### Restic repository + +- `backup_repo` **Required** restic repository URL (e.g. + `s3:http://machine.local:3900/backups`). The role appends `/{{ backup_name }}` to + this automatically, so multiple jobs can share one bucket. +- `backup_access_key_id` **Required** S3 access key ID. +- `backup_secret_access_key` **Required** S3 secret access key. +- `backup_password` **Required** restic repository encryption password. Losing this + makes existing snapshots unrecoverable — it's not stored anywhere except the target + host's credential file. + +### Restore (only used with `--tags restore`, see below) + +- `restore_target` **Required for restore** directory to restore snapshot contents + into. +- `backup_restore_id` snapshot ID to restore, or `"latest"` (default: `"latest"`). + +## Backup Process + +- Creates the `backup_user`/`backup_group` system account (`nologin` shell, no home + directory) — restic never runs as root. +- Deploys credentials to `/etc/credstore/{{ backup_name }}-backup.env`, root:root, + mode `0600`. Values are quoted so `source`-ing the file can't be broken or exploited + by special characters in a password. Variable names are `BACKUP_*`, not `RESTIC_*`/ + `AWS_*` — those are ambient names every S3/restic-aware tool auto-picks up from the + environment, so keeping the on-disk file under inert names avoids a stray `source` + elsewhere on the host silently redirecting some other tool's credentials. +- Creates a dedicated restic cache directory (`/var/cache/{{ backup_name }}-backup`), + owned by `backup_user`, since that account has no home directory for restic to use + by default. +- Schedules the actual backup job, split by init system: + - **systemd**: a `LoadCredential=`-based service + timer. `LoadCredential` copies + the credential file into a private, tmpfs-backed, per-run directory — the secret + never lands in the unit's own tracked environment (unlike `EnvironmentFile=`, + which would make it visible via `systemctl show`). + - **OpenRC**: a `blockinfile`-managed entry in `/etc/crontabs/root` (append-only — + won't clobber Alpine's default periodic maintenance jobs already in that file). + Since there's no systemd credential broker, the script itself runs as root, + reads the credential file directly, then drops to `backup_user` via `su -p` + before ever touching restic or the backup paths. +- The backup script itself: `restic snapshots` (init the repo if this is the first + run), `restic backup <paths>`, then `restic forget --prune` using the configured + retention. + +## Restore Process + +Restore never runs automatically — every restore-related task is tagged +`["never", "restore"]`, Ansible's idiom for "only run when explicitly asked." A normal +`ansible-playbook` run touches none of it, not even deploying the restore script. + +Restore always runs `restic restore --delete`, so `restore_target` ends up matching +the restored snapshot exactly — anything under the backed-up paths that was added +since the snapshot was taken gets removed, not just overlaid. `--delete` only prunes +within the backed-up paths themselves (e.g. `/srv/git`), not the rest of +`restore_target` even when that's `/`. + +To restore, run the play with `--tags restore` and supply `restore_target`: + +```bash +ansible-playbook service.yml --tags restore -e restore_target=/var/restore/git +``` + +To restore a specific snapshot instead of the latest one, also pass +`backup_restore_id`. To find available snapshot IDs, on the target host: + +```bash +source /etc/credstore/{{ backup_name }}-backup.env +export RESTIC_REPOSITORY="$BACKUP_REPO" RESTIC_PASSWORD="$BACKUP_PASSWORD" \ + AWS_ACCESS_KEY_ID="$BACKUP_ACCESS_KEY_ID" AWS_SECRET_ACCESS_KEY="$BACKUP_SECRET_ACCESS_KEY" +restic --cache-dir "/var/cache/{{ backup_name }}-backup" snapshots +``` + +Then: + +```bash +ansible-playbook service.yml --tags restore \ + -e restore_target=/var/restore/git -e backup_restore_id=<snapshot-id> +``` + +### Caveats + +- Unlike backup, restore runs as root rather than dropping to `backup_user`. It needs + to write into paths owned by other service accounts and restore their original + ownership/permissions (restic records each file's original UID/GID and restores it + by default, which requires root). This is a deliberately different privilege model + from the scheduled, unattended backup job — restore only ever runs when explicitly + invoked via `--tags restore`, a human decision each time, not something that runs + on its own. +- `restore_target` is always required explicitly — there's no default. This is + deliberate: restore is destructive if pointed at the wrong place, so the target + must be a conscious choice every time, not something that silently falls back to + a default path. |
