feat(delivery): integrate PostgreSQL delivery
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=XINFRA PostgreSQL delivery instance %i
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
User=postgres
|
||||
Group=postgres
|
||||
RuntimeDirectory=postgresql-xinfra/%i
|
||||
RuntimeDirectoryMode=0755
|
||||
ExecStart=/bin/sh -c 'exec /usr/lib/postgresql/${POSTGRESQL_VERSION}/bin/postgres -D "${POSTGRESQL_DATA_DIR}" -c "config_file=${POSTGRESQL_CONFIG_FILE}" -c "hba_file=${POSTGRESQL_HBA_FILE}"'
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
TimeoutStartSec=120s
|
||||
TimeoutStopSec=120s
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,331 @@
|
||||
---
|
||||
- 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
|
||||
@@ -0,0 +1,28 @@
|
||||
# Managed by XINFRA PostgreSQL delivery. instance={{ postgresql_instance_id }}
|
||||
listen_addresses = '*'
|
||||
port = {{ postgresql_port_value }}
|
||||
data_directory = '{{ postgresql_data_dir }}'
|
||||
hba_file = '{{ postgresql_hba_file }}'
|
||||
unix_socket_directories = '{{ postgresql_run_dir }}'
|
||||
external_pid_file = '{{ postgresql_run_dir }}/postmaster.pid'
|
||||
logging_collector = on
|
||||
log_directory = '{{ postgresql_log_dir }}'
|
||||
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
|
||||
log_min_messages = warning
|
||||
max_connections = {{ postgresql_max_connections }}
|
||||
shared_buffers = '{{ postgresql_shared_buffers }}'
|
||||
effective_cache_size = '{{ postgresql_effective_cache_size }}'
|
||||
work_mem = '4MB'
|
||||
maintenance_work_mem = '64MB'
|
||||
wal_level = replica
|
||||
max_wal_senders = {{ postgresql_max_wal_senders }}
|
||||
max_replication_slots = {{ postgresql_max_replication_slots }}
|
||||
wal_keep_size = '256MB'
|
||||
hot_standby = on
|
||||
synchronous_standby_names = ''
|
||||
{% if postgresql_role == 'primary' %}
|
||||
primary_conninfo = ''
|
||||
{% elif postgresql_role == 'replica' %}
|
||||
primary_conninfo = 'host={{ postgresql_primary_host }} port={{ postgresql_primary_port }} user={{ postgresql_replication_user }} password={{ postgresql_replication_password | replace("'", "''") }} application_name={{ postgresql_instance_id }}'
|
||||
primary_slot_name = '{{ postgresql_replication_slot }}'
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user