active_postgres 0.4.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 +7 -0
- data/LICENSE +23 -0
- data/README.md +158 -0
- data/exe/activepostgres +5 -0
- data/lib/active_postgres/cli.rb +157 -0
- data/lib/active_postgres/cluster_deployment_flow.rb +85 -0
- data/lib/active_postgres/component_resolver.rb +24 -0
- data/lib/active_postgres/components/base.rb +38 -0
- data/lib/active_postgres/components/core.rb +158 -0
- data/lib/active_postgres/components/extensions.rb +99 -0
- data/lib/active_postgres/components/monitoring.rb +55 -0
- data/lib/active_postgres/components/pgbackrest.rb +94 -0
- data/lib/active_postgres/components/pgbouncer.rb +137 -0
- data/lib/active_postgres/components/repmgr.rb +651 -0
- data/lib/active_postgres/components/ssl.rb +86 -0
- data/lib/active_postgres/configuration.rb +190 -0
- data/lib/active_postgres/connection_pooler.rb +429 -0
- data/lib/active_postgres/credentials.rb +17 -0
- data/lib/active_postgres/deployment_flow.rb +154 -0
- data/lib/active_postgres/error_handler.rb +185 -0
- data/lib/active_postgres/failover.rb +83 -0
- data/lib/active_postgres/generators/active_postgres/install_generator.rb +186 -0
- data/lib/active_postgres/health_checker.rb +244 -0
- data/lib/active_postgres/installer.rb +114 -0
- data/lib/active_postgres/log_sanitizer.rb +67 -0
- data/lib/active_postgres/logger.rb +125 -0
- data/lib/active_postgres/performance_tuner.rb +246 -0
- data/lib/active_postgres/rails/database_config.rb +174 -0
- data/lib/active_postgres/rails/migration_guard.rb +25 -0
- data/lib/active_postgres/railtie.rb +28 -0
- data/lib/active_postgres/retry_helper.rb +80 -0
- data/lib/active_postgres/rollback_manager.rb +140 -0
- data/lib/active_postgres/secrets.rb +86 -0
- data/lib/active_postgres/ssh_executor.rb +288 -0
- data/lib/active_postgres/standby_deployment_flow.rb +122 -0
- data/lib/active_postgres/validator.rb +143 -0
- data/lib/active_postgres/version.rb +3 -0
- data/lib/active_postgres.rb +67 -0
- data/lib/tasks/postgres.rake +855 -0
- data/lib/tasks/rolling_update.rake +258 -0
- data/lib/tasks/rotate_credentials.rake +193 -0
- data/templates/pg_hba.conf.erb +47 -0
- data/templates/pgbackrest.conf.erb +43 -0
- data/templates/pgbouncer.ini.erb +55 -0
- data/templates/postgresql.conf.erb +157 -0
- data/templates/repmgr.conf.erb +40 -0
- metadata +224 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dbeebfa934a6587194e20aa39a3d459b7db2568c996f4faec1b781d70a4254ee
|
|
4
|
+
data.tar.gz: 47867c12a1fbb44652da9a79bb775ce38aaf8ff7b527a0a14ce0e042ca091877
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cd6849746528e378cc7e5d3c7707b20d974dc44cc10e61f4600b7eaccbfd7c9793e20b8741ae7339ab866618aa7357ee6781bf4d7ba7ee0eff959ad9c0af1b24
|
|
7
|
+
data.tar.gz: fc7635c0908c5920e4f21f702badd030c305819b05e63e6a0a6b54793b563e7338738445d38f11e1269566afba9fb0e6efcbea436287ace088518f6bce42a777
|
data/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Gaurav
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# active_postgres
|
|
2
|
+
|
|
3
|
+
Production-grade PostgreSQL HA for Rails.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **High Availability**: Primary/standby replication with automatic failover (repmgr)
|
|
8
|
+
- **Connection Pooling**: PgBouncer integration
|
|
9
|
+
- **Rails Integration**: Automatic database.yml config, migration guard, read replica routing
|
|
10
|
+
- **Modular Components**: Core, Performance Tuning, repmgr, PgBouncer, pgBackRest, Monitoring, SSL, Extensions
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
### 1. Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
gem install active_postgres
|
|
18
|
+
# or add to Gemfile: gem 'active_postgres'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Configure (Rails)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
rails generate active_postgres:install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Edit `config/postgres.yml`:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
production:
|
|
31
|
+
version: 18
|
|
32
|
+
user: ubuntu
|
|
33
|
+
ssh_key: ~/.ssh/id_rsa
|
|
34
|
+
|
|
35
|
+
primary:
|
|
36
|
+
host: 34.12.234.81 # Public IP for SSH
|
|
37
|
+
private_ip: 10.8.0.10 # Private IP for database connections
|
|
38
|
+
|
|
39
|
+
standby:
|
|
40
|
+
- host: 52.23.45.67
|
|
41
|
+
private_ip: 10.8.0.11
|
|
42
|
+
|
|
43
|
+
components:
|
|
44
|
+
repmgr: {enabled: true}
|
|
45
|
+
pgbouncer: {enabled: true}
|
|
46
|
+
|
|
47
|
+
secrets:
|
|
48
|
+
superuser_password: $POSTGRES_SUPERUSER_PASSWORD
|
|
49
|
+
replication_password: $POSTGRES_REPLICATION_PASSWORD
|
|
50
|
+
repmgr_password: $POSTGRES_REPMGR_PASSWORD
|
|
51
|
+
app_password: $POSTGRES_APP_PASSWORD
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Add credentials (`rails credentials:edit`):
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
postgres:
|
|
58
|
+
username: myapp
|
|
59
|
+
password: "your_app_password"
|
|
60
|
+
database: myapp_production
|
|
61
|
+
primary_host: 10.8.0.10
|
|
62
|
+
replica_host: 10.8.0.11
|
|
63
|
+
port: 6432 # 6432 for PgBouncer, 5432 for direct
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Deploy
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
rake postgres:setup # Deploy cluster
|
|
70
|
+
rake postgres:status # Check health
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Common Operations
|
|
74
|
+
|
|
75
|
+
### Rake Tasks
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
rake postgres:setup # Deploy HA cluster
|
|
79
|
+
rake postgres:status # Check cluster status
|
|
80
|
+
rake postgres:verify # Comprehensive health check
|
|
81
|
+
rake postgres:promote[host] # Promote standby to primary
|
|
82
|
+
|
|
83
|
+
# Backups (requires pgBackRest)
|
|
84
|
+
rake postgres:backup:full
|
|
85
|
+
rake postgres:backup:list
|
|
86
|
+
rake postgres:backup:restore[backup_id]
|
|
87
|
+
|
|
88
|
+
# Credential rotation (zero downtime)
|
|
89
|
+
rake postgres:credentials:rotate_random
|
|
90
|
+
rake postgres:credentials:rotate_all
|
|
91
|
+
|
|
92
|
+
# Rolling updates (zero downtime)
|
|
93
|
+
rake postgres:update:version[18] # Major version upgrade
|
|
94
|
+
rake postgres:update:patch # Security patches
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### CLI (Standalone)
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
active_postgres setup --environment=production
|
|
101
|
+
active_postgres setup-standby HOST
|
|
102
|
+
active_postgres status
|
|
103
|
+
active_postgres promote HOST
|
|
104
|
+
active_postgres backup --type=full
|
|
105
|
+
active_postgres cache-secrets
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Components
|
|
109
|
+
|
|
110
|
+
| Component | Description | Config |
|
|
111
|
+
|-----------|-------------|--------|
|
|
112
|
+
| **Core** | PostgreSQL installation | Always enabled |
|
|
113
|
+
| **Performance Tuning** | Auto-optimization | Enabled by default |
|
|
114
|
+
| **repmgr** | HA & automatic failover | `repmgr: {enabled: true}` |
|
|
115
|
+
| **PgBouncer** | Connection pooling | `pgbouncer: {enabled: true}` |
|
|
116
|
+
| **pgBackRest** | Backup & restore | `pgbackrest: {enabled: true}` |
|
|
117
|
+
| **Monitoring** | postgres_exporter | `monitoring: {enabled: true}` |
|
|
118
|
+
| **SSL** | Encrypted connections | `ssl: {enabled: true}` |
|
|
119
|
+
| **Extensions** | pgvector, PostGIS, etc. | `extensions: {enabled: true, list: [pgvector]}` |
|
|
120
|
+
|
|
121
|
+
## Secrets Management
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
secrets:
|
|
125
|
+
# Environment variables
|
|
126
|
+
password: $POSTGRES_PASSWORD
|
|
127
|
+
|
|
128
|
+
# Command execution
|
|
129
|
+
password: $(op read "op://vault/item/field")
|
|
130
|
+
password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :password)")
|
|
131
|
+
|
|
132
|
+
# AWS Secrets Manager
|
|
133
|
+
password: $(aws secretsmanager get-secret-value --secret-id myapp/postgres --query SecretString)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Read/Write Splitting (Rails 6+)
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
140
|
+
connects_to database: { writing: :primary, reading: :primary_replica }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Writes go to primary, reads go to replica
|
|
144
|
+
User.create(name: "Alice") # ā Primary
|
|
145
|
+
User.all # ā Replica
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Requirements
|
|
149
|
+
|
|
150
|
+
- Ruby 3.0+
|
|
151
|
+
- PostgreSQL 12+
|
|
152
|
+
- Ubuntu 20.04+ / Debian 11+ with systemd
|
|
153
|
+
- SSH key-based authentication
|
|
154
|
+
- Rails 6.0+ (optional)
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
data/exe/activepostgres
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
require 'thor'
|
|
2
|
+
|
|
3
|
+
module ActivePostgres
|
|
4
|
+
class CLI < Thor
|
|
5
|
+
class_option :environment, aliases: '-e', default: ENV['BORING_ENVIRONMENT'] || ENV['RAILS_ENV'] || 'development'
|
|
6
|
+
class_option :config, aliases: '-c', default: 'config/postgres.yml'
|
|
7
|
+
|
|
8
|
+
desc 'setup', 'Setup PostgreSQL HA cluster'
|
|
9
|
+
option :dry_run, type: :boolean, default: false
|
|
10
|
+
option :only, type: :string, desc: 'Setup only specific component (core, repmgr, pgbouncer, etc.)'
|
|
11
|
+
def setup
|
|
12
|
+
config = load_config
|
|
13
|
+
installer = Installer.new(config, dry_run: options[:dry_run])
|
|
14
|
+
|
|
15
|
+
if options[:only]
|
|
16
|
+
installer.setup_component(options[:only])
|
|
17
|
+
else
|
|
18
|
+
installer.setup
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc 'setup-primary', '[DEPRECATED] Use "setup" instead - it auto-detects primary-only vs HA'
|
|
23
|
+
option :dry_run, type: :boolean, default: false
|
|
24
|
+
def setup_primary
|
|
25
|
+
puts 'ā ļø DEPRECATED: Use "active_postgres setup" instead.'
|
|
26
|
+
puts ' The setup command now auto-detects whether to deploy primary-only or HA based on your config.'
|
|
27
|
+
puts ''
|
|
28
|
+
setup
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc 'setup-standby HOST', 'Setup a single standby server without touching the primary'
|
|
32
|
+
option :dry_run, type: :boolean, default: false
|
|
33
|
+
def setup_standby(host)
|
|
34
|
+
config = load_config
|
|
35
|
+
installer = Installer.new(config, dry_run: options[:dry_run])
|
|
36
|
+
|
|
37
|
+
unless config.standby_hosts.include?(host)
|
|
38
|
+
puts "Error: #{host} is not configured as a standby in config/postgres.yml"
|
|
39
|
+
exit 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
installer.setup_standby_only(host)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc 'status', 'Show cluster status'
|
|
46
|
+
def status
|
|
47
|
+
config = load_config
|
|
48
|
+
health_checker = HealthChecker.new(config)
|
|
49
|
+
health_checker.show_status
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
desc 'health', 'Run health checks'
|
|
53
|
+
def health
|
|
54
|
+
config = load_config
|
|
55
|
+
health_checker = HealthChecker.new(config)
|
|
56
|
+
health_checker.run_health_checks
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
desc 'promote HOST', 'Promote standby to primary'
|
|
60
|
+
option :node, type: :string
|
|
61
|
+
def promote(host = nil)
|
|
62
|
+
host ||= options[:node]
|
|
63
|
+
|
|
64
|
+
unless host
|
|
65
|
+
puts 'Error: Must specify host or --node'
|
|
66
|
+
exit 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
config = load_config
|
|
70
|
+
failover = Failover.new(config)
|
|
71
|
+
failover.promote(host)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
desc 'backup', 'Create backup'
|
|
75
|
+
option :type, default: 'full', desc: 'Backup type: full, incremental'
|
|
76
|
+
def backup
|
|
77
|
+
config = load_config
|
|
78
|
+
|
|
79
|
+
unless config.component_enabled?(:pgbackrest)
|
|
80
|
+
puts 'Error: pgBackRest component not enabled'
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
installer = Installer.new(config)
|
|
85
|
+
installer.run_backup(options[:type])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
desc 'restore BACKUP_ID', 'Restore from backup'
|
|
89
|
+
def restore(backup_id)
|
|
90
|
+
config = load_config
|
|
91
|
+
|
|
92
|
+
unless config.component_enabled?(:pgbackrest)
|
|
93
|
+
puts 'Error: pgBackRest component not enabled'
|
|
94
|
+
exit 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
installer = Installer.new(config)
|
|
98
|
+
installer.run_restore(backup_id)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
desc 'list-backups', 'List available backups'
|
|
102
|
+
def list_backups
|
|
103
|
+
config = load_config
|
|
104
|
+
|
|
105
|
+
unless config.component_enabled?(:pgbackrest)
|
|
106
|
+
puts 'Error: pgBackRest component not enabled'
|
|
107
|
+
exit 1
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
installer = Installer.new(config)
|
|
111
|
+
installer.list_backups
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
desc 'install COMPONENT', 'Install specific component'
|
|
115
|
+
def install(component)
|
|
116
|
+
config = load_config
|
|
117
|
+
installer = Installer.new(config)
|
|
118
|
+
installer.setup_component(component)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
desc 'uninstall COMPONENT', 'Uninstall specific component'
|
|
122
|
+
def uninstall(component)
|
|
123
|
+
config = load_config
|
|
124
|
+
installer = Installer.new(config)
|
|
125
|
+
installer.uninstall_component(component)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
desc 'restart COMPONENT', 'Restart specific component'
|
|
129
|
+
def restart(component)
|
|
130
|
+
config = load_config
|
|
131
|
+
installer = Installer.new(config)
|
|
132
|
+
installer.restart_component(component)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
desc 'cache-secrets', 'Fetch and cache secrets locally'
|
|
136
|
+
option :directory, default: '.secrets'
|
|
137
|
+
def cache_secrets
|
|
138
|
+
config = load_config
|
|
139
|
+
secrets = Secrets.new(config)
|
|
140
|
+
secrets.cache_to_files(options[:directory])
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
desc 'version', 'Show version'
|
|
144
|
+
def version
|
|
145
|
+
puts "active_postgres #{ActivePostgres::VERSION}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def load_config
|
|
151
|
+
Configuration.load(options[:config], options[:environment])
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
puts "Error loading config: #{e.message}"
|
|
154
|
+
exit 1
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module ActivePostgres
|
|
2
|
+
class ClusterDeploymentFlow < DeploymentFlow
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
def operation_name
|
|
6
|
+
if standbys?
|
|
7
|
+
'PostgreSQL HA Cluster Setup'
|
|
8
|
+
else
|
|
9
|
+
'PostgreSQL Primary Setup'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def print_targets
|
|
14
|
+
logger.info "Primary: #{config.primary_host}"
|
|
15
|
+
if standbys?
|
|
16
|
+
logger.info "Standbys: #{config.standby_hosts.join(', ')}"
|
|
17
|
+
else
|
|
18
|
+
logger.info 'Standbys: None (primary-only setup)'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate_specific_requirements
|
|
23
|
+
return unless config.component_enabled?(:repmgr) && !standbys?
|
|
24
|
+
|
|
25
|
+
logger.warn 'ā ļø repmgr is enabled but no standbys configured - will skip repmgr setup'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def list_deployment_steps
|
|
29
|
+
if standbys?
|
|
30
|
+
logger.info " ⢠Install/recreate PostgreSQL #{config.version} on all servers"
|
|
31
|
+
logger.info ' ⢠Configure repmgr for high availability' if should_setup_repmgr?
|
|
32
|
+
else
|
|
33
|
+
logger.info " ⢠Install/recreate PostgreSQL #{config.version} on primary"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
logger.info ' ⢠Setup pgbouncer connection pooling' if config.component_enabled?(:pgbouncer)
|
|
37
|
+
logger.info ' ⢠Configure pgbackrest backups' if config.component_enabled?(:pgbackrest)
|
|
38
|
+
logger.info ' ⢠Install postgres_exporter monitoring' if config.component_enabled?(:monitoring)
|
|
39
|
+
logger.info ' ⢠Enable SSL/TLS connections' if config.component_enabled?(:ssl)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def deploy_components
|
|
43
|
+
hosts_to_deploy = standbys? ? config.all_hosts : [config.primary_host]
|
|
44
|
+
|
|
45
|
+
setup_component('ssl', hosts_to_deploy) if config.component_enabled?(:ssl)
|
|
46
|
+
setup_component('core', hosts_to_deploy)
|
|
47
|
+
|
|
48
|
+
components = %i[pgbouncer pgbackrest monitoring extensions]
|
|
49
|
+
components.unshift(:repmgr) if should_setup_repmgr?
|
|
50
|
+
|
|
51
|
+
components.each do |component|
|
|
52
|
+
setup_component(component.to_s, hosts_to_deploy) if config.component_enabled?(component)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Create application users AFTER repmgr to avoid being wiped by cluster recreation
|
|
56
|
+
create_application_users_if_configured
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def list_next_steps
|
|
60
|
+
logger.info ''
|
|
61
|
+
logger.info 'š Next Steps:'
|
|
62
|
+
logger.info ' 1. Verify cluster: rake postgres:verify'
|
|
63
|
+
logger.info " 2. Update database.yml to use: #{config.primary_host}:#{config.component_enabled?(:pgbouncer) ? '6432' : '5432'}"
|
|
64
|
+
logger.info ' 3. Run migrations: rake postgres:migrate'
|
|
65
|
+
logger.info ' 4. Update PgBouncer userlist: rake postgres:pgbouncer:update_userlist[your_app_user]' if config.component_enabled?(:pgbouncer)
|
|
66
|
+
|
|
67
|
+
return if standbys?
|
|
68
|
+
|
|
69
|
+
logger.info ' 5. To add HA later: Add standbys to config ā run: rake postgres:setup'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def standbys?
|
|
73
|
+
config.standby_hosts.any?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def should_setup_repmgr?
|
|
77
|
+
config.component_enabled?(:repmgr) && standbys?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_application_users_if_configured
|
|
81
|
+
core_component = Components::Core.new(config, ssh_executor, secrets)
|
|
82
|
+
core_component.create_application_users
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module ActivePostgres
|
|
2
|
+
module ComponentResolver
|
|
3
|
+
def component_class_for(component_name)
|
|
4
|
+
case component_name.to_s.downcase
|
|
5
|
+
when 'core'
|
|
6
|
+
Components::Core
|
|
7
|
+
when 'repmgr'
|
|
8
|
+
Components::Repmgr
|
|
9
|
+
when 'pgbouncer'
|
|
10
|
+
Components::PgBouncer
|
|
11
|
+
when 'pgbackrest'
|
|
12
|
+
Components::PgBackRest
|
|
13
|
+
when 'monitoring'
|
|
14
|
+
Components::Monitoring
|
|
15
|
+
when 'ssl'
|
|
16
|
+
Components::SSL
|
|
17
|
+
when 'extensions'
|
|
18
|
+
Components::Extensions
|
|
19
|
+
else
|
|
20
|
+
raise Error, "Unknown component: #{component_name}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module ActivePostgres
|
|
2
|
+
module Components
|
|
3
|
+
class Base
|
|
4
|
+
attr_reader :config, :ssh_executor, :secrets
|
|
5
|
+
|
|
6
|
+
def initialize(config, ssh_executor, secrets)
|
|
7
|
+
@config = config
|
|
8
|
+
@ssh_executor = ssh_executor
|
|
9
|
+
@secrets = secrets
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def install
|
|
13
|
+
raise NotImplementedError, 'Subclass must implement #install'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def uninstall
|
|
17
|
+
raise NotImplementedError, 'Subclass must implement #uninstall'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def restart
|
|
21
|
+
raise NotImplementedError, 'Subclass must implement #restart'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
protected
|
|
25
|
+
|
|
26
|
+
def render_template(template_name, binding_context)
|
|
27
|
+
template_path = File.join(ActivePostgres.root, 'templates', template_name)
|
|
28
|
+
template = ERB.new(File.read(template_path), trim_mode: '-')
|
|
29
|
+
template.result(binding_context)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def upload_template(host, template_name, remote_path, binding_context, mode: '644', owner: nil)
|
|
33
|
+
content = render_template(template_name, binding_context)
|
|
34
|
+
ssh_executor.upload_file(host, content, remote_path, mode: mode, owner: owner)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
module ActivePostgres
|
|
2
|
+
module Components
|
|
3
|
+
class Core < Base
|
|
4
|
+
def install
|
|
5
|
+
puts 'Installing PostgreSQL core...'
|
|
6
|
+
|
|
7
|
+
# Install on primary
|
|
8
|
+
install_on_host(config.primary_host, is_primary: true)
|
|
9
|
+
|
|
10
|
+
# NOTE: App user creation moved to after repmgr setup to avoid being wiped
|
|
11
|
+
# See: create_application_user_and_database method called from deployment flow
|
|
12
|
+
|
|
13
|
+
# Install on standbys
|
|
14
|
+
# If repmgr is enabled, only install packages (cluster will be cloned by repmgr)
|
|
15
|
+
# If repmgr is disabled, install everything including cluster creation
|
|
16
|
+
config.standby_hosts.each do |host|
|
|
17
|
+
if config.component_enabled?(:repmgr)
|
|
18
|
+
install_packages_only(host)
|
|
19
|
+
else
|
|
20
|
+
install_on_host(host, is_primary: false)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def uninstall
|
|
26
|
+
puts 'Uninstalling PostgreSQL is not recommended and must be done manually.'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def restart
|
|
30
|
+
puts 'Restarting PostgreSQL...'
|
|
31
|
+
|
|
32
|
+
# Restart on all hosts
|
|
33
|
+
config.all_hosts.each do |host|
|
|
34
|
+
ssh_executor.restart_postgres(host, config.version)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Public method to create application users - called after repmgr setup
|
|
39
|
+
# This is done after repmgr to avoid being wiped by cluster recreation
|
|
40
|
+
def create_application_users
|
|
41
|
+
return unless config.app_user && config.app_database
|
|
42
|
+
|
|
43
|
+
puts "\nš Creating application users and databases..."
|
|
44
|
+
create_app_user_and_database(config.primary_host)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def install_on_host(host, is_primary:)
|
|
50
|
+
puts " Installing on #{host}..."
|
|
51
|
+
|
|
52
|
+
ssh_executor.install_postgres(host, config.version)
|
|
53
|
+
ssh_executor.ensure_cluster_exists(host, config.version)
|
|
54
|
+
|
|
55
|
+
# Get base component config
|
|
56
|
+
component_config = config.component_config(:core)
|
|
57
|
+
|
|
58
|
+
# Calculate optimal PostgreSQL settings if performance tuning is enabled
|
|
59
|
+
# pg_config is used in ERB template via binding
|
|
60
|
+
pg_config = if config.component_enabled?(:performance_tuning)
|
|
61
|
+
calculate_tuned_settings(host, component_config)
|
|
62
|
+
else
|
|
63
|
+
component_config[:postgresql] || {}
|
|
64
|
+
end
|
|
65
|
+
_ = pg_config # Used in ERB template
|
|
66
|
+
|
|
67
|
+
upload_template(host, 'postgresql.conf.erb', "/etc/postgresql/#{config.version}/main/postgresql.conf", binding,
|
|
68
|
+
owner: 'postgres:postgres')
|
|
69
|
+
upload_template(host, 'pg_hba.conf.erb', "/etc/postgresql/#{config.version}/main/pg_hba.conf", binding,
|
|
70
|
+
owner: 'postgres:postgres')
|
|
71
|
+
|
|
72
|
+
ssh_executor.restart_postgres(host, config.version)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def calculate_tuned_settings(host, component_config)
|
|
76
|
+
tuning_config = config.component_config(:performance_tuning)
|
|
77
|
+
db_type = tuning_config[:db_type] || 'web'
|
|
78
|
+
|
|
79
|
+
puts " Auto-tuning PostgreSQL for #{db_type} workload..."
|
|
80
|
+
|
|
81
|
+
# Initialize tuner and calculate optimal settings
|
|
82
|
+
tuner = PerformanceTuner.new(config, ssh_executor)
|
|
83
|
+
optimal_settings = tuner.tune_for_host(host, db_type: db_type)
|
|
84
|
+
|
|
85
|
+
# Merge: user config overrides calculated settings
|
|
86
|
+
user_postgresql = component_config[:postgresql] || {}
|
|
87
|
+
optimal_settings.merge(user_postgresql)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def install_packages_only(host)
|
|
91
|
+
puts " Installing packages on #{host} (cluster will be created by repmgr)..."
|
|
92
|
+
ssh_executor.install_postgres(host, config.version)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def create_app_user_and_database(host)
|
|
96
|
+
app_user = config.app_user
|
|
97
|
+
app_database = config.app_database
|
|
98
|
+
|
|
99
|
+
return unless app_user && app_database
|
|
100
|
+
|
|
101
|
+
puts " Creating application user '#{app_user}' and database '#{app_database}'..."
|
|
102
|
+
app_password = resolve_app_password
|
|
103
|
+
sql = build_app_user_sql(app_user, app_database, app_password)
|
|
104
|
+
|
|
105
|
+
ssh_executor.execute_on_host(host) do
|
|
106
|
+
upload! StringIO.new(sql), '/tmp/create_app_user.sql'
|
|
107
|
+
execute :chmod, '644', '/tmp/create_app_user.sql'
|
|
108
|
+
execute :sudo, '-u', 'postgres', 'psql', '-f', '/tmp/create_app_user.sql'
|
|
109
|
+
execute :rm, '-f', '/tmp/create_app_user.sql'
|
|
110
|
+
|
|
111
|
+
puts " ā Created app user '#{app_user}' and database '#{app_database}'"
|
|
112
|
+
end
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
warn " Warning: Could not create app user: #{e.message}"
|
|
115
|
+
warn ' You may need to create the user manually'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def resolve_app_password
|
|
119
|
+
app_password = secrets.resolve('app_password')
|
|
120
|
+
if app_password.nil? || app_password.empty?
|
|
121
|
+
raise Error, 'app_password is empty or nil. Check your postgres.yml secrets section and ensure RAILS_ENV=production is set.'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
app_password
|
|
125
|
+
rescue StandardError => e
|
|
126
|
+
raise Error, "Cannot resolve app_password: #{e.message}. Make sure RAILS_ENV is set when running deployment."
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_app_user_sql(app_user, app_database, app_password)
|
|
130
|
+
escaped_password = app_password.gsub("'", "''")
|
|
131
|
+
|
|
132
|
+
[
|
|
133
|
+
'-- Create app user if not exists',
|
|
134
|
+
'DO $$',
|
|
135
|
+
'BEGIN',
|
|
136
|
+
" IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '#{app_user}') THEN",
|
|
137
|
+
" CREATE USER #{app_user} WITH PASSWORD '#{escaped_password}' CREATEDB;",
|
|
138
|
+
' ELSE',
|
|
139
|
+
" ALTER USER #{app_user} WITH PASSWORD '#{escaped_password}';",
|
|
140
|
+
' END IF;',
|
|
141
|
+
'END $$;',
|
|
142
|
+
'',
|
|
143
|
+
'-- Ensure user has CREATEDB',
|
|
144
|
+
"ALTER USER #{app_user} CREATEDB;",
|
|
145
|
+
'',
|
|
146
|
+
'-- Create database if not exists',
|
|
147
|
+
"SELECT 'CREATE DATABASE #{app_database} OWNER #{app_user}'",
|
|
148
|
+
"WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '#{app_database}')\\gexec",
|
|
149
|
+
'',
|
|
150
|
+
'-- Grant privileges',
|
|
151
|
+
"GRANT ALL PRIVILEGES ON DATABASE #{app_database} TO #{app_user};",
|
|
152
|
+
"\\c #{app_database}",
|
|
153
|
+
"GRANT ALL ON SCHEMA public TO #{app_user};"
|
|
154
|
+
].join("\n")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|