blumeops/ansible/roles/postgresql/tasks/main.yml
Erich Blume adf6f4fbe9 Add PostgreSQL and Miniflux services to tailnet (#16)
## Summary
- Add PostgreSQL 18 as a new service at `pg.tail8d86e.ts.net:5432`
- Add Miniflux RSS/Atom feed reader at `feed.tail8d86e.ts.net`
- Both services managed via homebrew/brew services
- Pulumi ACL tags added (tag:pg, tag:feed)
- Alloy log collection configured for both services
- Zettelkasten documentation updated

## Manual Setup Required

Before running ansible, the following steps are needed on indri:

### 1. Apply Pulumi tags
```bash
mise run tailnet-up
```
Then apply tags to indri in Tailscale admin console.

### 2. Create 1Password entries
- miniflux PostgreSQL user password
- miniflux admin password (for first run)

### 3. Set PostgreSQL user password (after ansible installs postgres)
```bash
ssh indri '/opt/homebrew/opt/postgresql@18/bin/psql -c "ALTER USER miniflux PASSWORD '\''your-password'\'';"'
```

### 4. Create password files on indri
```bash
ssh indri 'echo "your-db-password" > ~/.miniflux-db-password && chmod 600 ~/.miniflux-db-password'
ssh indri 'echo "your-admin-password" > ~/.miniflux-admin-password && chmod 600 ~/.miniflux-admin-password'
```

### 5. Create ~/.pgpass for borgmatic
```bash
ssh indri 'echo "localhost:5432:miniflux:miniflux:YOUR_PASSWORD" > ~/.pgpass && chmod 600 ~/.pgpass'
```

### 6. Run ansible with first-run admin creation
```bash
mise run provision-indri -- -e miniflux_create_admin=1
```

### 7. Update borgmatic config
Add to `~/.config/borgmatic/config.yaml` on indri:
```yaml
postgresql_databases:
    - name: miniflux
      hostname: localhost
      port: 5432
      username: miniflux
```

### 8. Cleanup after first run
```bash
ssh indri 'rm ~/.miniflux-admin-password'
```

## Test plan
- [ ] Run `mise run tailnet-up` and verify Pulumi changes
- [ ] Apply tags to indri in Tailscale admin
- [ ] Run `mise run provision-indri -- --check --diff` for dry run
- [ ] Run `mise run provision-indri -- -e miniflux_create_admin=1`
- [ ] Approve services in Tailscale admin
- [ ] Verify PostgreSQL: `ssh indri '/opt/homebrew/opt/postgresql@18/bin/pg_isready'`
- [ ] Verify Miniflux: `curl https://feed.tail8d86e.ts.net/healthcheck`
- [ ] Run `mise run indri-services-check`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/16
2026-01-16 12:30:20 -08:00

184 lines
5.8 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) ===
# 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: pg_superuser_password_result
changed_when: false
no_log: true
when: pg_superuser_password is not defined
- name: Set superuser password fact
ansible.builtin.set_fact:
pg_superuser_password: "{{ pg_superuser_password_result.stdout }}"
no_log: true
when: pg_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: pg_user_passwords_result
changed_when: false
no_log: true
when: pg_user_passwords is not defined
- 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
when: pg_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: 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 -d postgres -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 -d postgres -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 -d postgres -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 -d postgres -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 -d postgres -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