From ded892e34378c3675717384decebbf87e3f70039 Mon Sep 17 00:00:00 2001 From: Ahmed Abdelhalim Date: Mon, 22 Jun 2026 16:55:49 +0200 Subject: Add garage to rpi to use as s3-compatible backend for backups --- roles/garage/defaults/main.yml | 12 +++ roles/garage/handlers/main.yml | 6 ++ roles/garage/meta/argument_specs.yml | 48 ++++++++++++ roles/garage/meta/main.yml | 17 +++++ roles/garage/tasks/main.yml | 106 ++++++++++++++++++++++++++ roles/garage/templates/garage-provision.sh.j2 | 20 +++++ roles/garage/templates/garage.service.j2 | 15 ++++ roles/garage/templates/garage.toml.j2 | 15 ++++ 8 files changed, 239 insertions(+) create mode 100644 roles/garage/defaults/main.yml create mode 100644 roles/garage/handlers/main.yml create mode 100644 roles/garage/meta/argument_specs.yml create mode 100644 roles/garage/meta/main.yml create mode 100644 roles/garage/tasks/main.yml create mode 100644 roles/garage/templates/garage-provision.sh.j2 create mode 100644 roles/garage/templates/garage.service.j2 create mode 100644 roles/garage/templates/garage.toml.j2 (limited to 'roles') 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 }}" -- cgit v1.2.3