332 lines
15 KiB
YAML
332 lines
15 KiB
YAML
---
|
|
- name: Native PostgreSQL delivery
|
|
hosts: "{{ target_hosts }}"
|
|
become: true
|
|
gather_facts: true
|
|
any_errors_fatal: true
|
|
vars:
|
|
postgresql_version_value: "{{ postgresql_version | default('16') }}"
|
|
postgresql_instance_plan: "{{ postgresql_instances[inventory_hostname] | default({}) }}"
|
|
postgresql_instance_id: "{{ postgresql_instance_plan.instance_id | default(instance_name) }}"
|
|
postgresql_role: "{{ postgresql_instance_plan.role | default('standalone') }}"
|
|
postgresql_data_root: "{{ data_root | default('/data/postgresql') }}"
|
|
postgresql_data_dir: "{{ postgresql_instance_plan.data_dir | default(postgresql_data_root ~ '/' ~ postgresql_instance_id ~ '/data') }}"
|
|
postgresql_conf_dir: "{{ postgresql_instance_plan.config_dir | default(postgresql_data_root ~ '/' ~ postgresql_instance_id ~ '/conf') }}"
|
|
postgresql_log_dir: "{{ postgresql_instance_plan.log_dir | default(postgresql_data_root ~ '/' ~ postgresql_instance_id ~ '/log') }}"
|
|
postgresql_run_dir: "/run/postgresql-xinfra/{{ postgresql_instance_id }}"
|
|
postgresql_config_file: "{{ postgresql_conf_dir }}/postgresql.conf"
|
|
postgresql_hba_file: "{{ postgresql_conf_dir }}/pg_hba.conf"
|
|
postgresql_port_value: "{{ postgresql_instance_plan.port | default(postgresql_port | default(15432)) | int }}"
|
|
postgresql_max_connections: "{{ max_connections | default(200, true) | int }}"
|
|
postgresql_shared_buffers: "{{ ((memory_mb | default(4096) | int) * 25 / 100) | int }}MB"
|
|
postgresql_effective_cache_size: "{{ ((memory_mb | default(4096) | int) * 70 / 100) | int }}MB"
|
|
postgresql_max_wal_senders: "{{ (replica_count | default(0) | int) + 4 }}"
|
|
postgresql_max_replication_slots: "{{ (replica_count | default(0) | int) + 2 }}"
|
|
postgresql_replication_user: "{{ lookup('ansible.builtin.env', 'XINFRA_POSTGRES_REPLICATION_USER') | default('xinfra_replication', true) }}"
|
|
postgresql_replication_password: "{{ lookup('ansible.builtin.env', 'XINFRA_POSTGRES_REPLICATION_PASSWORD') }}"
|
|
postgresql_admin_password: "{{ lookup('ansible.builtin.env', 'XINFRA_POSTGRES_ADMIN_PASSWORD') }}"
|
|
postgresql_primary_host: "{{ hostvars[ansible_play_hosts_all[0]].ansible_host | default(ansible_play_hosts_all[0]) }}"
|
|
postgresql_primary_port: "{{ (postgresql_instances[ansible_play_hosts_all[0]].port | default(15432)) | int }}"
|
|
postgresql_replication_slot: "{{ postgresql_instance_plan.replication_slot | default('xinfra_' ~ postgresql_instance_id | replace('-', '_')) }}"
|
|
|
|
pre_tasks:
|
|
- name: Validate PostgreSQL delivery parameters
|
|
ansible.builtin.assert:
|
|
that:
|
|
- postgresql_version_value in ['15', '16']
|
|
- topology | default('standalone') in ['standalone', 'primary_replica']
|
|
- ansible_distribution == 'Ubuntu'
|
|
- ansible_distribution_version is version('24.04', '>=')
|
|
- ansible_architecture in ['x86_64', 'aarch64']
|
|
- postgresql_instance_id is match('^[a-z0-9][a-z0-9-]{0,62}$')
|
|
- (postgresql_port_value | int) >= 15432 and (postgresql_port_value | int) <= 15999
|
|
- (memory_mb | default(4096) | int) >= 2048
|
|
- (storage_gb | default(50) | int) >= 20
|
|
- (postgresql_max_connections | int) >= 1
|
|
- postgresql_admin_password | length >= 16
|
|
- (topology | default('standalone') == 'standalone') or (postgresql_replication_password | length >= 16)
|
|
fail_msg: "PostgreSQL parameters are outside the supported target-state whitelist"
|
|
no_log: true
|
|
|
|
- name: Check PostgreSQL port is free
|
|
ansible.builtin.shell:
|
|
cmd: |
|
|
if ss -lntH 'sport = :{{ postgresql_port_value }}' | grep -q .; then
|
|
systemctl is-active --quiet 'postgresql-xinfra@{{ postgresql_instance_id }}.service'
|
|
fi
|
|
register: postgresql_port_check
|
|
failed_when: postgresql_port_check.rc != 0
|
|
changed_when: false
|
|
|
|
- name: Check target memory and data-root capacity
|
|
ansible.builtin.assert:
|
|
that:
|
|
- (ansible_memtotal_mb | int) >= (memory_mb | default(4096) | int)
|
|
fail_msg: "Target host does not have enough memory or storage for the PostgreSQL instance"
|
|
|
|
- name: Ensure PostgreSQL data root exists
|
|
ansible.builtin.file:
|
|
path: "{{ postgresql_data_root }}"
|
|
state: directory
|
|
owner: postgres
|
|
group: postgres
|
|
mode: '0750'
|
|
|
|
- name: Check PostgreSQL data filesystem capacity
|
|
ansible.builtin.shell:
|
|
cmd: "df -P -B1 {{ postgresql_data_root | quote }} | awk 'NR==2 {print $4}'"
|
|
register: postgresql_root_available_bytes
|
|
changed_when: false
|
|
failed_when: >-
|
|
postgresql_root_available_bytes.rc != 0 or
|
|
(postgresql_root_available_bytes.stdout | trim | int) <
|
|
(storage_gb | default(50) | int) * 1073741824
|
|
|
|
- name: Install PostgreSQL packages
|
|
ansible.builtin.apt:
|
|
name:
|
|
- "postgresql-{{ postgresql_version_value }}"
|
|
- postgresql-client
|
|
state: present
|
|
update_cache: true
|
|
cache_valid_time: 3600
|
|
|
|
- name: Stop distribution-managed cluster
|
|
ansible.builtin.systemd_service:
|
|
name: "postgresql@{{ postgresql_version_value }}-main.service"
|
|
state: stopped
|
|
enabled: false
|
|
failed_when: false
|
|
|
|
- name: Create isolated PostgreSQL directories
|
|
ansible.builtin.file:
|
|
path: "{{ item }}"
|
|
state: directory
|
|
owner: postgres
|
|
group: postgres
|
|
mode: '0700'
|
|
loop:
|
|
- "{{ postgresql_data_root }}/{{ postgresql_instance_id }}"
|
|
- "{{ postgresql_data_dir }}"
|
|
- "{{ postgresql_conf_dir }}"
|
|
- "{{ postgresql_log_dir }}"
|
|
|
|
- name: Initialize primary or standalone data directory
|
|
ansible.builtin.command:
|
|
cmd: "/usr/lib/postgresql/{{ postgresql_version_value }}/bin/initdb -D {{ postgresql_data_dir }}"
|
|
creates: "{{ postgresql_data_dir }}/PG_VERSION"
|
|
become_user: postgres
|
|
when: postgresql_role != 'replica'
|
|
|
|
- name: Render PostgreSQL configuration
|
|
ansible.builtin.template:
|
|
src: templates/postgresql-instance.conf.j2
|
|
dest: "{{ postgresql_config_file }}"
|
|
owner: postgres
|
|
group: postgres
|
|
mode: '0600'
|
|
no_log: "{{ postgresql_role == 'replica' }}"
|
|
|
|
- name: Render pg_hba.conf
|
|
ansible.builtin.copy:
|
|
dest: "{{ postgresql_hba_file }}"
|
|
owner: postgres
|
|
group: postgres
|
|
mode: '0600'
|
|
content: |
|
|
local all postgres peer
|
|
local all all peer
|
|
host all all 127.0.0.1/32 scram-sha-256
|
|
{% for host in ansible_play_hosts_all %}
|
|
{% set host_address = hostvars[host].ansible_host | default(host) %}
|
|
host all all {{ host_address }}{% if host_address is match('^[0-9.]+$') %}/32{% endif %} scram-sha-256
|
|
host replication {{ postgresql_replication_user }} {{ host_address }}{% if host_address is match('^[0-9.]+$') %}/32{% endif %} scram-sha-256
|
|
{% endfor %}
|
|
|
|
- name: Validate primary or standalone PostgreSQL configuration syntax
|
|
ansible.builtin.command:
|
|
argv:
|
|
- "/usr/lib/postgresql/{{ postgresql_version_value }}/bin/postgres"
|
|
- "-D"
|
|
- "{{ postgresql_data_dir }}"
|
|
- "-C"
|
|
- "port"
|
|
- "-c"
|
|
- "config_file={{ postgresql_config_file }}"
|
|
- "-c"
|
|
- "hba_file={{ postgresql_hba_file }}"
|
|
become_user: postgres
|
|
changed_when: false
|
|
when: postgresql_role != 'replica'
|
|
|
|
- name: Install PostgreSQL systemd template
|
|
ansible.builtin.copy:
|
|
src: files/postgresql-xinfra@.service
|
|
dest: /etc/systemd/system/postgresql-xinfra@.service
|
|
owner: root
|
|
group: root
|
|
mode: '0644'
|
|
register: postgresql_unit
|
|
|
|
- name: Configure runtime unit environment
|
|
ansible.builtin.file:
|
|
dest: "/etc/systemd/system/postgresql-xinfra@{{ postgresql_instance_id }}.service.d"
|
|
state: directory
|
|
mode: '0755'
|
|
|
|
- name: Write runtime unit environment
|
|
ansible.builtin.copy:
|
|
dest: "/etc/systemd/system/postgresql-xinfra@{{ postgresql_instance_id }}.service.d/version.conf"
|
|
mode: '0644'
|
|
content: |
|
|
[Service]
|
|
Environment=POSTGRESQL_VERSION={{ postgresql_version_value }}
|
|
Environment=POSTGRESQL_DATA_DIR={{ postgresql_data_dir }}
|
|
Environment=POSTGRESQL_CONFIG_FILE={{ postgresql_config_file }}
|
|
Environment=POSTGRESQL_HBA_FILE={{ postgresql_hba_file }}
|
|
|
|
- name: Reload systemd
|
|
ansible.builtin.systemd_service:
|
|
daemon_reload: true
|
|
|
|
- name: Start PostgreSQL delivery instance
|
|
ansible.builtin.systemd_service:
|
|
name: "postgresql-xinfra@{{ postgresql_instance_id }}.service"
|
|
state: started
|
|
enabled: true
|
|
when: postgresql_role != 'replica'
|
|
|
|
- name: Wait for primary or standalone PostgreSQL TCP port
|
|
ansible.builtin.wait_for:
|
|
host: "{{ ansible_host | default(inventory_hostname) }}"
|
|
port: "{{ postgresql_port_value }}"
|
|
timeout: 60
|
|
when: postgresql_role != 'replica'
|
|
|
|
- name: Configure PostgreSQL administrator and replication roles
|
|
ansible.builtin.shell:
|
|
cmd: |
|
|
/usr/bin/psql -h {{ postgresql_run_dir }} -p {{ postgresql_port_value }} -U postgres -d postgres -v ON_ERROR_STOP=1 <<'SQL'
|
|
ALTER ROLE postgres PASSWORD '{{ postgresql_admin_password | replace("'", "''") }}';
|
|
{% if topology | default('standalone') == 'primary_replica' %}
|
|
DO $do$BEGIN
|
|
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{{ postgresql_replication_user }}') THEN
|
|
CREATE ROLE {{ postgresql_replication_user }} WITH REPLICATION LOGIN PASSWORD '{{ postgresql_replication_password | replace("'", "''") }}';
|
|
ELSE
|
|
ALTER ROLE {{ postgresql_replication_user }} WITH REPLICATION LOGIN PASSWORD '{{ postgresql_replication_password | replace("'", "''") }}';
|
|
END IF;
|
|
END$do$;
|
|
{% endif %}
|
|
SQL
|
|
become_user: postgres
|
|
no_log: true
|
|
when: postgresql_role in ['primary', 'standalone']
|
|
|
|
- name: Create physical replication slots on primary
|
|
ansible.builtin.shell:
|
|
cmd: |
|
|
/usr/bin/psql -h {{ postgresql_run_dir }} -p {{ postgresql_port_value }} -U postgres -d postgres -v ON_ERROR_STOP=1 <<'SQL'
|
|
{% for replica_index in range(1, ansible_play_hosts_all | length) %}
|
|
SELECT pg_create_physical_replication_slot('xinfra_{{ cluster_name | replace("-", "_") }}_replica_{{ replica_index }}')
|
|
WHERE NOT EXISTS (SELECT FROM pg_replication_slots WHERE slot_name = 'xinfra_{{ cluster_name | replace("-", "_") }}_replica_{{ replica_index }}');
|
|
{% endfor %}
|
|
SQL
|
|
become_user: postgres
|
|
changed_when: false
|
|
when: postgresql_role == 'primary' and topology | default('standalone') == 'primary_replica'
|
|
|
|
- name: Initialize replica from primary
|
|
ansible.builtin.shell:
|
|
cmd: >-
|
|
PGPASSWORD={{ postgresql_replication_password | quote }}
|
|
/usr/lib/postgresql/{{ postgresql_version_value }}/bin/pg_basebackup
|
|
-h {{ postgresql_primary_host }} -p {{ postgresql_primary_port }}
|
|
-U {{ postgresql_replication_user }} -D {{ postgresql_data_dir }}
|
|
-Fp -Xs -P -R -S {{ postgresql_replication_slot }}
|
|
args:
|
|
creates: "{{ postgresql_data_dir }}/PG_VERSION"
|
|
become_user: postgres
|
|
no_log: true
|
|
when: postgresql_role == 'replica'
|
|
|
|
- name: Validate replica PostgreSQL configuration syntax
|
|
ansible.builtin.command:
|
|
argv:
|
|
- "/usr/lib/postgresql/{{ postgresql_version_value }}/bin/postgres"
|
|
- "-D"
|
|
- "{{ postgresql_data_dir }}"
|
|
- "-C"
|
|
- "port"
|
|
- "-c"
|
|
- "config_file={{ postgresql_config_file }}"
|
|
- "-c"
|
|
- "hba_file={{ postgresql_hba_file }}"
|
|
become_user: postgres
|
|
changed_when: false
|
|
no_log: true
|
|
when: postgresql_role == 'replica'
|
|
|
|
- name: Start PostgreSQL replica instance
|
|
ansible.builtin.systemd_service:
|
|
name: "postgresql-xinfra@{{ postgresql_instance_id }}.service"
|
|
state: started
|
|
enabled: true
|
|
when: postgresql_role == 'replica'
|
|
|
|
- name: Wait for PostgreSQL replica TCP port
|
|
ansible.builtin.wait_for:
|
|
host: "{{ ansible_host | default(inventory_hostname) }}"
|
|
port: "{{ postgresql_port_value }}"
|
|
timeout: 60
|
|
when: postgresql_role == 'replica'
|
|
|
|
- name: Verify PostgreSQL readiness
|
|
ansible.builtin.command:
|
|
cmd: "/usr/lib/postgresql/{{ postgresql_version_value }}/bin/pg_isready -h 127.0.0.1 -p {{ postgresql_port_value }}"
|
|
changed_when: false
|
|
|
|
- name: Verify primary or standalone role
|
|
ansible.builtin.shell:
|
|
cmd: >-
|
|
test "$(/usr/bin/psql -h {{ postgresql_run_dir }} -p {{ postgresql_port_value }} -U postgres -d postgres -Atc 'SELECT pg_is_in_recovery()')" = "f"
|
|
become_user: postgres
|
|
changed_when: false
|
|
when: postgresql_role in ['primary', 'standalone']
|
|
|
|
- name: Verify replica recovery role
|
|
ansible.builtin.shell:
|
|
cmd: >-
|
|
test "$(/usr/bin/psql -h {{ postgresql_run_dir }} -p {{ postgresql_port_value }} -U postgres -d postgres -Atc 'SELECT pg_is_in_recovery()')" = "t"
|
|
become_user: postgres
|
|
changed_when: false
|
|
when: postgresql_role == 'replica'
|
|
|
|
- name: Verify primary streaming replicas and replication slots
|
|
ansible.builtin.shell:
|
|
cmd: |
|
|
streaming=$(/usr/bin/psql -h {{ postgresql_run_dir }} -p {{ postgresql_port_value }} -U postgres -d postgres -Atc "SELECT count(*) FROM pg_stat_replication WHERE state = 'streaming'")
|
|
slots=$(/usr/bin/psql -h {{ postgresql_run_dir }} -p {{ postgresql_port_value }} -U postgres -d postgres -Atc "SELECT count(*) FROM pg_replication_slots WHERE slot_type = 'physical'")
|
|
test "$streaming" -ge "{{ replica_count | default(0) | int }}"
|
|
test "$slots" -ge "{{ replica_count | default(0) | int }}"
|
|
become_user: postgres
|
|
register: postgresql_replication_health
|
|
until: postgresql_replication_health.rc == 0
|
|
retries: 30
|
|
delay: 2
|
|
changed_when: false
|
|
when: postgresql_role == 'primary' and topology | default('standalone') == 'primary_replica'
|
|
|
|
- name: Verify delivered version, port and data directory
|
|
ansible.builtin.shell:
|
|
cmd: |
|
|
set -euo pipefail
|
|
actual_version=$(/usr/bin/psql -h {{ postgresql_run_dir }} -p {{ postgresql_port_value }} -U postgres -d postgres -Atc "SHOW server_version")
|
|
actual_port=$(/usr/bin/psql -h {{ postgresql_run_dir }} -p {{ postgresql_port_value }} -U postgres -d postgres -Atc "SHOW port")
|
|
actual_data=$(/usr/bin/psql -h {{ postgresql_run_dir }} -p {{ postgresql_port_value }} -U postgres -d postgres -Atc "SHOW data_directory")
|
|
test "${actual_version%%.*}" = "{{ postgresql_version_value }}"
|
|
test "$actual_port" = "{{ postgresql_port_value }}"
|
|
test "$actual_data" = "{{ postgresql_data_dir }}"
|
|
executable: /bin/bash
|
|
become_user: postgres
|
|
changed_when: false
|