262 lines
7.4 KiB
Markdown
262 lines
7.4 KiB
Markdown
|
---
|
||
|
title: 'PostgreSQL ansible role'
|
||
|
description: 'The ansible role I use to manage my PostgreSQL databases'
|
||
|
date: '2024-10-09'
|
||
|
tags:
|
||
|
- ansible
|
||
|
- PostgreSQL
|
||
|
---
|
||
|
|
||
|
## Introduction
|
||
|
|
||
|
Before succumbing to nixos, I had been using an ansible role to manage my PostgreSQL databases. Now that I am in need of it again I refined it a bit: here is the result.
|
||
|
|
||
|
## The role
|
||
|
|
||
|
### Tasks
|
||
|
|
||
|
My `main.yaml` relies on OS specific tasks:
|
||
|
|
||
|
``` yaml
|
||
|
---
|
||
|
- name: 'Generate postgres user password'
|
||
|
include_tasks: 'generate_password.yaml'
|
||
|
vars:
|
||
|
name: 'postgres'
|
||
|
when: '(ansible_local["postgresql_postgres"]|default({})).password is undefined'
|
||
|
|
||
|
- name: 'Run OS tasks'
|
||
|
include_tasks: '{{ ansible_distribution }}.yaml'
|
||
|
|
||
|
- name: 'Start postgresql and activate it on boot'
|
||
|
service:
|
||
|
name: 'postgresql'
|
||
|
enabled: true
|
||
|
state: 'started'
|
||
|
```
|
||
|
|
||
|
Here is an example in `Debian.yaml`:
|
||
|
|
||
|
``` yaml
|
||
|
---
|
||
|
- name: 'Install postgresql'
|
||
|
package:
|
||
|
name:
|
||
|
- 'postgresql'
|
||
|
- 'python3-psycopg2' # necessary for the ansible postgresql modules
|
||
|
|
||
|
- name: 'Configure postgresql'
|
||
|
template:
|
||
|
src: 'pg_hba.conf'
|
||
|
dest: '/etc/postgresql/15/main/'
|
||
|
owner: 'root'
|
||
|
group: 'postgres'
|
||
|
mode: '0440'
|
||
|
notify: 'reload postgresql'
|
||
|
|
||
|
- name: 'Configure postgresql (file that require a restart when modified)'
|
||
|
template:
|
||
|
src: 'postgresql.conf'
|
||
|
dest: '/etc/postgresql/15/main/'
|
||
|
owner: 'root'
|
||
|
group: 'postgres'
|
||
|
mode: '0440'
|
||
|
notify: 'restart postgresql'
|
||
|
|
||
|
- meta: 'flush_handlers'
|
||
|
|
||
|
- name: 'Set postgres admin password'
|
||
|
shell:
|
||
|
cmd: "printf \"ALTER USER postgres WITH PASSWORD '%s';\" \"{{ ansible_local.postgresql_postgres.password }}\" | su -c psql - postgres"
|
||
|
when: 'postgresql_password_postgres is defined'
|
||
|
```
|
||
|
|
||
|
My `generate_password.yaml` will persist a password with a custom fact:
|
||
|
|
||
|
``` yaml
|
||
|
---
|
||
|
# Inputs:
|
||
|
# name: string
|
||
|
# Outputs:
|
||
|
# ansible_local["postgresql_" + postgresql.name].password
|
||
|
- name: 'Generate a password'
|
||
|
set_fact: { "postgresql_password_{{ name }}": "{{ lookup('password', '/dev/null length=32 chars=ascii_letters') }}" }
|
||
|
|
||
|
- name: 'Deploy ansible fact to persist the password'
|
||
|
template:
|
||
|
src: 'postgresql.fact'
|
||
|
dest: '/etc/ansible/facts.d/postgresql_{{ name }}.fact'
|
||
|
owner: 'root'
|
||
|
mode: '0500'
|
||
|
vars:
|
||
|
password: "{{ lookup('vars', 'postgresql_password_' + name) }}"
|
||
|
|
||
|
- name: 'reload ansible_local'
|
||
|
setup: 'filter=ansible_local'
|
||
|
```
|
||
|
|
||
|
The main entry point of the role is the `database.yaml` task:
|
||
|
|
||
|
``` yaml
|
||
|
---
|
||
|
# Inputs:
|
||
|
# postgresql:
|
||
|
# name: string
|
||
|
# extension: list
|
||
|
# Outputs:
|
||
|
# ansible_local["postgresql_" + postgresql.name].password
|
||
|
- name: 'Generate {{ postgresql.name }} password'
|
||
|
include_tasks: 'generate_password.yaml'
|
||
|
vars:
|
||
|
name: '{{ postgresql.name }}'
|
||
|
when: '(ansible_local["postgresql_" + postgresql.name]|default({})).password is undefined'
|
||
|
|
||
|
- name: 'Create {{ postgresql.name }} user'
|
||
|
community.postgresql.postgresql_user:
|
||
|
login_host: 'localhost'
|
||
|
login_password: '{{ ansible_local.postgresql_postgres.password }}'
|
||
|
name: '{{ postgresql.name }}'
|
||
|
password: '{{ ansible_local["postgresql_" + postgresql.name].password }}'
|
||
|
|
||
|
- name: 'Create {{ postgresql.name }} database'
|
||
|
community.postgresql.postgresql_db:
|
||
|
login_host: 'localhost'
|
||
|
login_password: '{{ ansible_local.postgresql_postgres.password }}'
|
||
|
name: '{{ postgresql.name }}'
|
||
|
owner: '{{ postgresql.name }}'
|
||
|
|
||
|
- name: 'Activate {{ postgres.name }} extensions'
|
||
|
community.postgresql.postgresql_ext:
|
||
|
db: '{{ postgresql.name }}'
|
||
|
login_host: 'localhost'
|
||
|
login_password: '{{ ansible_local.postgresql_postgres.password }}'
|
||
|
name: '{{ item }}'
|
||
|
loop: '{{ postgresql.extensions | default([]) }}'
|
||
|
```
|
||
|
|
||
|
### Handlers
|
||
|
|
||
|
Here are the two handlers:
|
||
|
|
||
|
``` yaml
|
||
|
---
|
||
|
- name: 'reload postgresql'
|
||
|
service:
|
||
|
name: 'postgresql'
|
||
|
state: 'reloaded'
|
||
|
|
||
|
- name: 'restart postgresql'
|
||
|
service:
|
||
|
name: 'postgresql'
|
||
|
state: 'restarted'
|
||
|
```
|
||
|
|
||
|
### Templates
|
||
|
|
||
|
Here is my usual `pg_hba.conf`:
|
||
|
|
||
|
``` yaml
|
||
|
###############################################################################
|
||
|
# \_o< WARNING : This file is being managed by ansible! >o_/ #
|
||
|
# ~~~~ ~~~~ #
|
||
|
###############################################################################
|
||
|
|
||
|
local all all peer #unix socket
|
||
|
|
||
|
host all all 127.0.0.0/8 scram-sha-256
|
||
|
host all all ::1/128 scram-sha-256
|
||
|
host all all 10.88.0.0/16 scram-sha-256 # podman
|
||
|
```
|
||
|
|
||
|
Here is my `postgresql.conf` for Debian:
|
||
|
|
||
|
``` yaml
|
||
|
###############################################################################
|
||
|
# \_o< WARNING : This file is being managed by ansible! >o_/ #
|
||
|
# ~~~~ ~~~~ #
|
||
|
###############################################################################
|
||
|
|
||
|
data_directory = '/var/lib/postgresql/15/main' # use data in another directory
|
||
|
hba_file = '/etc/postgresql/15/main/pg_hba.conf' # host-based authentication file
|
||
|
ident_file = '/etc/postgresql/15/main/pg_ident.conf' # ident configuration file
|
||
|
external_pid_file = '/var/run/postgresql/15-main.pid' # write an extra PID file
|
||
|
|
||
|
port = 5432 # (change requires restart)
|
||
|
max_connections = 100 # (change requires restart)
|
||
|
|
||
|
unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories
|
||
|
listen_addresses = 'localhost,10.88.0.1'
|
||
|
|
||
|
shared_buffers = 128MB # min 128kB
|
||
|
dynamic_shared_memory_type = posix # the default is usually the first option
|
||
|
max_wal_size = 1GB
|
||
|
min_wal_size = 80MB
|
||
|
log_line_prefix = '%m [%p] %q%u@%d ' # special values:
|
||
|
log_timezone = 'Europe/Paris'
|
||
|
cluster_name = '15/main' # added to process titles if nonempty
|
||
|
datestyle = 'iso, mdy'
|
||
|
timezone = 'Europe/Paris'
|
||
|
lc_messages = 'en_US.UTF-8' # locale for system error message
|
||
|
lc_monetary = 'en_US.UTF-8' # locale for monetary formatting
|
||
|
lc_numeric = 'en_US.UTF-8' # locale for number formatting
|
||
|
lc_time = 'en_US.UTF-8' # locale for time formatting
|
||
|
default_text_search_config = 'pg_catalog.english'
|
||
|
include_dir = 'conf.d' # include files ending in '.conf' from
|
||
|
```
|
||
|
|
||
|
And here is the simple fact script:
|
||
|
|
||
|
``` shell
|
||
|
#!/bin/sh
|
||
|
###############################################################################
|
||
|
# \_o< WARNING : This file is being managed by ansible! >o_/ #
|
||
|
# ~~~~ ~~~~ #
|
||
|
###############################################################################
|
||
|
set -eu
|
||
|
|
||
|
printf '{"password": "%s"}' "{{ password }}"
|
||
|
```
|
||
|
|
||
|
## Usage example
|
||
|
|
||
|
I do not call the role from a playbook, I prefer running the setup from an application's role that relies on postgresql using a `meta/main.yaml` containing something like:
|
||
|
|
||
|
``` yaml
|
||
|
---
|
||
|
dependencies:
|
||
|
- role: 'borg
|
||
|
- role: 'postgresql'
|
||
|
```
|
||
|
|
||
|
Then from a tasks file:
|
||
|
|
||
|
``` yaml
|
||
|
- include_role:
|
||
|
name: 'postgresql'
|
||
|
tasks_from: 'database'
|
||
|
vars:
|
||
|
postgresql:
|
||
|
extensions:
|
||
|
- 'pgcrypto'
|
||
|
name: 'eventline'
|
||
|
```
|
||
|
|
||
|
Backup jobs can be setup with:
|
||
|
|
||
|
``` yaml
|
||
|
- include_role:
|
||
|
name: 'borg'
|
||
|
tasks_from: 'client'
|
||
|
vars:
|
||
|
client:
|
||
|
jobs:
|
||
|
- name: 'postgres'
|
||
|
command_to_pipe: "su - postgres -c '/usr/bin/pg_dump -b -c -C -d eventline'"
|
||
|
name: 'eventline'
|
||
|
server: '{{ eventline_adyxax_org.borg }}'
|
||
|
```
|
||
|
|
||
|
## Conclusion
|
||
|
|
||
|
I enjoy this design, it has served me well.
|