active_postgres 0.4.0 → 0.5.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/lib/active_postgres/cluster_deployment_flow.rb +10 -0
- data/lib/active_postgres/components/pgbackrest.rb +16 -5
- data/lib/active_postgres/components/pgbouncer.rb +29 -7
- data/lib/active_postgres/generators/active_postgres/templates/database.active_postgres.yml.erb +1 -0
- data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +85 -0
- data/lib/active_postgres/health_checker.rb +81 -6
- data/lib/active_postgres/ssh_executor.rb +3 -1
- data/lib/active_postgres/standby_deployment_flow.rb +1 -1
- data/lib/active_postgres/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c54d617bfe700f38f1be17b6ff66e630bde27185b0dfdbeae52ef9472a25fd2
|
|
4
|
+
data.tar.gz: 92b80059f95ce83b62de7b047c0827906314d43a737360b32c88de67b4b2b11f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a55cf101289b0c3a7f88494b79abd6bd61d2f47d9946308da3612dc5be5334013d455c08ef923202679a14f0aba1a1749c33e8164c85f744e5ff32d2fe99776
|
|
7
|
+
data.tar.gz: 1c07e9b4fa3d29a6d11cf960d2836c930d64adca20104b8e2911b2d828e0c19b9e2e20d9b46d4491f22ea9b59b7cc56838c648b1dc1fc40351faece92043bfdd
|
|
@@ -54,6 +54,9 @@ module ActivePostgres
|
|
|
54
54
|
|
|
55
55
|
# Create application users AFTER repmgr to avoid being wiped by cluster recreation
|
|
56
56
|
create_application_users_if_configured
|
|
57
|
+
|
|
58
|
+
# Update pgbouncer userlist AFTER app users are created so they can authenticate
|
|
59
|
+
update_pgbouncer_userlist if config.component_enabled?(:pgbouncer)
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
def list_next_steps
|
|
@@ -81,5 +84,12 @@ module ActivePostgres
|
|
|
81
84
|
core_component = Components::Core.new(config, ssh_executor, secrets)
|
|
82
85
|
core_component.create_application_users
|
|
83
86
|
end
|
|
87
|
+
|
|
88
|
+
def update_pgbouncer_userlist
|
|
89
|
+
logger.task('Updating PgBouncer userlist with app users') do
|
|
90
|
+
component = Components::PgBouncer.new(config, ssh_executor, secrets)
|
|
91
|
+
component.update_userlist
|
|
92
|
+
end
|
|
93
|
+
end
|
|
84
94
|
end
|
|
85
95
|
end
|
|
@@ -4,14 +4,22 @@ module ActivePostgres
|
|
|
4
4
|
def install
|
|
5
5
|
puts 'Installing pgBackRest for backups...'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# Install on primary with full setup (stanza-create)
|
|
8
|
+
install_on_host(config.primary_host, create_stanza: true)
|
|
9
|
+
|
|
10
|
+
# Install on standbys (package + config only, no stanza-create)
|
|
11
|
+
config.standby_hosts.each do |host|
|
|
12
|
+
install_on_host(host, create_stanza: false)
|
|
13
|
+
end
|
|
8
14
|
end
|
|
9
15
|
|
|
10
16
|
def uninstall
|
|
11
17
|
puts 'Uninstalling pgBackRest...'
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
config.all_hosts.each do |host|
|
|
20
|
+
ssh_executor.execute_on_host(host) do
|
|
21
|
+
execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
|
|
22
|
+
end
|
|
15
23
|
end
|
|
16
24
|
end
|
|
17
25
|
|
|
@@ -55,7 +63,7 @@ module ActivePostgres
|
|
|
55
63
|
|
|
56
64
|
private
|
|
57
65
|
|
|
58
|
-
def install_on_host(host)
|
|
66
|
+
def install_on_host(host, create_stanza: true)
|
|
59
67
|
puts " Installing pgBackRest on #{host}..."
|
|
60
68
|
|
|
61
69
|
pgbackrest_config = config.component_config(:pgbackrest)
|
|
@@ -86,7 +94,10 @@ module ActivePostgres
|
|
|
86
94
|
execute :sudo, 'mkdir', '-p', '/var/spool/pgbackrest'
|
|
87
95
|
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", '/var/spool/pgbackrest'
|
|
88
96
|
|
|
89
|
-
|
|
97
|
+
# Only create stanza on primary - standbys share the same backup repo
|
|
98
|
+
if create_stanza
|
|
99
|
+
execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create'
|
|
100
|
+
end
|
|
90
101
|
end
|
|
91
102
|
end
|
|
92
103
|
end
|
|
@@ -4,27 +4,49 @@ module ActivePostgres
|
|
|
4
4
|
def install
|
|
5
5
|
puts 'Installing PgBouncer for connection pooling...'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
config.all_hosts.each do |host|
|
|
8
|
+
install_on_host(host)
|
|
9
|
+
end
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def uninstall
|
|
12
13
|
puts 'Uninstalling PgBouncer...'
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
config.all_hosts.each do |host|
|
|
16
|
+
ssh_executor.execute_on_host(host) do
|
|
17
|
+
execute :sudo, 'systemctl', 'stop', 'pgbouncer'
|
|
18
|
+
execute :sudo, 'apt-get', 'remove', '-y', 'pgbouncer'
|
|
19
|
+
end
|
|
17
20
|
end
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def restart
|
|
21
24
|
puts 'Restarting PgBouncer...'
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
config.all_hosts.each do |host|
|
|
27
|
+
ssh_executor.execute_on_host(host) do
|
|
28
|
+
execute :sudo, 'systemctl', 'restart', 'pgbouncer'
|
|
29
|
+
end
|
|
25
30
|
end
|
|
26
31
|
end
|
|
27
32
|
|
|
33
|
+
def update_userlist
|
|
34
|
+
puts 'Updating PgBouncer userlist on all hosts...'
|
|
35
|
+
|
|
36
|
+
config.all_hosts.each do |host|
|
|
37
|
+
create_userlist(host)
|
|
38
|
+
|
|
39
|
+
ssh_executor.execute_on_host(host) do
|
|
40
|
+
execute :sudo, 'systemctl', 'reload', 'pgbouncer'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def install_on_standby(standby_host)
|
|
46
|
+
puts "Installing PgBouncer on standby #{standby_host}..."
|
|
47
|
+
install_on_host(standby_host)
|
|
48
|
+
end
|
|
49
|
+
|
|
28
50
|
private
|
|
29
51
|
|
|
30
52
|
def install_on_host(host)
|
data/lib/active_postgres/generators/active_postgres/templates/database.active_postgres.yml.erb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= ActivePostgres::Rails::DatabaseConfig.render_partial('production', app_name: boring_app_name).strip %>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# PostgreSQL High Availability Configuration
|
|
2
|
+
# Generated by active_postgres
|
|
3
|
+
#
|
|
4
|
+
# 💡 Either:
|
|
5
|
+
# 1. Edit this file manually with your database servers
|
|
6
|
+
# 2. Use Terraform to auto-generate this file
|
|
7
|
+
#
|
|
8
|
+
# See: config/postgres.example.yml in gem for full examples
|
|
9
|
+
|
|
10
|
+
shared: &shared
|
|
11
|
+
version: 18
|
|
12
|
+
user: ubuntu
|
|
13
|
+
ssh_key: ~/.ssh/id_rsa
|
|
14
|
+
|
|
15
|
+
components:
|
|
16
|
+
core:
|
|
17
|
+
locale: en_US.UTF-8
|
|
18
|
+
encoding: UTF8
|
|
19
|
+
# Optional: Override default PostgreSQL user and app database config
|
|
20
|
+
# postgres_user: postgres
|
|
21
|
+
# app_user: app
|
|
22
|
+
# app_database: app_production
|
|
23
|
+
postgresql:
|
|
24
|
+
listen_addresses: '*'
|
|
25
|
+
port: 5432
|
|
26
|
+
max_connections: 100
|
|
27
|
+
shared_buffers: 256MB
|
|
28
|
+
|
|
29
|
+
repmgr:
|
|
30
|
+
enabled: false
|
|
31
|
+
# Optional: Override default repmgr user/database
|
|
32
|
+
# user: repmgr
|
|
33
|
+
# database: repmgr
|
|
34
|
+
|
|
35
|
+
pgbouncer:
|
|
36
|
+
enabled: false
|
|
37
|
+
# Optional: Override default pgbouncer user
|
|
38
|
+
# user: pgbouncer
|
|
39
|
+
|
|
40
|
+
pgbackrest:
|
|
41
|
+
enabled: false
|
|
42
|
+
|
|
43
|
+
monitoring:
|
|
44
|
+
enabled: false
|
|
45
|
+
|
|
46
|
+
ssl:
|
|
47
|
+
enabled: false
|
|
48
|
+
|
|
49
|
+
development:
|
|
50
|
+
<<: *shared
|
|
51
|
+
primary:
|
|
52
|
+
host: localhost
|
|
53
|
+
port: 5432
|
|
54
|
+
|
|
55
|
+
production:
|
|
56
|
+
<<: *shared
|
|
57
|
+
|
|
58
|
+
# TODO: Configure your primary database server
|
|
59
|
+
# host: Public IP for SSH deployment (like Kamal)
|
|
60
|
+
# private_ip: Private/VPC IP for database connections (optional, falls back to host)
|
|
61
|
+
primary:
|
|
62
|
+
host: YOUR_PRIMARY_PUBLIC_IP
|
|
63
|
+
private_ip: YOUR_PRIMARY_PRIVATE_IP
|
|
64
|
+
label: us-east-1
|
|
65
|
+
|
|
66
|
+
# TODO: Add standby servers for HA (optional)
|
|
67
|
+
# standby:
|
|
68
|
+
# - host: YOUR_STANDBY_PUBLIC_IP
|
|
69
|
+
# private_ip: YOUR_STANDBY_PRIVATE_IP
|
|
70
|
+
# label: us-west-2
|
|
71
|
+
|
|
72
|
+
# Enable components as needed
|
|
73
|
+
components:
|
|
74
|
+
repmgr:
|
|
75
|
+
enabled: false # Enable for HA with standbys
|
|
76
|
+
|
|
77
|
+
# Secrets: Using Rails credentials (update via: rails credentials:edit)
|
|
78
|
+
# The $(command) syntax allows executing commands to fetch secrets
|
|
79
|
+
secrets:
|
|
80
|
+
superuser_password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :superuser_password)")
|
|
81
|
+
replication_password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :replication_password)")
|
|
82
|
+
repmgr_password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :repmgr_password)")
|
|
83
|
+
pgbouncer_password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :password)")
|
|
84
|
+
app_password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :password)")
|
|
85
|
+
|
|
@@ -51,6 +51,27 @@ module ActivePostgres
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Check pgbouncer on all hosts
|
|
55
|
+
if config.component_enabled?(:pgbouncer)
|
|
56
|
+
puts
|
|
57
|
+
puts '==> Checking PgBouncer...'
|
|
58
|
+
config.all_hosts.each do |host|
|
|
59
|
+
print "PgBouncer (#{host})... "
|
|
60
|
+
pgbouncer_ok = check_pgbouncer_running(host)
|
|
61
|
+
userlist_ok = check_pgbouncer_userlist(host)
|
|
62
|
+
|
|
63
|
+
if pgbouncer_ok && userlist_ok
|
|
64
|
+
puts '✓'
|
|
65
|
+
elsif pgbouncer_ok && !userlist_ok
|
|
66
|
+
puts '✗ (missing app user in userlist)'
|
|
67
|
+
all_ok = false
|
|
68
|
+
else
|
|
69
|
+
puts '✗'
|
|
70
|
+
all_ok = false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
54
75
|
puts
|
|
55
76
|
if all_ok
|
|
56
77
|
puts '✓ All checks passed'
|
|
@@ -99,7 +120,8 @@ module ActivePostgres
|
|
|
99
120
|
label: primary_config&.dig('label') || '-',
|
|
100
121
|
status: check_postgres_running(config.primary_host) ? '✓ running' : '✗ down',
|
|
101
122
|
connections: get_connection_count(config.primary_host),
|
|
102
|
-
lag: '-'
|
|
123
|
+
lag: '-',
|
|
124
|
+
pgbouncer: check_pgbouncer_status(config.primary_host)
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
# Standbys
|
|
@@ -114,7 +136,8 @@ module ActivePostgres
|
|
|
114
136
|
label: standby_config&.dig('label') || '-',
|
|
115
137
|
status: running ? '✓ streaming' : '✗ down',
|
|
116
138
|
connections: running ? get_connection_count(host) : 0,
|
|
117
|
-
lag: running ? get_replication_lag(host) : '-'
|
|
139
|
+
lag: running ? get_replication_lag(host) : '-',
|
|
140
|
+
pgbouncer: check_pgbouncer_status(host)
|
|
118
141
|
}
|
|
119
142
|
end
|
|
120
143
|
|
|
@@ -128,7 +151,7 @@ module ActivePostgres
|
|
|
128
151
|
end
|
|
129
152
|
|
|
130
153
|
def calculate_column_widths(nodes)
|
|
131
|
-
{
|
|
154
|
+
cols = {
|
|
132
155
|
role: [4, nodes.map { |n| n[:role].length }.max].max,
|
|
133
156
|
host: [4, nodes.map { |n| n[:host].length }.max].max,
|
|
134
157
|
private_ip: [10, nodes.map { |n| n[:private_ip].to_s.length }.max].max,
|
|
@@ -137,12 +160,25 @@ module ActivePostgres
|
|
|
137
160
|
conn: 5,
|
|
138
161
|
lag: [3, nodes.map { |n| n[:lag].to_s.length }.max].max
|
|
139
162
|
}
|
|
163
|
+
|
|
164
|
+
if config.component_enabled?(:pgbouncer)
|
|
165
|
+
cols[:pgbouncer] = [9, nodes.map { |n| n[:pgbouncer].to_s.length }.max].max
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
cols
|
|
140
169
|
end
|
|
141
170
|
|
|
142
171
|
def print_table_header(cols)
|
|
143
172
|
fmt = "%-#{cols[:role]}s %-#{cols[:host]}s %-#{cols[:private_ip]}s " \
|
|
144
173
|
"%-#{cols[:label]}s %-#{cols[:status]}s %#{cols[:conn]}s %#{cols[:lag]}s"
|
|
145
|
-
|
|
174
|
+
headers = %w[Role Host Private\ IP Label Status Conn Lag]
|
|
175
|
+
|
|
176
|
+
if cols[:pgbouncer]
|
|
177
|
+
fmt += " %-#{cols[:pgbouncer]}s"
|
|
178
|
+
headers << 'PgBouncer'
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
header = format(fmt, *headers)
|
|
146
182
|
puts header
|
|
147
183
|
puts '-' * header.length
|
|
148
184
|
end
|
|
@@ -150,8 +186,15 @@ module ActivePostgres
|
|
|
150
186
|
def print_table_row(node, cols)
|
|
151
187
|
fmt = "%-#{cols[:role]}s %-#{cols[:host]}s %-#{cols[:private_ip]}s " \
|
|
152
188
|
"%-#{cols[:label]}s %-#{cols[:status]}s %#{cols[:conn]}d %#{cols[:lag]}s"
|
|
153
|
-
|
|
154
|
-
|
|
189
|
+
values = [node[:role], node[:host], node[:private_ip], node[:label],
|
|
190
|
+
node[:status], node[:connections], node[:lag]]
|
|
191
|
+
|
|
192
|
+
if cols[:pgbouncer]
|
|
193
|
+
fmt += " %-#{cols[:pgbouncer]}s"
|
|
194
|
+
values << node[:pgbouncer]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
puts format(fmt, *values)
|
|
155
198
|
end
|
|
156
199
|
|
|
157
200
|
def print_components
|
|
@@ -240,5 +283,37 @@ module ActivePostgres
|
|
|
240
283
|
mb = kb / 1024.0
|
|
241
284
|
"#{mb.round(1)} MB"
|
|
242
285
|
end
|
|
286
|
+
|
|
287
|
+
def check_pgbouncer_status(host)
|
|
288
|
+
return '-' unless config.component_enabled?(:pgbouncer)
|
|
289
|
+
|
|
290
|
+
ssh_executor.execute_on_host(host) do
|
|
291
|
+
result = capture(:sudo, 'systemctl', 'is-active', 'pgbouncer').strip
|
|
292
|
+
result == 'active' ? '✓ running' : '✗ down'
|
|
293
|
+
end
|
|
294
|
+
rescue StandardError
|
|
295
|
+
'✗ down'
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def check_pgbouncer_running(host)
|
|
299
|
+
ssh_executor.execute_on_host(host) do
|
|
300
|
+
result = capture(:sudo, 'systemctl', 'is-active', 'pgbouncer').strip
|
|
301
|
+
result == 'active'
|
|
302
|
+
end
|
|
303
|
+
rescue StandardError
|
|
304
|
+
false
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def check_pgbouncer_userlist(host)
|
|
308
|
+
app_user = config.app_user
|
|
309
|
+
return true unless app_user # No app user configured, skip check
|
|
310
|
+
|
|
311
|
+
ssh_executor.execute_on_host(host) do
|
|
312
|
+
userlist = capture(:sudo, 'cat', '/etc/pgbouncer/userlist.txt').strip
|
|
313
|
+
userlist.include?(app_user)
|
|
314
|
+
end
|
|
315
|
+
rescue StandardError
|
|
316
|
+
false
|
|
317
|
+
end
|
|
243
318
|
end
|
|
244
319
|
end
|
|
@@ -95,7 +95,7 @@ module ActivePostgres
|
|
|
95
95
|
def deploy_pgbouncer
|
|
96
96
|
logger.task('Setting up pgbouncer on standby') do
|
|
97
97
|
component = Components::PgBouncer.new(config, ssh_executor, secrets)
|
|
98
|
-
component.install_on_standby(standby_host)
|
|
98
|
+
component.install_on_standby(standby_host)
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_postgres
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BoringCache
|
|
@@ -172,6 +172,8 @@ files:
|
|
|
172
172
|
- lib/active_postgres/error_handler.rb
|
|
173
173
|
- lib/active_postgres/failover.rb
|
|
174
174
|
- lib/active_postgres/generators/active_postgres/install_generator.rb
|
|
175
|
+
- lib/active_postgres/generators/active_postgres/templates/database.active_postgres.yml.erb
|
|
176
|
+
- lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb
|
|
175
177
|
- lib/active_postgres/health_checker.rb
|
|
176
178
|
- lib/active_postgres/installer.rb
|
|
177
179
|
- lib/active_postgres/log_sanitizer.rb
|
|
@@ -218,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
218
220
|
- !ruby/object:Gem::Version
|
|
219
221
|
version: '0'
|
|
220
222
|
requirements: []
|
|
221
|
-
rubygems_version: 3.
|
|
223
|
+
rubygems_version: 3.7.2
|
|
222
224
|
specification_version: 4
|
|
223
225
|
summary: PostgreSQL High Availability for Rails, made simple
|
|
224
226
|
test_files: []
|