diff --git a/inventory.yml b/inventory.yml index a43386f..97e85a1 100644 --- a/inventory.yml +++ b/inventory.yml @@ -18,3 +18,10 @@ test: all: vars: passwordstore_path: cisti.org/ansible + restic_default_folders: [] + restic_password: "{{lookup('community.general.passwordstore', '{{passwordstore_path}}/restic/{{{{ansible_hostname}}_pwd create=True nosymbols=true')}}" + restic_repository_name: "{{ansible_hostname}}" + restic_ssh_private_key: "{{lookup('community.general.passwordstore', '{{passwordstore_path}}/restic/ssh_private returnall=true')}}" + restic_ssh_hostname: "{{lookup('community.general.passwordstore', '{{passwordstore_path}}/restic/ssh_hostname')}}" + restic_ssh_user: "{{lookup('community.general.passwordstore', '{{passwordstore_path}}/restic/ssh_user')}}" + restic_ssh_port: "{{lookup('community.general.passwordstore', '{{passwordstore_path}}/restic/ssh_port')}}" \ No newline at end of file diff --git a/roles/stable/etherpad/meta/main.yml b/roles/stable/etherpad/meta/main.yml index 2b11771..8af0f1c 100644 --- a/roles/stable/etherpad/meta/main.yml +++ b/roles/stable/etherpad/meta/main.yml @@ -13,8 +13,15 @@ dependencies: database: etherpad # install certbot nginx and configure it as reverse proxy - # - role: stable/nginx - # when: with_nginx | bool - # vars: - # with_certbot: true - # proxy_pass: http:// \ No newline at end of file + - role: stable/nginx + when: with_nginx | bool + vars: + with_certbot: true + proxy_pass: http://localhost:8001 + + # backup etherpad database + - role: stable/restic + when: with_backup | bool + vars: + restic_databases: + - {name: 'etherpad', dump_command: sudo -Hiu postgres pg_dump -Fc etherpad} \ No newline at end of file diff --git a/roles/stable/restic/README.md b/roles/stable/restic/README.md new file mode 100644 index 0000000..821fc4b --- /dev/null +++ b/roles/stable/restic/README.md @@ -0,0 +1,125 @@ +# Ansible role for Restic + +This role will setup [Restic](https://restic.net/) backups on a Debian/Ubuntu machine using a systemd service and timer. + +It supports S3 backend or SFTP backend and will thus setup the SSH config and SSH private keys (see variables below). + +## Role Variables + +### Restic installation + +The role will download and install the restic binary (version `restic_version`) into `restic_path` if the file does not exist. + +If you want to force the installation, overwrite the binary or update restic, you can run ansible with `--extra-vars restic_install=true`. + +### Restic configuration + +- `restic_user`: user to run restic as (`root`) +- `restic_user_home`: home directory of the restic_user (`/root`) +- `restic_password`: password used for repository encryption +- `restic_repository_name`: the name of the repository (`restic`) +- `restic_check`: run `restic check` as `ExecStartPre` if true (`false`) +- `restic_default_folders`: a default list of folders that restic will backup (`/etc/`, `/root` and `/var/log`) +- `restic_folders`: the list of folder you want to backup +- `restic_dump_compression_enabled`: enable piping to pigz for database dumps + +Each folder has a `path` and an `exclude` property (which defaults to nothing). The `exclude` property is the literal argument passed to restic (exemple: `--exclude .cache --exclude .local`). + +`restic_default_folders` and `restic_folders` are combined to form the final list of backuped folders. + +- `restic_databases`: a list of databases to dump + +Each database has a `name` property which will be the name of the restic snapshot (`{{ database.name }}.sql`). They also have a `dump_command` property which is the command to dump the database to stdout (like `mysqldump dbname`). + +- `restic_forget`: run `restic forget` as `ExecStartPost` with `--keep-within {{ restic_forget_keep_within }}` (`true`) +- `restic_forget_keep_within`: period of time to use with `--keep-within` (`30d`) +- `restic_prune`: run `restic prune` as `ExecStartPost` (`true`) + +### SSH/SFTP backend configuration + +The SSH configuration will be written in `{{ restic_user_home }}/.ssh/config`. + +- `restic_ssh_host`: backend name and SSH alias for the backup host +- `restic_ssh_user`: user for SSH connection +- `restic_ssh_hostname`: actual SSH hostname of the backup machine +- `restic_ssh_private_key`: private SSH key used to connect to the backup host +- `restic_ssh_private_key_path`: path of the private key to use (`~/.ssh/backup`) +- `restic_ssh_port`: SSH port to use with the backup machine (`23`) + +### S3 backend configuration + +- `restic_ssh_enabled`: set to false +- `restic_repository_name`: set to s3 endpoint + bucket, restic syntax (e.g. `s3:https://s3.fr-par.scw.cloud/restic-bucket`) +- `restic_aws_access_key_id`: `AWS_ACCESS_KEY_ID` +- `restic_aws_secret_access_key`: `AWS_SECRET_ACCESS_KEY` + +### Sytemd service and timer + +A `restic-backup.service` service will be created with all the parameters defined above. The service is of type `oneshot` and will be triggered periodically with `restic-backup.timer`. + +The timer is configurable as follows: + +- `restic_systemd_timer_on_calender`: defines the `OnCalendar` directive (`*-*-* 03:00:00`) +- `restic_systemd_timer_randomized_delay_sec`: Delay the timer by a random amount of time between 0 and the specified time value. (`0`) + +See the [systemd.timer](https://www.freedesktop.org/software/systemd/man/systemd.timer.html) documentation for more information. + +You can see the logs of the backup with `journalctl`. (`journalctl -xefu restic-backup`). + +## Example playbook + +```yaml +--- + +- hosts: myhost + roles: restic + vars: + restic_ssh_user: backupuser + restic_ssh_hostname: storage-server.infra.tld + restic_folders: + - {path: "/srv"} + - {path: "/var/www"} + restic_databases: + - {name: website, dump_command: sudo -Hiu postgres pg_dump -Fc website} + - {name: website2, dump_command: mysqldump website2} + restic_password: mysuperduperpassword + restic_ssh_private_key: |- + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW + QyNTUxOQAAACAocs5g1I4kFQ1HH/YZiVU+zLhRDu4tfzZ9CmFAfKhL2AAAAJi02XEwtNlx + MAAAAAtzc2gtZWQyNTUxOQAAACAocs5g1I4kFQ1HH/YZiVU+zLhRDu4tfzZ9CmFAfKhL2A + AAAEADZf2Pv4G74x+iNtuwSV/ItnR3YQJ/KUaNTH19umA/tChyzmDUjiQVDUcf9hmJVT7M + uFEO7i1/Nn0KYUB8qEvYAAAAE3N0YW5pc2xhc0BtYnAubG9jYWwBAg== + -----END OPENSSH PRIVATE KEY----- +``` + +S3 example: + +```yaml +--- + +- hosts: myhost + roles: restic + vars: + restic_ssh_enabled: false + restic_repository: "s3:https://s3.fr-par.scw.cloud/restic-bucket" + restic_aws_access_key_id: xxxxx + restic_aws_secret_access_key: xxxxx + restic_folders: + - {path: "/srv"} + - {path: "/var/www"} + restic_databases: + - {name: website, dump_command: sudo -Hiu postgres pg_dump -Fc website} + - {name: website2, dump_command: mysqldump website2} + restic_password: mysuperduperpassword +``` + +Of course, `restic_password` and `restic_ssh_private_key` should be stored using ansible-vault. + +## License + +MIT + +## Author Information + +See my other Ansible roles at [angristan/ansible-roles](https://github.com/angristan/ansible-roles). diff --git a/roles/stable/restic/defaults/main.yml b/roles/stable/restic/defaults/main.yml new file mode 100644 index 0000000..7b0f056 --- /dev/null +++ b/roles/stable/restic/defaults/main.yml @@ -0,0 +1,24 @@ +--- +restic_install: false +restic_version: 0.11.0 +restic_path: /usr/local/bin/restic +restic_user: root +restic_user_home: /root + +restic_repository_name: restic +restic_default_folders: [] +restic_folders: [] +restic_databases: [] +restic_dump_compression_enabled: true +restic_forget: true +restic_forget_keep_within: 30d +restic_prune: true +restic_check: true + +restic_ssh_enabled: true +restic_ssh_host: backup +restic_ssh_port: 22 +restic_ssh_private_key_path: '/root/.ssh/backup' + +restic_systemd_timer_on_calender: '*-*-* 03:00:00' +restic_systemd_timer_randomized_delay_sec: 1000 diff --git a/roles/stable/restic/handlers/main.yml b/roles/stable/restic/handlers/main.yml new file mode 100644 index 0000000..2bcb018 --- /dev/null +++ b/roles/stable/restic/handlers/main.yml @@ -0,0 +1,6 @@ +--- + +- name: systemd reload + become: yes + systemd: + daemon_reload: yes diff --git a/roles/stable/restic/tasks/install.yml b/roles/stable/restic/tasks/install.yml new file mode 100644 index 0000000..bfe2923 --- /dev/null +++ b/roles/stable/restic/tasks/install.yml @@ -0,0 +1,42 @@ +--- + +- name: Install fuse (to mount repositories) + become: yes + apt: + name: fuse + +- name: Install bzip2 (to install restic) + become: yes + apt: + name: bzip2 + +- name: Install pigz (to compress db dumps) + become: yes + apt: + name: pigz + +- name: Download restic + become: yes + get_url: + url: 'https://github.com/restic/restic/releases/download/v{{ restic_version }}/restic_{{ restic_version }}_linux_amd64.bz2' + dest: '/tmp/restic_{{ restic_version }}_linux_amd64.bz2' + +- name: Extract restic + become: yes + command: 'bzip2 -d /tmp/restic_{{ restic_version }}_linux_amd64.bz2' + args: + creates: '/tmp/restic_{{ restic_version }}_linux_amd64' + +- name: Install restic + become: yes + copy: + remote_src: true + src: '/tmp/restic_{{ restic_version }}_linux_amd64' + dest: "{{ restic_path }}" + mode: 0755 + +- name: Remove downloaded file + become: yes + file: + path: '/tmp/restic_{{ restic_version }}_linux_amd64' + state: absent diff --git a/roles/stable/restic/tasks/main.yml b/roles/stable/restic/tasks/main.yml new file mode 100644 index 0000000..3050aee --- /dev/null +++ b/roles/stable/restic/tasks/main.yml @@ -0,0 +1,67 @@ +--- +- name: Check if restic is installed + stat: + path: '{{ restic_path }}' + register: restic_binary + +- include_tasks: install.yml + when: not restic_binary.stat.exists or restic_install + +- name: Overwrite SSH config for backup server + become: yes + template: + src: ssh_config.j2 + dest: '{{ restic_user_home }}/.ssh/config' + owner: root + group: root + mode: '0600' + when: restic_ssh_enabled + +- name: Add SSH private key + become: yes + template: + src: ssh_private_key.j2 + dest: '{{ restic_ssh_private_key_path }}' + mode: '0600' + when: restic_ssh_private_key is defined and restic_ssh_enabled + +- name: Add restic_env in home folder + become: yes + template: + src: restic_env.j2 + dest: '{{ restic_user_home }}/.restic_env' + owner: root + group: root + mode: '0600' + +- name: Add systemd service for restic + become: yes + template: + src: restic-backup.service.j2 + dest: /etc/systemd/system/restic-backup.service + mode: '0644' + vars: + restic_folders_combined: '{{ restic_default_folders + restic_folders }}' + notify: systemd reload + +- name: Add systemd timer for restic + become: yes + template: + src: restic-backup.timer.j2 + dest: /etc/systemd/system/restic-backup.timer + mode: '0644' + notify: systemd reload + +- name: Enable and start restic timer + become: yes + systemd: + name: restic-backup.timer + enabled: true + state: started + +- name: Initialize restic repo if needed + become: yes + command: "{{restic_path}} init" + environment: + RESTIC_REPOSITORY: "sftp:{{ restic_ssh_host }}:{{ restic_repository_name }}" + RESTIC_PASSWORD: "{{restic_password}}" diff --git a/roles/stable/restic/templates/restic-backup.service.j2 b/roles/stable/restic/templates/restic-backup.service.j2 new file mode 100644 index 0000000..9d53831 --- /dev/null +++ b/roles/stable/restic/templates/restic-backup.service.j2 @@ -0,0 +1,40 @@ +[Unit] +Description=Restic backup + +[Service] +Type=oneshot +User={{ restic_user }} + +CPUQuota={{ 25 * ansible_processor_vcpus }}% + +{% if restic_ssh_enabled %} +Environment="RESTIC_REPOSITORY=sftp:{{ restic_ssh_host }}:{{ restic_repository_name }}" +{% else %} +Environment="RESTIC_REPOSITORY={{ restic_repository }}" +{% endif -%} +Environment="RESTIC_PASSWORD={{ restic_password}}" + +{% if restic_aws_access_key_id is defined and restic_aws_secret_access_key is defined %} +Environment="AWS_ACCESS_KEY_ID={{ restic_aws_access_key_id}}" +Environment="AWS_SECRET_ACCESS_KEY={{ restic_aws_secret_access_key}}" +{% endif %} + +{% if restic_check %} +ExecStartPre={{ restic_path }} check +{% endif -%} + +{% for folder in restic_folders_combined %} +ExecStart={{ restic_path }} backup --verbose {{ folder.path }} {{ folder.exclude if folder.exclude is defined else '' }} +{% endfor -%} + +{% for database in restic_databases %} +ExecStart=/bin/sh -c "{{ database.dump_command }} {{ '| pigz |' if restic_dump_compression_enabled else '|' }} {{ restic_path }} backup --verbose --stdin --stdin-filename {{ database.name }}{{ '.sql.gz' if restic_dump_compression_enabled else '.sql' }}" +{% endfor -%} + +{% if restic_forget %} +ExecStartPost={{ restic_path }} forget --keep-within {{ restic_forget_keep_within }} +{% endif -%} + +{% if restic_prune %} +ExecStartPost={{ restic_path }} prune +{% endif -%} diff --git a/roles/stable/restic/templates/restic-backup.timer.j2 b/roles/stable/restic/templates/restic-backup.timer.j2 new file mode 100644 index 0000000..a4168aa --- /dev/null +++ b/roles/stable/restic/templates/restic-backup.timer.j2 @@ -0,0 +1,9 @@ +[Unit] +Description=Restic backup + +[Timer] +OnCalendar={{ restic_systemd_timer_on_calender }} +RandomizedDelaySec={{ restic_systemd_timer_randomized_delay_sec }} + +[Install] +WantedBy=timers.target diff --git a/roles/stable/restic/templates/restic_env.j2 b/roles/stable/restic/templates/restic_env.j2 new file mode 100644 index 0000000..af9e565 --- /dev/null +++ b/roles/stable/restic/templates/restic_env.j2 @@ -0,0 +1,11 @@ +{% if restic_ssh_enabled %} +export RESTIC_REPOSITORY=sftp:{{ restic_ssh_host }}:{{ restic_repository_name }} +{% else %} +export RESTIC_REPOSITORY="{{ restic_repository }}" +{% endif -%} +export RESTIC_PASSWORD={{ restic_password}} + +{% if restic_aws_access_key_id is defined and restic_aws_secret_access_key is defined %} +export AWS_ACCESS_KEY_ID={{ restic_aws_access_key_id}} +export AWS_SECRET_ACCESS_KEY={{ restic_aws_secret_access_key}} +{% endif %} diff --git a/roles/stable/restic/templates/ssh_config.j2 b/roles/stable/restic/templates/ssh_config.j2 new file mode 100644 index 0000000..9f88fc2 --- /dev/null +++ b/roles/stable/restic/templates/ssh_config.j2 @@ -0,0 +1,5 @@ +Host {{ restic_ssh_host }} + User {{ restic_ssh_user }} + HostName {{ restic_ssh_hostname }} + IdentityFile {{ restic_ssh_private_key_path }} + Port {{ restic_ssh_port }} diff --git a/roles/stable/restic/templates/ssh_private_key.j2 b/roles/stable/restic/templates/ssh_private_key.j2 new file mode 100644 index 0000000..f4dc3f3 --- /dev/null +++ b/roles/stable/restic/templates/ssh_private_key.j2 @@ -0,0 +1 @@ +{{ restic_ssh_private_key }}