active_postgres 0.8.0 → 0.9.0
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 +4 -4
- data/README.md +87 -5
- data/lib/active_postgres/components/core.rb +2 -7
- data/lib/active_postgres/components/extensions.rb +40 -35
- data/lib/active_postgres/components/monitoring.rb +91 -4
- data/lib/active_postgres/components/pgbackrest.rb +38 -2
- data/lib/active_postgres/components/pgbouncer.rb +43 -4
- data/lib/active_postgres/components/repmgr.rb +431 -62
- data/lib/active_postgres/configuration.rb +49 -1
- data/lib/active_postgres/connection_pooler.rb +3 -12
- data/lib/active_postgres/credentials.rb +3 -3
- data/lib/active_postgres/direct_executor.rb +1 -2
- data/lib/active_postgres/generators/active_postgres/install_generator.rb +1 -0
- data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +18 -1
- data/lib/active_postgres/rollback_manager.rb +4 -8
- data/lib/active_postgres/secrets.rb +23 -5
- data/lib/active_postgres/ssh_executor.rb +36 -14
- data/lib/active_postgres/version.rb +1 -1
- data/lib/tasks/postgres.rake +10 -4
- data/lib/tasks/rotate_credentials.rake +4 -16
- data/templates/pg_hba.conf.erb +4 -1
- data/templates/pgbackrest.conf.erb +28 -0
- data/templates/pgbouncer-follow-primary.service.erb +8 -0
- data/templates/pgbouncer-follow-primary.timer.erb +11 -0
- data/templates/pgbouncer.ini.erb +2 -0
- data/templates/pgbouncer_follow_primary.sh.erb +34 -0
- data/templates/postgresql.conf.erb +4 -0
- data/templates/repmgr.conf.erb +7 -3
- data/templates/repmgr_dns_failover.sh.erb +49 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 967673143f28952b4f44c54cb97282ca9ca288fe8885fc525433baac00633db0
|
|
4
|
+
data.tar.gz: a43859d367af55c17c9a6a407a39e2e492cc829806ca615dd4f82b03604303f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6dc9378f1391e20051028100f9767a65a9de62642719b4a413605a9208fa9a25b5e0c6790c9fdd0e966080293e5f33e9a5b9e860c5622cc20b65d933d70dae6
|
|
7
|
+
data.tar.gz: 17b10e8adba8c2a502d0217f156ce68e7c2d1f4e4442fddd82b3ad8e560a940dc7a07a360b9a662dd8fbf0e8f80af7aff2f37bc6d9aaba7c0195b8590c32fbb3
|
data/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Production-grade PostgreSQL HA for Rails.
|
|
|
7
7
|
- **High Availability**: Primary/standby replication with automatic failover (repmgr)
|
|
8
8
|
- **Connection Pooling**: PgBouncer integration
|
|
9
9
|
- **Rails Integration**: Automatic database.yml config, migration guard, read replica routing
|
|
10
|
-
- **Modular Components**: Core, Performance Tuning, repmgr, PgBouncer, pgBackRest, Monitoring, SSL, Extensions
|
|
10
|
+
- **Modular Components**: Core, Performance Tuning (opt-in), repmgr, PgBouncer, pgBackRest, Monitoring, SSL, Extensions
|
|
11
11
|
|
|
12
12
|
## Quick Start
|
|
13
13
|
|
|
@@ -48,6 +48,7 @@ production:
|
|
|
48
48
|
superuser_password: $POSTGRES_SUPERUSER_PASSWORD
|
|
49
49
|
replication_password: $POSTGRES_REPLICATION_PASSWORD
|
|
50
50
|
repmgr_password: $POSTGRES_REPMGR_PASSWORD
|
|
51
|
+
monitoring_password: $POSTGRES_MONITORING_PASSWORD
|
|
51
52
|
app_password: $POSTGRES_APP_PASSWORD
|
|
52
53
|
```
|
|
53
54
|
|
|
@@ -110,14 +111,84 @@ active_postgres cache-secrets
|
|
|
110
111
|
| Component | Description | Config |
|
|
111
112
|
|-----------|-------------|--------|
|
|
112
113
|
| **Core** | PostgreSQL installation | Always enabled |
|
|
113
|
-
| **Performance Tuning** | Auto-optimization |
|
|
114
|
+
| **Performance Tuning** | Auto-optimization | Disabled by default |
|
|
114
115
|
| **repmgr** | HA & automatic failover | `repmgr: {enabled: true}` |
|
|
115
116
|
| **PgBouncer** | Connection pooling | `pgbouncer: {enabled: true}` |
|
|
116
117
|
| **pgBackRest** | Backup & restore | `pgbackrest: {enabled: true}` |
|
|
117
|
-
| **Monitoring** | postgres_exporter | `monitoring: {enabled: true}` |
|
|
118
|
+
| **Monitoring** | postgres_exporter (configures a dedicated pg_monitor user) | `monitoring: {enabled: true}` |
|
|
118
119
|
| **SSL** | Encrypted connections | `ssl: {enabled: true}` |
|
|
119
120
|
| **Extensions** | pgvector, PostGIS, etc. | `extensions: {enabled: true, list: [pgvector]}` |
|
|
120
121
|
|
|
122
|
+
### Monitoring credentials
|
|
123
|
+
|
|
124
|
+
`postgres_exporter` uses a dedicated `pg_monitor` user. Provide a password in secrets and optionally set the username:
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
components:
|
|
128
|
+
monitoring: {enabled: true, user: postgres_exporter}
|
|
129
|
+
secrets:
|
|
130
|
+
monitoring_password: $POSTGRES_MONITORING_PASSWORD
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### pgBackRest S3-compatible endpoints
|
|
134
|
+
|
|
135
|
+
For Tigris/MinIO/Wasabi/DO Spaces, set a custom endpoint and use path-style URLs:
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
components:
|
|
139
|
+
pgbackrest:
|
|
140
|
+
enabled: true
|
|
141
|
+
repo_type: s3
|
|
142
|
+
s3_bucket: myapp-backups
|
|
143
|
+
s3_region: auto
|
|
144
|
+
s3_endpoint: t3.storage.dev
|
|
145
|
+
s3_uri_style: path
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Stable app endpoint with PgBouncer
|
|
149
|
+
|
|
150
|
+
If you run PgBouncer on each PostgreSQL node and want a fixed app URL, enable `follow_primary`.
|
|
151
|
+
Each PgBouncer instance periodically repoints to the current primary using repmgr metadata.
|
|
152
|
+
Put a TCP load balancer or DNS record in front of the PgBouncer nodes.
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
components:
|
|
156
|
+
pgbouncer:
|
|
157
|
+
enabled: true
|
|
158
|
+
follow_primary: true
|
|
159
|
+
follow_primary_interval: 5
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Automatic DNS updates on failover (dnsmasq)
|
|
163
|
+
|
|
164
|
+
If you use Messhy’s mesh DNS (dnsmasq), repmgr can update writer/reader DNS records on failover.
|
|
165
|
+
Enable `dns_failover` under `repmgr` and provide DNS server IPs or hostnames.
|
|
166
|
+
If you run setup from outside the mesh, use objects with both the public SSH host
|
|
167
|
+
and the private WireGuard IP so setup can SSH in and failover updates stay on the mesh.
|
|
168
|
+
If you run setup from inside the mesh, you can use a simple list of private IPs.
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
components:
|
|
172
|
+
repmgr:
|
|
173
|
+
enabled: true
|
|
174
|
+
dns_failover:
|
|
175
|
+
enabled: true
|
|
176
|
+
provider: dnsmasq
|
|
177
|
+
domain: mesh.internal
|
|
178
|
+
dns_servers:
|
|
179
|
+
- host: 18.170.173.14
|
|
180
|
+
private_ip: 10.8.0.10
|
|
181
|
+
- host: 98.85.183.175
|
|
182
|
+
private_ip: 10.8.0.110
|
|
183
|
+
primary_record: db-primary.mesh.internal
|
|
184
|
+
replica_record: db-replica.mesh.internal
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This installs an event hook so repmgr updates `/etc/dnsmasq.d/active_postgres.conf`
|
|
188
|
+
on the DNS servers whenever the primary changes.
|
|
189
|
+
DNS servers must be reachable over the private network and allow SSH from the
|
|
190
|
+
database nodes (active_postgres installs SSH keys on first setup).
|
|
191
|
+
|
|
121
192
|
## Secrets Management
|
|
122
193
|
|
|
123
194
|
```yaml
|
|
@@ -127,7 +198,8 @@ secrets:
|
|
|
127
198
|
|
|
128
199
|
# Command execution
|
|
129
200
|
password: $(op read "op://vault/item/field")
|
|
130
|
-
password:
|
|
201
|
+
password: rails_credentials:postgres.password
|
|
202
|
+
password: credentials:postgres.password
|
|
131
203
|
|
|
132
204
|
# AWS Secrets Manager
|
|
133
205
|
password: $(aws secretsmanager get-secret-value --secret-id myapp/postgres --query SecretString)
|
|
@@ -150,9 +222,19 @@ User.all # → Replica
|
|
|
150
222
|
- Ruby 3.0+
|
|
151
223
|
- PostgreSQL 12+
|
|
152
224
|
- Ubuntu 20.04+ / Debian 11+ with systemd
|
|
153
|
-
- SSH key-based authentication
|
|
225
|
+
- SSH key-based authentication (hosts must be in `~/.ssh/known_hosts`)
|
|
154
226
|
- Rails 6.0+ (optional)
|
|
155
227
|
|
|
228
|
+
### SSH host key verification
|
|
229
|
+
|
|
230
|
+
By default, host keys are verified strictly (`always`). For first-time provisioning you can set:
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
ssh_host_key_verification: accept_new
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
This trusts a host key the first time and then pins it; prefer `always` once hosts are known.
|
|
237
|
+
|
|
156
238
|
## License
|
|
157
239
|
|
|
158
240
|
MIT
|
|
@@ -189,14 +189,9 @@ module ActivePostgres
|
|
|
189
189
|
app_password = resolve_app_password
|
|
190
190
|
sql = build_app_user_sql(app_user, app_database, app_password)
|
|
191
191
|
|
|
192
|
-
ssh_executor.
|
|
193
|
-
upload! StringIO.new(sql), '/tmp/create_app_user.sql'
|
|
194
|
-
execute :chmod, '644', '/tmp/create_app_user.sql'
|
|
195
|
-
execute :sudo, '-u', 'postgres', 'psql', '-f', '/tmp/create_app_user.sql'
|
|
196
|
-
execute :rm, '-f', '/tmp/create_app_user.sql'
|
|
192
|
+
ssh_executor.run_sql(host, sql, postgres_user: 'postgres', tuples_only: false, capture: false)
|
|
197
193
|
|
|
198
|
-
|
|
199
|
-
end
|
|
194
|
+
puts " ✓ Created app user '#{app_user}' and database '#{app_database}'"
|
|
200
195
|
rescue StandardError => e
|
|
201
196
|
warn " Warning: Could not create app user: #{e.message}"
|
|
202
197
|
warn ' You may need to create the user manually'
|
|
@@ -33,17 +33,27 @@ module ActivePostgres
|
|
|
33
33
|
puts 'Extensions uninstall not implemented (extensions remain in database)'
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
def restart
|
|
37
|
+
puts 'Extensions do not require restart (loaded at database connection time)'
|
|
38
|
+
end
|
|
37
39
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
db_name = config.secrets_config['database_name'] || 'postgres'
|
|
42
|
-
postgres_user = config.postgres_user
|
|
40
|
+
def install_on_standby(standby_host)
|
|
41
|
+
extensions_config = config.component_config(:extensions)
|
|
42
|
+
return unless extensions_config[:enabled]
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
extensions = extensions_config[:list] || []
|
|
45
|
+
return if extensions.empty?
|
|
46
|
+
|
|
47
|
+
puts "Installing extension packages on standby #{standby_host}..."
|
|
48
|
+
install_packages_on_host(standby_host, extensions)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
45
52
|
|
|
53
|
+
def install_packages_on_host(host, extensions)
|
|
54
|
+
version = config.version
|
|
46
55
|
packages_to_install = []
|
|
56
|
+
|
|
47
57
|
extensions.each do |ext_name|
|
|
48
58
|
package = EXTENSION_PACKAGES[ext_name]
|
|
49
59
|
next unless package
|
|
@@ -52,46 +62,41 @@ module ActivePostgres
|
|
|
52
62
|
packages_to_install << package
|
|
53
63
|
end
|
|
54
64
|
|
|
65
|
+
return if packages_to_install.empty?
|
|
66
|
+
|
|
55
67
|
ssh_executor.execute_on_host(host) do
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
execute :sudo, 'apt-get', 'update', '-qq'
|
|
69
|
+
execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq',
|
|
70
|
+
*packages_to_install
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def install_on_primary(extensions)
|
|
75
|
+
host = config.primary_host
|
|
76
|
+
db_name = config.secrets_config['database_name'] || 'postgres'
|
|
77
|
+
postgres_user = config.postgres_user
|
|
78
|
+
|
|
79
|
+
puts " Installing extensions on primary (#{host})..."
|
|
80
|
+
|
|
81
|
+
install_packages_on_host(host, extensions)
|
|
61
82
|
|
|
83
|
+
ssh_executor.execute_on_host(host) do
|
|
62
84
|
extensions.each do |ext_name|
|
|
63
|
-
sql = "CREATE EXTENSION IF NOT EXISTS #{ext_name};"
|
|
85
|
+
sql = "CREATE EXTENSION IF NOT EXISTS \"#{ext_name}\";"
|
|
64
86
|
begin
|
|
65
87
|
execute :sudo, '-u', postgres_user, 'psql', '-d', db_name, '-c', sql
|
|
66
|
-
|
|
67
|
-
|
|
88
|
+
info "✓ Extension #{ext_name} created/verified"
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
warn "⚠ Could not create extension #{ext_name}: #{e.message}"
|
|
68
91
|
end
|
|
69
92
|
end
|
|
70
93
|
end
|
|
71
94
|
end
|
|
72
95
|
|
|
73
96
|
def install_on_standbys(extensions)
|
|
74
|
-
version = config.version
|
|
75
|
-
|
|
76
97
|
config.standby_hosts.each do |host|
|
|
77
|
-
puts " Installing
|
|
78
|
-
|
|
79
|
-
packages_to_install = []
|
|
80
|
-
extensions.each do |ext_name|
|
|
81
|
-
package = EXTENSION_PACKAGES[ext_name]
|
|
82
|
-
next unless package
|
|
83
|
-
|
|
84
|
-
package = package.gsub('{version}', version.to_s)
|
|
85
|
-
packages_to_install << package
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
ssh_executor.execute_on_host(host) do
|
|
89
|
-
unless packages_to_install.empty?
|
|
90
|
-
execute :sudo, 'apt-get', 'update', '-qq'
|
|
91
|
-
execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq',
|
|
92
|
-
*packages_to_install
|
|
93
|
-
end
|
|
94
|
-
end
|
|
98
|
+
puts " Installing extension packages on standby (#{host})..."
|
|
99
|
+
install_packages_on_host(host, extensions)
|
|
95
100
|
end
|
|
96
101
|
end
|
|
97
102
|
end
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
require 'cgi'
|
|
2
|
+
|
|
1
3
|
module ActivePostgres
|
|
2
4
|
module Components
|
|
3
5
|
class Monitoring < Base
|
|
4
6
|
def install
|
|
5
7
|
puts 'Installing postgres_exporter for monitoring...'
|
|
6
8
|
|
|
9
|
+
ensure_monitoring_user
|
|
10
|
+
|
|
7
11
|
config.all_hosts.each do |host|
|
|
8
12
|
install_on_host(host)
|
|
9
13
|
end
|
|
@@ -14,8 +18,9 @@ module ActivePostgres
|
|
|
14
18
|
|
|
15
19
|
config.all_hosts.each do |host|
|
|
16
20
|
ssh_executor.execute_on_host(host) do
|
|
17
|
-
execute :sudo, 'systemctl', 'stop', '
|
|
18
|
-
execute :sudo, 'systemctl', 'disable', '
|
|
21
|
+
execute :sudo, 'systemctl', 'stop', 'prometheus-postgres-exporter'
|
|
22
|
+
execute :sudo, 'systemctl', 'disable', 'prometheus-postgres-exporter'
|
|
23
|
+
execute :sudo, 'apt-get', 'remove', '-y', 'prometheus-postgres-exporter'
|
|
19
24
|
end
|
|
20
25
|
end
|
|
21
26
|
end
|
|
@@ -25,11 +30,16 @@ module ActivePostgres
|
|
|
25
30
|
|
|
26
31
|
config.all_hosts.each do |host|
|
|
27
32
|
ssh_executor.execute_on_host(host) do
|
|
28
|
-
execute :sudo, 'systemctl', 'restart', '
|
|
33
|
+
execute :sudo, 'systemctl', 'restart', 'prometheus-postgres-exporter'
|
|
29
34
|
end
|
|
30
35
|
end
|
|
31
36
|
end
|
|
32
37
|
|
|
38
|
+
def install_on_standby(standby_host)
|
|
39
|
+
puts "Installing postgres_exporter on standby #{standby_host}..."
|
|
40
|
+
install_on_host(standby_host)
|
|
41
|
+
end
|
|
42
|
+
|
|
33
43
|
private
|
|
34
44
|
|
|
35
45
|
def install_on_host(host)
|
|
@@ -42,14 +52,91 @@ module ActivePostgres
|
|
|
42
52
|
ssh_executor.execute_on_host(host) do
|
|
43
53
|
# Install via package manager or download binary
|
|
44
54
|
execute :sudo, 'apt-get', 'install', '-y', '-qq', 'prometheus-postgres-exporter'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
configure_exporter_service(host, monitoring_config, exporter_port)
|
|
45
58
|
|
|
59
|
+
ssh_executor.execute_on_host(host) do
|
|
46
60
|
# Enable and start
|
|
47
61
|
execute :sudo, 'systemctl', 'enable', 'prometheus-postgres-exporter'
|
|
48
|
-
execute :sudo, 'systemctl', '
|
|
62
|
+
execute :sudo, 'systemctl', 'restart', 'prometheus-postgres-exporter'
|
|
49
63
|
end
|
|
50
64
|
|
|
51
65
|
puts " Metrics available at: http://#{host}:#{exporter_port}/metrics"
|
|
52
66
|
end
|
|
67
|
+
|
|
68
|
+
def ensure_monitoring_user
|
|
69
|
+
monitoring_config = config.component_config(:monitoring)
|
|
70
|
+
monitoring_user = monitoring_config[:user] || 'postgres_exporter'
|
|
71
|
+
monitoring_password = normalize_monitoring_password(secrets.resolve('monitoring_password'))
|
|
72
|
+
|
|
73
|
+
sql = build_monitoring_user_sql(monitoring_user, monitoring_password)
|
|
74
|
+
|
|
75
|
+
ssh_executor.run_sql(config.primary_host, sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
76
|
+
capture: false)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_monitoring_user_sql(user, password)
|
|
80
|
+
escaped_password = password.gsub("'", "''")
|
|
81
|
+
|
|
82
|
+
[
|
|
83
|
+
'DO $$',
|
|
84
|
+
'BEGIN',
|
|
85
|
+
" IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '#{user}') THEN",
|
|
86
|
+
" CREATE USER #{user} WITH LOGIN PASSWORD '#{escaped_password}';",
|
|
87
|
+
' ELSE',
|
|
88
|
+
" ALTER USER #{user} WITH LOGIN PASSWORD '#{escaped_password}';",
|
|
89
|
+
' END IF;',
|
|
90
|
+
'END $$;',
|
|
91
|
+
'',
|
|
92
|
+
"GRANT pg_monitor TO #{user};",
|
|
93
|
+
''
|
|
94
|
+
].join("\n")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def configure_exporter_service(host, monitoring_config, exporter_port)
|
|
98
|
+
monitoring_user = monitoring_config[:user] || 'postgres_exporter'
|
|
99
|
+
monitoring_password = normalize_monitoring_password(secrets.resolve('monitoring_password'))
|
|
100
|
+
|
|
101
|
+
dsn = build_exporter_dsn(monitoring_config, monitoring_user, monitoring_password)
|
|
102
|
+
override = <<~CONF
|
|
103
|
+
[Service]
|
|
104
|
+
Environment="DATA_SOURCE_NAME=#{escape_systemd_env(dsn)}"
|
|
105
|
+
Environment="PG_EXPORTER_WEB_LISTEN_ADDRESS=:#{exporter_port}"
|
|
106
|
+
CONF
|
|
107
|
+
|
|
108
|
+
ssh_executor.execute_on_host(host) do
|
|
109
|
+
execute :sudo, 'mkdir', '-p', '/etc/systemd/system/prometheus-postgres-exporter.service.d'
|
|
110
|
+
upload! StringIO.new(override), '/tmp/prometheus-postgres-exporter.override'
|
|
111
|
+
execute :sudo, 'mv', '/tmp/prometheus-postgres-exporter.override',
|
|
112
|
+
'/etc/systemd/system/prometheus-postgres-exporter.service.d/override.conf'
|
|
113
|
+
execute :sudo, 'chown', 'root:root', '/etc/systemd/system/prometheus-postgres-exporter.service.d/override.conf'
|
|
114
|
+
execute :sudo, 'chmod', '600', '/etc/systemd/system/prometheus-postgres-exporter.service.d/override.conf'
|
|
115
|
+
execute :sudo, 'systemctl', 'daemon-reload'
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_exporter_dsn(monitoring_config, monitoring_user, monitoring_password)
|
|
120
|
+
host = monitoring_config[:database_host] || 'localhost'
|
|
121
|
+
port = monitoring_config[:database_port] || 5432
|
|
122
|
+
database = monitoring_config[:database] || 'postgres'
|
|
123
|
+
sslmode = config.component_enabled?(:ssl) ? 'require' : 'prefer'
|
|
124
|
+
|
|
125
|
+
encoded_password = CGI.escape(monitoring_password.to_s)
|
|
126
|
+
"postgresql://#{monitoring_user}:#{encoded_password}@#{host}:#{port}/#{database}?sslmode=#{sslmode}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def normalize_monitoring_password(raw_password)
|
|
130
|
+
password = raw_password.to_s.rstrip
|
|
131
|
+
|
|
132
|
+
raise Error, 'monitoring_password secret is missing (required for monitoring)' if password.empty?
|
|
133
|
+
|
|
134
|
+
password
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def escape_systemd_env(value)
|
|
138
|
+
value.to_s.gsub('\\', '\\\\').gsub('"', '\\"')
|
|
139
|
+
end
|
|
53
140
|
end
|
|
54
141
|
end
|
|
55
142
|
end
|
|
@@ -19,6 +19,7 @@ module ActivePostgres
|
|
|
19
19
|
config.all_hosts.each do |host|
|
|
20
20
|
ssh_executor.execute_on_host(host) do
|
|
21
21
|
execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
|
|
22
|
+
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
|
|
22
23
|
end
|
|
23
24
|
end
|
|
24
25
|
end
|
|
@@ -27,6 +28,11 @@ module ActivePostgres
|
|
|
27
28
|
puts "pgBackRest is a backup tool and doesn't run as a service."
|
|
28
29
|
end
|
|
29
30
|
|
|
31
|
+
def install_on_standby(host)
|
|
32
|
+
puts " Installing pgBackRest on standby #{host}..."
|
|
33
|
+
install_on_host(host, create_stanza: false)
|
|
34
|
+
end
|
|
35
|
+
|
|
30
36
|
def run_backup(type = 'full')
|
|
31
37
|
puts "Running #{type} backup..."
|
|
32
38
|
postgres_user = config.postgres_user
|
|
@@ -66,7 +72,7 @@ module ActivePostgres
|
|
|
66
72
|
def install_on_host(host, create_stanza: true)
|
|
67
73
|
puts " Installing pgBackRest on #{host}..."
|
|
68
74
|
|
|
69
|
-
pgbackrest_config = config.component_config(:pgbackrest)
|
|
75
|
+
pgbackrest_config = secrets.resolve_value(config.component_config(:pgbackrest))
|
|
70
76
|
postgres_user = config.postgres_user
|
|
71
77
|
secrets_obj = secrets
|
|
72
78
|
_ = pgbackrest_config # Used in ERB template
|
|
@@ -78,7 +84,8 @@ module ActivePostgres
|
|
|
78
84
|
end
|
|
79
85
|
|
|
80
86
|
# Upload configuration
|
|
81
|
-
upload_template(host, 'pgbackrest.conf.erb', '/etc/pgbackrest.conf', binding,
|
|
87
|
+
upload_template(host, 'pgbackrest.conf.erb', '/etc/pgbackrest.conf', binding,
|
|
88
|
+
mode: '640', owner: "root:#{postgres_user}")
|
|
82
89
|
|
|
83
90
|
ssh_executor.execute_on_host(host) do
|
|
84
91
|
execute :sudo, 'rm', '-rf', '/var/lib/pgbackrest', '||', 'true'
|
|
@@ -99,6 +106,35 @@ module ActivePostgres
|
|
|
99
106
|
execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create'
|
|
100
107
|
end
|
|
101
108
|
end
|
|
109
|
+
|
|
110
|
+
# Set up scheduled backups on primary only
|
|
111
|
+
if create_stanza && pgbackrest_config[:schedule]
|
|
112
|
+
setup_backup_schedule(host, pgbackrest_config[:schedule])
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def setup_backup_schedule(host, schedule)
|
|
117
|
+
puts " Setting up backup schedule: #{schedule}"
|
|
118
|
+
postgres_user = config.postgres_user
|
|
119
|
+
|
|
120
|
+
# Create cron job for scheduled backups
|
|
121
|
+
# /etc/cron.d format requires username after time spec
|
|
122
|
+
cron_content = <<~CRON
|
|
123
|
+
# pgBackRest scheduled backups (managed by active_postgres)
|
|
124
|
+
SHELL=/bin/bash
|
|
125
|
+
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
|
126
|
+
#{schedule} #{postgres_user} pgbackrest --stanza=main --type=full backup
|
|
127
|
+
CRON
|
|
128
|
+
|
|
129
|
+
# Install cron job in /etc/cron.d (system cron directory)
|
|
130
|
+
# 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')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def remove_backup_schedule(host)
|
|
135
|
+
ssh_executor.execute_on_host(host) do
|
|
136
|
+
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
|
|
137
|
+
end
|
|
102
138
|
end
|
|
103
139
|
end
|
|
104
140
|
end
|
|
@@ -14,6 +14,11 @@ module ActivePostgres
|
|
|
14
14
|
|
|
15
15
|
config.all_hosts.each do |host|
|
|
16
16
|
ssh_executor.execute_on_host(host) do
|
|
17
|
+
execute :sudo, 'systemctl', 'disable', '--now', 'pgbouncer-follow-primary.timer', '||', 'true'
|
|
18
|
+
execute :sudo, 'rm', '-f', '/usr/local/bin/pgbouncer-follow-primary',
|
|
19
|
+
'/etc/systemd/system/pgbouncer-follow-primary.service',
|
|
20
|
+
'/etc/systemd/system/pgbouncer-follow-primary.timer'
|
|
21
|
+
execute :sudo, 'systemctl', 'daemon-reload'
|
|
17
22
|
execute :sudo, 'systemctl', 'stop', 'pgbouncer'
|
|
18
23
|
execute :sudo, 'apt-get', 'remove', '-y', 'pgbouncer'
|
|
19
24
|
end
|
|
@@ -53,12 +58,22 @@ module ActivePostgres
|
|
|
53
58
|
puts " Installing PgBouncer on #{host}..."
|
|
54
59
|
|
|
55
60
|
user_config = config.component_config(:pgbouncer)
|
|
61
|
+
follow_primary = user_config[:follow_primary] == true
|
|
62
|
+
if follow_primary && !config.component_enabled?(:repmgr)
|
|
63
|
+
raise Error, 'PgBouncer follow_primary requires repmgr to be enabled'
|
|
64
|
+
end
|
|
56
65
|
|
|
57
66
|
max_connections = get_postgres_max_connections(host)
|
|
58
67
|
optimal_pool = ConnectionPooler.calculate_optimal_pool_sizes(max_connections)
|
|
59
68
|
|
|
60
69
|
pgbouncer_config = optimal_pool.merge(user_config)
|
|
70
|
+
pgbouncer_config[:database_host] = config.primary_replication_host if follow_primary
|
|
61
71
|
ssl_enabled = config.component_enabled?(:ssl)
|
|
72
|
+
has_ca_cert = ssl_enabled && secrets.resolve('ssl_chain')
|
|
73
|
+
secrets_obj = secrets
|
|
74
|
+
_ = pgbouncer_config
|
|
75
|
+
_ = has_ca_cert
|
|
76
|
+
_ = secrets_obj
|
|
62
77
|
|
|
63
78
|
puts " Calculated pool settings for max_connections=#{max_connections}"
|
|
64
79
|
|
|
@@ -76,6 +91,8 @@ module ActivePostgres
|
|
|
76
91
|
execute :sudo, 'systemctl', 'enable', 'pgbouncer'
|
|
77
92
|
execute :sudo, 'systemctl', 'restart', 'pgbouncer'
|
|
78
93
|
end
|
|
94
|
+
|
|
95
|
+
install_follow_primary(host, pgbouncer_config) if follow_primary
|
|
79
96
|
end
|
|
80
97
|
|
|
81
98
|
def setup_ssl_certs(host)
|
|
@@ -130,10 +147,7 @@ module ActivePostgres
|
|
|
130
147
|
def fetch_user_hash(backend, user, postgres_user)
|
|
131
148
|
sql = build_user_hash_sql(user)
|
|
132
149
|
|
|
133
|
-
|
|
134
|
-
backend.execute :chmod, '644', '/tmp/get_user_hash.sql'
|
|
135
|
-
user_hash = backend.capture(:sudo, '-u', postgres_user, 'psql', '-t', '-f', '/tmp/get_user_hash.sql').strip
|
|
136
|
-
backend.execute :rm, '-f', '/tmp/get_user_hash.sql'
|
|
150
|
+
user_hash = ssh_executor.run_sql_on_backend(backend, sql, postgres_user: postgres_user).to_s.strip
|
|
137
151
|
|
|
138
152
|
if user_hash && !user_hash.empty?
|
|
139
153
|
puts " ✓ Added #{user} to PgBouncer userlist"
|
|
@@ -167,6 +181,31 @@ module ActivePostgres
|
|
|
167
181
|
warn ' Warning: No users added to userlist - connections may fail'
|
|
168
182
|
end
|
|
169
183
|
end
|
|
184
|
+
|
|
185
|
+
def install_follow_primary(host, pgbouncer_config)
|
|
186
|
+
interval = pgbouncer_config[:follow_primary_interval] || 5
|
|
187
|
+
interval = interval.to_i
|
|
188
|
+
interval = 5 if interval <= 0
|
|
189
|
+
|
|
190
|
+
repmgr_conf = '/etc/repmgr.conf'
|
|
191
|
+
postgres_user = config.postgres_user
|
|
192
|
+
_ = interval
|
|
193
|
+
_ = repmgr_conf
|
|
194
|
+
_ = postgres_user
|
|
195
|
+
|
|
196
|
+
upload_template(host, 'pgbouncer_follow_primary.sh.erb', '/usr/local/bin/pgbouncer-follow-primary', binding,
|
|
197
|
+
mode: '755', owner: 'root:root')
|
|
198
|
+
upload_template(host, 'pgbouncer-follow-primary.service.erb',
|
|
199
|
+
'/etc/systemd/system/pgbouncer-follow-primary.service', binding, mode: '644', owner: 'root:root')
|
|
200
|
+
upload_template(host, 'pgbouncer-follow-primary.timer.erb',
|
|
201
|
+
'/etc/systemd/system/pgbouncer-follow-primary.timer', binding, mode: '644', owner: 'root:root')
|
|
202
|
+
|
|
203
|
+
ssh_executor.execute_on_host(host) do
|
|
204
|
+
execute :sudo, 'systemctl', 'daemon-reload'
|
|
205
|
+
execute :sudo, 'systemctl', 'enable', '--now', 'pgbouncer-follow-primary.timer'
|
|
206
|
+
execute :sudo, 'systemctl', 'start', 'pgbouncer-follow-primary.service'
|
|
207
|
+
end
|
|
208
|
+
end
|
|
170
209
|
end
|
|
171
210
|
end
|
|
172
211
|
end
|