blumeops/ansible/roles/postgresql/tasks/main.yml
Erich Blume 544682e92e Restrict .pgpass to read-only borgmatic user only
Remove superuser from .pgpass since it's not needed for automated
operations. Only borgmatic (with pg_read_all_data role) needs
passwordless access for pg_dump backups.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 09:51:30 -08:00

178 lines
5.4 KiB
YAML

---
# 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_formula }} via homebrew
community.general.homebrew:
name: "{{ postgresql_formula }}"
state: present
# === Fetch passwords from 1Password (on control machine) ===
- 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: pg_superuser_password_result
changed_when: false
no_log: true
- name: Set superuser password fact
ansible.builtin.set_fact:
pg_superuser_password: "{{ pg_superuser_password_result.stdout }}"
no_log: true
- 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: pg_user_passwords_result
changed_when: false
no_log: true
- name: Build user password lookup
ansible.builtin.set_fact:
pg_user_passwords: "{{ pg_user_passwords | default({}) | combine({item.item.name: item.stdout}) }}"
loop: "{{ pg_user_passwords_result.results }}"
no_log: true
# === Initialize PostgreSQL cluster ===
- name: Check if postgresql data directory is initialized
ansible.builtin.stat:
path: "{{ postgresql_data_dir }}/PG_VERSION"
register: pg_data
- name: Create temporary password file for initdb
ansible.builtin.copy:
content: "{{ pg_superuser_password }}"
dest: /tmp/.pg_init_pwfile
mode: '0600'
when: not pg_data.stat.exists
no_log: true
- name: Initialize postgresql database cluster with superuser password
ansible.builtin.command: >
{{ postgresql_bin_dir }}/initdb
--locale=en_US.UTF-8 -E UTF-8
--pwfile=/tmp/.pg_init_pwfile
{{ postgresql_data_dir }}
when: not pg_data.stat.exists
- name: Remove temporary password file
ansible.builtin.file:
path: /tmp/.pg_init_pwfile
state: absent
when: not pg_data.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: brew_start
changed_when: "'Successfully started' in 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: pg_ready
until: pg_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 -tAc
"SELECT 1 FROM pg_roles WHERE rolname = '{{ item.name }}';"
environment:
PGPASSWORD: "{{ pg_superuser_password }}"
loop: "{{ postgresql_users }}"
register: 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 -c
"CREATE USER {{ item.item.name }} WITH PASSWORD '{{ pg_user_passwords[item.item.name] }}';"
environment:
PGPASSWORD: "{{ pg_superuser_password }}"
loop: "{{ 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 -c
"ALTER USER {{ item.name }} WITH PASSWORD '{{ pg_user_passwords[item.name] }}';"
environment:
PGPASSWORD: "{{ pg_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 -c "GRANT {{ item.1 }} TO {{ item.0.name }};"
environment:
PGPASSWORD: "{{ pg_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 -tAc
"SELECT 1 FROM pg_database WHERE datname = '{{ item.name }}';"
environment:
PGPASSWORD: "{{ pg_superuser_password }}"
loop: "{{ postgresql_databases }}"
register: db_check
changed_when: false
failed_when: false
no_log: true
- name: Create postgresql databases
ansible.builtin.command: >
{{ postgresql_bin_dir }}/createdb -h localhost
--owner={{ item.item.owner }}
{{ item.item.name }}
environment:
PGPASSWORD: "{{ pg_superuser_password }}"
loop: "{{ 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:{{ pg_user_passwords['borgmatic'] }}
dest: ~/.pgpass
mode: '0600'
no_log: true