From 8e11c3734b234accfcb4ea8f7988b1bd51973e10 Mon Sep 17 00:00:00 2001 From: Johanna Dorothea Reichmann Date: Fri, 28 Jul 2023 15:33:18 +0200 Subject: [PATCH] feat(restic): add migrated role from finallycoffee.services --- README.md | 3 + roles/restic/README.md | 77 ++++++++++++++++++++++++ roles/restic/defaults/main.yml | 37 ++++++++++++ roles/restic/handlers/main.yml | 13 ++++ roles/restic/tasks/main.yml | 77 ++++++++++++++++++++++++ roles/restic/templates/restic.service.j2 | 28 +++++++++ roles/restic/templates/restic.timer.j2 | 10 +++ 7 files changed, 245 insertions(+) create mode 100644 roles/restic/README.md create mode 100644 roles/restic/defaults/main.yml create mode 100644 roles/restic/handlers/main.yml create mode 100644 roles/restic/tasks/main.yml create mode 100644 roles/restic/templates/restic.service.j2 create mode 100644 roles/restic/templates/restic.timer.j2 diff --git a/README.md b/README.md index 934363b..a22b994 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ and configuring basic system utilities like gnupg, ssh etc - [`nginx`](roles/nginx/README.md): [nginx](https://www.nginx.com/), an advanced load balancer, webserver and reverse proxy. +- [`restic`](roles/restic/README.md): Manage backups using restic + and persist them to a configurable backend. + ## License [CNPLv7+](LICENSE.md): Cooperative Nonviolent Public License diff --git a/roles/restic/README.md b/roles/restic/README.md new file mode 100644 index 0000000..9ce327f --- /dev/null +++ b/roles/restic/README.md @@ -0,0 +1,77 @@ +# `finallycoffee.base.restic` + +Ansible role for backup up data using `restic`, utilizing `systemd` timers for scheduling. + +## Overview + +As restic encrypts the data before storing it, the `restic_repo_password` needs +to be populated with a strong key, and saved accordingly as only this key can +be used to decrypt the data for a restore! + +### Backends + +#### S3 Backend + +To use a `s3`-compatible backend like AWS buckets or minio, both `restic_s3_key_id` +and `restic_s3_access_key` need to be populated, and the `restic_repo_url` has the +format `s3:https://my.s3.endpoint:port/bucket-name`. + +#### SFTP Backend + +Using the `sftp` backend requires the configured `restic_user` to be able to +authenticate to the configured SFTP-Server using password-less methods like +publickey-authentication. The `restic_repo_url` then follows the format +`sftp:{user}@{server}:/my-restic-repository` (or without leading `/` for relative +paths to the `{user}`s home directory. + +### Backing up data + +A job name like `$service-postgres` or similar needs to be set in `restic_job_name`, +which is used for naming the `systemd` units, their syslog identifiers etc. + +If backing up filesystem locations, the paths need to be specified in +`restic_backup_paths` as lists of strings representing absolute filesystem +locations. + +If backing up f.ex. database or other data which is generating backups using +a command like `pg_dump`, use `restic_backup_stdin_command` (which needs to output +to `stdout`) in conjunction with `restic_backup_stdin_command_filename` to name +the resulting output (required). + +### Policy + +The backup policy can be adjusted by overriding the `restic_policy_keep_*` +variables, with the defaults being: + +```yaml +restic_policy_keep_all_within: 1d +restic_policy_keep_hourly: 6 +restic_policy_keep_daily: 2 +restic_policy_keep_weekly: 7 +restic_policy_keep_monthly: 4 +restic_policy_backup_frequency: hourly +``` + +**Note:** `restic_policy_backup_frequency` must conform to `systemd`s +`OnCalendar` syntax, which can be checked using `systemd-analyze calender $x`. + +## Role behaviour + +Per default, when the systemd unit for a job changes, the job is not immediately +started. This can be overridden using `restic_start_job_on_unit_change: true`, +which will immediately start the backup job if it's configuration changed. + +The systemd unit runs with `restic_user`, which is root by default, guaranteeing +that filesystem paths are always readable. The `restic_user` can be overridden, +but care needs to be taken to ensure the user has permission to read all the +provided filesystem paths / the backup command may be executed by the user. + +If ansible should create the user, set `restic_create_user` to `true`, which +will attempt to create the `restic_user` as a system user. + +### Installing + +For Debian and RedHat, the role attempts to install restic using the default +package manager's ansible module (apt/dnf). For other distributions, the generic +`package` module tries to install `restic_package_name` (default: `restic`), +which can be overridden if needed. diff --git a/roles/restic/defaults/main.yml b/roles/restic/defaults/main.yml new file mode 100644 index 0000000..0989414 --- /dev/null +++ b/roles/restic/defaults/main.yml @@ -0,0 +1,37 @@ +--- + +restic_repo_url: ~ +restic_repo_password: ~ +restic_s3_key_id: ~ +restic_s3_access_key: ~ + +restic_backup_paths: [] +restic_backup_stdin_command: ~ +restic_backup_stdin_command_filename: ~ + +restic_policy_keep_all_within: 1d +restic_policy_keep_hourly: 6 +restic_policy_keep_daily: 2 +restic_policy_keep_weekly: 7 +restic_policy_keep_monthly: 4 +restic_policy_backup_frequency: hourly + +restic_policy: + keep_within: "{{ restic_policy_keep_all_within }}" + hourly: "{{ restic_policy_keep_hourly }}" + daily: "{{ restic_policy_keep_daily }}" + weekly: "{{ restic_policy_keep_weekly }}" + monthly: "{{ restic_policy_keep_monthly }}" + frequency: "{{ restic_policy_backup_frequency }}" + +restic_user: root +restic_create_user: false +restic_start_job_on_unit_change: false + +restic_job_name: ~ +restic_job_description: "Restic backup job for {{ restic_job_name }}" +restic_systemd_unit_naming_scheme: "restic.{{ restic_job_name }}" +restic_systemd_working_directory: /tmp +restic_systemd_syslog_identifier: "restic-{{ restic_job_name }}" + +restic_package_name: restic diff --git a/roles/restic/handlers/main.yml b/roles/restic/handlers/main.yml new file mode 100644 index 0000000..e6c4867 --- /dev/null +++ b/roles/restic/handlers/main.yml @@ -0,0 +1,13 @@ +--- + +- name: Ensure system daemon is reloaded + listen: reload-systemd + systemd: + daemon_reload: true + +- name: Ensure systemd service for '{{ restic_job_name }}' is started immediately + listen: trigger-restic + systemd: + name: "{{ restic_systemd_unit_naming_scheme }}.service" + state: started + when: restic_start_job_on_unit_change diff --git a/roles/restic/tasks/main.yml b/roles/restic/tasks/main.yml new file mode 100644 index 0000000..f822b7b --- /dev/null +++ b/roles/restic/tasks/main.yml @@ -0,0 +1,77 @@ +--- + +- name: Ensure {{ restic_user }} system user exists + user: + name: "{{ restic_user }}" + state: present + system: true + when: restic_create_user + +- name: Ensure either backup_paths or backup_stdin_command is populated + when: restic_backup_paths|length > 0 and restic_backup_stdin_command + fail: + msg: "Setting both `restic_backup_paths` and `restic_backup_stdin_command` is not supported" + +- name: Ensure a filename for stdin_command backup is given + when: restic_backup_stdin_command and not restic_backup_stdin_command_filename + fail: + msg: "`restic_backup_stdin_command` was set but no filename for the resulting output was supplied in `restic_backup_stdin_command_filename`" + +- name: Ensure backup frequency adheres to systemd's OnCalender syntax + command: + cmd: "systemd-analyze calendar {{ restic_policy.frequency }}" + register: systemd_calender_parse_res + failed_when: systemd_calender_parse_res.rc != 0 + changed_when: false + +- name: Ensure restic is installed + block: + - name: Ensure restic is installed via apt + apt: + package: restic + state: latest + when: ansible_os_family == 'Debian' + - name: Ensure restic is installed via dnf + dnf: + name: restic + state: latest + when: ansible_os_family == 'RedHat' + - name: Ensure restic is installed using the auto-detected package-manager + package: + name: "{{ restic_package_name }}" + state: present + when: ansible_os_family not in ['RedHat', 'Debian'] + +- name: Ensure systemd service file for '{{ restic_job_name }}' is templated + template: + dest: "/etc/systemd/system/{{ restic_systemd_unit_naming_scheme }}.service" + src: restic.service.j2 + owner: root + group: root + mode: 0640 + notify: + - reload-systemd + - trigger-restic + +- name: Ensure systemd service file for '{{ restic_job_name }}' is templated + template: + dest: "/etc/systemd/system/{{ restic_systemd_unit_naming_scheme }}.timer" + src: restic.timer.j2 + owner: root + group: root + mode: 0640 + notify: + - reload-systemd + +- name: Flush handlers to ensure systemd knows about '{{ restic_job_name }}' + meta: flush_handlers + +- name: Ensure systemd timer for '{{ restic_job_name }}' is activated + systemd: + name: "{{ restic_systemd_unit_naming_scheme }}.timer" + enabled: true + +- name: Ensure systemd timer for '{{ restic_job_name }}' is started + systemd: + name: "{{ restic_systemd_unit_naming_scheme }}.timer" + state: started diff --git a/roles/restic/templates/restic.service.j2 b/roles/restic/templates/restic.service.j2 new file mode 100644 index 0000000..11e89d5 --- /dev/null +++ b/roles/restic/templates/restic.service.j2 @@ -0,0 +1,28 @@ +[Unit] +Description={{ restic_job_description }} + +[Service] +Type=oneshot +User={{ restic_user }} +WorkingDirectory={{ restic_systemd_working_directory }} +SyslogIdentifier={{ restic_systemd_syslog_identifier }} + +Environment=RESTIC_REPOSITORY={{ restic_repo_url }} +Environment=RESTIC_PASSWORD={{ restic_repo_password }} +{% if restic_s3_key_id and restic_s3_access_key %} +Environment=AWS_ACCESS_KEY_ID={{ restic_s3_key_id }} +Environment=AWS_SECRET_ACCESS_KEY={{ restic_s3_access_key }} +{% endif %} + +ExecStartPre=-/bin/sh -c '/usr/bin/restic snapshots || /usr/bin/restic init' +{% if restic_backup_stdin_command %} +ExecStart=/bin/sh -c '{{ restic_backup_stdin_command }} | /usr/bin/restic backup --verbose --stdin --stdin-filename {{ restic_backup_stdin_command_filename }}' +{% else %} +ExecStart=/usr/bin/restic --verbose backup {{ restic_backup_paths | join(' ') }} +{% endif %} +ExecStartPost=/usr/bin/restic forget --prune --keep-within={{ restic_policy.keep_within }} --keep-hourly={{ restic_policy.hourly }} --keep-daily={{ restic_policy.daily }} --keep-weekly={{ restic_policy.weekly }} --keep-monthly={{ restic_policy.monthly }} +ExecStartPost=-/usr/bin/restic snapshots +ExecStartPost=/usr/bin/restic check + +[Install] +WantedBy=multi-user.target diff --git a/roles/restic/templates/restic.timer.j2 b/roles/restic/templates/restic.timer.j2 new file mode 100644 index 0000000..2ec9503 --- /dev/null +++ b/roles/restic/templates/restic.timer.j2 @@ -0,0 +1,10 @@ +[Unit] +Description=Run {{ restic_job_name }} + +[Timer] +OnCalendar={{ restic_policy.frequency }} +Persistent=True +Unit={{ restic_systemd_unit_naming_scheme }}.service + +[Install] +WantedBy=timers.target