PostgreSQL ansible role
2024-10-09 - The ansible role I use to manage my PostgreSQL databases
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:
---
- 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
:
---
- 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:
---
# 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:
---
# 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:
---
- name: 'reload postgresql'
service:
name: 'postgresql'
state: 'reloaded'
- name: 'restart postgresql'
service:
name: 'postgresql'
state: 'restarted'
Templates
Here is my usual pg_hba.conf
:
###############################################################################
# \_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:
###############################################################################
# \_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:
#!/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:
---
dependencies:
- role: 'borg'
- role: 'postgresql'
Then from a tasks file:
- include_role:
name: 'postgresql'
tasks_from: 'database'
vars:
postgresql:
extensions:
- 'pgcrypto'
name: 'eventline'
Backup jobs can be setup with:
- 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.