Using dalibo.pglift Ansible collection

This tutorial illustrates the use of the dalibo.pglift collection that leverages pglift with Ansible. The collection ships the following modules: dalibo.pglift.instance, dalibo.pglift.database, dalibo.pglift.role, dalibo.pglift.postgres_exporter and dalibo.pglift.dsn_info. This tutorial also demonstrates how to integrate these modules with other PostgreSQL-related community modules, namely community.postgresql.

Note

Ansible modules require Python 3 so, depending on the Ansible version being used, one may need to configure managed machines to use Python 3 through the ansible_python_interpreter inventory variable or -e command-line option.

Setup

In the following we consider two nodes (or machines): the control node, where Ansible commands or playbooks will be executed and, the managed node where operations should apply.

On the control node, the collection should be installed:

user@control:~$ ansible-galaxy collection install dalibo.pglift

Documentation for each module can be obtained by using ansible-doc <modulename>, e.g.:

user@control:~$ ansible-doc dalibo.pglift.instance
> DALIBO.PGLIFT.INSTANCE
(.../ansible/ansible_collections/dalibo/pglift/plugins/modules/instance.py)

Manage a PostgreSQL server instance

OPTIONS (= is mandatory):

[...]

On the managed node, pglift needs to be installed and configured. Let’s detail a bit how to configure a pglift site with Ansible. We’ll use the following playbook:

site-configure.yml
- name: pglift site configuration for the Ansible tutorial
  hosts: localhost
  tasks:
    - name: The prefix path for all pglift data
      ansible.builtin.tempfile:
        state: directory
        suffix: .pglift
      register: pglift_prefix

    - name: Install pglift site settings
      ansible.builtin.template:
        src: tutorial-settings.yaml.j2
        dest: ~/.config/pglift/settings.yaml

    - name: Perform site configuration
      ansible.builtin.command: pglift site-configure install

which:

  1. creates a writable (temporary) directory to host PostgreSQL instances,

  2. installs pglift site settings based on the following template:

    tutorial-settings.yml
    prefix: "{{ pglift_prefix.path }}"
    run_prefix: "{{ pglift_prefix.path }}/run"
    postgresql:
      auth:
        local: md5
        host: md5
        passfile: "{{ pglift_prefix.path }}/.pgpass"
      surole:
        pgpass: true
      backuprole:
        pgpass: true
      replrole: replication
    pgbackrest:
      repository:
        mode: path
        path: "{{ pglift_prefix.path }}/backups"
    prometheus:
      execpath: /usr/bin/prometheus-postgres-exporter
    
  3. and finally perform site configuration.

Our site settings make use of the temporary directory created in the first task. The site is configured to use pgbackrest for physical backups (stored locally) and Prometheus postgres_exporter for monitoring. Finally note that we are using a PostgreSQL passfile in order to store passwords for managed roles.

Executing this playbook will complete the setup of the managed node:

user@control:~$ ansible-playbook site-configure.yml

Note

The hosts field in this playbook uses localhost for testing purpose and should be adapted to actual managed node.

Back on the control node, we will define passwords for the postgres user and other roles used in the following playbooks; these will be stored and encrypted with Ansible vault:

user@control:~$ cat << EOF | ansible-vault encrypt > pglift-vars
postgresql_surole_password: $(openssl rand -base64 9)
prod_bob_password: $(openssl rand -base64 9)
backup_role_password: $(openssl rand -base64 9)
prometheus_role_password: $(openssl rand -base64 9)
EOF

To view actual passwords:

user@control:~$ ansible-vault view pglift-vars

Initial deployment

The following playbook installs and configures 3 PostgreSQL instances on the managed node; the first two instances are started while the third one is not:

instances-create.yml
---
- name: my postgresql instances
  hosts: localhost
  tasks:
    - name: production instance
      dalibo.pglift.instance:
        name: prod
        state: started
        port: 5433
        settings:
          max_connections: 100
          shared_buffers: 1GB
          unix_socket_directories: /tmp
          shared_preload_libraries: "pg_stat_statements, passwordcheck"
        surole_password: "{{ postgresql_surole_password }}"
        pgbackrest:
          stanza: stanza_prod
          password: "{{ backup_role_password }}"
        prometheus:
          password: "{{ prometheus_role_password }}"
          port: 9186
        roles:
          - name: bob
            login: true
            password: "{{ prod_bob_password }}"
            connection_limit: 10
            validity: '2025-01-01T00:00'
            in_roles:
              - pg_read_all_stats
              - pg_signal_backend
          - name: peter
            in_roles:
              - pg_signal_backend
        databases:
          - name: db
            owner: bob
            settings:
              work_mem: 3MB
            extensions:
              - name: unaccent
                schema: public
          - name: db2
            owner: bob
            settings:
              work_mem: 3MB
            extensions:
              - name: unaccent
                schema: public
          - name: db3
            owner: peter
            extensions:
              - name: unaccent
                schema: public


    - name: pre-production instance
      dalibo.pglift.instance:
        name: preprod
        port: 5434
        state: started
        settings:
          unix_socket_directories: /tmp
        surole_password: "{{ postgresql_surole_password }}"
        pgbackrest:
          stanza: stanza_preprod
          password: "{{ backup_role_password }}"
        prometheus:
          password: "{{ prometheus_role_password }}"
          port: 9188

    - name: register preprod conninfo
      dalibo.pglift.dsn_info:
        name: preprod
      register: preprod

    - name: |
        create database test on pre-production instance using
        community.postgresql module
      community.postgresql.postgresql_db:
        name: test
        login_host: "{{ preprod.PGHOST }}"
        login_port: "{{ preprod.PGPORT }}"
      environment: "{{ preprod }}"

    - name: dev instance, not running at the moment
      dalibo.pglift.instance:
        name: dev
        port: 5444
        state: stopped
        settings:
          unix_socket_directories: /tmp
        surole_password: "{{ postgresql_surole_password }}"
        pgbackrest:
          stanza: stanza_dev
          password: "{{ backup_role_password }}"
        prometheus:
          password: "{{ prometheus_role_password }}"
          port: 9189

Finally, run:

user@control:~$ ansible-playbook --extra-vars @pglift-vars --ask-vault-password instances-create.yml
PLAY [my postgresql instances] ***************************************************************************

TASK [Gathering Facts] ***********************************************************************************
ok: [localhost]

TASK [production instance] *******************************************************************************
changed: [localhost]

TASK [pre-production instance] ***************************************************************************
changed: [localhost]

TASK [dev instance, not running at the moment] ***********************************************************
changed: [localhost]

PLAY RECAP ***********************************************************************************************
localhost                  : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

We can see our instances installed and running on the managed node:

user@managed:~$ tree -L 3 $tmpdir/srv/pgsql
/tmp/.../srv/pgsql
└── 16
    ├── dev
    │   ├── data
    │   └── wal
    ├── preprod
    │   ├── data
    │   └── wal
    └── prod
        ├── data
        └── wal
user@managed:~$ ps xf
  [...]
  19777 ?        Ss     0:00  \_ /usr/lib/postgresql/16/bin/postgres -D .../srv/pgsql/16/pr
  19780 ?        Ss     0:00  |   \_ postgres: prod: logger
  19781 ?        Ss     0:00  |   \_ postgres: prod: checkpointer
  19782 ?        Ss     0:00  |   \_ postgres: prod: background writer
  19784 ?        Ss     0:00  |   \_ postgres: prod: walwriter
  19785 ?        Ss     0:00  |   \_ postgres: prod: autovacuum launcher
  19786 ?        Ss     0:00  |   \_ postgres: prod: archiver last was 000000010000000000000001
  19787 ?        Ss     0:00  |   \_ postgres: prod: logical replication launcher
  19843 ?        Ssl    0:00  \_ /usr/bin/prometheus-postgres-exporter --log.level=info
  20180 ?        Ss     0:00  \_ /usr/lib/postgresql/16/bin/postgres -D .../srv/pgsql/16/pr
  20183 ?        Ss     0:00  |   \_ postgres: preprod: logger
  20184 ?        Ss     0:00  |   \_ postgres: preprod: checkpointer
  20185 ?        Ss     0:00  |   \_ postgres: preprod: background writer
  20187 ?        Ss     0:00  |   \_ postgres: preprod: walwriter
  20188 ?        Ss     0:00  |   \_ postgres: preprod: autovacuum launcher
  20189 ?        Ss     0:00  |   \_ postgres: preprod: archiver last was 000000010000000000000001
  20190 ?        Ss     0:00  |   \_ postgres: preprod: logical replication launcher
  20246 ?        Ssl    0:00  \_ /usr/bin/prometheus-postgres-exporter --log.level=info

pgBackRest is set up and initialized for started instances:

user@managed:~$ tree -L 2  $tmpdir/backups/backup
/tmp/.../backups/backup
├── stanza_dev
│   ├── backup.info
│   └── backup.info.copy
└── stanza_prod
    ├── backup.info
    └── backup.info.copy

And a systemd timer has been added for our instances:

user@managed:~$ systemctl --user list-timers
NEXT                          LEFT    LAST PASSED UNIT                               ACTIVATES
Sat 2023-09-30 00:00:00 CEST  7h left n/a  n/a    postgresql-backup@16-preprod.timer postgresql-backup@13-preprod.service
Sat 2023-00-30 00:00:00 CEST  7h left n/a  n/a    postgresql-backup@16-prod.timer    postgresql-backup@13-prod.service

2 timers listed.

Instances update

In the following version of our previous playbook, we are dropping the “preprod” instance and set the “dev” one to be started while changing its configuration a bit.

We also remove the pg_stat_statements from the shared_preload_libraries in the prod instance. For this to be taken into account, the instance needs to be restarted, hence the addition of restart_on_changes:

instances-update.yml
---
- name: my postgresql instances
  hosts: localhost
  tasks:
    - name: production instance
      dalibo.pglift.instance:
        name: prod
        port: 5433
        state: started
        restart_on_changes: true
        settings:
          max_connections: 100
          shared_buffers: 1GB
          unix_socket_directories: /tmp
          shared_preload_libraries: "passwordcheck"
        surole_password: "{{ postgresql_surole_password }}"
        pgbackrest:
          stanza: stanza_prod
          password: "{{ backup_role_password }}"
        prometheus:
          password: "{{ prometheus_role_password }}"
          port: 9186
      register: prod

    - name: db, dropped
      dalibo.pglift.database:
        name: db
        state: absent
        instance: prod

    - name: role bob, dropped
      dalibo.pglift.role:
        name: bob
        state: absent
        instance: prod
        drop_owned: true

    - name: role peter, dropped
      dalibo.pglift.role:
        name: peter
        state: absent
        instance: prod
        reassign_owned: postgres

    - name: pre-production instance, now dropped
      dalibo.pglift.instance:
        name: preprod
        state: absent
        pgbackrest:
          stanza: stanza_preprod

    - name: dev instance, re-configured and started
      dalibo.pglift.instance:
        name: dev
        port: 5455
        state: started
        settings:
          max_connections: 42
          unix_socket_directories: /tmp
        surole_password: "{{ postgresql_surole_password }}"
        pgbackrest:
          stanza: stanza_dev
          password: "{{ backup_role_password }}"
        prometheus:
          password: "{{ prometheus_role_password }}"
          port: 9189

As you can see you can feed third-party ansible modules (like community.postgresql) with libpq environment variables obtained by dalibo.pglift.instance or dalibo.pglift.dsn_info.

user@control:~$ ansible-playbook --extra-vars @pglift-vars --ask-vault-password instances-update.yml
PLAY [my postgresql instances] ***************************************************************************

...

PLAY RECAP ***********************************************************************************************
localhost                  : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
user@managed:~$ tree -L 2 $tmpdir/srv/pgsql
/tmp/.../srv/pgsql
└── 16
    ├── dev
    ├── preprod
    └── prod

Cleanup

Finally, in this last playbook, we drop all our instances:

instances-delete.yml
---
- name: my postgresql instances
  hosts: localhost
  tasks:
    - name: production instance, dropped
      dalibo.pglift.instance:
        name: prod
        state: absent
        pgbackrest:
          stanza: stanza_prod

    - name: preproduction instance, dropped
      dalibo.pglift.instance:
        name: preprod
        state: absent
        pgbackrest:
          stanza: stanza_preprod

    - name: dev instance, dropped
      dalibo.pglift.instance:
        name: dev
        state: absent
        pgbackrest:
          stanza: stanza_dev
user@control:~$ ansible-playbook --extra-vars @pglift-vars --ask-vault-password instances-delete.yml
PLAY [my postgresql instances] ***************************************************************************

...

PLAY RECAP ***********************************************************************************************
localhost                  : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0