From a6ee08561c1b0387b6ae0398b659d6115ed47691 Mon Sep 17 00:00:00 2001 From: Johanna Dorothea Reichmann Date: Sun, 2 Jan 2022 12:51:25 +0100 Subject: [PATCH] feat(restic-s3): add ansible role for managing backups using restic --- README.md | 3 + roles/restic-s3/README.md | 63 +++++++++++++++++ roles/restic-s3/defaults/main.yml | 37 ++++++++++ roles/restic-s3/handlers/main.yml | 13 ++++ roles/restic-s3/tasks/main.yml | 77 +++++++++++++++++++++ roles/restic-s3/templates/restic.service.j2 | 26 +++++++ roles/restic-s3/templates/restic.timer.j2 | 10 +++ 7 files changed, 229 insertions(+) create mode 100644 roles/restic-s3/README.md create mode 100644 roles/restic-s3/defaults/main.yml create mode 100644 roles/restic-s3/handlers/main.yml create mode 100644 roles/restic-s3/tasks/main.yml create mode 100644 roles/restic-s3/templates/restic.service.j2 create mode 100644 roles/restic-s3/templates/restic.timer.j2 diff --git a/README.md b/README.md index 5a2d3f7..25a006f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ concise area of concern. ## Roles +- [`roles/restic-s3`](roles/restic-s3/README.md): Manage backups using restic + and persist them to an s3-compatible backend. + ## License [CNPLv7+](LICENSE.md): Cooperative Nonviolent Public License diff --git a/roles/restic-s3/README.md b/roles/restic-s3/README.md new file mode 100644 index 0000000..b5d3528 --- /dev/null +++ b/roles/restic-s3/README.md @@ -0,0 +1,63 @@ +# `finallycoffee.services.restic-s3` + +Ansible role for backup up data using `restic` to an `s3`-compatible backend, +utilizing `systemd` timers for scheduling + +## Overview + +The s3 repository and the credentials for it are specified in `restic_repo_url`, +`restic_s3_key_id` and `restic_s3_access_key`. 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! + +### 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-s3/defaults/main.yml b/roles/restic-s3/defaults/main.yml new file mode 100644 index 0000000..0989414 --- /dev/null +++ b/roles/restic-s3/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-s3/handlers/main.yml b/roles/restic-s3/handlers/main.yml new file mode 100644 index 0000000..e6c4867 --- /dev/null +++ b/roles/restic-s3/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-s3/tasks/main.yml b/roles/restic-s3/tasks/main.yml new file mode 100644 index 0000000..f822b7b --- /dev/null +++ b/roles/restic-s3/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-s3/templates/restic.service.j2 b/roles/restic-s3/templates/restic.service.j2 new file mode 100644 index 0000000..fa16f29 --- /dev/null +++ b/roles/restic-s3/templates/restic.service.j2 @@ -0,0 +1,26 @@ +[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 }} +Environment=AWS_ACCESS_KEY_ID={{ restic_s3_key_id }} +Environment=AWS_SECRET_ACCESS_KEY={{ restic_s3_access_key }} + +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-s3/templates/restic.timer.j2 b/roles/restic-s3/templates/restic.timer.j2 new file mode 100644 index 0000000..2ec9503 --- /dev/null +++ b/roles/restic-s3/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