From 83b6a389991883880c7639768de05daa8d5defe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20D=C3=A9siles?= <1536672+cdesiles@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:24:43 +0100 Subject: [PATCH] feat: add postgres support --- requirements.yml | 1 + roles/postgres/README.md | 277 ++++++++++++++++++++++++ roles/postgres/defaults/main.yml | 18 ++ roles/postgres/handlers/main.yml | 10 + roles/postgres/tasks/main.yml | 75 +++++++ roles/postgres/templates/custom.conf.j2 | 9 + roles/postgres/vars/archlinux.yml | 9 + roles/postgres/vars/debian.yml | 10 + 8 files changed, 409 insertions(+) create mode 100644 roles/postgres/README.md create mode 100644 roles/postgres/defaults/main.yml create mode 100644 roles/postgres/handlers/main.yml create mode 100644 roles/postgres/tasks/main.yml create mode 100644 roles/postgres/templates/custom.conf.j2 create mode 100644 roles/postgres/vars/archlinux.yml create mode 100644 roles/postgres/vars/debian.yml diff --git a/requirements.yml b/requirements.yml index aa74901..8efe0d9 100644 --- a/requirements.yml +++ b/requirements.yml @@ -2,3 +2,4 @@ collections: - name: ansible.netcommon - name: community.general + - name: community.postgresql diff --git a/roles/postgres/README.md b/roles/postgres/README.md new file mode 100644 index 0000000..13afa9f --- /dev/null +++ b/roles/postgres/README.md @@ -0,0 +1,277 @@ +# PostgreSQL Role + +This Ansible role installs and configures PostgreSQL for local use only. It provides a shared PostgreSQL instance that multiple services can use with isolated databases and users. + +## Features + +- Installs PostgreSQL +- Local-only access (localhost) +- Configurable performance settings +- Each service manages its own database/user (see below) + +## Requirements + +- Systemd-based Linux distribution +- Root/sudo access +- Python `psycopg2` package (for database operations from service roles) + +## Role Variables + +Available variables with defaults (see `defaults/main.yml`): + +```yaml +# Performance tuning +postgres_shared_buffers: 256MB +postgres_effective_cache_size: 1GB +postgres_maintenance_work_mem: 64MB +postgres_work_mem: 4MB +postgres_max_connections: 100 +``` + +## Dependencies + +None. + +## Example Playbook + +```yaml +--- +- hosts: servers + become: true + roles: + - role: postgres + - role: immich # Will create its own database + - role: nextcloud # Will create its own database +``` + +## Database Isolation Strategy + +This role follows a **decentralized database management** pattern: + +### 1. PostgreSQL Role Responsibility +- Install and configure PostgreSQL +- Manage global performance settings +- Ensure the service is running + +### 2. Service Role Responsibility +Each service role (immich, nextcloud, etc.) manages its own: +- Database creation +- User creation +- Password management +- Schema migrations + +### 3. Security & Isolation + +**Database Isolation:** +- Each service gets its own database +- Example: `immich`, `nextcloud`, `gitea` + +**User Isolation:** +- Each service gets its own PostgreSQL user +- Users can only access their own database +- Example: `immich` → `immich` database only + +**Authentication:** +- Each user has a unique password +- Passwords stored in service role variables (use Ansible Vault for production) + +## How to Use from Service Roles + +### Pattern for Service Roles + +When creating a service role that needs PostgreSQL: + +**1. Add postgres as a dependency** (`meta/main.yml`): +```yaml +dependencies: + - role: postgres +``` + +**2. Define database variables** (`defaults/main.yml`): +```yaml +myservice_db_name: myservice +myservice_db_user: myservice_user +myservice_db_password: changeme # Use Ansible Vault in production! +myservice_db_host: localhost +myservice_db_port: 5432 +``` + +**3. Create database and user** (`tasks/main.yml`): +```yaml +- name: Create PostgreSQL database for myservice + community.postgresql.postgresql_db: + name: "{{ myservice_db_name }}" + state: present + become: true + become_user: "{{ postgres_admin_user }}" + +- name: Create PostgreSQL user for myservice + community.postgresql.postgresql_user: + name: "{{ myservice_db_user }}" + password: "{{ myservice_db_password }}" + db: "{{ myservice_db_name }}" + priv: ALL + state: present + become: true + become_user: "{{ postgres_admin_user }}" + +- name: Ensure user has no superuser privileges + community.postgresql.postgresql_user: + name: "{{ myservice_db_user }}" + role_attr_flags: NOSUPERUSER,NOCREATEDB,NOCREATEROLE + state: present + become: true + become_user: "{{ postgres_admin_user }}" +``` + +**Note:** `postgres_admin_user` is provided by the postgres role and defaults to `postgres`. + +**4. Configure your service** to connect to: +``` +Host: localhost +Port: 5432 +Database: myservice +User: myservice_user +Password: changeme +``` + +### Real Example: Immich + +See `roles/immich/` for a complete working example of using this pattern. + +## Connection Methods + +### From Containers + +If your service runs in a container (Docker/Podman), you need to: + +**Option 1: Use host network mode** +```yaml +network_mode: host +``` +Then connect to `localhost:5432` + +**Option 2: Use host.containers.internal (Podman/Docker)** +```yaml +DB_HOSTNAME: host.containers.internal +DB_PORT: 5432 +``` + +**Option 3: Bridge with firewall (less secure)** +Bind postgres to `0.0.0.0` and use container gateway IP. + +### From System Services + +Services running directly on the host can connect to `localhost:5432` without any special configuration. + +## Security Best Practices + +### 1. Use Ansible Vault for Passwords + +```bash +# Create encrypted variables +ansible-vault encrypt_string 'my_secure_password' --name 'immich_db_password' +``` + +Add to your inventory or vars: +```yaml +immich_db_password: !vault | + $ANSIBLE_VAULT;1.1;AES256 + ...encrypted... +``` + +### 2. Unique Passwords per Service + +Never reuse passwords between services: +```yaml +immich_db_password: unique_password_1 +nextcloud_db_password: unique_password_2 +gitea_db_password: unique_password_3 +``` + +### 3. Minimal Privileges + +The pattern above ensures users have: +- ✅ Access to their database only +- ❌ No superuser privileges +- ❌ Cannot create databases +- ❌ Cannot create roles +- ❌ Cannot access other databases + +### 4. Local-Only Access + +PostgreSQL is configured to listen on `localhost` only: +- No remote connections allowed +- Services must run on the same host + +## Troubleshooting + +### Check PostgreSQL status +```bash +systemctl status postgresql +``` + +### Connect to PostgreSQL +```bash +sudo -u postgres psql +``` + +### List databases +```sql +\l +``` + +### List users and permissions +```sql +\du +``` + +### Test connection from service +```bash +psql -h localhost -U immich -d immich +``` + +### View logs +```bash +journalctl -u postgresql -f +``` + +## Performance Tuning + +Adjust variables based on your hardware: + +**For systems with 4GB RAM:** +```yaml +postgres_shared_buffers: 1GB +postgres_effective_cache_size: 3GB +``` + +**For systems with 16GB RAM:** +```yaml +postgres_shared_buffers: 4GB +postgres_effective_cache_size: 12GB +``` + +**Rule of thumb:** +- `shared_buffers`: 25% of total RAM +- `effective_cache_size`: 50-75% of total RAM + +## Backup Recommendations + +Consider implementing: +1. **pg_dump** for logical backups +2. **WAL archiving** for point-in-time recovery +3. **Automated backup scripts** via cron + +Example backup script for a service: +```bash +pg_dump -h localhost -U immich immich > /backup/immich_$(date +%Y%m%d).sql +``` + +## License + +MIT + +## Author Information + +Created for managing shared PostgreSQL instances in NAS/homelab environments. diff --git a/roles/postgres/defaults/main.yml b/roles/postgres/defaults/main.yml new file mode 100644 index 0000000..7be1b01 --- /dev/null +++ b/roles/postgres/defaults/main.yml @@ -0,0 +1,18 @@ +--- +# PostgreSQL admin user (used by service roles for database management) +postgres_admin_user: postgres + +# PostgreSQL admin password (REQUIRED - must be set explicitly) +# Set via inventory, host_vars, or ansible-vault +# See this file's comments for setup instructions +# postgres_admin_password: "" # Intentionally undefined - role will fail if not set + +# PostgreSQL data directory +postgres_data_dir: /var/lib/postgres/data + +# Performance tuning (adjust based on your hardware) +postgres_shared_buffers: 256MB +postgres_effective_cache_size: 1GB +postgres_maintenance_work_mem: 64MB +postgres_work_mem: 4MB +postgres_max_connections: 100 diff --git a/roles/postgres/handlers/main.yml b/roles/postgres/handlers/main.yml new file mode 100644 index 0000000..b548554 --- /dev/null +++ b/roles/postgres/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: Restart PostgreSQL + ansible.builtin.systemd: + name: "{{ postgres_service_name }}" + state: restarted + +- name: Reload PostgreSQL + ansible.builtin.systemd: + name: "{{ postgres_service_name }}" + state: reloaded diff --git a/roles/postgres/tasks/main.yml b/roles/postgres/tasks/main.yml new file mode 100644 index 0000000..0809429 --- /dev/null +++ b/roles/postgres/tasks/main.yml @@ -0,0 +1,75 @@ +--- +- name: Validate required password is set + ansible.builtin.assert: + that: + - postgres_admin_password is defined + - postgres_admin_password | length >= 12 + fail_msg: | + postgres_admin_password is required (min 12 chars). + See roles/postgres/defaults/main.yml for configuration instructions. + success_msg: "Password validation passed" + +- name: Load OS-specific variables + ansible.builtin.include_vars: "{{ item }}" + with_first_found: + - "{{ ansible_facts['os_family'] }}.yml" + - debian.yml + +- name: Install PostgreSQL packages + ansible.builtin.package: + name: "{{ postgres_packages }}" + state: present + +- name: Create current version symlink (Debian) + ansible.builtin.shell: + cmd: set -o pipefail && ln -sf $(ls -1 /etc/postgresql/ | grep -E '^[0-9]+$' | sort -V | tail -n1) /etc/postgresql/current + creates: /etc/postgresql/current + executable: /bin/bash + when: ansible_facts['os_family'] == 'Debian' + +- name: Ensure PostgreSQL is initialized (Arch) + ansible.builtin.command: + cmd: initdb -D {{ postgres_data_dir }} + creates: "{{ postgres_data_dir }}/PG_VERSION" + become: true + become_user: "{{ postgres_admin_user }}" + when: ansible_facts['os_family'] == 'Archlinux' + +- name: Ensure PostgreSQL config directory exists + ansible.builtin.file: + path: "{{ postgres_config_dir }}" + state: directory + owner: postgres + group: postgres + mode: "0750" + +- name: Enable include_dir in main postgresql.conf + ansible.builtin.lineinfile: + path: "{{ postgres_config_path }}" + regexp: "^#?include_dir =" + line: "include_dir = 'conf.d'" + state: present + notify: Restart PostgreSQL + +- name: Deploy custom PostgreSQL configuration + ansible.builtin.template: + src: custom.conf.j2 + dest: "{{ postgres_config_dir }}/custom.conf" + owner: postgres + group: postgres + mode: "0640" + notify: Restart PostgreSQL + +- name: Enable and start PostgreSQL service + ansible.builtin.systemd: + name: "{{ postgres_service_name }}" + enabled: true + state: started + +- name: Set PostgreSQL admin user password + community.postgresql.postgresql_user: + name: "{{ postgres_admin_user }}" + password: "{{ postgres_admin_password }}" + state: present + become: true + become_user: "{{ postgres_admin_user }}" diff --git a/roles/postgres/templates/custom.conf.j2 b/roles/postgres/templates/custom.conf.j2 new file mode 100644 index 0000000..3774d6b --- /dev/null +++ b/roles/postgres/templates/custom.conf.j2 @@ -0,0 +1,9 @@ +# Custom PostgreSQL configuration managed by Ansible +# Override settings from main postgresql.conf + +# Performance tuning +shared_buffers = {{ postgres_shared_buffers }} +effective_cache_size = {{ postgres_effective_cache_size }} +maintenance_work_mem = {{ postgres_maintenance_work_mem }} +work_mem = {{ postgres_work_mem }} +max_connections = {{ postgres_max_connections }} diff --git a/roles/postgres/vars/archlinux.yml b/roles/postgres/vars/archlinux.yml new file mode 100644 index 0000000..1c263f6 --- /dev/null +++ b/roles/postgres/vars/archlinux.yml @@ -0,0 +1,9 @@ +--- +postgres_packages: + - postgresql + - python-psycopg2 + +postgres_service_name: postgresql +postgres_config_path: "{{ postgres_data_dir }}/postgresql.conf" +postgres_config_dir: "{{ postgres_data_dir }}/conf.d" +postgres_hba_path: "{{ postgres_data_dir }}/pg_hba.conf" diff --git a/roles/postgres/vars/debian.yml b/roles/postgres/vars/debian.yml new file mode 100644 index 0000000..fd71fd0 --- /dev/null +++ b/roles/postgres/vars/debian.yml @@ -0,0 +1,10 @@ +--- +postgres_packages: + - postgresql + - postgresql-contrib + - python3-psycopg2 + +postgres_service_name: postgresql +postgres_config_path: /etc/postgresql/current/main/postgresql.conf +postgres_config_dir: /etc/postgresql/current/main/conf.d +postgres_hba_path: /etc/postgresql/current/main/pg_hba.conf