/
/
/
Ansible role that can run restic backups and push it to a remote storage server.
1---
2# Helper vars
3- name: Derive project_name and targets
4 ansible.builtin.set_fact:
5 _project_name: "{{ (item.project_name | default((item.path | basename))) }}"
6 _targets: "{{ item.targets | default([ item.path ]) }}"
7
8# Optional pre-commands (host shell)
9- name: Run pre_cmd tasks (if any)
10 ansible.builtin.shell: "{{ cmd }}"
11 args:
12 chdir: "{{ item.path }}"
13 loop: "{{ item.pre_cmd | default([]) }}"
14 loop_control:
15 loop_var: cmd
16 when: (item.pre_cmd | default([])) | length > 0
17 become: true
18
19# Pre-backup health check (only if enabled)
20- name: Gather containers for project (pre)
21 community.docker.docker_container_info:
22 filters:
23 label: "com.docker.compose.project={{ _project_name }}"
24 register: _pre_containers
25 when: backup_health_check | bool
26
27- name: Determine if any container has a healthcheck (pre)
28 ansible.builtin.set_fact:
29 _has_healthchecks_pre: "{{ (_pre_containers.containers | default([])) | selectattr('Health','defined') | list | length > 0 }}"
30 when: backup_health_check | bool
31
32- name: Assert project healthy before backup (if healthchecks exist)
33 ansible.builtin.assert:
34 that:
35 - (_pre_containers.containers | selectattr('Health','defined') | map(attribute='Health') | map(attribute='Status') | list) | difference(['healthy']) | length == 0
36 success_msg: "All containers with healthchecks are healthy (pre-backup)."
37 fail_msg: "Some containers are not healthy before backup."
38 when: backup_health_check | bool and _has_healthchecks_pre
39
40# Stop the project
41- name: Stop compose project
42 community.docker.docker_compose_v2:
43 project_src: "{{ item.path }}"
44 project_name: "{{ _project_name }}"
45 files: "{{ item.compose_files | default(omit) }}"
46 state: stopped
47 register: _stop_result
48
49# Backup: run restic on targets
50- name: Run restic backup
51 ansible.builtin.command: >
52 restic backup
53 {{ restic_backup_args }}
54 --tag {{ restic_tag_format }}
55 {{ _targets | join(' ') }}
56 environment:
57 RESTIC_REPOSITORY: "{{ restic_repo }}"
58 RESTIC_PASSWORD: "{{ restic_password }}"
59 {% for k, v in restic_env_extra.items() %}{{ k }}: "{{ v }}"
60 {% endfor %}
61 register: _restic_backup
62 changed_when: "'snapshot' in _restic_backup.stdout or 'created new' in _restic_backup.stdout"
63
64# Housekeeping: retention/prune (optional)
65- name: Enforce retention policy
66 ansible.builtin.command: >
67 restic forget --prune {{ restic_retention_args }} --tag {{ restic_tag_format }}
68 environment:
69 RESTIC_REPOSITORY: "{{ restic_repo }}"
70 RESTIC_PASSWORD: "{{ restic_password }}"
71 {% for k, v in restic_env_extra.items() %}{{ k }}: "{{ v }}"
72 {% endfor %}
73 when: restic_retention_enable | bool
74
75# Start the project
76- name: Start compose project
77 community.docker.docker_compose_v2:
78 project_src: "{{ item.path }}"
79 project_name: "{{ _project_name }}"
80 files: "{{ item.compose_files | default(omit) }}"
81 state: present
82 register: _start_result
83
84# Post-backup: wait until healthy (if healthchecks exist)
85- name: Wait for containers to be healthy (post)
86 vars:
87 _deadline: "{{ (backup_health_timeout_sec | int) // (backup_health_interval_sec | int) }}"
88 community.docker.docker_container_info:
89 filters:
90 label: "com.docker.compose.project={{ _project_name }}"
91 register: _post_containers
92 until: >
93 (
94 (_post_containers.containers | default([])) | selectattr('Health','defined')
95 | map(attribute='Health') | map(attribute='Status') | list
96 ) | difference(['healthy']) | length == 0
97 or
98 (
99 (_post_containers.containers | default([])) | selectattr('Health','defined') | list | length == 0
100 )
101 retries: "{{ _deadline | int | max(1) }}"
102 delay: "{{ backup_health_interval_sec | int }}"
103 when: backup_health_check | bool
104 changed_when: false
105
106# Optional post-commands (host shell)
107- name: Run post_cmd tasks (if any)
108 ansible.builtin.shell: "{{ cmd }}"
109 args:
110 chdir: "{{ item.path }}"
111 loop: "{{ item.post_cmd | default([]) }}"
112 loop_control:
113 loop_var: cmd
114 when: (item.post_cmd | default([])) | length > 0
115 become: true
116
117