feat(delivery): integrate PostgreSQL delivery

This commit is contained in:
l x
2026-07-31 10:30:03 +08:00
parent 048ac6507e
commit 66fde73792
25 changed files with 2110 additions and 139 deletions
+21
View File
@@ -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
+331
View File
@@ -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 %}