summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAhmed AbdelHalim <[email protected]>2026-06-22 16:55:49 +0200
committerAhmed AbdelHalim <[email protected]>2026-06-22 16:55:49 +0200
commit83cb69983a56d3d2c37a37a484a40c0a50707afa (patch)
tree79d6b85a1c592c0ba5ae9245edb5610c3b78ff66
parentcb8f807a5d24645b67f0dcdf5afae032c7dd5ce0 (diff)
Add garage to rpi to use as s3-compatible backend for backups
-rw-r--r--configure.yml1
-rw-r--r--host_vars/rpi5.local.ymlbin2394 -> 2618 bytes
-rw-r--r--host_vars/rpi5.local.yml.example6
-rw-r--r--molecule/raspberrypi/converge.yml1
-rw-r--r--roles/garage/defaults/main.yml12
-rw-r--r--roles/garage/handlers/main.yml6
-rw-r--r--roles/garage/meta/argument_specs.yml48
-rw-r--r--roles/garage/meta/main.yml17
-rw-r--r--roles/garage/tasks/main.yml106
-rw-r--r--roles/garage/templates/garage-provision.sh.j220
-rw-r--r--roles/garage/templates/garage.service.j215
-rw-r--r--roles/garage/templates/garage.toml.j215
12 files changed, 246 insertions, 1 deletions
diff --git a/configure.yml b/configure.yml
index 7bfc5fb9..8cb3bb79 100644
--- a/configure.yml
+++ b/configure.yml
@@ -106,6 +106,7 @@
- role: "grafana"
- role: "neomutt"
- role: "backup"
+ - role: "garage"
- name: "Proxmox VE configure"
gather_facts: true
diff --git a/host_vars/rpi5.local.yml b/host_vars/rpi5.local.yml
index b5708d51..ae758bf5 100644
--- a/host_vars/rpi5.local.yml
+++ b/host_vars/rpi5.local.yml
Binary files differ
diff --git a/host_vars/rpi5.local.yml.example b/host_vars/rpi5.local.yml.example
index 1ad9bc02..702f99b5 100644
--- a/host_vars/rpi5.local.yml.example
+++ b/host_vars/rpi5.local.yml.example
@@ -75,4 +75,8 @@ grafana_dashboards:
- key: "${DS__VICTORIAMETRICS}"
value: "prometheus"
-backup_device: "UUID=your_backup_device_uuid_here"
+backup_device: "UUID=changeme"
+
+garage_rpc_secret: "changeme"
+garage_access_key_id: "GK_changeme"
+garage_secret_access_key: "changeme"
diff --git a/molecule/raspberrypi/converge.yml b/molecule/raspberrypi/converge.yml
index db7b6303..75e3d693 100644
--- a/molecule/raspberrypi/converge.yml
+++ b/molecule/raspberrypi/converge.yml
@@ -129,3 +129,4 @@
- role: "claude"
- role: "localsend"
- role: "backup"
+ - role: "garage"
diff --git a/roles/garage/defaults/main.yml b/roles/garage/defaults/main.yml
new file mode 100644
index 00000000..6daa372c
--- /dev/null
+++ b/roles/garage/defaults/main.yml
@@ -0,0 +1,12 @@
+---
+garage_version: "2.3.0"
+garage_capacity: "100G"
+garage_data_dir: "/mnt/backup/garage/data"
+garage_meta_dir: "/mnt/backup/garage/meta"
+garage_rpc_secret: "changeme"
+garage_s3_port: "3900"
+garage_admin_port: "3903"
+garage_rpc_bind_addr: "127.0.0.1:3901"
+garage_bucket: "restic"
+garage_access_key_id: "changeme"
+garage_secret_access_key: "changeme"
diff --git a/roles/garage/handlers/main.yml b/roles/garage/handlers/main.yml
new file mode 100644
index 00000000..425f6ffa
--- /dev/null
+++ b/roles/garage/handlers/main.yml
@@ -0,0 +1,6 @@
+---
+- name: "Restart garage"
+ become: true
+ ansible.builtin.systemd_service:
+ name: "garage"
+ state: "restarted"
diff --git a/roles/garage/meta/argument_specs.yml b/roles/garage/meta/argument_specs.yml
new file mode 100644
index 00000000..a98f9991
--- /dev/null
+++ b/roles/garage/meta/argument_specs.yml
@@ -0,0 +1,48 @@
+---
+argument_specs:
+ main:
+ options:
+ garage_version:
+ type: "str"
+ description: "Garage release version to install"
+ default: "2.3.0"
+ garage_data_dir:
+ type: "str"
+ description: "Directory for Garage object data"
+ default: "/mnt/backup/garage/data"
+ garage_meta_dir:
+ type: "str"
+ description: "Directory for Garage metadata"
+ default: "/mnt/backup/garage/meta"
+ garage_rpc_secret:
+ type: "str"
+ description: "32-byte hex RPC secret shared across nodes"
+ required: true
+ garage_s3_port:
+ type: "str"
+ description: "Port for S3 API"
+ default: "3900"
+ garage_admin_port:
+ type: "str"
+ description: "Port for admin API"
+ default: "3903"
+ garage_rpc_bind_addr:
+ type: "str"
+ description: "RPC bind address"
+ default: "127.0.0.1:3901"
+ garage_capacity:
+ type: "str"
+ description: "Storage capacity for this node (e.g. 100G, 200G)"
+ default: "100G"
+ garage_bucket:
+ type: "str"
+ description: "Bucket name to create for restic backups"
+ default: "restic"
+ garage_access_key_id:
+ type: "str"
+ description: "S3 access key ID"
+ required: true
+ garage_secret_access_key:
+ type: "str"
+ description: "S3 secret access key"
+ required: true
diff --git a/roles/garage/meta/main.yml b/roles/garage/meta/main.yml
new file mode 100644
index 00000000..e59db6e0
--- /dev/null
+++ b/roles/garage/meta/main.yml
@@ -0,0 +1,17 @@
+---
+dependencies: []
+galaxy_info:
+ author: "a14m"
+ description: "Install Garage S3-compatible object store"
+ license: "MIT"
+ min_ansible_version: "2.18"
+ platforms:
+ - name: "ArchLinux"
+ versions:
+ - "all"
+ - name: "Ubuntu"
+ versions:
+ - "noble"
+ - name: "Debian"
+ versions:
+ - "bookworm"
diff --git a/roles/garage/tasks/main.yml b/roles/garage/tasks/main.yml
new file mode 100644
index 00000000..5ab27b49
--- /dev/null
+++ b/roles/garage/tasks/main.yml
@@ -0,0 +1,106 @@
+---
+- name: "Install garage binary"
+ become: true
+ vars:
+ garage_arch: "{{ ansible_facts['architecture'] }}-unknown-linux-musl"
+ garage_url: "https://garagehq.deuxfleurs.fr/_releases/v{{ garage_version }}/{{ garage_arch }}/garage"
+ block:
+ - name: "Check if garage is installed"
+ ansible.builtin.stat:
+ path: "/usr/local/bin/garage"
+ register: garage_binary_check
+
+ - name: "Download garage binary"
+ when: not garage_binary_check.stat.exists
+ ansible.builtin.get_url:
+ url: "{{ garage_url }}"
+ dest: "/usr/local/bin/garage"
+ mode: "0755"
+ owner: "root"
+ group: "root"
+
+- name: "Configure garage"
+ become: true
+ block:
+ - name: "Create garage group"
+ ansible.builtin.group:
+ name: "garage"
+ system: true
+
+ - name: "Create garage user"
+ ansible.builtin.user:
+ name: "garage"
+ group: "garage"
+ system: true
+ shell: "/usr/sbin/nologin"
+ create_home: false
+
+ - name: "Create garage config directory"
+ ansible.builtin.file:
+ path: "/etc/garage"
+ state: "directory"
+ owner: "root"
+ group: "garage"
+ mode: "0750"
+
+ - name: "Create garage data directory"
+ ansible.builtin.file:
+ path: "{{ garage_data_dir }}"
+ state: "directory"
+ owner: "garage"
+ group: "garage"
+ mode: "0750"
+
+ - name: "Create garage meta directory"
+ ansible.builtin.file:
+ path: "{{ garage_meta_dir }}"
+ state: "directory"
+ owner: "garage"
+ group: "garage"
+ mode: "0750"
+
+ - name: "Create garage configuration file"
+ ansible.builtin.template:
+ src: "garage.toml.j2"
+ dest: "/etc/garage/garage.toml"
+ owner: "root"
+ group: "garage"
+ mode: "0640"
+ no_log: true
+ notify: "Restart garage"
+
+ - name: "Create garage systemd service"
+ ansible.builtin.template:
+ src: "garage.service.j2"
+ dest: "/etc/systemd/system/garage.service"
+ owner: "root"
+ group: "root"
+ mode: "0644"
+ notify: "Restart garage"
+
+ - name: "Start and enable garage"
+ ansible.builtin.systemd_service:
+ name: "garage"
+ state: "started"
+ enabled: true
+ daemon_reload: true
+
+- name: "Provision garage bucket and key"
+ become: true
+ block:
+ - name: "Deploy provision script"
+ ansible.builtin.template:
+ src: "garage-provision.sh.j2"
+ dest: "/etc/garage/provision.sh"
+ owner: "root"
+ group: "garage"
+ mode: "0750"
+ no_log: true
+
+ # Script is idempotent via internal state checks but always reports changed
+ - name: "Run provision script"
+ ansible.builtin.command:
+ cmd: "/etc/garage/provision.sh"
+ changed_when: true
+ tags:
+ - molecule-idempotence-notest
diff --git a/roles/garage/templates/garage-provision.sh.j2 b/roles/garage/templates/garage-provision.sh.j2
new file mode 100644
index 00000000..0402c926
--- /dev/null
+++ b/roles/garage/templates/garage-provision.sh.j2
@@ -0,0 +1,20 @@
+#!/bin/bash
+set -e
+
+GARAGE="garage -c /etc/garage/garage.toml"
+
+if ! $GARAGE layout show 2>/dev/null | grep -q "Current cluster layout"; then
+ NODE_ID=$($GARAGE node id -q | cut -d@ -f1)
+ $GARAGE layout assign --zone dc1 --capacity {{ garage_capacity }} "$NODE_ID"
+ $GARAGE layout apply --version 1
+fi
+
+if ! $GARAGE bucket list | grep -q "{{ garage_bucket }}"; then
+ $GARAGE bucket create {{ garage_bucket }}
+fi
+
+if ! $GARAGE key list | grep -q "restic"; then
+ $GARAGE key import --yes -n restic {{ garage_access_key_id }} {{ garage_secret_access_key }}
+fi
+
+$GARAGE bucket allow --read --write --owner --key {{ garage_access_key_id }} {{ garage_bucket }}
diff --git a/roles/garage/templates/garage.service.j2 b/roles/garage/templates/garage.service.j2
new file mode 100644
index 00000000..994f6ac9
--- /dev/null
+++ b/roles/garage/templates/garage.service.j2
@@ -0,0 +1,15 @@
+[Unit]
+Description=Garage S3-compatible object store
+After=network.target
+
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/garage -c /etc/garage/garage.toml server
+User=garage
+Group=garage
+Restart=on-failure
+StandardOutput=journal
+StandardError=journal
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/garage/templates/garage.toml.j2 b/roles/garage/templates/garage.toml.j2
new file mode 100644
index 00000000..ab6d51a3
--- /dev/null
+++ b/roles/garage/templates/garage.toml.j2
@@ -0,0 +1,15 @@
+metadata_dir = "{{ garage_meta_dir }}"
+data_dir = "{{ garage_data_dir }}"
+db_engine = "sqlite"
+
+replication_factor = 1
+
+rpc_bind_addr = "{{ garage_rpc_bind_addr }}"
+rpc_secret = "{{ garage_rpc_secret }}"
+
+[s3_api]
+s3_region = "garage"
+api_bind_addr = "0.0.0.0:{{ garage_s3_port }}"
+
+[admin]
+api_bind_addr = "127.0.0.1:{{ garage_admin_port }}"