@sysnee/pgs 0.1.7-rc.5 → 0.1.7-rc.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.
- package/README.md +155 -188
- package/docs/ARCHITECTURE_RECOMMENDATIONS.md +11 -9
- package/docs/CRITICAL_REVIEW.md +10 -8
- package/docs/EXECUTIVE_SUMMARY.md +9 -9
- package/docs/PROJECT.md +130 -77
- package/manager.js +79 -23
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,151 +1,164 @@
|
|
|
1
1
|
# PostgreSQL Multi-Tenant Instance Manager
|
|
2
2
|
|
|
3
|
-
Dynamic PostgreSQL multi-tenant management system providing complete database isolation by creating dedicated PostgreSQL instances per tenant, with intelligent routing via
|
|
3
|
+
Dynamic PostgreSQL multi-tenant management system providing complete database isolation by creating dedicated PostgreSQL instances per tenant, with intelligent SNI-based routing via Traefik v3.
|
|
4
4
|
|
|
5
|
-
> 📖 **For detailed project definition, architecture, and comparison with similar solutions, see [PROJECT.md](./PROJECT.md)**
|
|
5
|
+
> 📖 **For detailed project definition, architecture, and comparison with similar solutions, see [docs/PROJECT.md](./docs/PROJECT.md)**
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### Initial Setup
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pgs setup
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This creates the configuration directory at `~/.sysnee-config/` with:
|
|
16
|
+
- `docker-compose.yml` - Container orchestration
|
|
17
|
+
- `traefik.yml` - Traefik static configuration
|
|
18
|
+
- `dynamic.yml` - Traefik dynamic routing configuration
|
|
19
|
+
- `tenant-access.json` - Access control
|
|
20
|
+
- `certs/` - SSL certificates directory
|
|
21
|
+
|
|
22
|
+
### SSL Certificate Setup
|
|
23
|
+
|
|
24
|
+
Place your wildcard SSL certificate in the certs directory:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Using Let's Encrypt
|
|
28
|
+
sudo cp /etc/letsencrypt/live/pgs.YOUR-DOMAIN.com/fullchain.pem ~/.sysnee-config/certs/
|
|
29
|
+
sudo cp /etc/letsencrypt/live/pgs.YOUR-DOMAIN.com/privkey.pem ~/.sysnee-config/certs/
|
|
30
|
+
sudo chown $USER:$USER ~/.sysnee-config/certs/*
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### DNS Configuration
|
|
34
|
+
|
|
35
|
+
Add a wildcard DNS record pointing to your server:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
*.pgs.YOUR-DOMAIN.com → YOUR_SERVER_IP
|
|
39
|
+
```
|
|
6
40
|
|
|
7
41
|
## Usage
|
|
8
42
|
|
|
9
43
|
### Create a new tenant instance
|
|
10
44
|
|
|
11
45
|
```bash
|
|
12
|
-
|
|
46
|
+
pgs create <tenant-id> [--password <password>]
|
|
13
47
|
```
|
|
14
48
|
|
|
15
49
|
Example:
|
|
16
50
|
```bash
|
|
17
|
-
|
|
18
|
-
|
|
51
|
+
pgs create tenant1
|
|
52
|
+
pgs create tenant2 --password mycustompass
|
|
19
53
|
```
|
|
20
54
|
|
|
21
|
-
New tenants are created with external access **
|
|
55
|
+
New tenants are created with external access **enabled** by default.
|
|
22
56
|
|
|
23
57
|
### List all tenants
|
|
24
58
|
|
|
25
59
|
```bash
|
|
26
|
-
|
|
60
|
+
pgs list
|
|
27
61
|
```
|
|
28
62
|
|
|
29
|
-
Shows all tenants with their external access status.
|
|
63
|
+
Shows all tenants with their hostnames and external access status.
|
|
30
64
|
|
|
31
65
|
### Remove a tenant
|
|
32
66
|
|
|
33
67
|
```bash
|
|
34
|
-
|
|
68
|
+
pgs remove <tenant-id>
|
|
35
69
|
```
|
|
36
70
|
|
|
37
71
|
### Start services
|
|
38
72
|
|
|
39
73
|
```bash
|
|
40
|
-
|
|
41
|
-
|
|
74
|
+
pgs start # Start all services (including Traefik)
|
|
75
|
+
pgs start <tenant-id> # Start specific tenant
|
|
42
76
|
```
|
|
43
77
|
|
|
44
78
|
### Stop services
|
|
45
79
|
|
|
46
80
|
```bash
|
|
47
|
-
|
|
48
|
-
|
|
81
|
+
pgs stop # Stop all services
|
|
82
|
+
pgs stop <tenant-id> # Stop specific tenant
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Access Control
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pgs enable-access <tenant-id> # Enable external access
|
|
89
|
+
pgs disable-access <tenant-id> # Disable external access
|
|
49
90
|
```
|
|
50
91
|
|
|
51
92
|
## How it works
|
|
52
93
|
|
|
53
|
-
-
|
|
54
|
-
-
|
|
94
|
+
- **Traefik v3** listens on port 5432 with TLS/SSL enabled
|
|
95
|
+
- Routes connections based on **SNI (Server Name Indication)** hostname
|
|
96
|
+
- Each tenant connects using their unique hostname (e.g., `tenant1-abc123.pgs.domain.com`)
|
|
55
97
|
- External access is controlled via `tenant-access.json`
|
|
56
98
|
- Tenants are isolated in their own PostgreSQL containers on a Docker bridge network
|
|
57
|
-
- Only
|
|
99
|
+
- Only Traefik has external port mapping; PostgreSQL containers are internal only
|
|
58
100
|
|
|
59
101
|
## Connection
|
|
60
102
|
|
|
61
|
-
After creating a tenant
|
|
103
|
+
After creating a tenant:
|
|
62
104
|
|
|
63
105
|
```bash
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
## Project Definition
|
|
68
|
-
|
|
69
|
-
### What It Is
|
|
70
|
-
|
|
71
|
-
A **dynamic PostgreSQL multi-tenant management system** that provides complete database isolation by creating dedicated PostgreSQL instances per tenant. The system uses HAProxy with custom PostgreSQL protocol parsing to route connections intelligently while maintaining complete tenant isolation at the database server level.
|
|
72
|
-
|
|
73
|
-
### Core Concept
|
|
106
|
+
# Using psql
|
|
107
|
+
psql "postgresql://postgres:PASSWORD@TENANT-ID.pgs.YOUR-DOMAIN.com:5432/DATABASE?sslmode=require"
|
|
74
108
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- **Independent Scaling**: Resources can be allocated per tenant
|
|
79
|
-
- **Enhanced Security**: No risk of cross-tenant data access
|
|
80
|
-
- **Simplified Operations**: Each tenant can be managed independently
|
|
81
|
-
|
|
82
|
-
### Key Features
|
|
83
|
-
|
|
84
|
-
1. **Dynamic Tenant Provisioning**
|
|
85
|
-
- Create new PostgreSQL instances on-demand
|
|
86
|
-
- Automatic volume and network configuration
|
|
87
|
-
- Custom initialization scripts per tenant
|
|
88
|
-
|
|
89
|
-
2. **Intelligent Routing**
|
|
90
|
-
- HAProxy parses PostgreSQL protocol to extract username
|
|
91
|
-
- Routes connections to correct tenant backend automatically
|
|
92
|
-
- Single external port (5432) for all tenants
|
|
93
|
-
|
|
94
|
-
3. **Access Control**
|
|
95
|
-
- Per-tenant external access enable/disable
|
|
96
|
-
- Secure by default (access disabled on creation)
|
|
97
|
-
- Runtime access control without service restart
|
|
109
|
+
# Example
|
|
110
|
+
psql "postgresql://postgres:mypass@tenant1-abc123.pgs.us-central-1.sysnee.com:5432/tenant1-abc123?sslmode=require"
|
|
111
|
+
```
|
|
98
112
|
|
|
99
|
-
|
|
100
|
-
- Separate Docker containers per tenant
|
|
101
|
-
- Isolated volumes for data persistence
|
|
102
|
-
- Network isolation via Docker bridge network
|
|
103
|
-
- No shared processes or memory
|
|
113
|
+
### DBeaver / GUI Clients
|
|
104
114
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
1. **Host**: `tenant-id.pgs.your-domain.com`
|
|
116
|
+
2. **Port**: `5432`
|
|
117
|
+
3. **Database**: `tenant-id`
|
|
118
|
+
4. **Username**: `postgres`
|
|
119
|
+
5. **Password**: (your password)
|
|
120
|
+
6. **SSL**: Enable SSL, set mode to `require`
|
|
109
121
|
|
|
110
122
|
## Architecture
|
|
111
123
|
|
|
112
124
|
```
|
|
113
|
-
|
|
114
|
-
│
|
|
115
|
-
│
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
│
|
|
121
|
-
│
|
|
122
|
-
│ │
|
|
123
|
-
│ │ -
|
|
124
|
-
│ │ -
|
|
125
|
-
│ │ -
|
|
126
|
-
│ │ -
|
|
127
|
-
│
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
┌──────────────┐
|
|
134
|
-
│
|
|
135
|
-
│
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
│
|
|
142
|
-
│
|
|
143
|
-
│
|
|
144
|
-
│
|
|
145
|
-
│
|
|
146
|
-
│
|
|
147
|
-
│
|
|
148
|
-
|
|
125
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
126
|
+
│ External Access │
|
|
127
|
+
│ (tenant-id.pgs.domain.com:5432 + TLS/SNI) │
|
|
128
|
+
└───────────────────────────┬─────────────────────────────────────┘
|
|
129
|
+
│
|
|
130
|
+
▼
|
|
131
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
132
|
+
│ Traefik v3 Proxy │
|
|
133
|
+
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
134
|
+
│ │ EntryPoint: postgres (port 5432) │ │
|
|
135
|
+
│ │ - TLS termination with wildcard certificate │ │
|
|
136
|
+
│ │ - PostgreSQL STARTTLS protocol support │ │
|
|
137
|
+
│ │ - SNI-based routing (HostSNI rule) │ │
|
|
138
|
+
│ │ - Dynamic configuration via dynamic.yml │ │
|
|
139
|
+
│ └───────────────────────────────────────────────────────────┘ │
|
|
140
|
+
└───────────────────────────┬─────────────────────────────────────┘
|
|
141
|
+
│
|
|
142
|
+
┌──────────────────┼──────────────────┐
|
|
143
|
+
│ │ │
|
|
144
|
+
▼ ▼ ▼
|
|
145
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
146
|
+
│ TCP Router │ │ TCP Router │ │ TCP Router │
|
|
147
|
+
│ tenant1 │ │ tenant2 │ │ tenant3 │
|
|
148
|
+
│ HostSNI() │ │ HostSNI() │ │ HostSNI() │
|
|
149
|
+
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
|
150
|
+
│ │ │
|
|
151
|
+
▼ ▼ ▼
|
|
152
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
153
|
+
│ PostgreSQL │ │ PostgreSQL │ │ PostgreSQL │
|
|
154
|
+
│ Container 1 │ │ Container 2 │ │ Container 3 │
|
|
155
|
+
│ │ │ │ │ │
|
|
156
|
+
│ Port: 5432 │ │ Port: 5432 │ │ Port: 5432 │
|
|
157
|
+
│ (internal) │ │ (internal) │ │ (internal) │
|
|
158
|
+
│ │ │ │ │ │
|
|
159
|
+
│ Volume: │ │ Volume: │ │ Volume: │
|
|
160
|
+
│ pgdata_1 │ │ pgdata_2 │ │ pgdata_3 │
|
|
161
|
+
└──────────────┘ └──────────────┘ └──────────────┘
|
|
149
162
|
```
|
|
150
163
|
|
|
151
164
|
## Technical Implementation
|
|
@@ -155,22 +168,16 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
155
168
|
1. **Manager Script (manager.js)**
|
|
156
169
|
- Node.js CLI tool for tenant lifecycle management
|
|
157
170
|
- Dynamically generates docker-compose.yml entries
|
|
158
|
-
- Manages
|
|
171
|
+
- Manages Traefik dynamic configuration
|
|
159
172
|
- Controls tenant access permissions
|
|
160
173
|
|
|
161
|
-
2. **
|
|
162
|
-
- TCP-level
|
|
163
|
-
-
|
|
164
|
-
-
|
|
165
|
-
-
|
|
166
|
-
|
|
167
|
-
3. **PostgreSQL Protocol Parser (pg-route.lua)**
|
|
168
|
-
- Parses binary PostgreSQL startup packet
|
|
169
|
-
- Extracts username and connection parameters
|
|
170
|
-
- Handles SSL negotiation
|
|
171
|
-
- Enforces access control policies
|
|
174
|
+
2. **Traefik v3 Reverse Proxy**
|
|
175
|
+
- TCP-level routing with TLS termination
|
|
176
|
+
- Native PostgreSQL STARTTLS protocol support
|
|
177
|
+
- SNI-based tenant routing
|
|
178
|
+
- Dynamic configuration without restarts
|
|
172
179
|
|
|
173
|
-
|
|
180
|
+
3. **Docker Infrastructure**
|
|
174
181
|
- Separate container per tenant
|
|
175
182
|
- Bridge network for internal communication
|
|
176
183
|
- Persistent volumes for data
|
|
@@ -178,13 +185,22 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
178
185
|
|
|
179
186
|
### Connection Flow
|
|
180
187
|
|
|
181
|
-
1. Client connects to `
|
|
182
|
-
2.
|
|
183
|
-
3.
|
|
184
|
-
4.
|
|
185
|
-
5. If
|
|
186
|
-
6.
|
|
187
|
-
|
|
188
|
+
1. Client connects to `tenant-id.pgs.domain.com:5432` with `sslmode=require`
|
|
189
|
+
2. Traefik receives connection and initiates PostgreSQL STARTTLS handshake
|
|
190
|
+
3. Client sends TLS ClientHello with SNI (hostname)
|
|
191
|
+
4. Traefik extracts SNI and matches against configured routers
|
|
192
|
+
5. If tenant has access enabled, routes to backend `pgs_{tenant_id}:5432`
|
|
193
|
+
6. Connection established with complete isolation
|
|
194
|
+
|
|
195
|
+
### Why Traefik v3?
|
|
196
|
+
|
|
197
|
+
PostgreSQL uses a non-standard TLS negotiation (STARTTLS):
|
|
198
|
+
1. Client sends PostgreSQL SSLRequest packet
|
|
199
|
+
2. Server responds 'S' (SSL supported)
|
|
200
|
+
3. Client sends TLS ClientHello with SNI
|
|
201
|
+
4. TLS handshake completes
|
|
202
|
+
|
|
203
|
+
**Traefik v3** is one of the few proxies that natively understands this PostgreSQL-specific flow, allowing SNI-based routing for PostgreSQL connections.
|
|
188
204
|
|
|
189
205
|
## Comparison with Similar Solutions
|
|
190
206
|
|
|
@@ -195,65 +211,29 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
195
211
|
- Multiple databases/schemas per instance
|
|
196
212
|
- Shared processes and memory
|
|
197
213
|
- Risk of cross-tenant data access
|
|
198
|
-
- Less isolation
|
|
199
214
|
|
|
200
215
|
**This Solution:**
|
|
201
216
|
- Multiple PostgreSQL instances
|
|
202
217
|
- One instance per tenant
|
|
203
218
|
- Complete process isolation
|
|
204
219
|
- Zero risk of cross-tenant access
|
|
205
|
-
- Maximum isolation
|
|
206
220
|
|
|
207
221
|
### Similar Open Source Solutions
|
|
208
222
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
- **Purpose**: PostgreSQL extension for distributed PostgreSQL
|
|
216
|
-
- **Difference**: Shards data across nodes; this creates separate instances per tenant
|
|
217
|
-
- **Use Case**: Horizontal scaling vs. tenant isolation
|
|
218
|
-
|
|
219
|
-
#### 3. **Patroni + HAProxy**
|
|
220
|
-
- **Purpose**: High availability and load balancing
|
|
221
|
-
- **Difference**: Replicates single database; this creates isolated instances
|
|
222
|
-
- **Use Case**: HA for single database vs. multi-tenant isolation
|
|
223
|
-
|
|
224
|
-
#### 4. **Schema-based Multi-tenancy**
|
|
225
|
-
- **Purpose**: Share database, separate schemas
|
|
226
|
-
- **Difference**: Shared instance; this uses separate instances
|
|
227
|
-
- **Use Case**: Resource efficiency vs. complete isolation
|
|
223
|
+
| Solution | Purpose | Difference |
|
|
224
|
+
|----------|---------|------------|
|
|
225
|
+
| **PgBouncer** | Connection pooling | Pools to single instance; this creates separate instances |
|
|
226
|
+
| **Citus** | Distributed PostgreSQL | Shards data; this isolates tenants completely |
|
|
227
|
+
| **Patroni** | High availability | Replicates single DB; this creates isolated instances |
|
|
228
|
+
| **RLS** | Row-level security | Logic-based separation; this uses infrastructure isolation |
|
|
228
229
|
|
|
229
|
-
|
|
230
|
-
- **Purpose**: Application-level tenant isolation
|
|
231
|
-
- **Difference**: Logic-based separation; this uses infrastructure isolation
|
|
232
|
-
- **Use Case**: Application isolation vs. infrastructure isolation
|
|
230
|
+
### Unique Aspects
|
|
233
231
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
2. **Dynamic provisioning with single external port**
|
|
241
|
-
- No need for port management
|
|
242
|
-
- Automatic routing based on connection parameters
|
|
243
|
-
|
|
244
|
-
3. **Protocol-aware routing**
|
|
245
|
-
- Parses PostgreSQL binary protocol
|
|
246
|
-
- Routes before connection completion
|
|
247
|
-
- Handles SSL negotiation
|
|
248
|
-
|
|
249
|
-
4. **Runtime access control**
|
|
250
|
-
- Enable/disable tenant access without restart
|
|
251
|
-
- No downtime for access changes
|
|
252
|
-
|
|
253
|
-
5. **Docker-native architecture**
|
|
254
|
-
- Leverages container isolation
|
|
255
|
-
- Simple deployment and scaling
|
|
256
|
-
- Resource limits per tenant
|
|
232
|
+
1. **Instance-per-tenant** - Complete process and memory isolation
|
|
233
|
+
2. **SNI-based routing** - Single port, automatic hostname-based routing
|
|
234
|
+
3. **TLS by default** - Secure connections required
|
|
235
|
+
4. **Dynamic provisioning** - Create tenants on-demand via CLI
|
|
236
|
+
5. **Docker-native** - Simple deployment and resource limits per tenant
|
|
257
237
|
|
|
258
238
|
## Use Cases
|
|
259
239
|
|
|
@@ -263,52 +243,39 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
263
243
|
- **Healthcare/Finance** applications with compliance requirements
|
|
264
244
|
- **Multi-tenant platforms** needing independent scaling
|
|
265
245
|
- **Development/Testing** environments with isolated databases
|
|
266
|
-
- **Legacy application migration** requiring tenant separation
|
|
267
246
|
|
|
268
247
|
### Not Ideal For
|
|
269
248
|
|
|
270
249
|
- Thousands of tenants (resource overhead)
|
|
271
|
-
- Shared resource requirements
|
|
272
250
|
- Simple multi-tenant applications without strict isolation needs
|
|
273
251
|
- Environments requiring minimal resource usage
|
|
274
252
|
|
|
275
|
-
## Advantages
|
|
276
|
-
|
|
277
|
-
✅ **Maximum Isolation**: Complete process and data separation
|
|
278
|
-
✅ **Security**: Zero risk of cross-tenant data access
|
|
279
|
-
✅ **Flexibility**: Independent scaling and management per tenant
|
|
280
|
-
✅ **Simplicity**: Single external port, automatic routing
|
|
281
|
-
✅ **Compliance**: Easier to meet regulatory requirements
|
|
282
|
-
✅ **Debugging**: Isolated environments simplify troubleshooting
|
|
283
|
-
|
|
284
|
-
## Trade-offs
|
|
285
|
-
|
|
286
|
-
⚠️ **Resource Usage**: Higher memory/CPU per tenant
|
|
287
|
-
⚠️ **Management Overhead**: More containers to manage
|
|
288
|
-
⚠️ **Scaling Limits**: Practical limit on number of tenants per host
|
|
289
|
-
⚠️ **Backup Complexity**: Need to backup multiple instances
|
|
290
|
-
|
|
291
253
|
## Technology Stack
|
|
292
254
|
|
|
293
255
|
- **Runtime**: Node.js (ES Modules)
|
|
294
256
|
- **Container Orchestration**: Docker Compose
|
|
295
|
-
- **Reverse Proxy**:
|
|
257
|
+
- **Reverse Proxy**: Traefik v3 (PostgreSQL STARTTLS + SNI routing)
|
|
296
258
|
- **Database**: PostgreSQL 18+
|
|
297
|
-
- **
|
|
298
|
-
- **Configuration**: YAML (docker-compose.yml), JSON (tenant-access.json)
|
|
259
|
+
- **TLS**: Wildcard SSL certificate
|
|
260
|
+
- **Configuration**: YAML (docker-compose.yml, traefik.yml, dynamic.yml), JSON (tenant-access.json)
|
|
261
|
+
|
|
262
|
+
## Requirements
|
|
263
|
+
|
|
264
|
+
- Docker & Docker Compose
|
|
265
|
+
- Node.js 18+
|
|
266
|
+
- Wildcard SSL certificate for your domain
|
|
267
|
+
- Wildcard DNS record pointing to your server
|
|
299
268
|
|
|
300
269
|
## Future Enhancements
|
|
301
270
|
|
|
302
271
|
- [ ] Health checks and automatic failover
|
|
303
272
|
- [ ] Backup/restore automation per tenant
|
|
304
|
-
- [ ] Resource limits (CPU/memory) per tenant
|
|
305
273
|
- [ ] Monitoring and metrics collection
|
|
306
274
|
- [ ] Tenant migration tools
|
|
307
275
|
- [ ] Kubernetes support
|
|
308
276
|
- [ ] Connection pooling per tenant
|
|
309
|
-
- [ ]
|
|
277
|
+
- [ ] Web dashboard
|
|
310
278
|
|
|
311
279
|
## License & Status
|
|
312
280
|
|
|
313
|
-
This is a custom solution built for specific multi-tenant requirements. It combines open-source tools (
|
|
314
|
-
|
|
281
|
+
This is a custom solution built for specific multi-tenant requirements. It combines open-source tools (Traefik, PostgreSQL, Docker) with SNI-based routing to achieve instance-per-tenant isolation with intelligent connection routing.
|
|
@@ -8,9 +8,11 @@ Para transformar esta solução em um produto comercial viável (PaaS de Postgre
|
|
|
8
8
|
2. **Alta Disponibilidade**: Sem replicação → Patroni/pg_auto_failover
|
|
9
9
|
3. **Backup/Recovery**: Inexistente → pgBackRest + S3
|
|
10
10
|
4. **Monitoramento**: Básico → Prometheus + Grafana
|
|
11
|
-
5. **Segurança**:
|
|
11
|
+
5. **Segurança**: ✅ TLS/SNI implementado via Traefik v3 → Adicionar Vault para secrets
|
|
12
12
|
6. **API**: CLI apenas → REST API completa
|
|
13
13
|
|
|
14
|
+
> **Nota**: A solução atual usa Traefik v3 para roteamento SNI com suporte nativo ao protocolo PostgreSQL STARTTLS.
|
|
15
|
+
|
|
14
16
|
---
|
|
15
17
|
|
|
16
18
|
## Abordagem por Fase
|
|
@@ -32,8 +34,8 @@ Host 2 (Região A)
|
|
|
32
34
|
├── docker-compose-3.yml # Tenants 101-150
|
|
33
35
|
└── docker-compose-4.yml # Tenants 151-200
|
|
34
36
|
|
|
35
|
-
Load Balancer (
|
|
36
|
-
├── Roteia para host correto baseado em
|
|
37
|
+
Load Balancer (Traefik v3)
|
|
38
|
+
├── Roteia para host correto baseado em SNI (hostname)
|
|
37
39
|
└── Health checks de todos os hosts
|
|
38
40
|
```
|
|
39
41
|
|
|
@@ -141,12 +143,12 @@ pgs_tenant1_replica:
|
|
|
141
143
|
PATRONI_ROLE: replica
|
|
142
144
|
```
|
|
143
145
|
|
|
144
|
-
#### 2.
|
|
146
|
+
#### 2. Traefik Redundante
|
|
145
147
|
|
|
146
148
|
```
|
|
147
|
-
|
|
149
|
+
Traefik 1 ──┐
|
|
148
150
|
├── Keepalived VIP (Virtual IP)
|
|
149
|
-
|
|
151
|
+
Traefik 2 ──┘
|
|
150
152
|
```
|
|
151
153
|
|
|
152
154
|
#### 3. Backup com pgBackRest
|
|
@@ -301,7 +303,7 @@ spec:
|
|
|
301
303
|
```yaml
|
|
302
304
|
# docker-compose.yml com profiles e múltiplos arquivos
|
|
303
305
|
services:
|
|
304
|
-
|
|
306
|
+
traefik:
|
|
305
307
|
# ...
|
|
306
308
|
|
|
307
309
|
pgs_tenant1:
|
|
@@ -340,7 +342,7 @@ API centralizada gerencia:
|
|
|
340
342
|
|
|
341
343
|
```
|
|
342
344
|
Orquestração: Docker Compose (melhorado)
|
|
343
|
-
Proxy:
|
|
345
|
+
Proxy: Traefik v3 (SNI routing + PostgreSQL STARTTLS)
|
|
344
346
|
Backup: pg_dump + AWS S3
|
|
345
347
|
Monitoring: Prometheus + Grafana
|
|
346
348
|
Secrets: HashiCorp Vault (básico)
|
|
@@ -352,7 +354,7 @@ Database: PostgreSQL 18+
|
|
|
352
354
|
|
|
353
355
|
```
|
|
354
356
|
Orquestração: Kubernetes
|
|
355
|
-
Proxy:
|
|
357
|
+
Proxy: Traefik IngressRoute (TCP + SNI) ou Nginx Ingress
|
|
356
358
|
Backup: pgBackRest + S3/GCS
|
|
357
359
|
Monitoring: Prometheus + Grafana + Alertmanager
|
|
358
360
|
Secrets: HashiCorp Vault (completo)
|
package/docs/CRITICAL_REVIEW.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
Esta análise identifica os pontos críticos que precisam ser endereçados para transformar esta solução em um produto comercial viável.
|
|
6
6
|
|
|
7
|
+
> **Nota**: A solução atual utiliza **Traefik v3** para roteamento baseado em SNI (Server Name Indication) com suporte nativo ao protocolo PostgreSQL STARTTLS. Isso permite roteamento por hostname com TLS obrigatório.
|
|
8
|
+
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
## 🔴 PROBLEMAS CRÍTICOS
|
|
@@ -118,8 +120,8 @@ Melhorias possíveis:
|
|
|
118
120
|
- Sem backup automático em tempo real
|
|
119
121
|
- RTO (Recovery Time Objective) alto
|
|
120
122
|
|
|
121
|
-
**b)
|
|
122
|
-
- Se
|
|
123
|
+
**b) Traefik como SPOF**
|
|
124
|
+
- Se Traefik cair, todos os tenants ficam inacessíveis
|
|
123
125
|
- Sem redundância do proxy
|
|
124
126
|
|
|
125
127
|
#### Soluções Necessárias:
|
|
@@ -137,12 +139,12 @@ Primary (writable) ──streaming──> Standby (read-only)
|
|
|
137
139
|
↓ failover ↑ promotion
|
|
138
140
|
```
|
|
139
141
|
|
|
140
|
-
**2.
|
|
142
|
+
**2. Traefik Redundância**
|
|
141
143
|
```
|
|
142
|
-
- Múltiplas instâncias
|
|
144
|
+
- Múltiplas instâncias Traefik
|
|
143
145
|
- Keepalived para VIP (Virtual IP)
|
|
144
|
-
- Health checks entre
|
|
145
|
-
- Load balancing do
|
|
146
|
+
- Health checks entre Traefik instances
|
|
147
|
+
- Load balancing do Traefik
|
|
146
148
|
```
|
|
147
149
|
|
|
148
150
|
**3. Multi-Region (Futuro)**
|
|
@@ -296,7 +298,7 @@ Implementação:
|
|
|
296
298
|
**2. SSL/TLS**
|
|
297
299
|
```
|
|
298
300
|
- Certificados SSL para conexões
|
|
299
|
-
- TLS entre
|
|
301
|
+
- TLS entre Traefik e PostgreSQL (interno)
|
|
300
302
|
- Certificate management automático (Let's Encrypt)
|
|
301
303
|
- TLS 1.2+ obrigatório
|
|
302
304
|
```
|
|
@@ -557,7 +559,7 @@ resource "managed_postgres_tenant" "example" {
|
|
|
557
559
|
```
|
|
558
560
|
✅ Alta Disponibilidade
|
|
559
561
|
- Replicação PostgreSQL (Patroni)
|
|
560
|
-
-
|
|
562
|
+
- Traefik redundante (Keepalived)
|
|
561
563
|
- Auto-failover
|
|
562
564
|
|
|
563
565
|
✅ Backup Avançado
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
|
|
31
31
|
**Problema**: Zero redundância
|
|
32
32
|
- 1 container PostgreSQL = 1 ponto de falha
|
|
33
|
-
-
|
|
33
|
+
- Traefik sem redundância
|
|
34
34
|
- Sem failover automático
|
|
35
35
|
|
|
36
36
|
**Solução**:
|
|
37
37
|
- Replicação PostgreSQL (Patroni/pg_auto_failover)
|
|
38
|
-
-
|
|
38
|
+
- Traefik redundante com Keepalived
|
|
39
39
|
- Auto-failover configurado
|
|
40
40
|
|
|
41
41
|
### 3. Backup e Recovery - ESSENCIAL
|
|
@@ -60,14 +60,13 @@
|
|
|
60
60
|
|
|
61
61
|
### 5. Segurança
|
|
62
62
|
|
|
63
|
-
**
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
63
|
+
**Status Atual** ✅:
|
|
64
|
+
- TLS/SSL implementado via Traefik v3
|
|
65
|
+
- Roteamento SNI (hostname-based)
|
|
66
|
+
- Certificado wildcard
|
|
67
67
|
|
|
68
|
-
**
|
|
68
|
+
**Melhorias Necessárias**:
|
|
69
69
|
- HashiCorp Vault para secrets
|
|
70
|
-
- SSL/TLS obrigatório
|
|
71
70
|
- Rotação automática de senhas
|
|
72
71
|
- Network policies
|
|
73
72
|
|
|
@@ -103,7 +102,8 @@
|
|
|
103
102
|
✅ API REST
|
|
104
103
|
✅ Monitoring básico (Prometheus + Grafana)
|
|
105
104
|
✅ Secrets management básico
|
|
106
|
-
✅ SSL/TLS
|
|
105
|
+
✅ SSL/TLS via Traefik v3 (IMPLEMENTADO)
|
|
106
|
+
✅ SNI-based routing (IMPLEMENTADO)
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
**Custo estimado**: $5K-15K (infraestrutura + desenvolvimento)
|
package/docs/PROJECT.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
### What It Is
|
|
6
6
|
|
|
7
|
-
A **dynamic PostgreSQL multi-tenant management system** that provides complete database isolation by creating dedicated PostgreSQL instances per tenant. The system uses
|
|
7
|
+
A **dynamic PostgreSQL multi-tenant management system** that provides complete database isolation by creating dedicated PostgreSQL instances per tenant. The system uses **Traefik v3** with native PostgreSQL STARTTLS support to route connections based on SNI (Server Name Indication), enabling hostname-based tenant routing with TLS security.
|
|
8
8
|
|
|
9
9
|
### Core Concept
|
|
10
10
|
|
|
@@ -22,15 +22,16 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
22
22
|
- Automatic volume and network configuration
|
|
23
23
|
- Custom initialization scripts per tenant
|
|
24
24
|
|
|
25
|
-
2. **
|
|
26
|
-
-
|
|
27
|
-
- Routes connections
|
|
25
|
+
2. **SNI-Based Routing**
|
|
26
|
+
- Traefik v3 handles PostgreSQL STARTTLS protocol
|
|
27
|
+
- Routes connections based on hostname (SNI)
|
|
28
28
|
- Single external port (5432) for all tenants
|
|
29
|
+
- TLS/SSL encryption required
|
|
29
30
|
|
|
30
31
|
3. **Access Control**
|
|
31
32
|
- Per-tenant external access enable/disable
|
|
32
|
-
- Secure by default (access disabled on creation)
|
|
33
33
|
- Runtime access control without service restart
|
|
34
|
+
- Dynamic Traefik configuration updates
|
|
34
35
|
|
|
35
36
|
4. **Complete Isolation**
|
|
36
37
|
- Separate Docker containers per tenant
|
|
@@ -39,49 +40,50 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
39
40
|
- No shared processes or memory
|
|
40
41
|
|
|
41
42
|
5. **Zero-Downtime Operations**
|
|
42
|
-
-
|
|
43
|
+
- Traefik dynamic configuration (no restarts needed)
|
|
43
44
|
- Independent tenant management
|
|
44
45
|
- No impact on other tenants during operations
|
|
45
46
|
|
|
46
47
|
## Architecture
|
|
47
48
|
|
|
48
49
|
```
|
|
49
|
-
|
|
50
|
-
│
|
|
51
|
-
│
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
│
|
|
57
|
-
│
|
|
58
|
-
│ │
|
|
59
|
-
│ │ -
|
|
60
|
-
│ │ -
|
|
61
|
-
│ │ -
|
|
62
|
-
│ │ -
|
|
63
|
-
│
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
┌──────────────┐
|
|
70
|
-
│
|
|
71
|
-
│
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
│
|
|
78
|
-
│
|
|
79
|
-
│
|
|
80
|
-
│
|
|
81
|
-
│
|
|
82
|
-
│
|
|
83
|
-
│
|
|
84
|
-
|
|
50
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
51
|
+
│ External Access │
|
|
52
|
+
│ (tenant-id.pgs.domain.com:5432 + TLS/SNI) │
|
|
53
|
+
└───────────────────────────┬─────────────────────────────────────┘
|
|
54
|
+
│
|
|
55
|
+
▼
|
|
56
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
57
|
+
│ Traefik v3 Proxy │
|
|
58
|
+
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
59
|
+
│ │ EntryPoint: postgres (port 5432) │ │
|
|
60
|
+
│ │ - TLS termination with wildcard certificate │ │
|
|
61
|
+
│ │ - PostgreSQL STARTTLS protocol support │ │
|
|
62
|
+
│ │ - SNI-based routing (HostSNI rule) │ │
|
|
63
|
+
│ │ - Dynamic configuration via dynamic.yml │ │
|
|
64
|
+
│ └───────────────────────────────────────────────────────────┘ │
|
|
65
|
+
└───────────────────────────┬─────────────────────────────────────┘
|
|
66
|
+
│
|
|
67
|
+
┌──────────────────┼──────────────────┐
|
|
68
|
+
│ │ │
|
|
69
|
+
▼ ▼ ▼
|
|
70
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
71
|
+
│ TCP Router │ │ TCP Router │ │ TCP Router │
|
|
72
|
+
│ tenant1 │ │ tenant2 │ │ tenant3 │
|
|
73
|
+
│ HostSNI() │ │ HostSNI() │ │ HostSNI() │
|
|
74
|
+
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
|
75
|
+
│ │ │
|
|
76
|
+
▼ ▼ ▼
|
|
77
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
78
|
+
│ PostgreSQL │ │ PostgreSQL │ │ PostgreSQL │
|
|
79
|
+
│ Container 1 │ │ Container 2 │ │ Container 3 │
|
|
80
|
+
│ │ │ │ │ │
|
|
81
|
+
│ Port: 5432 │ │ Port: 5432 │ │ Port: 5432 │
|
|
82
|
+
│ (internal) │ │ (internal) │ │ (internal) │
|
|
83
|
+
│ │ │ │ │ │
|
|
84
|
+
│ Volume: │ │ Volume: │ │ Volume: │
|
|
85
|
+
│ pgdata_1 │ │ pgdata_2 │ │ pgdata_3 │
|
|
86
|
+
└──────────────┘ └──────────────┘ └──────────────┘
|
|
85
87
|
```
|
|
86
88
|
|
|
87
89
|
## Technical Implementation
|
|
@@ -91,37 +93,88 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
91
93
|
1. **Manager Script (manager.js)**
|
|
92
94
|
- Node.js CLI tool for tenant lifecycle management
|
|
93
95
|
- Dynamically generates docker-compose.yml entries
|
|
94
|
-
- Manages
|
|
96
|
+
- Manages Traefik dynamic configuration (dynamic.yml)
|
|
95
97
|
- Controls tenant access permissions
|
|
96
98
|
|
|
97
|
-
2. **
|
|
98
|
-
- TCP-level
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
99
|
+
2. **Traefik v3 Reverse Proxy**
|
|
100
|
+
- TCP-level routing with TLS termination
|
|
101
|
+
- Native PostgreSQL STARTTLS protocol support
|
|
102
|
+
- SNI-based routing via HostSNI() rule
|
|
103
|
+
- Dynamic configuration without restarts
|
|
102
104
|
|
|
103
|
-
3. **
|
|
104
|
-
- Parses binary PostgreSQL startup packet
|
|
105
|
-
- Extracts username and connection parameters
|
|
106
|
-
- Handles SSL negotiation
|
|
107
|
-
- Enforces access control policies
|
|
108
|
-
|
|
109
|
-
4. **Docker Infrastructure**
|
|
105
|
+
3. **Docker Infrastructure**
|
|
110
106
|
- Separate container per tenant
|
|
111
107
|
- Bridge network for internal communication
|
|
112
108
|
- Persistent volumes for data
|
|
113
109
|
- Isolated execution environments
|
|
114
110
|
|
|
111
|
+
### Why Traefik v3 for PostgreSQL?
|
|
112
|
+
|
|
113
|
+
PostgreSQL uses a non-standard TLS negotiation called STARTTLS:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Standard TLS (HTTPS):
|
|
117
|
+
Client → TLS ClientHello (with SNI) → Server
|
|
118
|
+
|
|
119
|
+
PostgreSQL STARTTLS:
|
|
120
|
+
Client → SSLRequest (PG protocol) → Server
|
|
121
|
+
Server → 'S' (SSL supported) → Client
|
|
122
|
+
Client → TLS ClientHello (with SNI) → Server
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Most SNI proxies expect TLS ClientHello as the first packet. PostgreSQL sends its own protocol first, then TLS.
|
|
126
|
+
|
|
127
|
+
**Traefik v3** is one of the few proxies that natively understands this PostgreSQL-specific flow:
|
|
128
|
+
- Detects PostgreSQL SSLRequest
|
|
129
|
+
- Responds with 'S'
|
|
130
|
+
- Captures TLS ClientHello with SNI
|
|
131
|
+
- Routes based on hostname
|
|
132
|
+
|
|
115
133
|
### Connection Flow
|
|
116
134
|
|
|
117
|
-
1. Client connects to `
|
|
118
|
-
2.
|
|
119
|
-
3.
|
|
120
|
-
4.
|
|
121
|
-
5.
|
|
122
|
-
6.
|
|
135
|
+
1. Client connects to `tenant-id.pgs.domain.com:5432` with `sslmode=require`
|
|
136
|
+
2. Traefik receives connection and detects PostgreSQL SSLRequest
|
|
137
|
+
3. Traefik responds 'S' (SSL supported)
|
|
138
|
+
4. Client sends TLS ClientHello with SNI (hostname)
|
|
139
|
+
5. Traefik extracts SNI and matches against configured routers in dynamic.yml
|
|
140
|
+
6. If tenant has access enabled, routes to backend `pgs_{tenant_id}:5432`
|
|
123
141
|
7. Connection established with complete isolation
|
|
124
142
|
|
|
143
|
+
### Configuration Files
|
|
144
|
+
|
|
145
|
+
**traefik.yml** (Static Configuration):
|
|
146
|
+
```yaml
|
|
147
|
+
entryPoints:
|
|
148
|
+
postgres:
|
|
149
|
+
address: ":5432"
|
|
150
|
+
|
|
151
|
+
providers:
|
|
152
|
+
file:
|
|
153
|
+
filename: /etc/traefik/dynamic.yml
|
|
154
|
+
watch: true
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**dynamic.yml** (Dynamic Configuration - Auto-generated):
|
|
158
|
+
```yaml
|
|
159
|
+
tcp:
|
|
160
|
+
routers:
|
|
161
|
+
router_tenant1:
|
|
162
|
+
entryPoints:
|
|
163
|
+
- postgres
|
|
164
|
+
rule: "HostSNI(`tenant1-abc123.pgs.domain.com`)"
|
|
165
|
+
service: svc_tenant1
|
|
166
|
+
tls: {}
|
|
167
|
+
services:
|
|
168
|
+
svc_tenant1:
|
|
169
|
+
loadBalancer:
|
|
170
|
+
servers:
|
|
171
|
+
- address: "pgs_tenant1-abc123:5432"
|
|
172
|
+
tls:
|
|
173
|
+
certificates:
|
|
174
|
+
- certFile: /etc/traefik/certs/fullchain.pem
|
|
175
|
+
keyFile: /etc/traefik/certs/privkey.pem
|
|
176
|
+
```
|
|
177
|
+
|
|
125
178
|
## Comparison with Similar Solutions
|
|
126
179
|
|
|
127
180
|
### Shared Database Architecture
|
|
@@ -152,7 +205,7 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
152
205
|
- **Difference**: Shards data across nodes; this creates separate instances per tenant
|
|
153
206
|
- **Use Case**: Horizontal scaling vs. tenant isolation
|
|
154
207
|
|
|
155
|
-
#### 3. **Patroni + HAProxy**
|
|
208
|
+
#### 3. **Patroni + Traefik/HAProxy**
|
|
156
209
|
- **Purpose**: High availability and load balancing
|
|
157
210
|
- **Difference**: Replicates single database; this creates isolated instances
|
|
158
211
|
- **Use Case**: HA for single database vs. multi-tenant isolation
|
|
@@ -173,18 +226,18 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
173
226
|
- Not just database or schema separation
|
|
174
227
|
- Complete process and memory isolation
|
|
175
228
|
|
|
176
|
-
2. **
|
|
229
|
+
2. **SNI-based routing with single external port**
|
|
177
230
|
- No need for port management
|
|
178
|
-
- Automatic routing based on
|
|
231
|
+
- Automatic routing based on hostname
|
|
232
|
+
- TLS/SSL security by default
|
|
179
233
|
|
|
180
|
-
3. **
|
|
181
|
-
-
|
|
182
|
-
-
|
|
183
|
-
- Handles SSL negotiation
|
|
234
|
+
3. **PostgreSQL STARTTLS support**
|
|
235
|
+
- Traefik v3 handles non-standard PG protocol
|
|
236
|
+
- Proper SNI extraction after PG handshake
|
|
184
237
|
|
|
185
238
|
4. **Runtime access control**
|
|
186
239
|
- Enable/disable tenant access without restart
|
|
187
|
-
-
|
|
240
|
+
- Dynamic Traefik configuration
|
|
188
241
|
|
|
189
242
|
5. **Docker-native architecture**
|
|
190
243
|
- Leverages container isolation
|
|
@@ -211,9 +264,9 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
211
264
|
## Advantages
|
|
212
265
|
|
|
213
266
|
✅ **Maximum Isolation**: Complete process and data separation
|
|
214
|
-
✅ **Security**: Zero risk of cross-tenant data access
|
|
267
|
+
✅ **Security**: Zero risk of cross-tenant data access + TLS encryption
|
|
215
268
|
✅ **Flexibility**: Independent scaling and management per tenant
|
|
216
|
-
✅ **Simplicity**: Single external port, automatic routing
|
|
269
|
+
✅ **Simplicity**: Single external port, automatic SNI routing
|
|
217
270
|
✅ **Compliance**: Easier to meet regulatory requirements
|
|
218
271
|
✅ **Debugging**: Isolated environments simplify troubleshooting
|
|
219
272
|
|
|
@@ -223,15 +276,16 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
223
276
|
⚠️ **Management Overhead**: More containers to manage
|
|
224
277
|
⚠️ **Scaling Limits**: Practical limit on number of tenants per host
|
|
225
278
|
⚠️ **Backup Complexity**: Need to backup multiple instances
|
|
279
|
+
⚠️ **SSL Required**: Clients must support TLS with SNI
|
|
226
280
|
|
|
227
281
|
## Technology Stack
|
|
228
282
|
|
|
229
283
|
- **Runtime**: Node.js (ES Modules)
|
|
230
284
|
- **Container Orchestration**: Docker Compose
|
|
231
|
-
- **Reverse Proxy**:
|
|
285
|
+
- **Reverse Proxy**: Traefik v3 (PostgreSQL STARTTLS + SNI routing)
|
|
232
286
|
- **Database**: PostgreSQL 18+
|
|
233
|
-
- **
|
|
234
|
-
- **Configuration**: YAML (docker-compose.yml), JSON (tenant-access.json)
|
|
287
|
+
- **TLS**: Wildcard SSL certificate
|
|
288
|
+
- **Configuration**: YAML (docker-compose.yml, traefik.yml, dynamic.yml), JSON (tenant-access.json)
|
|
235
289
|
|
|
236
290
|
## Future Enhancements
|
|
237
291
|
|
|
@@ -242,9 +296,8 @@ Instead of sharing a single PostgreSQL instance with multiple databases (shared
|
|
|
242
296
|
- [ ] Tenant migration tools
|
|
243
297
|
- [ ] Kubernetes support
|
|
244
298
|
- [ ] Connection pooling per tenant
|
|
245
|
-
- [ ]
|
|
299
|
+
- [ ] Web dashboard
|
|
246
300
|
|
|
247
301
|
## License & Status
|
|
248
302
|
|
|
249
|
-
This is a custom solution built for specific multi-tenant requirements. It combines open-source tools (
|
|
250
|
-
|
|
303
|
+
This is a custom solution built for specific multi-tenant requirements. It combines open-source tools (Traefik v3, PostgreSQL, Docker) with SNI-based routing to achieve instance-per-tenant isolation with intelligent connection routing.
|
package/manager.js
CHANGED
|
@@ -62,7 +62,7 @@ function removeTenantAccess(tenantId) {
|
|
|
62
62
|
|
|
63
63
|
// ==================== Traefik Configuration ====================
|
|
64
64
|
|
|
65
|
-
function generateTraefikDynamicConfig() {
|
|
65
|
+
function generateTraefikDynamicConfig(tenantsManifests = {}) {
|
|
66
66
|
console.debug('Generating Traefik dynamic config')
|
|
67
67
|
const doc = loadCompose();
|
|
68
68
|
const access = loadTenantAccess();
|
|
@@ -75,6 +75,7 @@ function generateTraefikDynamicConfig() {
|
|
|
75
75
|
const dynamicConfig = {
|
|
76
76
|
tcp: {
|
|
77
77
|
routers: {},
|
|
78
|
+
middlewares: {},
|
|
78
79
|
services: {}
|
|
79
80
|
},
|
|
80
81
|
tls: {
|
|
@@ -96,15 +97,37 @@ function generateTraefikDynamicConfig() {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
const routerName = `router_${tenantId}`.replace(/-/g, '_');
|
|
100
|
+
const middlewareName = `ipwhitelist_${tenantId}`.replace(/-/g, '_');
|
|
99
101
|
const svcName = `svc_${tenantId}`.replace(/-/g, '_');
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
const router = {
|
|
102
104
|
entryPoints: ['postgres'],
|
|
103
|
-
rule: `HostSNI(\`${tenantId}.pgs.
|
|
105
|
+
rule: `HostSNI(\`${tenantId}.pgs.br-sp-1.sysnee.com\`)`,
|
|
104
106
|
service: svcName,
|
|
105
107
|
tls: {}
|
|
106
108
|
};
|
|
107
109
|
|
|
110
|
+
const manifest = tenantsManifests[tenantId];
|
|
111
|
+
if (manifest && manifest.firewall && manifest.firewall.rules) {
|
|
112
|
+
const sourceRanges = manifest.firewall.rules
|
|
113
|
+
.filter(rule => rule.type === 'allow')
|
|
114
|
+
.map(rule => rule.source);
|
|
115
|
+
|
|
116
|
+
if (sourceRanges.length > 0) {
|
|
117
|
+
router.middlewares = [middlewareName];
|
|
118
|
+
|
|
119
|
+
if (!dynamicConfig.tcp.middlewares[middlewareName]) {
|
|
120
|
+
dynamicConfig.tcp.middlewares[middlewareName] = {
|
|
121
|
+
ipAllowList: {
|
|
122
|
+
sourceRange: sourceRanges
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
dynamicConfig.tcp.routers[routerName] = router;
|
|
130
|
+
|
|
108
131
|
dynamicConfig.tcp.services[svcName] = {
|
|
109
132
|
loadBalancer: {
|
|
110
133
|
servers: [
|
|
@@ -209,11 +232,23 @@ function ensureNetwork(doc) {
|
|
|
209
232
|
}
|
|
210
233
|
|
|
211
234
|
function initialSetup() {
|
|
235
|
+
installDocker()
|
|
212
236
|
createInitialFiles()
|
|
213
237
|
ensureDockerPrivilegies()
|
|
214
238
|
console.log('All ready!')
|
|
215
239
|
}
|
|
216
240
|
|
|
241
|
+
function installDocker() {
|
|
242
|
+
console.log('Installing Docker...')
|
|
243
|
+
execSync('curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh', { stdio: 'inherit' })
|
|
244
|
+
execSync('rm get-docker.sh')
|
|
245
|
+
console.log('Docker installed successfully')
|
|
246
|
+
|
|
247
|
+
console.log('Installing Docker Compose plugin...')
|
|
248
|
+
execSync('sudo apt-get install -y docker-compose-plugin', { stdio: 'inherit' })
|
|
249
|
+
console.log('Docker Compose plugin installed successfully')
|
|
250
|
+
}
|
|
251
|
+
|
|
217
252
|
function ensureDockerPrivilegies() {
|
|
218
253
|
execSync('sudo usermod -aG docker $USER && newgrp docker')
|
|
219
254
|
writeFileSync(SETUP_STATUS_PATH, 'container:ok')
|
|
@@ -282,7 +317,7 @@ function checkSetupStatus() {
|
|
|
282
317
|
}
|
|
283
318
|
|
|
284
319
|
function createTenant(tenantId, options = {}) {
|
|
285
|
-
const { version = '18', password, limits = DEFAULT_LIMITS } = options;
|
|
320
|
+
const { version = '18', password, limits = DEFAULT_LIMITS, manifest = null } = options;
|
|
286
321
|
const doc = loadCompose();
|
|
287
322
|
|
|
288
323
|
if (doc.services && doc.services[`pgs_${tenantId}`]) {
|
|
@@ -311,19 +346,17 @@ function createTenant(tenantId, options = {}) {
|
|
|
311
346
|
|
|
312
347
|
saveCompose(doc);
|
|
313
348
|
|
|
314
|
-
// Add tenant to access control
|
|
315
349
|
setTenantAccess(tenantId, true);
|
|
316
350
|
|
|
317
|
-
|
|
318
|
-
generateTraefikDynamicConfig();
|
|
351
|
+
const manifests = manifest ? { [tenantId]: manifest } : {};
|
|
352
|
+
generateTraefikDynamicConfig(manifests);
|
|
319
353
|
updateTraefikService();
|
|
320
354
|
|
|
321
|
-
// start the tenant
|
|
322
355
|
executeCommand(`docker compose up -d ${serviceName}`.trim());
|
|
323
356
|
|
|
324
357
|
console.log(`✓ Created tenant: ${tenantId}`);
|
|
325
358
|
console.log(` Service: ${serviceName}`);
|
|
326
|
-
console.log(` Host: ${tenantId}.pgs.
|
|
359
|
+
console.log(` Host: ${tenantId}.pgs.br-sp-1.sysnee.com`);
|
|
327
360
|
console.log(` Port: 5432`);
|
|
328
361
|
console.log(` Database: ${tenantId}`);
|
|
329
362
|
console.log(` Password: ${password}`);
|
|
@@ -364,13 +397,13 @@ function listTenants() {
|
|
|
364
397
|
console.log('─'.repeat(90));
|
|
365
398
|
tenants.forEach(t => {
|
|
366
399
|
const accessStr = t.access ? '✓ enabled' : '✗ disabled';
|
|
367
|
-
const host = `${t.tenantId}.pgs.
|
|
400
|
+
const host = `${t.tenantId}.pgs.br-sp-1.sysnee.com`;
|
|
368
401
|
console.log(` ${t.tenantId.padEnd(25)} ${host.padEnd(45)} ${accessStr}`);
|
|
369
402
|
});
|
|
370
403
|
console.log('─'.repeat(90));
|
|
371
404
|
}
|
|
372
405
|
|
|
373
|
-
function enableTenantAccess(tenantId) {
|
|
406
|
+
function enableTenantAccess(tenantId, manifests = {}) {
|
|
374
407
|
const doc = loadCompose();
|
|
375
408
|
const serviceName = `pgs_${tenantId}`;
|
|
376
409
|
|
|
@@ -379,12 +412,12 @@ function enableTenantAccess(tenantId) {
|
|
|
379
412
|
}
|
|
380
413
|
|
|
381
414
|
setTenantAccess(tenantId, true);
|
|
382
|
-
generateTraefikDynamicConfig();
|
|
415
|
+
generateTraefikDynamicConfig(manifests);
|
|
383
416
|
|
|
384
417
|
console.log(`✓ External access enabled for tenant: ${tenantId}`);
|
|
385
418
|
}
|
|
386
419
|
|
|
387
|
-
function disableTenantAccess(tenantId) {
|
|
420
|
+
function disableTenantAccess(tenantId, manifests = {}) {
|
|
388
421
|
const doc = loadCompose();
|
|
389
422
|
const serviceName = `pgs_${tenantId}`;
|
|
390
423
|
|
|
@@ -393,12 +426,12 @@ function disableTenantAccess(tenantId) {
|
|
|
393
426
|
}
|
|
394
427
|
|
|
395
428
|
setTenantAccess(tenantId, false);
|
|
396
|
-
generateTraefikDynamicConfig();
|
|
429
|
+
generateTraefikDynamicConfig(manifests);
|
|
397
430
|
|
|
398
431
|
console.log(`✓ External access disabled for tenant: ${tenantId}`);
|
|
399
432
|
}
|
|
400
433
|
|
|
401
|
-
function removeTenant(tenantId) {
|
|
434
|
+
function removeTenant(tenantId, manifests = {}) {
|
|
402
435
|
const doc = loadCompose();
|
|
403
436
|
const serviceName = `pgs_${tenantId}`;
|
|
404
437
|
|
|
@@ -421,7 +454,7 @@ function removeTenant(tenantId) {
|
|
|
421
454
|
saveCompose(doc);
|
|
422
455
|
|
|
423
456
|
removeTenantAccess(tenantId);
|
|
424
|
-
generateTraefikDynamicConfig();
|
|
457
|
+
generateTraefikDynamicConfig(manifests);
|
|
425
458
|
updateTraefikService();
|
|
426
459
|
|
|
427
460
|
console.log(`✓ Removed tenant: ${tenantId}`);
|
|
@@ -478,7 +511,8 @@ program
|
|
|
478
511
|
const tenantOptions = {
|
|
479
512
|
password: null,
|
|
480
513
|
version: null,
|
|
481
|
-
limits: null
|
|
514
|
+
limits: null,
|
|
515
|
+
manifest: null
|
|
482
516
|
}
|
|
483
517
|
|
|
484
518
|
if (opts.file) {
|
|
@@ -492,6 +526,7 @@ program
|
|
|
492
526
|
|
|
493
527
|
tenantOptions.limits = optsFromManifest.shared_limits
|
|
494
528
|
tenantOptions.version = optsFromManifest.version
|
|
529
|
+
tenantOptions.manifest = optsFromManifest
|
|
495
530
|
} else {
|
|
496
531
|
tenantOptions.version = opts.version;
|
|
497
532
|
tenantOptions.limits = {
|
|
@@ -526,10 +561,17 @@ program
|
|
|
526
561
|
.command('remove')
|
|
527
562
|
.description('Remove a tenant instance')
|
|
528
563
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
529
|
-
.
|
|
564
|
+
.option('-f, --file <file>', 'Manifest file path (optional, for firewall rules)')
|
|
565
|
+
.action((tenantId, opts) => {
|
|
530
566
|
checkSetupStatus()
|
|
531
567
|
try {
|
|
532
|
-
|
|
568
|
+
const manifests = {};
|
|
569
|
+
if (opts.file) {
|
|
570
|
+
const manifestFilePath = path.resolve(opts.file)
|
|
571
|
+
const manifest = JSON.parse(readFileSync(manifestFilePath))
|
|
572
|
+
manifests[tenantId] = manifest;
|
|
573
|
+
}
|
|
574
|
+
removeTenant(tenantId, manifests);
|
|
533
575
|
} catch (error) {
|
|
534
576
|
console.error(`Error: ${error.message}`);
|
|
535
577
|
process.exit(1);
|
|
@@ -560,10 +602,17 @@ program
|
|
|
560
602
|
.command('enable-access')
|
|
561
603
|
.description('Enable external access for a tenant')
|
|
562
604
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
563
|
-
.
|
|
605
|
+
.option('-f, --file <file>', 'Manifest file path (optional, for firewall rules)')
|
|
606
|
+
.action((tenantId, opts) => {
|
|
564
607
|
checkSetupStatus()
|
|
565
608
|
try {
|
|
566
|
-
|
|
609
|
+
const manifests = {};
|
|
610
|
+
if (opts.file) {
|
|
611
|
+
const manifestFilePath = path.resolve(opts.file)
|
|
612
|
+
const manifest = JSON.parse(readFileSync(manifestFilePath))
|
|
613
|
+
manifests[tenantId] = manifest;
|
|
614
|
+
}
|
|
615
|
+
enableTenantAccess(tenantId, manifests);
|
|
567
616
|
} catch (error) {
|
|
568
617
|
console.error(`Error: ${error.message}`);
|
|
569
618
|
process.exit(1);
|
|
@@ -574,10 +623,17 @@ program
|
|
|
574
623
|
.command('disable-access')
|
|
575
624
|
.description('Disable external access for a tenant')
|
|
576
625
|
.argument('<tenant-id>', 'Tenant identifier')
|
|
577
|
-
.
|
|
626
|
+
.option('-f, --file <file>', 'Manifest file path (optional, for firewall rules)')
|
|
627
|
+
.action((tenantId, opts) => {
|
|
578
628
|
checkSetupStatus()
|
|
579
629
|
try {
|
|
580
|
-
|
|
630
|
+
const manifests = {};
|
|
631
|
+
if (opts.file) {
|
|
632
|
+
const manifestFilePath = path.resolve(opts.file)
|
|
633
|
+
const manifest = JSON.parse(readFileSync(manifestFilePath))
|
|
634
|
+
manifests[tenantId] = manifest;
|
|
635
|
+
}
|
|
636
|
+
disableTenantAccess(tenantId, manifests);
|
|
581
637
|
} catch (error) {
|
|
582
638
|
console.error(`Error: ${error.message}`);
|
|
583
639
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sysnee/pgs",
|
|
3
|
-
"version": "0.1.7-rc.
|
|
3
|
+
"version": "0.1.7-rc.7",
|
|
4
4
|
"description": "Dynamic PostgreSQL service instance manager",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"stop": "node manager.js stop",
|
|
15
15
|
"enable-access": "node manager.js enable-access",
|
|
16
16
|
"disable-access": "node manager.js disable-access",
|
|
17
|
-
"
|
|
17
|
+
"setup": "node manager.js setup"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"js-yaml": "^4.1.0",
|