setup_pgbouncer
The setup_pgbouncer role configures and starts pgBouncer on each pooled
pgEdge node. The pooler listens on pgbouncer_port and forwards every
database to the node's own PostgreSQL instance on 127.0.0.1, giving the node
a second endpoint that shares the cluster's roles and passwords but has its
own client authentication rules.
The role performs the following tasks on inventory hosts:
- Create
/etc/pgbouncerowned by the platform's pgBouncer service user. - Write a systemd drop-in that sets
Restart=alwaysand raisesLimitNOFILEto match the configured connection ceiling. - Write
userlist.txtcontaining the pooler's own PostgreSQL login. - Stage the TLS certificate and key the pooled endpoint presents to clients.
- Render the pooler's own
pg_hba.conffrom the cluster's HBA variables. - Render
pgbouncer.ini. - Mask the packaged
pgbouncer.socketunit on Debian-based systems. - Start and enable the pgBouncer service, restarting it only when something it reads has changed.
Role Dependencies
This role requires the following role for normal operation:
role_configprovides shared configuration variables to the role.
The role also requires the following to have run already:
install_pgbouncerinstalls the pgBouncer package.setup_postgrescreates the pooler's PostgreSQL login and thepgbouncer.get_authcredential lookup thatauth_querycalls. Both are created whereverpgbouncer_enabledis set.
When to Use
Execute this role on the pgEdge nodes after PostgreSQL is running: after
setup_postgres in a simple cluster, and after setup_patroni in a high
availability cluster. In an HA cluster the pooler forwards to the
node's local PostgreSQL whether that node is currently the primary or a
replica, and HAProxy decides which pooler receives traffic.
In the following example, the playbook configures the pooler only where the cluster pools:
- hosts: pgedge
collections:
- pgedge.platform
roles:
- setup_postgres
- role: setup_pgbouncer
when: pgbouncer_enabled | bool
Run the play against the whole pgedge group and gate the role on
pgbouncer_enabled, as shown. The pooler's HBA template reads
ansible_default_ipv4 out of hostvars for every pgEdge node and for the
zone's proxies, and a narrower play leaves those facts ungathered.
Configuration
This role uses the following parameters from the inventory file:
| Parameter | Use Case |
|---|---|
pgbouncer_port |
Port the pooler listens on (default: 6432). Must differ from pg_port. |
pgbouncer_listen_addr |
Addresses the pooler binds (default: *). |
pgbouncer_auth_user |
PostgreSQL role the pooler logs in as to look up client credentials. |
pgbouncer_auth_password |
Password for pgbouncer_auth_user. Must be changed from the default. |
pgbouncer_pool_mode |
session or transaction. See Pooling Configuration. |
pgbouncer_max_client_conn |
Client connections the pooler accepts. |
pgbouncer_default_pool_size |
Backend connections per user and database pair. |
pgbouncer_max_prepared_statements |
Protocol-level prepared statements tracked in transaction mode. |
pgbouncer_ignore_startup_parameters |
Startup parameters the pooler accepts and discards. |
pgbouncer_client_tls_sslmode |
TLS policy on the pooled endpoint. |
pgbouncer_tls_cert_source |
Controller-side certificate the pooler serves. |
pgbouncer_tls_key_source |
Controller-side key for that certificate. |
pgbouncer_hba_rules |
Client authentication rules for the pooled endpoint only. |
pgbouncer_limit_nofile |
File descriptor limit for the service unit. |
See the Pooling Configuration document for descriptions and defaults.
How It Works
The role writes every file the pooler reads before starting the service, so the first start already has the restart policy, the credentials, the rules and the certificate in place.
- Create
/etc/pgbouncermode0750, owned by the platform's pgBouncer service user. - Write
/etc/systemd/system/pgbouncer.service.d/override.confwithRestart=always,RestartSec=2,StartLimitIntervalSec=0andLimitNOFILE. - Write
/etc/pgbouncer/userlist.txtwith a single entry forpgbouncer_auth_user. - Copy the TLS certificate and key from the controller to
/etc/pgbouncer/, unlesspgbouncer_client_tls_sslmodeisdisable. - Render
/etc/pgbouncer/pg_hba.conf, the pooler's own client authentication rules. - Render
/etc/pgbouncer/pgbouncer.ini. - On Debian-based systems, stop, disable and mask
pgbouncer.socket. - Enable the service, and restart it when any of the files above changed.
Databases and Backends
The pooler routes every requested database to the node's local PostgreSQL:
[databases]
* = host=127.0.0.1 port=5432
The client's database name and role are forwarded as they arrive, so the
pooled endpoint exposes the same databases as the direct one. PostgreSQL
already admits that loopback connection: setup_postgres writes
host all all 127.0.0.1/32 scram-sha-256 in a simple cluster, and Patroni
renders the same rule in an HA one, so neither PostgreSQL's configuration nor
Patroni's changes for pooling.
Authentication
The pooler keeps no list of PostgreSQL passwords. It sets auth_type = hba,
so its own pg_hba.conf decides which method each client is offered, and then
resolves the requested role's stored SCRAM verifier by calling the lookup
setup_postgres created:
auth_user = pgbouncer_auth
auth_query = SELECT username, password FROM pgbouncer.get_auth($1)
auth_dbname = postgres
Any PostgreSQL role therefore works through the pooler, including a role
created long after deployment, and a rotated password takes effect
immediately. The lookup also honors VALID UNTIL: it returns no row once a
role's password has expired, so the pooler refuses the client rather than
passing a doomed login to PostgreSQL. userlist.txt holds one line, for pgbouncer_auth_user itself,
because the pooler has to authenticate to PostgreSQL before it can run the
lookup. That password is stored in plain text because a stored SCRAM verifier
cannot be used to log in; pgBouncer derives the verifier from it, so SCRAM is
still what goes on the wire.
admin_users and stats_users name pgbouncer_auth_user and nobody else.
The admin console cannot use auth_query, so its users must appear in
auth_file, and that is the only account there.
Client Authentication Rules
The pooler enforces its own rule file rather than PostgreSQL's. It cannot
share PostgreSQL's: on RHEL-based systems pg_hba.conf lives in a 0700
PGDATA owned by postgres while the pooler runs as pgbouncer. The rendered
file admits the following, in order:
pgbouncer_auth_userover the unix socket, for the admin console.postgresover the unix socket, mirroring PostgreSQL's own local rule.- Nothing else as
pgbouncer_auth_user: the next rules reject it on every address, including the replication pseudo-database, which theallkeyword does not cover. Its one privilege is the credential lookup, which returns any role's stored verifier, and the loopback and proxy rules below name every role — so without an explicit rejection it would be a network login as well. Administration is over the socket only. - The node's own loopback address.
- Every pgEdge node in the cluster, for
pgedge_useranddb_user. - The zone's HAProxy nodes, plus
proxy_nodewhen it is set, for every role. custom_hba_rules, which also apply to PostgreSQL.pgbouncer_hba_rules, which apply to the pooled endpoint alone.
The rules are rendered from the same variables that drive the PostgreSQL
rules, so the two lists stay in step. They differ deliberately in six ways:
physical replication rules are not mirrored, because a replication connection
cannot be pooled; the rules setup_backrest adds for the backup host are not
mirrored either; pgbouncer_hba_rules has no PostgreSQL counterpart; the
proxy rules admit every role rather than the cluster's two; the local rule
for the authentication user exists because the pooler has an admin console and
PostgreSQL does not; and that user is rejected on every host rule, which
PostgreSQL needs no counterpart to because it has no such account.
The proxy rules are the one place the pooled rules are wider than the direct ones. HAProxy's pooled listener is TCP passthrough, so the pooler sees the proxy's address rather than the client's, and a client-address rule cannot gate that path at all. Restrict it at the proxy or in the network.
TLS
PostgreSQL presents a certificate on every node, so a pooled client should not
have to drop to cleartext to use the pooler. The role stages the same
certificate setup_postgres generated, from the controller-side directory
rather than from PGDATA on the node, and points client_tls_cert_file at it.
The controller is the source because on an HA replica the PGDATA copy arrives
with Patroni's clone, which may not have finished when this role runs. The
pooler needs its own copy because on RHEL-based systems it runs as pgbouncer
and cannot read a 0700 PGDATA.
The default pgbouncer_client_tls_sslmode of allow accepts exactly what the
direct endpoint accepts today: TLS when the client asks for it, plaintext when
it does not. require makes TLS mandatory on the pooled endpoint. disable
turns it off and skips the certificate entirely, which also makes any
hostssl or hostnossl rule in the pooler's HBA file meaningless.
verify-ca and verify-full are not supported, because they need the
pg_hba.conf option syntax pgBouncer cannot parse.
Service Unit
Both packaged units ship LimitNOFILE commented out, leaving the pooler with
systemd's default of 1024, fewer descriptors than max_client_conn alone.
The drop-in budgets two descriptors per client connection plus a fixed
allowance for the idle server pools, the listening sockets, the log and the
admin console.
Restart=always with StartLimitIntervalSec=0 is the mitigation for the
deliberate failure mode: nothing fails a pooled client over to PostgreSQL,
because pooled and direct connections resolve against different pg_hba
rules and are not interchangeable. A dead pooler is an outage of the pooled
endpoint by design.
Usage Examples
In the following example, the playbook configures the pooler with defaults on every node of a pooled cluster:
- hosts: pgedge
collections:
- pgedge.platform
vars:
pgbouncer_auth_password: "{{ vault_pgbouncer_auth_password }}"
roles:
- role: setup_pgbouncer
when: pgbouncer_enabled | bool
In the following example, the playbook puts the pooler in transaction mode with prepared statement tracking, requires TLS on the pooled endpoint, and admits one application subnet to that endpoint alone:
- hosts: pgedge
collections:
- pgedge.platform
vars:
pgbouncer_auth_password: "{{ vault_pgbouncer_auth_password }}"
pgbouncer_pool_mode: transaction
pgbouncer_max_prepared_statements: 200
pgbouncer_client_tls_sslmode: require
pgbouncer_hba_rules:
- databases: demo
users: appuser
source: 10.0.4.0/24
method: scram-sha-256
roles:
- role: setup_pgbouncer
when: pgbouncer_enabled | bool
Artifacts
This role generates the following files on inventory hosts:
| File | New / Modified | Explanation |
|---|---|---|
/etc/pgbouncer/pgbouncer.ini |
Modified | Pooler configuration, replacing the packaged default. |
/etc/pgbouncer/pg_hba.conf |
New | Client authentication rules for the pooled endpoint. |
/etc/pgbouncer/userlist.txt |
New | The pooler's own PostgreSQL login, mode 0640. |
/etc/pgbouncer/server.crt |
New | Certificate presented to pooled clients. Skipped when client TLS is disabled. |
/etc/pgbouncer/server.key |
New | Key for that certificate, mode 0600. Skipped when client TLS is disabled. |
/etc/systemd/system/pgbouncer.service.d/override.conf |
New | Restart policy and file descriptor limit. |
Platform-Specific Behavior
The role uses each platform's own runtime paths and service user. It creates
/etc/pgbouncer and /etc/systemd/system/pgbouncer.service.d to hold the
files above, and no others: the log and socket directories come from the
package, and log rotation is left to whatever the platform already provides
rather than to a rule of the role's own.
| RHEL-based | Debian-based | |
|---|---|---|
| service user | pgbouncer |
postgres (the package creates no pgbouncer user) |
| log file | /var/log/pgbouncer/pgbouncer.log |
/var/log/postgresql/pgbouncer.log |
| pid file | /run/pgbouncer/pgbouncer.pid |
/var/run/postgresql/pgbouncer.pid |
| unix socket directory | /run/pgbouncer, mode 0700 |
/var/run/postgresql, mode 2775 |
| log rotation | /etc/logrotate.d/pgbouncer, from the package |
the pgedge-postgresql-common glob over /var/log/postgresql/*.log |
The socket directory permissions decide who can administer the pooler
locally: root on RHEL-based systems, root or postgres on Debian-based
ones. /var/run/postgresql is not an option on RHEL, where it is mode 0755
and owned by postgres, so the pooler cannot bind in it.
On Debian-based systems the role also masks pgbouncer.socket. The package
ships that unit disabled, for systemd socket activation, and this collection
does not use activation: the pooler is always-on, and pgbouncer.ini stays
the authority on the port and listen address. Masking keeps the two
mechanisms from both being live.
Idempotency
This role is idempotent and safe to re-run on inventory hosts. It restarts
pgBouncer only when the drop-in, the userlist, the certificate, the HBA file
or pgbouncer.ini changed, and otherwise ensures the service is running. The
restart is a task rather than a handler on purpose: with any_errors_fatal, a
notified handler never fires when a later role fails, which would leave the
pooler running stale configuration while Ansible reported the change as
applied.
Administering the Pooler
Reach the admin console over the unix socket as the authentication user,
which is the one account auth_file holds:
sudo psql -h /run/pgbouncer -p 6432 -U pgbouncer_auth pgbouncer -c 'SHOW POOLS'
Use -h /var/run/postgresql on Debian-based systems.