--- # PostgreSQL installation and configuration # # Passwords are fetched from 1Password at runtime using the `op` CLI. # Requires: `op` authenticated on the control machine (run `op signin` first). - name: Install postgresql via homebrew community.general.homebrew: name: "{{ postgresql_formula }}" state: present # === Fetch passwords from 1Password (on control machine) === # These are skipped when running full playbook (pre_tasks sets them) # but run when using --tags postgresql - name: Fetch superuser password from 1Password ansible.builtin.command: cmd: op --vault {{ postgresql_op_vault }} item get {{ postgresql_op_superuser_item }} --fields password --reveal delegate_to: localhost register: postgresql_superuser_password_result changed_when: false no_log: true check_mode: false when: postgresql_superuser_password is not defined - name: Set superuser password fact ansible.builtin.set_fact: postgresql_superuser_password: "{{ postgresql_superuser_password_result.stdout }}" no_log: true when: postgresql_superuser_password is not defined - name: Fetch user passwords from 1Password ansible.builtin.command: cmd: op --vault {{ postgresql_op_vault }} item get {{ item.op_item }} --fields {{ item.op_field }} --reveal delegate_to: localhost loop: "{{ postgresql_users }}" register: postgresql_user_passwords_result changed_when: false no_log: true check_mode: false when: postgresql_user_passwords is not defined - name: Build user password lookup ansible.builtin.set_fact: postgresql_user_passwords: "{{ postgresql_user_passwords | default({}) | combine({item.item.name: item.stdout}) }}" loop: "{{ postgresql_user_passwords_result.results }}" no_log: true when: postgresql_user_passwords is not defined # === Initialize PostgreSQL cluster === - name: Check if postgresql data directory is initialized ansible.builtin.stat: path: "{{ postgresql_data_dir }}/PG_VERSION" register: postgresql_data_check - name: Create temporary password file for initdb ansible.builtin.copy: content: "{{ postgresql_superuser_password }}" dest: /tmp/.pg_init_pwfile mode: '0600' when: not postgresql_data_check.stat.exists no_log: true - name: Initialize postgresql database cluster with superuser password ansible.builtin.command: > {{ postgresql_bin_dir }}/initdb -U {{ postgresql_superuser }} --locale=en_US.UTF-8 -E UTF-8 --pwfile=/tmp/.pg_init_pwfile {{ postgresql_data_dir }} when: not postgresql_data_check.stat.exists changed_when: true - name: Remove temporary password file ansible.builtin.file: path: /tmp/.pg_init_pwfile state: absent when: not postgresql_data_check.stat.exists # === Configure and start PostgreSQL === - name: Deploy pg_hba.conf ansible.builtin.template: src: pg_hba.conf.j2 dest: "{{ postgresql_config_dir }}/pg_hba.conf" mode: '0600' notify: Restart postgresql - name: Ensure postgresql service is started ansible.builtin.command: brew services start {{ postgresql_formula }} register: postgresql_brew_start changed_when: "'Successfully started' in postgresql_brew_start.stdout" failed_when: false - name: Wait for postgresql to accept connections ansible.builtin.command: > {{ postgresql_bin_dir }}/pg_isready -h localhost -p {{ postgresql_port }} register: postgresql_ready until: postgresql_ready.rc == 0 retries: 10 delay: 2 changed_when: false # === Create users with passwords === - name: Check if postgresql users exist ansible.builtin.command: > {{ postgresql_bin_dir }}/psql -h localhost -U {{ postgresql_superuser }} -d postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname = '{{ item.name }}';" environment: PGPASSWORD: "{{ postgresql_superuser_password }}" loop: "{{ postgresql_users }}" register: postgresql_user_check changed_when: false failed_when: false no_log: true - name: Create postgresql users with passwords ansible.builtin.command: > {{ postgresql_bin_dir }}/psql -h localhost -U {{ postgresql_superuser }} -d postgres -c "CREATE USER {{ item.item.name }} WITH PASSWORD '{{ postgresql_user_passwords[item.item.name] }}';" environment: PGPASSWORD: "{{ postgresql_superuser_password }}" loop: "{{ postgresql_user_check.results }}" when: item.stdout != "1" changed_when: true no_log: true - name: Update postgresql user passwords (idempotent) ansible.builtin.command: > {{ postgresql_bin_dir }}/psql -h localhost -U {{ postgresql_superuser }} -d postgres -c "ALTER USER {{ item.name }} WITH PASSWORD '{{ postgresql_user_passwords[item.name] }}';" environment: PGPASSWORD: "{{ postgresql_superuser_password }}" loop: "{{ postgresql_users }}" changed_when: false no_log: true # === Grant roles to users === - name: Grant roles to users ansible.builtin.command: > {{ postgresql_bin_dir }}/psql -h localhost -U {{ postgresql_superuser }} -d postgres -c "GRANT {{ item.1 }} TO {{ item.0.name }};" environment: PGPASSWORD: "{{ postgresql_superuser_password }}" loop: "{{ postgresql_users | subelements('roles', skip_missing=True) }}" changed_when: false no_log: true # === Create databases === - name: Check if postgresql databases exist ansible.builtin.command: > {{ postgresql_bin_dir }}/psql -h localhost -U {{ postgresql_superuser }} -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname = '{{ item.name }}';" environment: PGPASSWORD: "{{ postgresql_superuser_password }}" loop: "{{ postgresql_databases }}" register: postgresql_db_check changed_when: false failed_when: false no_log: true - name: Create postgresql databases ansible.builtin.command: > {{ postgresql_bin_dir }}/createdb -h localhost -U {{ postgresql_superuser }} --owner={{ item.item.owner }} {{ item.item.name }} environment: PGPASSWORD: "{{ postgresql_superuser_password }}" loop: "{{ postgresql_db_check.results }}" when: item.stdout != "1" changed_when: true no_log: true # === Write credential files for local access === # .pgpass is used by borgmatic for pg_dump backups # Only includes read-only roles (borgmatic has pg_read_all_data) - name: Write .pgpass file for borgmatic backups ansible.builtin.copy: content: | # Managed by ansible - only read-only roles localhost:{{ postgresql_port }}:*:borgmatic:{{ postgresql_user_passwords['borgmatic'] }} dest: ~/.pgpass mode: '0600' no_log: true