active_postgres 0.9.5 → 0.9.7
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/CHANGELOG.md +40 -0
- data/README.md +47 -6
- data/SECURITY.md +21 -0
- data/lib/active_postgres/components/base.rb +2 -0
- data/lib/active_postgres/components/core.rb +11 -4
- data/lib/active_postgres/components/monitoring.rb +2 -2
- data/lib/active_postgres/components/pgbackrest.rb +13 -17
- data/lib/active_postgres/components/pgbouncer.rb +5 -9
- data/lib/active_postgres/components/repmgr.rb +165 -105
- data/lib/active_postgres/configuration/pgbackrest_policy.rb +19 -0
- data/lib/active_postgres/configuration/standby_policy.rb +24 -0
- data/lib/active_postgres/configuration.rb +64 -38
- data/lib/active_postgres/deployment_flow.rb +5 -1
- data/lib/active_postgres/direct_executor.rb +3 -9
- data/lib/active_postgres/error_handler.rb +18 -18
- data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +7 -0
- data/lib/active_postgres/health_checker.rb +10 -12
- data/lib/active_postgres/installer.rb +2 -2
- data/lib/active_postgres/overview.rb +10 -10
- data/lib/active_postgres/secrets.rb +6 -7
- data/lib/active_postgres/ssh_executor.rb +66 -9
- data/lib/active_postgres/standby_deployment_flow.rb +46 -8
- data/lib/active_postgres/version.rb +1 -1
- data/lib/tasks/postgres.rake +26 -26
- data/templates/pgbackrest.conf.erb +9 -1
- data/templates/postgresql.conf.erb +5 -0
- data/templates/repmgr_dns_failover.sh.erb +2 -2
- metadata +18 -56
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f7bf86efa4e32bcb1543e317bd6219983f9f42eb0ed0bf8787ff75b6a573a48
|
|
4
|
+
data.tar.gz: 8a2b2f0ec44bdfd94552dfa0fd84145b64ded472b4f421f216c86af71e2d859e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d4a26f7284e14c84bcaa4d1e982da9162a25a098b15f9423b4923e7b7a9ab007112620761e5184a1da514f40495edd00647e8c2a269bbbb0511e2611e44118f
|
|
7
|
+
data.tar.gz: 4dfb7b6e19345e3324d38a798a82bdfa111001cb94736348aa88ba6146f9c3a6bc47fed8977039faefc271d412529f61995ad36f42be93b96a8789437187423e
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Active Postgres will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project follows [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.9.7 - 2026-09-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Support strict operator SSH through declared jump hosts.
|
|
15
|
+
- Allow a standby to override repmgr failover and priority policy.
|
|
16
|
+
- Allow a standby to be seeded from the latest valid pgBackRest backup.
|
|
17
|
+
- Make deployment-flow `--dry-run` stop before confirmation or mutation.
|
|
18
|
+
- Fail standby setup before mutation when required credentials do not resolve.
|
|
19
|
+
- Scope standby rollback to the requested host and keep existing cluster nodes intact.
|
|
20
|
+
- Preserve pgBackRest recovery options as one shell-safe argument during remote restore.
|
|
21
|
+
- Preserve the complete pgBackRest command as the single `bash -lc` command string.
|
|
22
|
+
- Run component rollback in local Ruby context instead of an SSHKit backend scope.
|
|
23
|
+
- Explicitly stop SysV-backed repmgrd before disabling it on manual-failover standbys.
|
|
24
|
+
- Support opt-in bundled block backups for large, frequently changed PostgreSQL relations.
|
|
25
|
+
- Support independent full, differential, and incremental pgBackRest schedules.
|
|
26
|
+
|
|
27
|
+
## 0.9.6 - 2026-08-20
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Require Ruby 4.0 or newer and Rails 8.1 or newer for Rails integration.
|
|
32
|
+
- Align runtime and development dependencies with the maintained toolchain.
|
|
33
|
+
- Add blocking test, lint, dependency-audit, and package-build CI checks.
|
|
34
|
+
- Add automated Bundler and GitHub Actions dependency updates.
|
|
35
|
+
- Document network, credential, failover, and recovery responsibilities.
|
|
36
|
+
|
|
37
|
+
## 0.9.5
|
|
38
|
+
|
|
39
|
+
- Current published release. See the repository history for changes included in
|
|
40
|
+
earlier releases.
|
data/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# active_postgres
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
PostgreSQL high-availability automation for Ruby and Rails applications.
|
|
4
|
+
|
|
5
|
+
Active Postgres configures hosts you control over SSH. It is an operations tool,
|
|
6
|
+
not a managed database service: you remain responsible for network isolation,
|
|
7
|
+
capacity planning, monitoring, backups, recovery drills, and failover decisions.
|
|
4
8
|
|
|
5
9
|
## Features
|
|
6
10
|
|
|
@@ -31,6 +35,11 @@ production:
|
|
|
31
35
|
version: 18
|
|
32
36
|
user: ubuntu
|
|
33
37
|
ssh_key: ~/.ssh/id_rsa
|
|
38
|
+
ssh_known_hosts_file: config/ssh_known_hosts
|
|
39
|
+
|
|
40
|
+
jump_hosts:
|
|
41
|
+
app-oci:
|
|
42
|
+
host: 129.213.194.40
|
|
34
43
|
|
|
35
44
|
primary:
|
|
36
45
|
host: 34.12.234.81 # Public IP for SSH
|
|
@@ -39,6 +48,13 @@ production:
|
|
|
39
48
|
standby:
|
|
40
49
|
- host: 52.23.45.67
|
|
41
50
|
private_ip: 10.8.0.11
|
|
51
|
+
- host: 100.63.4.227
|
|
52
|
+
private_ip: 10.8.0.12
|
|
53
|
+
jump_host: app-oci
|
|
54
|
+
seed_method: pgbackrest
|
|
55
|
+
repmgr:
|
|
56
|
+
auto_failover: false
|
|
57
|
+
priority: 0
|
|
42
58
|
|
|
43
59
|
components:
|
|
44
60
|
repmgr: {enabled: true}
|
|
@@ -52,6 +68,14 @@ production:
|
|
|
52
68
|
app_password: $POSTGRES_APP_PASSWORD
|
|
53
69
|
```
|
|
54
70
|
|
|
71
|
+
`jump_host` keeps the operator SSH key on the workstation and proxies the
|
|
72
|
+
connection through a declared host. A standby-level `repmgr` block overrides
|
|
73
|
+
the cluster defaults, so a cross-provider disaster-recovery replica can remain
|
|
74
|
+
manual-promotion while the ordinary standby retains automatic failover.
|
|
75
|
+
`seed_method: pgbackrest` restores the latest valid backup with standby recovery
|
|
76
|
+
settings before repmgr registration, avoiding a full base-backup stream from
|
|
77
|
+
the primary. The `pgbackrest` component must be enabled.
|
|
78
|
+
|
|
55
79
|
Add credentials (`rails credentials:edit`):
|
|
56
80
|
|
|
57
81
|
```yaml
|
|
@@ -175,8 +199,13 @@ components:
|
|
|
175
199
|
pgbackrest:
|
|
176
200
|
retention_full: 7
|
|
177
201
|
retention_archive: 14
|
|
178
|
-
#
|
|
179
|
-
#
|
|
202
|
+
# Reduce incremental backup churn for large, frequently changed relations.
|
|
203
|
+
# Block backups require repository bundling.
|
|
204
|
+
repo_bundle: true
|
|
205
|
+
repo_block: true
|
|
206
|
+
# You can schedule full, differential, and incremental backups separately:
|
|
207
|
+
# schedule_full: "0 2 * * 0"
|
|
208
|
+
# schedule_differential: "35 2 * * 1-6"
|
|
180
209
|
# schedule_incremental: "0 * * * *"
|
|
181
210
|
```
|
|
182
211
|
|
|
@@ -272,11 +301,23 @@ User.all # → Replica
|
|
|
272
301
|
|
|
273
302
|
## Requirements
|
|
274
303
|
|
|
275
|
-
- Ruby
|
|
276
|
-
- PostgreSQL
|
|
304
|
+
- Ruby 4.0+
|
|
305
|
+
- A PostgreSQL version supported by the target Linux distribution
|
|
277
306
|
- Ubuntu 20.04+ / Debian 11+ with systemd
|
|
278
307
|
- SSH key-based authentication (hosts must be in `~/.ssh/known_hosts`)
|
|
279
|
-
- Rails
|
|
308
|
+
- Rails 8.1+ (optional)
|
|
309
|
+
|
|
310
|
+
## Operational safety
|
|
311
|
+
|
|
312
|
+
- Keep PostgreSQL, PgBouncer, exporters, and backup endpoints behind a private
|
|
313
|
+
network or narrowly scoped firewall rules.
|
|
314
|
+
- Test promotion, application reconnection, backup restore, and point-in-time
|
|
315
|
+
recovery in a disposable environment before using them in production.
|
|
316
|
+
- Review generated configuration and package changes before each rollout;
|
|
317
|
+
automatic failover cannot determine whether an application is safe to write
|
|
318
|
+
after a network partition.
|
|
319
|
+
- Store database, replication, monitoring, and backup credentials in a secret
|
|
320
|
+
manager. Do not commit resolved values or generated configuration.
|
|
280
321
|
|
|
281
322
|
### SSH host key verification
|
|
282
323
|
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
Please do not open a public issue for a potential vulnerability. Email
|
|
6
|
+
[security@boringcache.com](mailto:security@boringcache.com) or use
|
|
7
|
+
[GitHub private vulnerability reporting](https://github.com/boringcache/active_postgres/security/advisories/new).
|
|
8
|
+
|
|
9
|
+
Include the affected version, expected impact, and enough reproduction detail
|
|
10
|
+
for us to investigate. Do not include database credentials, private keys,
|
|
11
|
+
backups, customer data, or production configuration.
|
|
12
|
+
|
|
13
|
+
## Supported Versions
|
|
14
|
+
|
|
15
|
+
Only the latest published release is supported. Security fixes are normally
|
|
16
|
+
shipped in a new release rather than backported.
|
|
17
|
+
|
|
18
|
+
## Scope
|
|
19
|
+
|
|
20
|
+
Reports about PostgreSQL authentication, replication, backup/restore, SSH host
|
|
21
|
+
verification, secret handling, generated SQL, or release integrity are welcome.
|
|
@@ -136,7 +136,7 @@ module ActivePostgres
|
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def ensure_standby_compatible(pg_config)
|
|
139
|
-
primary_settings =
|
|
139
|
+
primary_settings = primary_replication_settings
|
|
140
140
|
return pg_config if primary_settings.empty?
|
|
141
141
|
|
|
142
142
|
adjustments = []
|
|
@@ -154,17 +154,24 @@ module ActivePostgres
|
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
if adjustments.any?
|
|
157
|
-
puts
|
|
157
|
+
puts ' ⚠️ Adjusting standby settings to match primary minimum:'
|
|
158
158
|
adjustments.each { |adj| puts " #{adj}" }
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
pg_config
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
def
|
|
164
|
+
def primary_replication_settings
|
|
165
165
|
@primary_replication_settings ||= begin
|
|
166
166
|
settings = {}
|
|
167
|
-
|
|
167
|
+
setting_names = %w[
|
|
168
|
+
max_connections
|
|
169
|
+
max_worker_processes
|
|
170
|
+
max_wal_senders
|
|
171
|
+
max_prepared_transactions
|
|
172
|
+
max_locks_per_transaction
|
|
173
|
+
]
|
|
174
|
+
sql = "SELECT name || '=' || setting FROM pg_settings WHERE name IN (#{setting_names.map { |name| "'#{name}'" }.join(', ')})"
|
|
168
175
|
result = ssh_executor.run_sql(config.primary_host, sql)
|
|
169
176
|
result.strip.split("\n").each do |line|
|
|
170
177
|
name, val = line.split('=')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require 'cgi'
|
|
1
|
+
require 'cgi/escape'
|
|
2
2
|
|
|
3
3
|
module ActivePostgres
|
|
4
4
|
module Components
|
|
@@ -83,7 +83,7 @@ module ActivePostgres
|
|
|
83
83
|
sql = build_monitoring_user_sql(monitoring_user, monitoring_password)
|
|
84
84
|
|
|
85
85
|
ssh_executor.run_sql(config.primary_host, sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
86
|
-
|
|
86
|
+
capture: false)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def build_monitoring_user_sql(user, password)
|
|
@@ -3,9 +3,9 @@ require 'shellwords'
|
|
|
3
3
|
module ActivePostgres
|
|
4
4
|
module Components
|
|
5
5
|
class PgBackRest < Base
|
|
6
|
-
LOG_ARCHIVE_CRON_FILE = '/etc/cron.d/postgres-log-archive'
|
|
7
|
-
LOG_ARCHIVE_ENV_FILE = '/etc/active-postgres-log-archive.env'
|
|
8
|
-
LOG_ARCHIVE_SCRIPT = '/usr/local/bin/active-postgres-archive-logs'
|
|
6
|
+
LOG_ARCHIVE_CRON_FILE = '/etc/cron.d/postgres-log-archive'.freeze
|
|
7
|
+
LOG_ARCHIVE_ENV_FILE = '/etc/active-postgres-log-archive.env'.freeze
|
|
8
|
+
LOG_ARCHIVE_SCRIPT = '/usr/local/bin/active-postgres-archive-logs'.freeze
|
|
9
9
|
|
|
10
10
|
def install
|
|
11
11
|
puts 'Installing pgBackRest for backups...'
|
|
@@ -26,6 +26,7 @@ module ActivePostgres
|
|
|
26
26
|
ssh_executor.execute_on_host(host) do
|
|
27
27
|
execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
|
|
28
28
|
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
|
|
29
|
+
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-differential'
|
|
29
30
|
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
|
|
30
31
|
execute :sudo, 'rm', '-f', LOG_ARCHIVE_CRON_FILE
|
|
31
32
|
execute :sudo, 'rm', '-f', LOG_ARCHIVE_ENV_FILE
|
|
@@ -135,9 +136,7 @@ module ActivePostgres
|
|
|
135
136
|
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", '/var/spool/pgbackrest'
|
|
136
137
|
|
|
137
138
|
# Only create stanza on primary - standbys share the same backup repo
|
|
138
|
-
if create_stanza
|
|
139
|
-
execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create'
|
|
140
|
-
end
|
|
139
|
+
execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create' if create_stanza
|
|
141
140
|
end
|
|
142
141
|
|
|
143
142
|
if log_archive_enabled?(pgbackrest_config)
|
|
@@ -164,15 +163,13 @@ module ActivePostgres
|
|
|
164
163
|
|
|
165
164
|
def backup_schedules(pgbackrest_config)
|
|
166
165
|
schedule_full = pgbackrest_config[:schedule_full] || pgbackrest_config[:schedule]
|
|
166
|
+
schedule_differential = pgbackrest_config[:schedule_differential]
|
|
167
167
|
schedule_incremental = pgbackrest_config[:schedule_incremental]
|
|
168
168
|
|
|
169
169
|
schedules = []
|
|
170
|
-
if schedule_full
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if schedule_incremental
|
|
174
|
-
schedules << { type: 'incremental', schedule: schedule_incremental, file: '/etc/cron.d/pgbackrest-backup-incremental' }
|
|
175
|
-
end
|
|
170
|
+
schedules << { type: 'full', schedule: schedule_full, file: '/etc/cron.d/pgbackrest-backup' } if schedule_full
|
|
171
|
+
schedules << { type: 'diff', schedule: schedule_differential, file: '/etc/cron.d/pgbackrest-backup-differential' } if schedule_differential
|
|
172
|
+
schedules << { type: 'incr', schedule: schedule_incremental, file: '/etc/cron.d/pgbackrest-backup-incremental' } if schedule_incremental
|
|
176
173
|
|
|
177
174
|
schedules
|
|
178
175
|
end
|
|
@@ -198,15 +195,14 @@ module ActivePostgres
|
|
|
198
195
|
def remove_backup_schedule(host)
|
|
199
196
|
ssh_executor.execute_on_host(host) do
|
|
200
197
|
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
|
|
198
|
+
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-differential'
|
|
201
199
|
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
|
|
202
200
|
end
|
|
203
201
|
end
|
|
204
202
|
|
|
205
203
|
def setup_log_archive(host, pgbackrest_config)
|
|
206
204
|
log_archive_config = pgbackrest_config[:log_archive] || {}
|
|
207
|
-
unless pgbackrest_config[:repo_type] == 's3'
|
|
208
|
-
raise Error, 'pgbackrest.log_archive requires pgbackrest.repo_type: s3'
|
|
209
|
-
end
|
|
205
|
+
raise Error, 'pgbackrest.log_archive requires pgbackrest.repo_type: s3' unless pgbackrest_config[:repo_type] == 's3'
|
|
210
206
|
|
|
211
207
|
postgres_user = config.postgres_user
|
|
212
208
|
env_content = log_archive_env(pgbackrest_config, log_archive_config, host)
|
|
@@ -260,13 +256,13 @@ module ActivePostgres
|
|
|
260
256
|
'POSTGRES_LOG_ARCHIVE_NODE' => config.node_label_for(host) || host
|
|
261
257
|
}.compact
|
|
262
258
|
|
|
263
|
-
env.map { |key, value| "#{key}=#{Shellwords.escape(value.to_s)}" }.join("\n")
|
|
259
|
+
"#{env.map { |key, value| "#{key}=#{Shellwords.escape(value.to_s)}" }.join("\n")}\n"
|
|
264
260
|
end
|
|
265
261
|
|
|
266
262
|
def s3_endpoint_url(pgbackrest_config)
|
|
267
263
|
endpoint = pgbackrest_config[:s3_endpoint]
|
|
268
264
|
return nil if endpoint.to_s.empty?
|
|
269
|
-
return endpoint if endpoint.match?(
|
|
265
|
+
return endpoint if endpoint.match?(%r{\Ahttps?://})
|
|
270
266
|
|
|
271
267
|
"https://#{endpoint}"
|
|
272
268
|
end
|
|
@@ -81,9 +81,7 @@ module ActivePostgres
|
|
|
81
81
|
|
|
82
82
|
follow_primary = follow_primary_for?(host, is_standby: is_standby, user_config: user_config)
|
|
83
83
|
|
|
84
|
-
if follow_primary && !config.component_enabled?(:repmgr)
|
|
85
|
-
raise Error, 'PgBouncer follow_primary requires repmgr to be enabled'
|
|
86
|
-
end
|
|
84
|
+
raise Error, 'PgBouncer follow_primary requires repmgr to be enabled' if follow_primary && !config.component_enabled?(:repmgr)
|
|
87
85
|
|
|
88
86
|
max_connections = get_postgres_max_connections(host)
|
|
89
87
|
optimal_pool = ConnectionPooler.calculate_optimal_pool_sizes(max_connections)
|
|
@@ -188,9 +186,9 @@ module ActivePostgres
|
|
|
188
186
|
end
|
|
189
187
|
|
|
190
188
|
ssl_chain = secrets.resolve('ssl_chain')
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
return unless ssl_chain
|
|
190
|
+
|
|
191
|
+
ssh_executor.upload_file(host, ssl_chain, '/etc/pgbouncer/ca.crt', mode: '644', owner: 'postgres:postgres')
|
|
194
192
|
end
|
|
195
193
|
|
|
196
194
|
def setup_app_ssl_certs(host)
|
|
@@ -200,9 +198,7 @@ module ActivePostgres
|
|
|
200
198
|
ssl_key = secrets.resolve('ssl_key')
|
|
201
199
|
ssl_chain = secrets.resolve('ssl_chain')
|
|
202
200
|
|
|
203
|
-
unless ssl_cert && ssl_key
|
|
204
|
-
raise Error, 'PgBouncer app_hosts require ssl_cert and ssl_key secrets when ssl is enabled'
|
|
205
|
-
end
|
|
201
|
+
raise Error, 'PgBouncer app_hosts require ssl_cert and ssl_key secrets when ssl is enabled' unless ssl_cert && ssl_key
|
|
206
202
|
|
|
207
203
|
full_cert = if ssl_chain
|
|
208
204
|
"#{ssl_cert.strip}\n#{ssl_chain.strip}\n"
|