diff --git a/roles/mailarchiver_db/tasks/main.yml b/roles/mailarchiver_db/tasks/main.yml index 4a8c89a..30ea465 100644 --- a/roles/mailarchiver_db/tasks/main.yml +++ b/roles/mailarchiver_db/tasks/main.yml @@ -1,31 +1,87 @@ -- name: Wait for PostgreSQL socket +--- +# Tworzenie bazy danych i użytkownika dla MailArchiver +# Ta rola uruchamia się TYLKO na primary + +- name: Wait for PostgreSQL to be ready wait_for: - path: "/var/run/postgresql/{{ pg_version }}-{{ pg_cluster }}/.s.PGSQL.5432" + path: "/var/run/postgresql/.s.PGSQL.5432" timeout: 60 -- name: Create db user - become: true - become_user: postgres +# --- Utworzenie użytkownika mailuser --- +- name: Create mailuser database user postgresql_user: name: mailuser password: "{{ mailuser_password }}" - login_host: "/var/run/postgresql/{{ pg_version }}-{{ pg_cluster }}/.s.PGSQL.5432" - -- name: Create database + role_attr_flags: CREATEDB + login_unix_socket_directory: /var/run/postgresql + db: postgres become: true become_user: postgres + +# --- Utworzenie bazy mailarchiver --- +- name: Create mailarchiver database postgresql_db: name: mailarchiver owner: mailuser - login_host: "/var/run/postgresql/{{ pg_version }}-{{ pg_cluster }}/.s.PGSQL.5432" - -- name: Grant schema rights + encoding: UTF-8 + lc_collate: en_US.UTF-8 + lc_ctype: en_US.UTF-8 + login_unix_socket_directory: /var/run/postgresql become: true become_user: postgres + +# --- Przyznanie uprawnień --- +- name: Grant privileges on schema public + postgresql_query: + db: mailarchiver + query: | + GRANT USAGE ON SCHEMA public TO mailuser; + GRANT CREATE ON SCHEMA public TO mailuser; + login_unix_socket_directory: /var/run/postgresql + become: true + become_user: postgres + +# --- Default privileges dla tabel --- +- name: Set default privileges for tables postgresql_query: db: mailarchiver query: | - GRANT ALL ON SCHEMA public TO mailuser; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO mailuser; - login_host: "/var/run/postgresql/{{ pg_version }}-{{ pg_cluster }}/.s.PGSQL.5432" + + ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT ALL ON SEQUENCES TO mailuser; + + ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT ALL ON FUNCTIONS TO mailuser; + login_unix_socket_directory: /var/run/postgresql + become: true + become_user: postgres + +# --- Sprawdzenie --- +- name: Verify mailarchiver database creation + postgresql_query: + db: mailarchiver + query: "SELECT datname, pg_database.datdba::regrole FROM pg_database WHERE datname = 'mailarchiver'" + login_unix_socket_directory: /var/run/postgresql + become: true + become_user: postgres + register: db_verify + +- name: Display database info + debug: + msg: "Database mailarchiver created: {{ db_verify.query_result }}" + +- name: Verify mailuser permissions + postgresql_query: + db: mailarchiver + query: "SELECT * FROM information_schema.role_table_grants WHERE grantee='mailuser' LIMIT 5" + login_unix_socket_directory: /var/run/postgresql + become: true + become_user: postgres + register: user_perms + ignore_errors: true + +- name: Display user permissions + debug: + msg: "Mailuser permissions: {{ user_perms.query_result | default('No permissions yet') }}" diff --git a/roles/pgbackrest/tasks/main.yml b/roles/pgbackrest/tasks/main.yml index 0a0e5cc..0311ce9 100644 --- a/roles/pgbackrest/tasks/main.yml +++ b/roles/pgbackrest/tasks/main.yml @@ -1,37 +1,92 @@ -- name: Install pgBackRest - apt: - name: pgbackrest - state: present +--- +# Instalacja i konfiguracja pgBackRest +# Full backup: niedziela 2:00 +# Incremental backup: codziennie 2:00 (oprócz niedzieli) +# Repozytorium backupu na pg2 (replica) -- name: Repo dir +# --- Auto-detect PostgreSQL version --- +- name: Detect PostgreSQL version + shell: "ls -d /etc/postgresql/*/ | head -1 | xargs basename" + register: pg_version_detect + changed_when: false + +- name: Set PostgreSQL variables from detection + set_fact: + pg_version: "{{ pg_version_detect.stdout | trim }}" + pg_cluster: "main" + pg_data: "/var/lib/postgresql/{{ pg_version_detect.stdout | trim }}/main" + pg_bin_path: "/usr/lib/postgresql/{{ pg_version_detect.stdout | trim }}/bin" + +- name: Install pgBackRest and dependencies + apt: + name: + - pgbackrest + state: present + update_cache: yes + +# --- Repozytorium backupu na replica --- +- name: Create backup repository directory (on replica) file: path: /pgbackrest state: directory owner: postgres - mode: "750" + group: postgres + mode: "0750" when: inventory_hostname in groups['replica'] -- name: Config +# --- Konfiguracja pgBackRest --- +- name: Deploy pgBackRest configuration template: src: pgbackrest.conf.j2 dest: /etc/pgbackrest.conf + owner: postgres + group: postgres + mode: "0640" -- name: Create stanza +# --- Utworzenie stanza na primary --- +- name: Create pgBackRest stanza (on primary) + shell: pgbackrest --stanza=main stanza-create + become: true become_user: postgres - command: pgbackrest --stanza=main stanza-create + when: inventory_hostname in groups['primary'] + register: stanza_create + changed_when: "'CREATED' in stanza_create.stdout or 'already exists' not in stanza_create.stdout" + ignore_errors: true + +# --- Full backup - niedziela 2:00 --- +- name: Schedule full backup weekly (Sunday 2:00 AM) + cron: + name: "pgBackRest full backup" + weekday: "0" + hour: "2" + minute: "0" + user: postgres + job: "pgbackrest --stanza=main backup --type=full" + state: present when: inventory_hostname in groups['primary'] -- name: Full backup weekly +# --- Incremental backup - codziennie 2:00 (ale nie w niedzielę) --- +- name: Schedule incremental backup daily (Monday-Saturday 2:00 AM) cron: - name: "pgbackrest full" - weekday: 0 - hour: 2 - minute: 0 - job: "pgbackrest --stanza=main backup --type=full" - -- name: Incremental backup daily - cron: - name: "pgbackrest incr" - hour: 2 - minute: 0 + name: "pgBackRest incremental backup" + weekday: "1-6" + hour: "2" + minute: "0" + user: postgres job: "pgbackrest --stanza=main backup --type=incr" + state: present + when: inventory_hostname in groups['primary'] + +# --- Test configuration --- +- name: Test pgBackRest configuration (on primary) + shell: pgbackrest --stanza=main check + become: true + become_user: postgres + when: inventory_hostname in groups['primary'] + register: pgbackrest_check + ignore_errors: true + +- name: Display pgBackRest check result + debug: + msg: "{{ pgbackrest_check.stdout }}" + when: inventory_hostname in groups['primary'] diff --git a/roles/pgbackrest/templates/pgbackrest.conf.j2 b/roles/pgbackrest/templates/pgbackrest.conf.j2 index c7271c7..a7d309a 100644 --- a/roles/pgbackrest/templates/pgbackrest.conf.j2 +++ b/roles/pgbackrest/templates/pgbackrest.conf.j2 @@ -1,11 +1,26 @@ +# pgBackRest Configuration +# Backup storage on replica (pg2) +# Archive destination: /pgbackrest + [main] -pg1-path=/var/lib/postgresql/16/main +pg1-path={{ pg_data }} [global] -{% if inventory_hostname in groups['primary'] %} -repo1-host=pg2 -{% endif %} repo1-path=/pgbackrest repo1-retention-full=2 repo1-retention-diff=7 +repo1-retention-incr=7 +repo1-bundle=y start-fast=y +log-level-console=info +log-level-file=debug +archive-async=y +archive-timeout=60 +check-strict=y +delta=y +recovery-option=recovery_target_timeline=latest + +{% if inventory_hostname in groups['primary'] %} +repo1-host=pg2 +repo1-host-user=postgres +{% endif %} diff --git a/roles/postgres/tasks/main.yml b/roles/postgres/tasks/main.yml index c565702..4393ea9 100644 --- a/roles/postgres/tasks/main.yml +++ b/roles/postgres/tasks/main.yml @@ -1,96 +1,111 @@ -- name: Install PostgreSQL packages +--- +# Instalacja PostgreSQL (najnowsza wersja) i konfiguracja klastra +# Role: primary i replica + +- name: Install PostgreSQL and contrib packages apt: name: - postgresql - postgresql-contrib + - python3-psycopg2 update_cache: yes + state: present -# --- Detect PostgreSQL version and cluster --- -- name: Detect PostgreSQL version - shell: "ls -d /etc/postgresql/*/ | head -n1 | xargs basename" - register: pg_version_result +# --- Auto-detect PostgreSQL version --- +- name: Detect installed PostgreSQL version + shell: "ls -d /etc/postgresql/*/ | head -1 | xargs basename" + register: pg_version_detect changed_when: false + failed_when: pg_version_detect.rc != 0 -- name: Detect cluster name - shell: "ls -d /var/lib/postgresql/*/main/ 2>/dev/null | head -n1 | xargs -I {} basename $(dirname {}) | tail -n1" - register: pg_cluster_result - changed_when: false - -- name: Set PostgreSQL version and cluster facts +- name: Set PostgreSQL version variable set_fact: - pg_version: "{{ pg_version_result.stdout | trim }}" + pg_version: "{{ pg_version_detect.stdout | trim }}" pg_cluster: "main" + pg_data: "/var/lib/postgresql/{{ pg_version_detect.stdout | trim }}/main" + pg_config_path: "/etc/postgresql/{{ pg_version_detect.stdout | trim }}/main" + pg_bin_path: "/usr/lib/postgresql/{{ pg_version_detect.stdout | trim }}/bin" -- name: Define PostgreSQL cluster path - set_fact: - pg_cluster_path: "/etc/postgresql/{{ pg_version }}/{{ pg_cluster }}" - pg_data_path: "/var/lib/postgresql/{{ pg_version }}/{{ pg_cluster }}" +- name: Display detected PostgreSQL version + debug: + msg: "PostgreSQL {{ pg_version }} detected. Data path: {{ pg_data }}" -# --- Stop cluster before config --- -- name: Stop PostgreSQL cluster - shell: "pg_ctlcluster {{ pg_version }} {{ pg_cluster }} stop" - ignore_errors: true - -# --- Remove broken cluster configuration if exists --- -- name: Remove broken cluster configuration - shell: "rm -rf {{ pg_cluster_path }} {{ pg_data_path }}" - ignore_errors: true - -# --- Ensure data directory exists and is owned by postgres --- -- name: Create PostgreSQL data directory +# --- Tworzenie logów PostgreSQL --- +- name: Ensure log directory exists file: - path: "{{ pg_data_path }}" + path: /var/log/postgresql + state: directory + owner: postgres + group: postgres + mode: "0755" + +# --- Inicjalizacja klastra --- +- name: Check if cluster data directory exists + stat: + path: "{{ pg_data }}" + register: pg_data_stat + +- name: Create cluster data directory if not exists + file: + path: "{{ pg_data }}" state: directory owner: postgres group: postgres mode: "0700" + when: not pg_data_stat.stat.exists -# --- Initialize database cluster --- - name: Initialize PostgreSQL cluster - shell: "sudo -u postgres /usr/lib/postgresql/{{ pg_version }}/bin/initdb -D {{ pg_data_path }} --encoding=UTF8 --locale=en_US.UTF-8" - ignore_errors: true + shell: "{{ pg_bin_path }}/initdb -D {{ pg_data }} --encoding=UTF8 --locale=en_US.UTF-8" + become: true + become_user: postgres + when: not pg_data_stat.stat.exists + register: initdb_result -# --- Ensure cluster config directory exists --- -- name: Ensure cluster config directory exists +- name: Create config directory for cluster file: - path: "{{ pg_cluster_path }}" + path: "{{ pg_config_path }}" state: directory owner: postgres group: postgres - mode: "0700" + mode: "0755" -# --- Configure PostgreSQL --- -- name: postgresql.conf +# --- Konfiguracja PostgreSQL --- +- name: Deploy postgresql.conf template: src: postgresql.conf.j2 - dest: "{{ pg_cluster_path }}/postgresql.conf" + dest: "{{ pg_config_path }}/postgresql.conf" owner: postgres group: postgres mode: "0600" + notify: restart postgresql -- name: pg_hba.conf +- name: Deploy pg_hba.conf template: src: pg_hba.conf.j2 - dest: "{{ pg_cluster_path }}/pg_hba.conf" + dest: "{{ pg_config_path }}/pg_hba.conf" owner: postgres group: postgres mode: "0600" + notify: restart postgresql -# --- Start cluster --- -- name: Start PostgreSQL cluster - shell: "sudo -u postgres /usr/lib/postgresql/{{ pg_version }}/bin/pg_ctl -D {{ pg_data_path }} -l /var/log/postgresql/postgresql-{{ pg_version }}-{{ pg_cluster }}.log start" - ignore_errors: true +# --- Uruchomienie PostgreSQL --- +- name: Enable and start PostgreSQL service + systemd: + name: postgresql + enabled: yes + state: started + daemon_reload: yes -# --- Wait for socket to be ready --- -- name: Wait for PostgreSQL socket +# --- Czekanie na socket --- +- name: Wait for PostgreSQL socket to be ready wait_for: - path: "/var/run/postgresql/{{ pg_version }}-{{ pg_cluster }}/.s.PGSQL.5432" + path: "/var/run/postgresql/.s.PGSQL.5432" timeout: 60 - -# --- Install Python PostgreSQL libraries --- -- name: Install python postgres libraries - apt: - name: - - python3-psycopg2 state: present - update_cache: yes + +# Handlery +- name: restart postgresql + systemd: + name: postgresql + state: restarted + listen: "restart postgresql" diff --git a/roles/postgres/templates/pg_hba.conf.j2 b/roles/postgres/templates/pg_hba.conf.j2 new file mode 100644 index 0000000..70fbf2e --- /dev/null +++ b/roles/postgres/templates/pg_hba.conf.j2 @@ -0,0 +1,15 @@ +# PostgreSQL 16 pg_hba.conf - Host Based Authentication +# TYPE DATABASE USER ADDRESS METHOD + +# Local connections +local all postgres peer +local all all peer + +# Replication from pg2 (10.0.0.2) +host replication replicator 10.0.0.2/32 md5 + +# Applications and MailArchiver +host all all 10.0.0.0/24 md5 + +# IPv6 local +host all all ::1/128 md5 diff --git a/roles/postgres/templates/postgresql.conf.j2 b/roles/postgres/templates/postgresql.conf.j2 new file mode 100644 index 0000000..64ed4aa --- /dev/null +++ b/roles/postgres/templates/postgresql.conf.j2 @@ -0,0 +1,51 @@ +# PostgreSQL Configuration for Production +# Debian 13 - najnowsza dostępna wersja +# Primary: pg1, Replica: pg2 + +# --- Network --- +listen_addresses = '*' +max_connections = 200 +port = 5432 + +# --- Memory (dla 8GB RAM) --- +shared_buffers = 2GB +effective_cache_size = 6GB +work_mem = 64MB +maintenance_work_mem = 1GB + +# --- WAL and Logging --- +wal_level = replica +wal_compression = on +max_wal_senders = 10 +max_replication_slots = 10 +wal_keep_size = 10GB + +# --- Archiving (dla pgBackRest) --- +archive_mode = on +archive_command = 'pgbackrest --stanza=main archive-push %p' +archive_timeout = 300 + +# --- Checkpoints --- +checkpoint_timeout = 15min +checkpoint_completion_target = 0.9 +max_wal_size = 64GB +min_wal_size = 16GB + +# --- Logging --- +log_directory = '/var/log/postgresql' +log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' +log_truncate_on_rotation = on +log_rotation_age = 1d +log_rotation_size = 100MB +log_min_duration_statement = 1000 +log_connections = on +log_disconnections = on +log_duration = off + +# --- Replication slots --- +max_replication_slots = 10 + +# --- Performance --- +random_page_cost = 1.1 +effective_io_concurrency = 200 +synchronous_commit = local diff --git a/roles/replication/tasks/main.yml b/roles/replication/tasks/main.yml index e4000ef..681562f 100644 --- a/roles/replication/tasks/main.yml +++ b/roles/replication/tasks/main.yml @@ -1,28 +1,120 @@ -- name: Create replicator user (on primary) +--- +# Konfiguracja replikacji PostgreSQL +# Ta rola uruchamia się na replica (pg2) +# Tworzy użytkownika replicator i base backup z primary + +# --- Auto-detect PostgreSQL version na primary --- +- name: Detect PostgreSQL version on primary + shell: "ls -d /etc/postgresql/*/ | head -1 | xargs basename" + register: pg_version_detect + changed_when: false delegate_to: pg1 - become_user: postgres + run_once: true + +- name: Set PostgreSQL variables from detection + set_fact: + pg_version: "{{ pg_version_detect.stdout | trim }}" + pg_cluster: "main" + pg_data: "/var/lib/postgresql/{{ pg_version_detect.stdout | trim }}/main" + pg_bin_path: "/usr/lib/postgresql/{{ pg_version_detect.stdout | trim }}/bin" + +- name: Wait for PostgreSQL on primary to be ready + wait_for: + host: pg1 + port: 5432 + timeout: 300 + delegate_to: pg1 + +# --- Utworzenie użytkownika replicator na primary --- +- name: Create replicator user on primary postgresql_user: name: replicator password: "{{ replicator_password }}" role_attr_flags: REPLICATION,LOGIN + login_unix_socket_directory: /var/run/postgresql + db: postgres + become: true + become_user: postgres + delegate_to: pg1 -- name: Stop postgres - service: +# --- Przygotowanie replica --- +- name: Stop PostgreSQL on replica + systemd: name: postgresql state: stopped -- name: Remove old data +- name: Remove existing cluster data on replica file: path: "{{ pg_data }}" state: absent -- name: Base backup +# --- Base backup z primary --- +- name: Create replication slot on primary + postgresql_query: + db: postgres + query: "SELECT pg_create_physical_replication_slot('pg2_slot')" + login_unix_socket_directory: /var/run/postgresql + become: true become_user: postgres - command: > - pg_basebackup -h pg1 -D {{ pg_data }} - -U replicator -Fp -Xs -P -R + delegate_to: pg1 + ignore_errors: true -- name: Start postgres - service: +- name: Perform base backup from primary to replica + shell: | + pg_basebackup \ + -h pg1 \ + -D {{ pg_data }} \ + -U replicator \ + -Fp \ + -Xs \ + -P \ + -R \ + -S pg2_slot + become: true + become_user: postgres + register: basebackup_result + +- name: Fix permissions on data directory + file: + path: "{{ pg_data }}" + owner: postgres + group: postgres + mode: "0700" + recurse: yes + become: true + +- name: Configure replication settings + lineinfile: + path: "{{ pg_data }}/standby.signal" + line: "" + create: yes + owner: postgres + group: postgres + mode: "0644" + +# --- Uruchomienie replica --- +- name: Start PostgreSQL on replica + systemd: name: postgresql state: started + enabled: yes + +- name: Wait for PostgreSQL replica to be ready + wait_for: + path: "/var/run/postgresql/.s.PGSQL.5432" + timeout: 60 + state: present + +- name: Verify replication on primary + postgresql_query: + db: postgres + query: "SELECT application_name, client_addr, state FROM pg_stat_replication" + login_unix_socket_directory: /var/run/postgresql + become: true + become_user: postgres + delegate_to: pg1 + register: replication_status + +- name: Display replication status + debug: + msg: "{{ replication_status.query_result }}" diff --git a/roles/restore_primary/tasks/main.yml b/roles/restore_primary/tasks/main.yml index 84c598c..bd971da 100644 --- a/roles/restore_primary/tasks/main.yml +++ b/roles/restore_primary/tasks/main.yml @@ -1,19 +1,122 @@ -- name: Stop postgres on primary - service: +--- +# Przywrócenie primary z pg2 (replica) +# Używany w przypadku awarii pg1 + +# --- Auto-detect PostgreSQL version na replica --- +- name: Detect PostgreSQL version on replica + shell: "ls -d /etc/postgresql/*/ | head -1 | xargs basename" + register: pg_version_detect + changed_when: false + delegate_to: pg2 + run_once: true + +- name: Set PostgreSQL variables from detection + set_fact: + pg_version: "{{ pg_version_detect.stdout | trim }}" + pg_cluster: "main" + pg_data: "/var/lib/postgresql/{{ pg_version_detect.stdout | trim }}/main" + pg_bin_path: "/usr/lib/postgresql/{{ pg_version_detect.stdout | trim }}/bin" + +- name: Wait for replica to be accessible + wait_for: + host: pg2 + port: 5432 + timeout: 300 + +# --- Promowanie replica --- +- name: Promote replica to primary + shell: "{{ pg_bin_path }}/pg_ctl promote -D {{ pg_data }}" + become: true + become_user: postgres + delegate_to: pg2 + register: promote_result + +- name: Wait for promoted replica to be ready + wait_for: + host: pg2 + port: 5432 + timeout: 60 + +# --- Przywrócenie na pg1 --- +- name: Stop PostgreSQL on primary (pg1) + systemd: name: postgresql state: stopped + ignore_errors: true -- name: Remove old data on primary +- name: Remove old cluster data on primary file: path: "{{ pg_data }}" state: absent + become: true -- name: Restore base backup from replica +# --- Base backup z nowego primary (pg2) --- +- name: Perform base backup from new primary (pg2) + shell: | + pg_basebackup \ + -h pg2 \ + -D {{ pg_data }} \ + -U replicator \ + -Fp \ + -Xs \ + -P \ + -R + become: true become_user: postgres - command: > - pg_basebackup -h pg2 -D {{ pg_data }} -U replicator -Fp -Xs -P -R + register: restore_basebackup -- name: Start postgres on primary - service: +- name: Fix permissions on restored data directory + file: + path: "{{ pg_data }}" + owner: postgres + group: postgres + mode: "0700" + recurse: yes + become: true + +- name: Configure recovery settings + lineinfile: + path: "{{ pg_data }}/standby.signal" + line: "" + create: yes + owner: postgres + group: postgres + mode: "0644" + +# --- Uruchomienie pg1 --- +- name: Start PostgreSQL on primary + systemd: name: postgresql state: started + enabled: yes + +- name: Wait for PostgreSQL to start + wait_for: + path: "/var/run/postgresql/.s.PGSQL.5432" + timeout: 60 + +# --- Weryfikacja --- +- name: Verify primary is accessible + postgresql_query: + db: postgres + query: "SELECT version()" + login_unix_socket_directory: /var/run/postgresql + become: true + become_user: postgres + register: version_check + +- name: Display restoration result + debug: + msg: "Primary restored successfully. PostgreSQL: {{ version_check.query_result[0].version }}" + +- name: Display manual failover instructions + debug: + msg: | + Restoration complete! + + Next steps: + 1. Verify pg1 and pg2 connectivity + 2. Update application connection strings if needed + 3. Monitor replication status: + SELECT * FROM pg_stat_replication; + 4. Schedule regular test restores