active_postgres 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 967673143f28952b4f44c54cb97282ca9ca288fe8885fc525433baac00633db0
4
- data.tar.gz: a43859d367af55c17c9a6a407a39e2e492cc829806ca615dd4f82b03604303f0
3
+ metadata.gz: e51d5f1214cedf497a9b060e5c667c43711f9ff5e36024bfd8dc926b50fe272f
4
+ data.tar.gz: 618fa70d39a43e1c74ba2ed449b75138670bca92cb6bf418606f772de34c6a7d
5
5
  SHA512:
6
- metadata.gz: f6dc9378f1391e20051028100f9767a65a9de62642719b4a413605a9208fa9a25b5e0c6790c9fdd0e966080293e5f33e9a5b9e860c5622cc20b65d933d70dae6
7
- data.tar.gz: 17b10e8adba8c2a502d0217f156ce68e7c2d1f4e4442fddd82b3ad8e560a940dc7a07a360b9a662dd8fbf0e8f80af7aff2f37bc6d9aaba7c0195b8590c32fbb3
6
+ metadata.gz: 254b6fb4342d4e510076d05ebfbeaa0e7990b463df13ad6ddb7d6eb5a70562c23cc4f502ce6c435c6372fc3dd5f695285ee3fdfed522cc188f1b1cc21c6cc90d
7
+ data.tar.gz: d9147a4679529344ccb0ffd3d5c2ffacc3072464b9cf4e1c6e11150a66c142ef0d0b45f9b9cd9d34eceba6f4d0e50baa7c2f227a5b2b1719502972e2b4b39ee4
data/README.md CHANGED
@@ -69,6 +69,8 @@ postgres:
69
69
  ```bash
70
70
  rake postgres:setup # Deploy cluster
71
71
  rake postgres:status # Check health
72
+ rake postgres:overview # Control tower overview
73
+ rake postgres:help # Task summary
72
74
  ```
73
75
 
74
76
  ## Common Operations
@@ -83,8 +85,10 @@ rake postgres:promote[host] # Promote standby to primary
83
85
 
84
86
  # Backups (requires pgBackRest)
85
87
  rake postgres:backup:full
88
+ rake postgres:backup:incremental
86
89
  rake postgres:backup:list
87
90
  rake postgres:backup:restore[backup_id]
91
+ rake postgres:backup:restore_at["2026-01-29 01:15:00",promote]
88
92
 
89
93
  # Credential rotation (zero downtime)
90
94
  rake postgres:credentials:rotate_random
@@ -101,6 +105,7 @@ rake postgres:update:patch # Security patches
101
105
  active_postgres setup --environment=production
102
106
  active_postgres setup-standby HOST
103
107
  active_postgres status
108
+ active_postgres overview
104
109
  active_postgres promote HOST
105
110
  active_postgres backup --type=full
106
111
  active_postgres cache-secrets
@@ -115,21 +120,37 @@ active_postgres cache-secrets
115
120
  | **repmgr** | HA & automatic failover | `repmgr: {enabled: true}` |
116
121
  | **PgBouncer** | Connection pooling | `pgbouncer: {enabled: true}` |
117
122
  | **pgBackRest** | Backup & restore | `pgbackrest: {enabled: true}` |
118
- | **Monitoring** | postgres_exporter (configures a dedicated pg_monitor user) | `monitoring: {enabled: true}` |
123
+ | **Monitoring** | postgres_exporter + optional node_exporter/Grafana | `monitoring: {enabled: true}` |
119
124
  | **SSL** | Encrypted connections | `ssl: {enabled: true}` |
120
125
  | **Extensions** | pgvector, PostGIS, etc. | `extensions: {enabled: true, list: [pgvector]}` |
121
126
 
122
127
  ### Monitoring credentials
123
128
 
124
- `postgres_exporter` uses a dedicated `pg_monitor` user. Provide a password in secrets and optionally set the username:
129
+ `postgres_exporter` uses a dedicated `pg_monitor` user. Provide a password in secrets and optionally set the username.
130
+ Enable node_exporter for host-level metrics, and Grafana if you want a local UI:
125
131
 
126
132
  ```yaml
127
133
  components:
128
- monitoring: {enabled: true, user: postgres_exporter}
134
+ monitoring:
135
+ enabled: true
136
+ user: postgres_exporter
137
+ node_exporter: true
138
+ node_exporter_port: 9100
139
+ # node_exporter_listen_address: 10.8.0.10
140
+ # grafana:
141
+ # enabled: true
142
+ # host: grafana.example.com
143
+ # listen_address: 10.8.0.10
144
+ # port: 3000
145
+ # prometheus_url: http://prometheus.example.com:9090
129
146
  secrets:
130
147
  monitoring_password: $POSTGRES_MONITORING_PASSWORD
148
+ # grafana_admin_password: $GRAFANA_ADMIN_PASSWORD
131
149
  ```
132
150
 
151
+ When Grafana is enabled, `grafana_admin_password` is required and Grafana will be installed
152
+ on the configured host. If `prometheus_url` is provided, a Prometheus datasource is provisioned.
153
+
133
154
  ### pgBackRest S3-compatible endpoints
134
155
 
135
156
  For Tigris/MinIO/Wasabi/DO Spaces, set a custom endpoint and use path-style URLs:
@@ -145,6 +166,35 @@ components:
145
166
  s3_uri_style: path
146
167
  ```
147
168
 
169
+ ### Point-in-time recovery (PITR)
170
+
171
+ Ensure WAL retention is long enough for your recovery window:
172
+
173
+ ```yaml
174
+ components:
175
+ pgbackrest:
176
+ retention_full: 7
177
+ retention_archive: 14
178
+ # You can schedule full + incremental separately:
179
+ # schedule_full: "0 2 * * *"
180
+ # schedule_incremental: "0 * * * *"
181
+ ```
182
+
183
+ Run a PITR restore:
184
+
185
+ ```bash
186
+ rake postgres:backup:restore_at["2026-01-29 01:15:00",promote]
187
+ ```
188
+
189
+ If you want the old primary to rejoin without a full re-clone after failover,
190
+ enable pg_rewind support in repmgr:
191
+
192
+ ```yaml
193
+ components:
194
+ repmgr:
195
+ use_rewind: true
196
+ ```
197
+
148
198
  ### Stable app endpoint with PgBouncer
149
199
 
150
200
  If you run PgBouncer on each PostgreSQL node and want a fixed app URL, enable `follow_primary`.
@@ -175,6 +225,8 @@ components:
175
225
  enabled: true
176
226
  provider: dnsmasq
177
227
  domain: mesh.internal
228
+ # Or use multiple domains during cutover:
229
+ # domains: [mesh.internal, mesh.v2.internal]
178
230
  dns_servers:
179
231
  - host: 18.170.173.14
180
232
  private_ip: 10.8.0.10
@@ -182,6 +234,9 @@ components:
182
234
  private_ip: 10.8.0.110
183
235
  primary_record: db-primary.mesh.internal
184
236
  replica_record: db-replica.mesh.internal
237
+ # Or multiple records explicitly:
238
+ # primary_records: [db-primary.mesh.internal, db-primary.mesh.v2.internal]
239
+ # replica_records: [db-replica.mesh.internal, db-replica.mesh.v2.internal]
185
240
  ```
186
241
 
187
242
  This installs an event hook so repmgr updates `/etc/dnsmasq.d/active_postgres.conf`
@@ -49,6 +49,13 @@ module ActivePostgres
49
49
  health_checker.show_status
50
50
  end
51
51
 
52
+ desc 'overview', 'Show control tower overview'
53
+ def overview
54
+ config = load_config
55
+ overview = Overview.new(config)
56
+ overview.show
57
+ end
58
+
52
59
  desc 'health', 'Run health checks'
53
60
  def health
54
61
  config = load_config
@@ -49,6 +49,11 @@ module ActivePostgres
49
49
  create_app_user_and_database(config.primary_host)
50
50
  end
51
51
 
52
+ def install_packages_only(host)
53
+ puts " Installing packages on #{host} (cluster will be created by repmgr)..."
54
+ ssh_executor.install_postgres(host, config.version)
55
+ end
56
+
52
57
  private
53
58
 
54
59
  def install_on_host(host, is_primary:)
@@ -96,11 +101,6 @@ module ActivePostgres
96
101
  optimal_settings.merge(user_postgresql)
97
102
  end
98
103
 
99
- def install_packages_only(host)
100
- puts " Installing packages on #{host} (cluster will be created by repmgr)..."
101
- ssh_executor.install_postgres(host, config.version)
102
- end
103
-
104
104
  def cluster_exists?(host)
105
105
  exists = false
106
106
  version = config.version
@@ -11,6 +11,8 @@ module ActivePostgres
11
11
  config.all_hosts.each do |host|
12
12
  install_on_host(host)
13
13
  end
14
+
15
+ install_grafana_if_enabled
14
16
  end
15
17
 
16
18
  def uninstall
@@ -21,8 +23,16 @@ module ActivePostgres
21
23
  execute :sudo, 'systemctl', 'stop', 'prometheus-postgres-exporter'
22
24
  execute :sudo, 'systemctl', 'disable', 'prometheus-postgres-exporter'
23
25
  execute :sudo, 'apt-get', 'remove', '-y', 'prometheus-postgres-exporter'
26
+
27
+ if node_exporter_enabled?
28
+ execute :sudo, 'systemctl', 'stop', 'prometheus-node-exporter'
29
+ execute :sudo, 'systemctl', 'disable', 'prometheus-node-exporter'
30
+ execute :sudo, 'apt-get', 'remove', '-y', 'prometheus-node-exporter'
31
+ end
24
32
  end
25
33
  end
34
+
35
+ uninstall_grafana_if_enabled
26
36
  end
27
37
 
28
38
  def restart
@@ -31,6 +41,7 @@ module ActivePostgres
31
41
  config.all_hosts.each do |host|
32
42
  ssh_executor.execute_on_host(host) do
33
43
  execute :sudo, 'systemctl', 'restart', 'prometheus-postgres-exporter'
44
+ execute :sudo, 'systemctl', 'restart', 'prometheus-node-exporter' if node_exporter_enabled?
34
45
  end
35
46
  end
36
47
  end
@@ -63,6 +74,8 @@ module ActivePostgres
63
74
  end
64
75
 
65
76
  puts " Metrics available at: http://#{host}:#{exporter_port}/metrics"
77
+
78
+ install_node_exporter(host, monitoring_config) if node_exporter_enabled?
66
79
  end
67
80
 
68
81
  def ensure_monitoring_user
@@ -134,6 +147,158 @@ module ActivePostgres
134
147
  password
135
148
  end
136
149
 
150
+ def node_exporter_enabled?
151
+ monitoring_config = config.component_config(:monitoring)
152
+ monitoring_config.fetch(:node_exporter, false) == true
153
+ end
154
+
155
+ def install_node_exporter(host, monitoring_config)
156
+ port = monitoring_config[:node_exporter_port] || 9100
157
+ listen_address = monitoring_config[:node_exporter_listen_address].to_s.strip
158
+ puts " Installing node_exporter on #{host}..."
159
+
160
+ ssh_executor.execute_on_host(host) do
161
+ execute :sudo, 'apt-get', 'install', '-y', '-qq', 'prometheus-node-exporter'
162
+ end
163
+
164
+ configure_node_exporter_service(host, port, listen_address)
165
+
166
+ ssh_executor.execute_on_host(host) do
167
+ execute :sudo, 'systemctl', 'enable', 'prometheus-node-exporter'
168
+ execute :sudo, 'systemctl', 'restart', 'prometheus-node-exporter'
169
+ end
170
+
171
+ puts " Node metrics available at: http://#{host}:#{port}/metrics"
172
+ end
173
+
174
+ def configure_node_exporter_service(host, port, listen_address)
175
+ listen = if listen_address.empty?
176
+ ":#{port}"
177
+ else
178
+ "#{listen_address}:#{port}"
179
+ end
180
+
181
+ return if listen == ':9100'
182
+
183
+ override = <<~CONF
184
+ [Service]
185
+ ExecStart=
186
+ ExecStart=/usr/bin/prometheus-node-exporter --web.listen-address=#{listen}
187
+ CONF
188
+
189
+ ssh_executor.execute_on_host(host) do
190
+ execute :sudo, 'mkdir', '-p', '/etc/systemd/system/prometheus-node-exporter.service.d'
191
+ upload! StringIO.new(override), '/tmp/prometheus-node-exporter.override'
192
+ execute :sudo, 'mv', '/tmp/prometheus-node-exporter.override',
193
+ '/etc/systemd/system/prometheus-node-exporter.service.d/override.conf'
194
+ execute :sudo, 'chown', 'root:root', '/etc/systemd/system/prometheus-node-exporter.service.d/override.conf'
195
+ execute :sudo, 'chmod', '644', '/etc/systemd/system/prometheus-node-exporter.service.d/override.conf'
196
+ execute :sudo, 'systemctl', 'daemon-reload'
197
+ end
198
+ end
199
+
200
+ def install_grafana_if_enabled
201
+ monitoring_config = config.component_config(:monitoring)
202
+ grafana_config = monitoring_config[:grafana] || {}
203
+ return unless grafana_config[:enabled]
204
+
205
+ host = grafana_config[:host].to_s.strip
206
+ raise Error, 'monitoring.grafana.host is required when grafana is enabled' if host.empty?
207
+
208
+ admin_password = normalize_grafana_password(secrets.resolve('grafana_admin_password'))
209
+ prometheus_url = grafana_config[:prometheus_url]
210
+ listen_address = grafana_config[:listen_address].to_s.strip
211
+ port = (grafana_config[:port] || 3000).to_i
212
+
213
+ puts "Installing Grafana on #{host}..."
214
+
215
+ ssh_executor.execute_on_host(host) do
216
+ execute :sudo, 'apt-get', 'install', '-y', '-qq', 'apt-transport-https', 'software-properties-common', 'wget'
217
+ execute :sudo, 'wget', '-q', '-O', '/usr/share/keyrings/grafana.gpg', 'https://apt.grafana.com/gpg.key'
218
+ execute :sudo, 'sh', '-c',
219
+ 'echo "deb [signed-by=/usr/share/keyrings/grafana.gpg] https://apt.grafana.com stable main" > /etc/apt/sources.list.d/grafana.list'
220
+ execute :sudo, 'apt-get', 'update', '-qq'
221
+ execute :sudo, 'apt-get', 'install', '-y', '-qq', 'grafana'
222
+ execute :sudo, 'systemctl', 'enable', '--now', 'grafana-server'
223
+ execute :sudo, 'grafana-cli', 'admin', 'reset-admin-password', admin_password
224
+ end
225
+
226
+ configure_grafana_service(host, listen_address, port)
227
+ configure_grafana_datasource(host, prometheus_url) if prometheus_url
228
+
229
+ puts " Grafana available at: http://#{host}:#{port}"
230
+ end
231
+
232
+ def configure_grafana_service(host, listen_address, port)
233
+ env_lines = []
234
+ env_lines << "Environment=\"GF_SERVER_HTTP_ADDR=#{listen_address}\"" unless listen_address.empty?
235
+ env_lines << "Environment=\"GF_SERVER_HTTP_PORT=#{port}\"" if port && port != 3000
236
+ return if env_lines.empty?
237
+
238
+ override = <<~CONF
239
+ [Service]
240
+ #{env_lines.join("\n ")}
241
+ CONF
242
+
243
+ ssh_executor.execute_on_host(host) do
244
+ execute :sudo, 'mkdir', '-p', '/etc/systemd/system/grafana-server.service.d'
245
+ upload! StringIO.new(override), '/tmp/active_postgres_grafana.override'
246
+ execute :sudo, 'mv', '/tmp/active_postgres_grafana.override',
247
+ '/etc/systemd/system/grafana-server.service.d/override.conf'
248
+ execute :sudo, 'chown', 'root:root', '/etc/systemd/system/grafana-server.service.d/override.conf'
249
+ execute :sudo, 'chmod', '644', '/etc/systemd/system/grafana-server.service.d/override.conf'
250
+ execute :sudo, 'systemctl', 'daemon-reload'
251
+ execute :sudo, 'systemctl', 'restart', 'grafana-server'
252
+ end
253
+ end
254
+
255
+ def uninstall_grafana_if_enabled
256
+ monitoring_config = config.component_config(:monitoring)
257
+ grafana_config = monitoring_config[:grafana] || {}
258
+ return unless grafana_config[:enabled]
259
+
260
+ host = grafana_config[:host].to_s.strip
261
+ return if host.empty?
262
+
263
+ ssh_executor.execute_on_host(host) do
264
+ execute :sudo, 'systemctl', 'stop', 'grafana-server'
265
+ execute :sudo, 'systemctl', 'disable', 'grafana-server'
266
+ execute :sudo, 'apt-get', 'remove', '-y', 'grafana'
267
+ execute :sudo, 'rm', '-f', '/etc/apt/sources.list.d/grafana.list'
268
+ execute :sudo, 'rm', '-f', '/usr/share/keyrings/grafana.gpg'
269
+ execute :sudo, 'rm', '-rf', '/etc/grafana/provisioning/datasources/active_postgres.yml'
270
+ end
271
+ end
272
+
273
+ def configure_grafana_datasource(host, prometheus_url)
274
+ datasource = <<~YAML
275
+ apiVersion: 1
276
+ datasources:
277
+ - name: Prometheus
278
+ type: prometheus
279
+ access: proxy
280
+ url: #{prometheus_url}
281
+ isDefault: true
282
+ YAML
283
+
284
+ ssh_executor.execute_on_host(host) do
285
+ execute :sudo, 'mkdir', '-p', '/etc/grafana/provisioning/datasources'
286
+ upload! StringIO.new(datasource), '/tmp/active_postgres_grafana_ds.yml'
287
+ execute :sudo, 'mv', '/tmp/active_postgres_grafana_ds.yml', '/etc/grafana/provisioning/datasources/active_postgres.yml'
288
+ execute :sudo, 'chown', 'root:root', '/etc/grafana/provisioning/datasources/active_postgres.yml'
289
+ execute :sudo, 'chmod', '644', '/etc/grafana/provisioning/datasources/active_postgres.yml'
290
+ execute :sudo, 'systemctl', 'restart', 'grafana-server'
291
+ end
292
+ end
293
+
294
+ def normalize_grafana_password(raw_password)
295
+ password = raw_password.to_s.rstrip
296
+
297
+ raise Error, 'grafana_admin_password secret is missing (required for grafana)' if password.empty?
298
+
299
+ password
300
+ end
301
+
137
302
  def escape_systemd_env(value)
138
303
  value.to_s.gsub('\\', '\\\\').gsub('"', '\\"')
139
304
  end
@@ -20,6 +20,7 @@ module ActivePostgres
20
20
  ssh_executor.execute_on_host(host) do
21
21
  execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
22
22
  execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
23
+ execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
23
24
  end
24
25
  end
25
26
  end
@@ -37,9 +38,12 @@ module ActivePostgres
37
38
  puts "Running #{type} backup..."
38
39
  postgres_user = config.postgres_user
39
40
 
41
+ output = nil
40
42
  ssh_executor.execute_on_host(config.primary_host) do
41
- execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--type=#{type}", 'backup'
43
+ output = capture(:sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--type=#{type}", 'backup')
42
44
  end
45
+
46
+ puts output if output
43
47
  end
44
48
 
45
49
  def run_restore(backup_id)
@@ -51,20 +55,43 @@ module ActivePostgres
51
55
  execute :sudo, 'systemctl', 'stop', 'postgresql'
52
56
 
53
57
  # Restore
54
- execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--set=#{backup_id}", 'restore'
58
+ output = capture(:sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--set=#{backup_id}", 'restore')
59
+ puts output if output
55
60
 
56
61
  # Start PostgreSQL
57
62
  execute :sudo, 'systemctl', 'start', 'postgresql'
58
63
  end
59
64
  end
60
65
 
66
+ def run_restore_at(target_time, target_action: 'promote')
67
+ puts "Restoring to #{target_time} (PITR)..."
68
+ postgres_user = config.postgres_user
69
+
70
+ ssh_executor.execute_on_host(config.primary_host) do
71
+ execute :sudo, 'systemctl', 'stop', 'postgresql'
72
+
73
+ output = capture(:sudo, '-u', postgres_user, 'pgbackrest',
74
+ '--stanza=main',
75
+ '--type=time',
76
+ "--target=#{target_time}",
77
+ "--target-action=#{target_action}",
78
+ 'restore')
79
+ puts output if output
80
+
81
+ execute :sudo, 'systemctl', 'start', 'postgresql'
82
+ end
83
+ end
84
+
61
85
  def list_backups
62
86
  puts 'Available backups:'
63
87
  postgres_user = config.postgres_user
64
88
 
89
+ output = nil
65
90
  ssh_executor.execute_on_host(config.primary_host) do
66
- execute :sudo, '-u', postgres_user, 'pgbackrest', 'info'
91
+ output = capture(:sudo, '-u', postgres_user, 'pgbackrest', 'info')
67
92
  end
93
+
94
+ puts output if output
68
95
  end
69
96
 
70
97
  private
@@ -108,13 +135,35 @@ module ActivePostgres
108
135
  end
109
136
 
110
137
  # Set up scheduled backups on primary only
111
- if create_stanza && pgbackrest_config[:schedule]
112
- setup_backup_schedule(host, pgbackrest_config[:schedule])
138
+ if create_stanza
139
+ setup_backup_schedules(host, pgbackrest_config)
140
+ end
141
+ end
142
+
143
+ def setup_backup_schedules(host, pgbackrest_config)
144
+ schedules = backup_schedules(pgbackrest_config)
145
+ schedules.each do |entry|
146
+ setup_backup_schedule(host, entry[:schedule], entry[:type], entry[:file])
147
+ end
148
+ end
149
+
150
+ def backup_schedules(pgbackrest_config)
151
+ schedule_full = pgbackrest_config[:schedule_full] || pgbackrest_config[:schedule]
152
+ schedule_incremental = pgbackrest_config[:schedule_incremental]
153
+
154
+ schedules = []
155
+ if schedule_full
156
+ schedules << { type: 'full', schedule: schedule_full, file: '/etc/cron.d/pgbackrest-backup' }
113
157
  end
158
+ if schedule_incremental
159
+ schedules << { type: 'incremental', schedule: schedule_incremental, file: '/etc/cron.d/pgbackrest-backup-incremental' }
160
+ end
161
+
162
+ schedules
114
163
  end
115
164
 
116
- def setup_backup_schedule(host, schedule)
117
- puts " Setting up backup schedule: #{schedule}"
165
+ def setup_backup_schedule(host, schedule, backup_type = 'full', cron_file = '/etc/cron.d/pgbackrest-backup')
166
+ puts " Setting up #{backup_type} backup schedule: #{schedule}"
118
167
  postgres_user = config.postgres_user
119
168
 
120
169
  # Create cron job for scheduled backups
@@ -123,17 +172,18 @@ module ActivePostgres
123
172
  # pgBackRest scheduled backups (managed by active_postgres)
124
173
  SHELL=/bin/bash
125
174
  PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
126
- #{schedule} #{postgres_user} pgbackrest --stanza=main --type=full backup
175
+ #{schedule} #{postgres_user} pgbackrest --stanza=main --type=#{backup_type} backup
127
176
  CRON
128
177
 
129
178
  # Install cron job in /etc/cron.d (system cron directory)
130
179
  # Use a temp file + sudo mv to avoid shell redirection permission issues.
131
- ssh_executor.upload_file(host, cron_content, '/etc/cron.d/pgbackrest-backup', mode: '644', owner: 'root:root')
180
+ ssh_executor.upload_file(host, cron_content, cron_file, mode: '644', owner: 'root:root')
132
181
  end
133
182
 
134
183
  def remove_backup_schedule(host)
135
184
  ssh_executor.execute_on_host(host) do
136
185
  execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
186
+ execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
137
187
  end
138
188
  end
139
189
  end
@@ -5,7 +5,8 @@ module ActivePostgres
5
5
  puts 'Installing PgBouncer for connection pooling...'
6
6
 
7
7
  config.all_hosts.each do |host|
8
- install_on_host(host)
8
+ is_standby = config.standby_hosts.include?(host)
9
+ install_on_host(host, is_standby: is_standby)
9
10
  end
10
11
  end
11
12
 
@@ -49,16 +50,26 @@ module ActivePostgres
49
50
 
50
51
  def install_on_standby(standby_host)
51
52
  puts "Installing PgBouncer on standby #{standby_host}..."
52
- install_on_host(standby_host)
53
+ install_on_host(standby_host, is_standby: true)
53
54
  end
54
55
 
55
56
  private
56
57
 
57
- def install_on_host(host)
58
+ def install_on_host(host, is_standby: false)
58
59
  puts " Installing PgBouncer on #{host}..."
59
60
 
60
61
  user_config = config.component_config(:pgbouncer)
61
- follow_primary = user_config[:follow_primary] == true
62
+
63
+ # Determine follow_primary behavior:
64
+ # - Primary: use global follow_primary setting
65
+ # - Standby: check per-standby pgbouncer_follow_primary, default false (use localhost for read replicas)
66
+ if is_standby
67
+ standby_config = config.standby_config_for(host) || {}
68
+ follow_primary = standby_config['pgbouncer_follow_primary'] == true
69
+ else
70
+ follow_primary = user_config[:follow_primary] == true
71
+ end
72
+
62
73
  if follow_primary && !config.component_enabled?(:repmgr)
63
74
  raise Error, 'PgBouncer follow_primary requires repmgr to be enabled'
64
75
  end
@@ -67,7 +78,8 @@ module ActivePostgres
67
78
  optimal_pool = ConnectionPooler.calculate_optimal_pool_sizes(max_connections)
68
79
 
69
80
  pgbouncer_config = optimal_pool.merge(user_config)
70
- pgbouncer_config[:database_host] = config.primary_replication_host if follow_primary
81
+ # For standbys not following primary, use localhost; otherwise use primary host
82
+ pgbouncer_config[:database_host] = follow_primary ? config.primary_replication_host : '127.0.0.1'
71
83
  ssl_enabled = config.component_enabled?(:ssl)
72
84
  has_ca_cert = ssl_enabled && secrets.resolve('ssl_chain')
73
85
  secrets_obj = secrets