arvandor/ansible/playbooks/postgres-ha.yml
2026-01-26 00:44:31 -05:00

278 lines
7.4 KiB
YAML

---
# PostgreSQL High Availability with Patroni + etcd
# Run on postgres group hosts
#
# Usage:
# # Initialize first node (with existing data):
# ansible-playbook -i inventory.ini playbooks/postgres-ha.yml --limit postgres-01 -e "patroni_bootstrap=true"
#
# # Join additional nodes:
# ansible-playbook -i inventory.ini playbooks/postgres-ha.yml --limit postgres-02
#
# # All nodes at once (after bootstrap):
# ansible-playbook -i inventory.ini playbooks/postgres-ha.yml --limit postgres
- name: Configure PostgreSQL HA with Patroni + etcd
hosts: postgres
become: true
vars:
patroni_superuser_password: "{{ lookup('env', 'PATRONI_SUPERUSER_PASSWORD') | default('changeme', true) }}"
patroni_replicator_password: "{{ lookup('env', 'PATRONI_REPLICATOR_PASSWORD') | default('changeme', true) }}"
patroni_bootstrap: false
etcd_version: "3.5.17"
tasks:
# ============================================
# ETCD SETUP
# ============================================
- name: Check if etcd is installed
stat:
path: /usr/local/bin/etcd
register: etcd_binary
- name: Download etcd
get_url:
url: "https://github.com/etcd-io/etcd/releases/download/v{{ etcd_version }}/etcd-v{{ etcd_version }}-linux-amd64.tar.gz"
dest: /tmp/etcd.tar.gz
mode: '0644'
when: not etcd_binary.stat.exists
- name: Extract etcd
unarchive:
src: /tmp/etcd.tar.gz
dest: /tmp
remote_src: true
when: not etcd_binary.stat.exists
- name: Install etcd binaries
copy:
src: "/tmp/etcd-v{{ etcd_version }}-linux-amd64/{{ item }}"
dest: "/usr/local/bin/{{ item }}"
mode: '0755'
remote_src: true
loop:
- etcd
- etcdctl
- etcdutl
when: not etcd_binary.stat.exists
- name: Create symlinks for etcd binaries
file:
src: "/usr/local/bin/{{ item }}"
dest: "/usr/bin/{{ item }}"
state: link
loop:
- etcd
- etcdctl
- etcdutl
- name: Create etcd user
user:
name: etcd
system: true
shell: /sbin/nologin
home: /var/lib/etcd
create_home: true
- name: Create etcd config directory
file:
path: /etc/etcd
state: directory
mode: '0755'
- name: Create etcd data directory
file:
path: /var/lib/etcd
state: directory
owner: etcd
group: etcd
mode: '0700'
- name: Deploy etcd configuration
template:
src: ../templates/etcd.conf.j2
dest: /etc/etcd/etcd.conf
mode: '0644'
notify: restart etcd
- name: Deploy etcd systemd service
template:
src: ../templates/etcd.service.j2
dest: /etc/systemd/system/etcd.service
mode: '0644'
notify:
- reload systemd
- restart etcd
- name: Enable and start etcd
systemd:
name: etcd
state: started
enabled: true
daemon_reload: true
- name: Wait for etcd to be healthy
command: etcdctl endpoint health --endpoints=http://127.0.0.1:2379
register: etcd_health
until: etcd_health.rc == 0
retries: 30
delay: 2
changed_when: false
# ============================================
# POSTGRESQL SETUP
# ============================================
- name: Install PostgreSQL
community.general.pacman:
name: postgresql
state: present
# ============================================
# PATRONI SETUP
# ============================================
- name: Install Patroni dependencies
community.general.pacman:
name:
- python
- python-pip
- python-psycopg2
- python-yaml
- python-urllib3
- python-certifi
- python-virtualenv
state: present
- name: Create Patroni virtual environment
command: python -m venv /opt/patroni
args:
creates: /opt/patroni/bin/python
- name: Install Patroni in virtual environment
pip:
name:
- patroni[etcd3]
- psycopg2-binary
state: present
virtualenv: /opt/patroni
- name: Create PostgreSQL run directory
file:
path: /run/postgresql
state: directory
owner: postgres
group: postgres
mode: '0755'
- name: Create tmpfiles config for postgresql run directory
copy:
content: "d /run/postgresql 0755 postgres postgres -"
dest: /etc/tmpfiles.d/postgresql.conf
mode: '0644'
- name: Create patroni symlink
file:
src: /opt/patroni/bin/patroni
dest: /usr/local/bin/patroni
state: link
- name: Create patroni config directory
file:
path: /etc/patroni
state: directory
mode: '0755'
- name: Stop PostgreSQL service (Patroni will manage it)
systemd:
name: postgresql
state: stopped
enabled: false
ignore_errors: true
# For bootstrap node with existing data
- name: Prepare existing data directory for Patroni takeover
block:
- name: Ensure postgres owns data directory
file:
path: /var/lib/postgres/data
owner: postgres
group: postgres
recurse: true
- name: Create replicator role
become_user: postgres
command: >
psql -c "DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'replicator') THEN
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD '{{ patroni_replicator_password }}';
END IF;
END $$;"
when: patroni_bootstrap | bool
ignore_errors: true
- name: Set postgres superuser password
become_user: postgres
command: psql -c "ALTER USER postgres WITH PASSWORD '{{ patroni_superuser_password }}';"
when: patroni_bootstrap | bool
ignore_errors: true
when: patroni_bootstrap | bool
- name: Deploy Patroni configuration
template:
src: ../templates/patroni.yml.j2
dest: /etc/patroni/patroni.yml
owner: postgres
group: postgres
mode: '0600'
notify: restart patroni
- name: Create .pgpass file for postgres user
copy:
content: |
*:*:*:postgres:{{ patroni_superuser_password }}
*:*:*:replicator:{{ patroni_replicator_password }}
dest: /var/lib/postgres/.pgpass
owner: postgres
group: postgres
mode: '0600'
- name: Deploy Patroni systemd service
template:
src: ../templates/patroni.service.j2
dest: /etc/systemd/system/patroni.service
mode: '0644'
notify:
- reload systemd
- restart patroni
- name: Enable and start Patroni
systemd:
name: patroni
state: started
enabled: true
daemon_reload: true
- name: Wait for Patroni to be healthy
uri:
url: "http://{{ nebula_ip }}:8008/health"
status_code: 200
register: patroni_health
until: patroni_health.status == 200
retries: 30
delay: 5
handlers:
- name: reload systemd
systemd:
daemon_reload: true
- name: restart etcd
systemd:
name: etcd
state: restarted
- name: restart patroni
systemd:
name: patroni
state: restarted