Browse Source

add backup restic role

les 3 years ago
parent
commit
126e3ffd0d

+ 7 - 0
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')}}"

+ 12 - 5
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://
+  - 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}

+ 125 - 0
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).

+ 24 - 0
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

+ 6 - 0
roles/stable/restic/handlers/main.yml

@@ -0,0 +1,6 @@
+---
+
+- name: systemd reload
+  become: yes
+  systemd:
+    daemon_reload: yes

+ 42 - 0
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

+ 67 - 0
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}}"

+ 40 - 0
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 -%}

+ 9 - 0
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

+ 11 - 0
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 %}

+ 5 - 0
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 }}

+ 1 - 0
roles/stable/restic/templates/ssh_private_key.j2

@@ -0,0 +1 @@
+{{ restic_ssh_private_key }}