--- # 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