Skip to content

feat: MariaDB Backup#1125

Merged
bancey merged 3 commits intomainfrom
feat/mariadb-backup
Mar 30, 2026
Merged

feat: MariaDB Backup#1125
bancey merged 3 commits intomainfrom
feat/mariadb-backup

Conversation

@bancey
Copy link
Copy Markdown
Owner

@bancey bancey commented Mar 30, 2026

No description provided.

@autonomous-bancey
Copy link
Copy Markdown
Contributor

Plan Result (prod_twingate)

No changes. Your infrastructure matches the configuration.

@autonomous-bancey
Copy link
Copy Markdown
Contributor

Plan Result (generate_ansible_inventory)

No changes. Your infrastructure matches the configuration.

@autonomous-bancey
Copy link
Copy Markdown
Contributor

Plan Result (test_vpn_gateway)

No changes. Your infrastructure matches the configuration.

@autonomous-bancey
Copy link
Copy Markdown
Contributor

Plan Result (test_gameserver)

No changes. Your infrastructure matches the configuration.

@autonomous-bancey
Copy link
Copy Markdown
Contributor

Plan Result (prod_vpn_gateway)

No changes. Your infrastructure matches the configuration.

@autonomous-bancey
Copy link
Copy Markdown
Contributor

Plan Result (prod_gameserver)

No changes. Your infrastructure matches the configuration.

@autonomous-bancey
Copy link
Copy Markdown
Contributor

Plan Result (prod_dns)

No changes. Your infrastructure matches the configuration.

@autonomous-bancey
Copy link
Copy Markdown
Contributor

Plan Result (tiny_virtual_machines)

⚠️ Resource Deletion will happen

This plan contains resource delete operation. Please check the plan result very carefully!

Plan: 1 to add, 0 to change, 1 to destroy.
  • Replace
    • terraform_data.ansible["mariadb"]
Change Result (Click me)
  # terraform_data.ansible["mariadb"] must be replaced
-/+ resource "terraform_data" "ansible" {
      ~ id               = "a265f822-46b0-a056-d73d-91b6b3bd6f50" -> (known after apply)
      ~ triggers_replace = [
            "30-03-2026-1315",
          ~ <<-EOT
                - name: Install and configure MariaDB Galera Cluster
                  hosts: mariadb0, mariadb1, mariadb2
                  gather_facts: true
                  vars:
                    galera_cluster_name: "galera_cluster"
                    galera_nodes: ["10.151.14.210", "10.151.14.211", "10.151.14.212"]
                    mariadb_bind_address: "0.0.0.0"
                    mariadb_datadir: "/var/lib/mysql"
                    enable_alloy: false
                    mariadb_databases:
                    - name: "bunkerweb"
                      user: "bunkerweb"
              -       password: "changeme"
              +       password_var: "mariadb_bunkerweb_password"
                    - name: "home-assistant"
                      user: "homeassistant"
              -       password: "changeme"
              +       password_var: "mariadb_homeassistant_password"
              +     backup_nfs_server: "storage.heimelska.co.uk"
              +     backup_nfs_path: "/var/nfs/shared/Tiny_K8S/mariadb-backups"
              +     backup_mount_point: "/mnt/mariadb-backups"
              +     backup_retention_days: 14
              +     backup_cron_hour: "2"
              +     backup_cron_minute: "0"
                    mariadb_version: "11.4"
                  roles:
                  - role: prep
                    install_pkgs: ["apt-transport-https", "ca-certificates", "curl", "gnupg", "software-properties-common", "python3-pip", "python3-setuptools"]
                    disable_multipath: false
                  tasks:
                  - set_fact:
                      mariadb_root_password: "{{ lookup('ansible.builtin.file', 'mariadb_root_password') }}"
                    when: mariadb_root_password is undefined
                  - set_fact:
                      mariadb_galera_password: "{{ lookup('ansible.builtin.file', 'mariadb_galera_password') }}"
                    when: mariadb_galera_password is undefined
                  - set_fact:
                      proxysql_monitor_password: "{{ lookup('ansible.builtin.file', 'proxysql_monitor_password') }}"
                    when: proxysql_monitor_password is undefined
                  - set_fact:
                      alloy_mysql_password: "{{ lookup('ansible.builtin.file', 'alloy_mysql_password') }}"
                    when: alloy_mysql_password is undefined and enable_alloy | bool
              +   - name: Load database passwords from secret files
              +     set_fact:
              +       mariadb_databases_resolved: "{{ mariadb_databases_resolved | default([]) + [item | combine({'password': lookup('ansible.builtin.file', item.password_var)})] }}"
              +     loop: "{{ mariadb_databases }}"
              +     no_log: true
                  - name: Check if MariaDB is already installed
                    ansible.builtin.shell:
                      cmd: dpkg-query -W -f='${Status}' mariadb-server 2>/dev/null
                    register: mariadb_pkg_check
                    failed_when: false
                    changed_when: false
                    when: inventory_hostname == 'mariadb0'
                  - name: Determine if Galera needs bootstrapping
                    set_fact:
                      bootstrap_galera: "{{ 'install ok installed' not in (mariadb_pkg_check.stdout | default('')) }}"
                    when: inventory_hostname == 'mariadb0'
                  - name: Propagate bootstrap decision to all nodes
                    set_fact:
                      bootstrap_galera: "{{ hostvars['mariadb0']['bootstrap_galera'] }}"
                    when: inventory_hostname != 'mariadb0'
                  - name: Download MariaDB apt keyring
                    ansible.builtin.get_url:
                      url: https://supplychain.mariadb.com/mariadb-keyring-2025.gpg
                      dest: /usr/share/keyrings/mariadb-keyring-2025.gpg
                      mode: '0644'
                  - name: Add MariaDB repository sources file
                    ansible.builtin.copy:
                      dest: /etc/apt/sources.list.d/mariadb.list
                      content: "deb [arch=amd64 signed-by=/usr/share/keyrings/mariadb-keyring-2025.gpg] https://dlm.mariadb.com/repo/mariadb-server/{{ mariadb_version }}/repo/ubuntu {{ ansible_facts['distribution_release'] }} main\n"
                      owner: root
                      group: root
                      mode: '0644'
                  - name: Install MariaDB and Galera packages
                    apt:
                      pkg:
                      - mariadb-server
                      - galera-4
                      - mariadb-backup
                      - rsync
                      - python3-pymysql
                      state: latest
                      update_cache: true
                    when: not ansible_check_mode
                  - name: Comment out default bind-address in 50-server.cnf
                    ansible.builtin.replace:
                      path: /etc/mysql/mariadb.conf.d/50-server.cnf
                      regexp: '^(bind-address\s*=.*)$'
                      replace: '# \1 # Managed by Ansible - overridden in 60-galera.cnf'
                  - name: Copy Galera configuration template
                    template:
                      src: templates/galera.cnf.j2
                      dest: /etc/mysql/mariadb.conf.d/60-galera.cnf
                      owner: root
                      group: root
                      mode: '0644'
                  - name: Stop MariaDB service on all nodes before bootstrapping
                    service:
                      name: mariadb
                      state: stopped
                    when: bootstrap_galera | bool
                  - name: Bootstrap Galera cluster on primary node (mariadb0)
                    command: galera_new_cluster
                    when: inventory_hostname == 'mariadb0' and bootstrap_galera | bool
                  - name: Set MariaDB root password
                    community.mysql.mysql_user:
                      name: root
                      password: "{{ mariadb_root_password }}"
                      login_unix_socket: /run/mysqld/mysqld.sock
                      state: present
                    when: inventory_hostname == 'mariadb0'
                  - name: Write root credentials file
                    ansible.builtin.copy:
                      dest: /root/.my.cnf
                      content: |
                        [client]
                        user=root
                        password={{ mariadb_root_password }}
                      owner: root
                      group: root
                      mode: '0600'
                    when: inventory_hostname == 'mariadb0'
                  - name: Create Galera SST user
                    community.mysql.mysql_user:
                      name: galera_sst
                      password: "{{ mariadb_galera_password }}"
                      priv: "*.*:RELOAD,LOCK TABLES,PROCESS,REPLICATION CLIENT"
                      host: "{{ item }}"
                      login_unix_socket: /run/mysqld/mysqld.sock
                      state: present
                    loop: "{{ galera_nodes + ['localhost'] }}"
                    when: inventory_hostname == 'mariadb0'
                  - name: Create ProxySQL monitor user
                    community.mysql.mysql_user:
                      name: proxysql_monitor
                      password: "{{ proxysql_monitor_password }}"
                      priv: "*.*:USAGE,REPLICATION CLIENT"
                      host: "%"
                      login_unix_socket: /run/mysqld/mysqld.sock
                      state: present
                    when: inventory_hostname == 'mariadb0'
                  - name: Start MariaDB on secondary nodes
                    service:
                      name: mariadb
                      state: started
                      enabled: true
                    when: inventory_hostname != 'mariadb0' and bootstrap_galera | bool
                  - name: Ensure MariaDB is started and enabled
                    service:
                      name: mariadb
                      state: started
                      enabled: true
                    when: not (bootstrap_galera | bool)
                  - name: Create application databases
                    community.mysql.mysql_db:
                      name: "{{ item.name }}"
                      state: present
                      login_unix_socket: /run/mysqld/mysqld.sock
              -     loop: "{{ mariadb_databases }}"
              +     loop: "{{ mariadb_databases_resolved }}"
                    when:
                    - inventory_hostname == 'mariadb0'
              -     - mariadb_databases | length > 0
              +     - mariadb_databases_resolved | length > 0
                  - name: Create application database users
                    community.mysql.mysql_user:
                      name: "{{ item.user }}"
                      password: "{{ item.password }}"
                      priv: "{{ item.name }}.*:{{ item.privs | default('ALL PRIVILEGES') }}"
                      host: "{{ item.host | default('%') }}"
                      login_unix_socket: /run/mysqld/mysqld.sock
                      state: present
              -     loop: "{{ mariadb_databases }}"
              +     loop: "{{ mariadb_databases_resolved }}"
              +     no_log: true
                    when:
                    - inventory_hostname == 'mariadb0'
              -     - mariadb_databases | length > 0
              +     - mariadb_databases_resolved | length > 0
                  - name: Create Alloy monitoring user
                    community.mysql.mysql_user:
                      name: alloy_monitor
                      password: "{{ alloy_mysql_password }}"
                      priv: "*.*:PROCESS,REPLICATION CLIENT,SLAVE MONITOR,SELECT"
                      host: "localhost"
                      login_unix_socket: /run/mysqld/mysqld.sock
                      state: present
                    when: inventory_hostname == 'mariadb0' and enable_alloy | bool
                  - name: Add Grafana Alloy apt key
                    ansible.builtin.get_url:
                      url: https://apt.grafana.com/gpg.key
                      dest: /etc/apt/keyrings/grafana.gpg
                      mode: '0644'
                    when: enable_alloy | bool
                  - name: Add Grafana apt repository
                    ansible.builtin.copy:
                      dest: /etc/apt/sources.list.d/grafana.list
                      content: "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main\n"
                      owner: root
                      group: root
                      mode: '0644'
                    when: enable_alloy | bool
                  - name: Install Grafana Alloy
                    apt:
                      pkg:
                      - alloy
                      state: latest
                      update_cache: true
                    when: not ansible_check_mode and enable_alloy | bool
                  - name: Copy Alloy configuration for MariaDB monitoring
                    template:
                      src: templates/alloy-mariadb.alloy.j2
                      dest: /etc/alloy/config.alloy
                      owner: alloy
                      group: alloy
                      mode: '0644'
                    vars:
                      alloy_mysql_user: "alloy_monitor"
                    notify:
                    - Restart Alloy
                    when: enable_alloy | bool
                  - name: Start and enable Alloy
                    service:
                      name: alloy
                      state: started
                      enabled: true
                    when: enable_alloy | bool
              +   - name: Mount NFS backup share
              +     ansible.posix.mount:
              +       path: "{{ backup_mount_point }}"
              +       src: "{{ backup_nfs_server }}:{{ backup_nfs_path }}"
              +       fstype: nfs
              +       opts: "nfsvers=4.1,soft,timeo=50"
              +       state: mounted
              +     when: inventory_hostname == 'mariadb0'
              +   - name: Create backup script
              +     ansible.builtin.copy:
              +       dest: /usr/local/bin/mariadb-backup.sh
              +       mode: '0750'
              +       owner: root
              +       group: root
              +       content: |
              +         #!/bin/bash
              +         set -euo pipefail
              +         BACKUP_DIR="{{ backup_mount_point }}"
              +         RETENTION_DAYS={{ backup_retention_days }}
              +         TIMESTAMP=$(date +%Y%m%d_%H%M%S)
              +         HOSTNAME=$(hostname)
              + 
              +         for DB in {{ mariadb_databases_resolved | map(attribute='name') | join(' ') }}; do
              +           DEST="${BACKUP_DIR}/${DB}"
              +           mkdir -p "${DEST}"
              +           mariadb-dump --single-transaction --routines --triggers \
              +             --databases "${DB}" | gzip > "${DEST}/${DB}_${HOSTNAME}_${TIMESTAMP}.sql.gz"
              +         done
              + 
              +         # Prune old backups
              +         find "${BACKUP_DIR}" -name '*.sql.gz' -mtime +${RETENTION_DAYS} -delete
              +     when: inventory_hostname == 'mariadb0'
              +   - name: Schedule nightly database backup via cron
              +     ansible.builtin.cron:
              +       name: "MariaDB nightly backup"
              +       hour: "{{ backup_cron_hour }}"
              +       minute: "{{ backup_cron_minute }}"
              +       job: "/usr/local/bin/mariadb-backup.sh >> /var/log/mariadb-backup.log 2>&1"
              +       user: root
              +     when: inventory_hostname == 'mariadb0'
                  handlers:
                  - name: Restart Alloy
                    service:
                      name: alloy
                      state: restarted
                
                - name: Install and configure ProxySQL
                  hosts: proxysql0, proxysql1, proxysql2
                  gather_facts: true
                  vars:
                    galera_nodes: ["10.151.14.210", "10.151.14.211", "10.151.14.212"]
                    mariadb_databases:
                    - name: "bunkerweb"
                      user: "bunkerweb"
              -       password: "changeme"
              +       password_var: "mariadb_bunkerweb_password"
                    - name: "home-assistant"
                      user: "homeassistant"
              -       password: "changeme"
              +       password_var: "mariadb_homeassistant_password"
                    enable_alloy: false
                    vip: "10.151.14.219/24"
                    keepalived_virtual_router_id: 152
                    keepalive:
                      proxysql0:
                        peers: |-
                          "10.151.14.217"
                          "10.151.14.218"
                        state: "MASTER"
                        priority: 150
                      proxysql1:
                        peers: |-
                          "10.151.14.216"
                          "10.151.14.218"
                        state: "BACKUP"
                        priority: 100
                      proxysql2:
                        peers: |-
                          "10.151.14.216"
                          "10.151.14.217"
                        state: "BACKUP"
                        priority: 100
                  roles:
                  - role: prep
                    install_pkgs: ["apt-transport-https", "ca-certificates", "curl", "gnupg", "software-properties-common", "python3-pymysql"]
                    disable_multipath: false
                  tasks:
                  - set_fact:
                      proxysql_admin_password: "{{ lookup('ansible.builtin.file', 'proxysql_admin_password') }}"
                    when: proxysql_admin_password is undefined
                  - set_fact:
                      proxysql_monitor_password: "{{ lookup('ansible.builtin.file', 'proxysql_monitor_password') }}"
                    when: proxysql_monitor_password is undefined
                  - set_fact:
                      keepalived_proxysql_pass: "{{ lookup('ansible.builtin.file', 'ProxySQL-Keepalived-Password') }}"
                    when: keepalived_proxysql_pass is undefined
              +   - name: Load database passwords from secret files
              +     set_fact:
              +       mariadb_databases_resolved: "{{ mariadb_databases_resolved | default([]) + [item | combine({'password': lookup('ansible.builtin.file', item.password_var)})] }}"
              +     loop: "{{ mariadb_databases }}"
              +     no_log: true
                  - name: ensures /etc/keepalived dir exists
                    file:
                      path: /etc/keepalived
                      state: directory
                  - name: Copy KeepAlived configuration template
                    template:
                      src: templates/keepalived-proxysql.conf.j2
                      dest: /etc/keepalived/keepalived.conf
                    vars:
                      keepalived_pass: "{{ keepalived_proxysql_pass }}"
                    notify:
                    - Restart Keepalived
                  - name: Update apt and install keepalived
                    apt:
                      pkg:
                      - keepalived
                      - libipset13
                      state: latest
                      update_cache: true
                    when: not ansible_check_mode
                  - name: Start and enable Keepalived
                    service:
                      name: keepalived
                      state: started
                      enabled: true
                  - name: Download ProxySQL apt key
                    ansible.builtin.get_url:
                      url: https://repo.proxysql.com/ProxySQL/proxysql-3.0.x/repo_pub_key.gpg
                      dest: /etc/apt/trusted.gpg.d/proxysql-3.0.x-keyring.gpg
                      mode: '0644'
                  - name: Add ProxySQL repository sources file
                    ansible.builtin.copy:
                      dest: /etc/apt/sources.list.d/proxysql.list
                      content: "deb https://repo.proxysql.com/ProxySQL/proxysql-3.0.x/{{ ansible_facts['distribution_release'] }}/ ./\n"
                      owner: root
                      group: root
                      mode: '0644'
                  - name: Install ProxySQL
                    apt:
                      pkg:
                      - proxysql
                      state: latest
                      update_cache: true
                    when: not ansible_check_mode
                  - name: Copy ProxySQL configuration template
                    template:
                      src: templates/proxysql.cnf.j2
                      dest: /etc/proxysql.cnf
                      owner: proxysql
                      group: proxysql
                      mode: '0600'
                    notify:
                    - Restart ProxySQL
                  - name: Start and enable ProxySQL
                    service:
                      name: proxysql
                      state: started
                      enabled: true
                  - name: Manage ProxySQL config via admin interface
                    module_defaults:
                      community.mysql.mysql_query:
                        login_host: 127.0.0.1
                        login_port: 6032
                        login_user: admin
                        login_password: "{{ proxysql_admin_password }}"
                    block:
                    - name: Clear ProxySQL mysql_users
                      community.mysql.mysql_query:
                        query: "DELETE FROM mysql_users"
                    - name: Add mysql_users to ProxySQL
                      community.mysql.mysql_query:
                        query: "INSERT INTO mysql_users (username, password, default_hostgroup, active) VALUES ('{{ item.user }}', '{{ item.password }}', 0, 1)"
              -       loop: "{{ mariadb_databases }}"
              +       loop: "{{ mariadb_databases_resolved }}"
                      no_log: true
                    - name: Load mysql_users to runtime
                      community.mysql.mysql_query:
                        query: "LOAD MYSQL USERS TO RUNTIME"
                    - name: Save mysql_users to disk
                      community.mysql.mysql_query:
                        query: "SAVE MYSQL USERS TO DISK"
                    - name: Clear ProxySQL mysql_servers
                      community.mysql.mysql_query:
                        query: "DELETE FROM mysql_servers"
                    - name: Add Galera nodes to ProxySQL writer hostgroup
                      community.mysql.mysql_query:
                        query: "INSERT INTO mysql_servers (hostgroup_id, hostname, port, max_connections) VALUES (0, '{{ item }}', 3306, 100)"
                      loop: "{{ galera_nodes }}"
                    - name: Add Galera nodes to ProxySQL reader hostgroup
                      community.mysql.mysql_query:
                        query: "INSERT INTO mysql_servers (hostgroup_id, hostname, port, max_connections) VALUES (1, '{{ item }}', 3306, 100)"
                      loop: "{{ galera_nodes }}"
                    - name: Load mysql_servers to runtime
                      community.mysql.mysql_query:
                        query: "LOAD MYSQL SERVERS TO RUNTIME"
                    - name: Save mysql_servers to disk
                      community.mysql.mysql_query:
                        query: "SAVE MYSQL SERVERS TO DISK"
                    - name: Clear ProxySQL mysql_query_rules
                      community.mysql.mysql_query:
                        query: "DELETE FROM mysql_query_rules"
                    - name: Add ProxySQL query rules
                      community.mysql.mysql_query:
                        query: "INSERT INTO mysql_query_rules (rule_id, active, match_digest, destination_hostgroup, apply) VALUES ({{ item.rule_id }}, 1, '{{ item.match_digest }}', {{ item.destination_hostgroup }}, 1)"
                      loop:
                      - {rule_id: 1, match_digest: "^SELECT .* FOR UPDATE", destination_hostgroup: 0}
                      - {rule_id: 2, match_digest: "^SELECT", destination_hostgroup: 1}
                      - {rule_id: 3, match_digest: ".*", destination_hostgroup: 0}
                    - name: Load mysql_query_rules to runtime
                      community.mysql.mysql_query:
                        query: "LOAD MYSQL QUERY RULES TO RUNTIME"
                    - name: Save mysql_query_rules to disk
                      community.mysql.mysql_query:
                        query: "SAVE MYSQL QUERY RULES TO DISK"
                  - name: Add Grafana Alloy apt key
                    ansible.builtin.get_url:
                      url: https://apt.grafana.com/gpg.key
                      dest: /etc/apt/keyrings/grafana.gpg
                      mode: '0644'
                    when: enable_alloy | bool
                  - name: Add Grafana apt repository
                    ansible.builtin.copy:
                      dest: /etc/apt/sources.list.d/grafana.list
                      content: "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main\n"
                      owner: root
                      group: root
                      mode: '0644'
                    when: enable_alloy | bool
                  - name: Install Grafana Alloy
                    apt:
                      pkg:
                      - alloy
                      state: latest
                      update_cache: true
                    when: not ansible_check_mode and enable_alloy | bool
                  - name: Copy Alloy configuration for ProxySQL monitoring
                    template:
                      src: templates/alloy-proxysql.alloy.j2
                      dest: /etc/alloy/config.alloy
                      owner: alloy
                      group: alloy
                      mode: '0644'
                    vars:
                      proxysql_stats_user: "radmin"
                      proxysql_stats_password: "{{ proxysql_admin_password }}"
                    notify:
                    - Restart Alloy
                    when: enable_alloy | bool
                  - name: Start and enable Alloy
                    service:
                      name: alloy
                      state: started
                      enabled: true
                    when: enable_alloy | bool
                  handlers:
                  - name: Restart Keepalived
                    service:
                      name: keepalived
                      state: restarted
                  - name: Restart ProxySQL
                    service:
                      name: proxysql
                      state: restarted
                  - name: Restart Alloy
                    service:
                      name: alloy
                      state: restarted
            EOT,
        ]
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Copy link
Copy Markdown

@mergify mergify bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 LGTM! beep boop

@bancey bancey changed the title Feat/mariadb backup feat: MariaDB Backup Mar 30, 2026
@bancey bancey merged commit 2d95673 into main Mar 30, 2026
18 checks passed
@bancey bancey deleted the feat/mariadb-backup branch March 30, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant