boring_services 0.2.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 +22 -0
- data/README.md +461 -0
- data/exe/boringservices +5 -0
- data/lib/boring_services/cli.rb +100 -0
- data/lib/boring_services/configuration.rb +80 -0
- data/lib/boring_services/generators/boring_services/install_generator.rb +25 -0
- data/lib/boring_services/health_checker.rb +47 -0
- data/lib/boring_services/installer.rb +142 -0
- data/lib/boring_services/railtie.rb +19 -0
- data/lib/boring_services/secrets.rb +55 -0
- data/lib/boring_services/services/base.rb +168 -0
- data/lib/boring_services/services/haproxy.rb +272 -0
- data/lib/boring_services/services/memcached.rb +113 -0
- data/lib/boring_services/services/nginx.rb +96 -0
- data/lib/boring_services/services/redis.rb +55 -0
- data/lib/boring_services/ssh_executor.rb +206 -0
- data/lib/boring_services/version.rb +3 -0
- data/lib/boring_services.rb +32 -0
- data/lib/tasks/services.rake +47 -0
- metadata +164 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: afc3d44acfb6c7fe4d741a643dc87d08b2b9ae371daa24a054dea6c25f6c6429
|
|
4
|
+
data.tar.gz: d2c240bdbfe39bf716ac2025e4f8249b01e3b5dceda5fa6766601aeca308abb0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 924f46cbdd506ecd7212bdd0836aa557f8e397986861f21f684f9d0e7a02d1ede2a627e1aca67adb11f4a7faf94ef207ca1dc4476c2e5768ee80ddc6420cd714
|
|
7
|
+
data.tar.gz: 180534ff00c723563bd8e09bd022d94e100cf9f2e864cfe561db2021d288cd2a818957fe9baef5436af267d69b5876df7c1e67560dc974ccb3036eea1891c65c
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 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
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
# BoringServices
|
|
2
|
+
|
|
3
|
+
Deploy infrastructure services (Memcached, Redis, HAProxy, Nginx) to servers with Ruby and SSH. No Ansible, no Python, no Kubernetes.
|
|
4
|
+
|
|
5
|
+
Works standalone with any Ruby application or integrates seamlessly with Rails.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Memcached** - In-memory caching with configurable parameters
|
|
10
|
+
- **Redis** - Key-value store and cache
|
|
11
|
+
- **HAProxy** - Load balancer with SSL/TLS support
|
|
12
|
+
- **Nginx** - Web server and reverse proxy
|
|
13
|
+
- **Simple Configuration** - YAML files with environment support
|
|
14
|
+
- **Flexible Overrides** - Custom config templates or parameter overrides
|
|
15
|
+
- **Secret Management** - Environment variables or command execution
|
|
16
|
+
- **SSH Deployment** - Direct deployment to Ubuntu servers
|
|
17
|
+
- **Health Checks** - Monitor service status across hosts
|
|
18
|
+
- **VPN Support** - Optional private_ip field for VPN/WireGuard deployments
|
|
19
|
+
- **Rails Integration** - Generators, rake tasks, and seamless integration
|
|
20
|
+
- **Standalone Support** - Works with any Ruby application
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Standalone
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
gem install boring_services
|
|
28
|
+
boringservices setup
|
|
29
|
+
boringservices status
|
|
30
|
+
|
|
31
|
+
# Target a different config/environment
|
|
32
|
+
BORING_SERVICES_CONFIG=config/services.staging.yml \
|
|
33
|
+
BORING_SERVICES_ENV=staging boringservices setup
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Rails Integration
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# Gemfile
|
|
40
|
+
gem 'boring_services'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Generate config
|
|
45
|
+
rails generate boring_services:install
|
|
46
|
+
|
|
47
|
+
# Deploy services
|
|
48
|
+
rails boring_services:setup
|
|
49
|
+
|
|
50
|
+
# Deploy using a different config/environment
|
|
51
|
+
BORING_SERVICES_CONFIG=config/services.staging.yml BORING_SERVICES_ENV=staging \
|
|
52
|
+
rails boring_services:setup
|
|
53
|
+
|
|
54
|
+
# Check health
|
|
55
|
+
rails boring_services:health
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
Create `config/services.yml`:
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
production:
|
|
64
|
+
user: ubuntu
|
|
65
|
+
ssh_key: ~/.ssh/id_rsa
|
|
66
|
+
forward_agent: true
|
|
67
|
+
use_ssh_agent: false
|
|
68
|
+
ssh_auth_methods:
|
|
69
|
+
- publickey
|
|
70
|
+
|
|
71
|
+
services:
|
|
72
|
+
- name: memcached
|
|
73
|
+
enabled: true
|
|
74
|
+
hosts:
|
|
75
|
+
- host: 10.8.0.20
|
|
76
|
+
label: cache-a
|
|
77
|
+
- host: 10.8.0.21
|
|
78
|
+
label: cache-b
|
|
79
|
+
port: 11211
|
|
80
|
+
memory_mb: 256
|
|
81
|
+
|
|
82
|
+
- name: redis
|
|
83
|
+
enabled: true
|
|
84
|
+
host: 10.8.0.22
|
|
85
|
+
port: 6379
|
|
86
|
+
memory_mb: 1024
|
|
87
|
+
|
|
88
|
+
- name: haproxy
|
|
89
|
+
enabled: true
|
|
90
|
+
hosts:
|
|
91
|
+
- 10.8.0.30
|
|
92
|
+
port: 80
|
|
93
|
+
stats_port: 8404
|
|
94
|
+
backends:
|
|
95
|
+
- host: 10.8.0.10
|
|
96
|
+
port: 3000
|
|
97
|
+
- host: 10.8.0.11
|
|
98
|
+
port: 3000
|
|
99
|
+
|
|
100
|
+
secrets:
|
|
101
|
+
redis_password: $REDIS_PASSWORD
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Use `host:` for a single target or `hosts:` for multiple VMs. When using `hosts`, each entry can be a simple hostname/IP or a hash with per-host overrides (e.g., `label`, `port`, or `memory_mb`).
|
|
105
|
+
|
|
106
|
+
### Service Options
|
|
107
|
+
|
|
108
|
+
#### Memcached
|
|
109
|
+
|
|
110
|
+
**Basic Configuration:**
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
- name: memcached
|
|
114
|
+
enabled: true
|
|
115
|
+
hosts:
|
|
116
|
+
- 10.8.0.20
|
|
117
|
+
port: 11211 # Default: 11211
|
|
118
|
+
memory_mb: 256 # Memory allocation in MB
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**With Custom Parameters:**
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
- name: memcached
|
|
125
|
+
enabled: true
|
|
126
|
+
host: 10.8.0.20
|
|
127
|
+
port: 11211
|
|
128
|
+
memory_mb: 512
|
|
129
|
+
custom_params:
|
|
130
|
+
listen_address: 0.0.0.0 # Default: 0.0.0.0
|
|
131
|
+
max_connections: 2048 # Default: 1024
|
|
132
|
+
max_item_size: 2m # Optional: max item size
|
|
133
|
+
verbosity: 2 # Optional: 0-3 for debug output
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**With Custom Config Template:**
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
- name: memcached
|
|
140
|
+
enabled: true
|
|
141
|
+
host: 10.8.0.20
|
|
142
|
+
custom_config_template: config/memcached.custom.conf # Path to custom config file
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Redis
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
- name: redis
|
|
149
|
+
enabled: true
|
|
150
|
+
hosts:
|
|
151
|
+
- 10.8.0.21
|
|
152
|
+
port: 6379 # Default: 6379
|
|
153
|
+
memory_mb: 512 # Memory allocation
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Optional password protection via secrets:
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
secrets:
|
|
160
|
+
redis_password: $REDIS_PASSWORD
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### HAProxy
|
|
164
|
+
|
|
165
|
+
**Basic HTTP Load Balancer:**
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
- name: haproxy
|
|
169
|
+
enabled: true
|
|
170
|
+
host: 10.8.0.30
|
|
171
|
+
port: 80 # Frontend port
|
|
172
|
+
stats_port: 8404 # Stats dashboard port
|
|
173
|
+
backends:
|
|
174
|
+
- host: 10.8.0.10
|
|
175
|
+
port: 3000
|
|
176
|
+
- host: 10.8.0.11
|
|
177
|
+
port: 3000
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**HTTPS with SSL Termination:**
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
- name: haproxy
|
|
184
|
+
enabled: true
|
|
185
|
+
host: 34.123.78.90
|
|
186
|
+
port: 80 # Redirects to HTTPS
|
|
187
|
+
https_port: 443 # SSL/TLS port
|
|
188
|
+
stats_port: 8404
|
|
189
|
+
ssl: true
|
|
190
|
+
ssl_cert: $(op read "op://MyVault/haproxy-ssl/certificate")
|
|
191
|
+
ssl_key: $(op read "op://MyVault/haproxy-ssl/private_key")
|
|
192
|
+
backends:
|
|
193
|
+
- host: 192.168.1.10
|
|
194
|
+
port: 3000
|
|
195
|
+
- host: 192.168.1.11
|
|
196
|
+
port: 3000
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**With Custom Parameters:**
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
- name: haproxy
|
|
203
|
+
enabled: true
|
|
204
|
+
host: 10.8.0.30
|
|
205
|
+
port: 80
|
|
206
|
+
stats_port: 8404
|
|
207
|
+
custom_params:
|
|
208
|
+
timeout_connect: 10000 # Default: 5000ms
|
|
209
|
+
timeout_client: 60000 # Default: 50000ms
|
|
210
|
+
timeout_server: 60000 # Default: 50000ms
|
|
211
|
+
balance: leastconn # Default: roundrobin
|
|
212
|
+
health_check_path: /healthz # Default: /health
|
|
213
|
+
ssl_ciphers: CUSTOM_CIPHERS # Custom SSL cipher suite
|
|
214
|
+
ssl_options: no-sslv3 # Custom SSL options
|
|
215
|
+
backends:
|
|
216
|
+
- host: 10.8.0.10
|
|
217
|
+
port: 3000
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**With Custom Config Template:**
|
|
221
|
+
|
|
222
|
+
```yaml
|
|
223
|
+
- name: haproxy
|
|
224
|
+
enabled: true
|
|
225
|
+
host: 10.8.0.30
|
|
226
|
+
custom_config_template: config/haproxy.custom.cfg # Path to custom HAProxy config
|
|
227
|
+
# Note: When using custom template, most other options are ignored
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**SSL Certificate Options:**
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
# Option 1: 1Password CLI
|
|
234
|
+
ssl_cert: $(op read "op://MyVault/haproxy-ssl/certificate")
|
|
235
|
+
ssl_key: $(op read "op://MyVault/haproxy-ssl/private_key")
|
|
236
|
+
|
|
237
|
+
# Option 2: Environment variables
|
|
238
|
+
ssl_cert: $HAPROXY_SSL_CERT
|
|
239
|
+
ssl_key: $HAPROXY_SSL_KEY
|
|
240
|
+
|
|
241
|
+
# Option 3: Rails credentials (Rails only)
|
|
242
|
+
ssl_cert: $(rails credentials:show | yq .haproxy.ssl.cert)
|
|
243
|
+
ssl_key: $(rails credentials:show | yq .haproxy.ssl.key)
|
|
244
|
+
|
|
245
|
+
# Option 4: Local files
|
|
246
|
+
ssl_cert: $(cat /path/to/cert.pem)
|
|
247
|
+
ssl_key: $(cat /path/to/key.pem)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**HAProxy Features:**
|
|
251
|
+
- ā
**Config Validation** - Automatically validates HAProxy config before applying
|
|
252
|
+
- ā
**SSL/TLS Support** - Automatic HTTPS setup with certificate management
|
|
253
|
+
- ā
**Self-Signed Fallback** - Generates self-signed cert if none provided
|
|
254
|
+
- ā
**HTTP ā HTTPS Redirect** - Automatic redirect when SSL is enabled
|
|
255
|
+
- ā
**Health Checks** - Configurable health check endpoint (default: GET /health)
|
|
256
|
+
- ā
**Stats Dashboard** - Access at `http://your-host:8404/`
|
|
257
|
+
- ā
**Custom Overrides** - Override timeouts, balance algorithms, SSL ciphers
|
|
258
|
+
- ā
**Custom Templates** - Bring your own HAProxy config file
|
|
259
|
+
|
|
260
|
+
#### Nginx
|
|
261
|
+
|
|
262
|
+
```yaml
|
|
263
|
+
- name: nginx
|
|
264
|
+
enabled: true
|
|
265
|
+
hosts:
|
|
266
|
+
- 10.8.0.40
|
|
267
|
+
port: 80
|
|
268
|
+
ssl: true # Enable HTTPS on port 443
|
|
269
|
+
backends:
|
|
270
|
+
- host: 10.8.0.10
|
|
271
|
+
port: 3000
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## CLI Commands
|
|
275
|
+
|
|
276
|
+
### Install Services
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
boringservices install
|
|
280
|
+
|
|
281
|
+
boringservices install redis
|
|
282
|
+
|
|
283
|
+
boringservices install -e staging
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Check Status
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
boringservices status
|
|
290
|
+
|
|
291
|
+
boringservices status -e production
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Restart Services
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
boringservices restart redis
|
|
298
|
+
|
|
299
|
+
boringservices restart haproxy -e staging
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Uninstall Services
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
boringservices uninstall memcached
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Rails Integration
|
|
309
|
+
|
|
310
|
+
Add to your `Gemfile`:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
gem 'boring_services'
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Generate configuration:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
rails generate boring_services:install
|
|
320
|
+
# Creates config/services.yml
|
|
321
|
+
|
|
322
|
+
rails boring_services:setup
|
|
323
|
+
# Deploys all enabled services
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Use services in your Rails app:
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
Rails.application.configure do
|
|
330
|
+
# Connect to Memcached
|
|
331
|
+
config.cache_store = :mem_cache_store, '10.8.0.20:11211', '10.8.0.21:11211'
|
|
332
|
+
|
|
333
|
+
# Connect to Redis for sessions
|
|
334
|
+
config.session_store :redis_store,
|
|
335
|
+
servers: ['redis://10.8.0.22:6379/0/session'],
|
|
336
|
+
expire_after: 90.minutes
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Available Rake Tasks:**
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
rails boring_services:setup # Deploy all services
|
|
344
|
+
rails boring_services:health # Check service health
|
|
345
|
+
rails boring_services:status # Show service status
|
|
346
|
+
rails boring_services:restart # Restart all services
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Secrets Management
|
|
350
|
+
|
|
351
|
+
Use environment variables or command execution:
|
|
352
|
+
|
|
353
|
+
```yaml
|
|
354
|
+
secrets:
|
|
355
|
+
# Option 1: Environment variable
|
|
356
|
+
redis_password: $REDIS_PASSWORD
|
|
357
|
+
|
|
358
|
+
# Option 2: 1Password CLI
|
|
359
|
+
redis_password: $(op read "op://MyVault/redis/password")
|
|
360
|
+
|
|
361
|
+
# Option 3: Rails credentials (Rails only)
|
|
362
|
+
redis_password: $(rails credentials:show | yq .redis.password)
|
|
363
|
+
|
|
364
|
+
# Option 4: File-based secrets
|
|
365
|
+
redis_password: $(cat .secrets/redis_password)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## VPN/Private Network Support
|
|
369
|
+
|
|
370
|
+
BoringServices supports deploying services over VPN or private networks (e.g., WireGuard, Tailscale):
|
|
371
|
+
|
|
372
|
+
- **Public IPs** (`host`) for SSH deployment access
|
|
373
|
+
- **Private IPs** (`private_ip`) for service communication
|
|
374
|
+
- Services listen on all interfaces (0.0.0.0) by default
|
|
375
|
+
- Accessible via private network IPs
|
|
376
|
+
- No public exposure of services
|
|
377
|
+
|
|
378
|
+
**Example with Private IPs:**
|
|
379
|
+
|
|
380
|
+
```yaml
|
|
381
|
+
services:
|
|
382
|
+
- name: redis
|
|
383
|
+
host: 18.234.67.89 # ā Public IP for SSH deployment
|
|
384
|
+
private_ip: 10.8.0.32 # ā Private VPN IP for connections
|
|
385
|
+
label: us-east-1
|
|
386
|
+
port: 6379
|
|
387
|
+
memory_mb: 1024
|
|
388
|
+
|
|
389
|
+
- name: memcached
|
|
390
|
+
host: 51.15.214.103 # ā Public IP for SSH
|
|
391
|
+
private_ip: 10.8.0.61 # ā Private VPN IP
|
|
392
|
+
label: cache-eu
|
|
393
|
+
port: 11211
|
|
394
|
+
memory_mb: 256
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Your application connects via the private IP:
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
# Ruby/Rails app
|
|
401
|
+
Redis.new(url: 'redis://10.8.0.32:6379')
|
|
402
|
+
|
|
403
|
+
# Memcached
|
|
404
|
+
Dalli::Client.new('10.8.0.61:11211')
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
The `private_ip` field is optional and purely for documentation - use it to track which IPs your application should connect to.
|
|
408
|
+
|
|
409
|
+
## Health Monitoring
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
boringservices status
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Output:
|
|
416
|
+
|
|
417
|
+
```
|
|
418
|
+
memcached: healthy
|
|
419
|
+
ā 10.8.0.20: running
|
|
420
|
+
ā 10.8.0.21: running
|
|
421
|
+
|
|
422
|
+
redis: healthy
|
|
423
|
+
ā 10.8.0.22: running
|
|
424
|
+
|
|
425
|
+
haproxy: healthy
|
|
426
|
+
ā 10.8.0.30: running
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Requirements
|
|
430
|
+
|
|
431
|
+
- Ruby 3.0+
|
|
432
|
+
- Ubuntu 20.04+ servers (Debian-based distributions)
|
|
433
|
+
- SSH key-based authentication
|
|
434
|
+
- Optional: VPN solution (WireGuard, Tailscale, etc.) for private networking
|
|
435
|
+
|
|
436
|
+
## Installation
|
|
437
|
+
|
|
438
|
+
**Standalone Ruby:**
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
gem install boring_services
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**With Bundler:**
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
# Gemfile
|
|
448
|
+
gem 'boring_services'
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Then run:
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
bundle install
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**For Rails projects**, the gem will automatically integrate with Rails and provide generators and rake tasks.
|
|
458
|
+
|
|
459
|
+
## License
|
|
460
|
+
|
|
461
|
+
MIT
|
data/exe/boringservices
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require 'thor'
|
|
2
|
+
|
|
3
|
+
module BoringServices
|
|
4
|
+
class CLI < Thor
|
|
5
|
+
class_option :config, aliases: '-c', default: ENV['BORING_SERVICES_CONFIG'] || 'config/services.yml',
|
|
6
|
+
desc: 'Path to services.yml'
|
|
7
|
+
class_option :environment, aliases: '-e',
|
|
8
|
+
default: ENV['BORING_SERVICES_ENV'] || ENV['BORING_ENVIRONMENT'] ||
|
|
9
|
+
ENV['RAILS_ENV'] || 'production',
|
|
10
|
+
desc: 'Environment (production, staging, development)'
|
|
11
|
+
|
|
12
|
+
def self.exit_on_failure?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc 'setup', 'Setup/install all services (alias for install)'
|
|
17
|
+
def setup
|
|
18
|
+
config = Configuration.load(options[:config], options[:environment])
|
|
19
|
+
installer = Installer.new(config)
|
|
20
|
+
installer.install_all
|
|
21
|
+
rescue Error => e
|
|
22
|
+
puts "Error: #{e.message}"
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc 'install [SERVICE]', 'Install service(s) - all services or specific service'
|
|
27
|
+
def install(service_name = nil)
|
|
28
|
+
config = Configuration.load(options[:config], options[:environment])
|
|
29
|
+
installer = Installer.new(config)
|
|
30
|
+
|
|
31
|
+
if service_name
|
|
32
|
+
installer.install_service(service_name)
|
|
33
|
+
else
|
|
34
|
+
installer.install_all
|
|
35
|
+
end
|
|
36
|
+
rescue Error => e
|
|
37
|
+
puts "Error: #{e.message}"
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc 'uninstall SERVICE', 'Uninstall a specific service'
|
|
42
|
+
def uninstall(service_name)
|
|
43
|
+
config = Configuration.load(options[:config], options[:environment])
|
|
44
|
+
installer = Installer.new(config)
|
|
45
|
+
installer.uninstall_service(service_name)
|
|
46
|
+
rescue Error => e
|
|
47
|
+
puts "Error: #{e.message}"
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc 'restart SERVICE', 'Restart a specific service'
|
|
52
|
+
def restart(service_name)
|
|
53
|
+
config = Configuration.load(options[:config], options[:environment])
|
|
54
|
+
installer = Installer.new(config)
|
|
55
|
+
installer.restart_service(service_name)
|
|
56
|
+
rescue Error => e
|
|
57
|
+
puts "Error: #{e.message}"
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
desc 'reconfigure [SERVICE]', 'Reconfigure service(s) - skips package installation, only updates config and restarts'
|
|
62
|
+
def reconfigure(service_name = nil)
|
|
63
|
+
config = Configuration.load(options[:config], options[:environment])
|
|
64
|
+
installer = Installer.new(config)
|
|
65
|
+
|
|
66
|
+
if service_name
|
|
67
|
+
installer.reconfigure_service(service_name)
|
|
68
|
+
else
|
|
69
|
+
installer.reconfigure_all
|
|
70
|
+
end
|
|
71
|
+
rescue Error => e
|
|
72
|
+
puts "Error: #{e.message}"
|
|
73
|
+
exit 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
desc 'status', 'Check health status of all services'
|
|
77
|
+
def status
|
|
78
|
+
Configuration.load(options[:config], options[:environment])
|
|
79
|
+
results = BoringServices.status
|
|
80
|
+
|
|
81
|
+
results.each do |service_name, result|
|
|
82
|
+
puts "\n#{service_name}: #{result[:status]}"
|
|
83
|
+
next unless result[:hosts]
|
|
84
|
+
|
|
85
|
+
result[:hosts].each do |host, host_result|
|
|
86
|
+
status_icon = host_result[:running] ? 'ā' : 'ā'
|
|
87
|
+
puts " #{status_icon} #{host}: #{host_result[:running] ? 'running' : 'stopped'}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
rescue Error => e
|
|
91
|
+
puts "Error: #{e.message}"
|
|
92
|
+
exit 1
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
desc 'version', 'Show version'
|
|
96
|
+
def version
|
|
97
|
+
puts "boring_services #{VERSION}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'erb'
|
|
3
|
+
|
|
4
|
+
module BoringServices
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_reader :config, :environment
|
|
7
|
+
|
|
8
|
+
def self.load(config_path = 'config/services.yml', environment = nil)
|
|
9
|
+
env = environment || ENV['BORING_SERVICES_ENV'] || ENV['BORING_ENVIRONMENT'] || ENV['RAILS_ENV'] || 'production'
|
|
10
|
+
new(config_path, env)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(config_path, environment)
|
|
14
|
+
@config_path = config_path
|
|
15
|
+
@environment = environment.to_s
|
|
16
|
+
@config = load_config
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def service_config(service_name)
|
|
20
|
+
services = @config['services'] || []
|
|
21
|
+
services.find { |s| s['name'] == service_name.to_s }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def service_enabled?(service_name)
|
|
25
|
+
service = service_config(service_name)
|
|
26
|
+
service && service['enabled'] != false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def services
|
|
30
|
+
@config['services'] || []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def enabled_services
|
|
34
|
+
services.reject { |s| s['enabled'] == false }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def user
|
|
38
|
+
@config['user'] || 'ubuntu'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ssh_key
|
|
42
|
+
@config['ssh_key'] || '~/.ssh/id_rsa'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def forward_agent
|
|
46
|
+
return @config['forward_agent'] unless @config['forward_agent'].nil?
|
|
47
|
+
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def use_ssh_agent
|
|
52
|
+
return @config['use_ssh_agent'] unless @config['use_ssh_agent'].nil?
|
|
53
|
+
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ssh_auth_methods
|
|
58
|
+
(@config['ssh_auth_methods'] || ['publickey']).map(&:to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def secrets
|
|
62
|
+
@config['secrets'] || {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def load_config
|
|
68
|
+
raise Error, "Config file not found: #{@config_path}" unless File.exist?(@config_path)
|
|
69
|
+
|
|
70
|
+
content = File.read(@config_path)
|
|
71
|
+
erb_result = ERB.new(content).result
|
|
72
|
+
full_config = YAML.safe_load(erb_result, permitted_classes: [Symbol], aliases: true)
|
|
73
|
+
|
|
74
|
+
env_config = full_config[@environment]
|
|
75
|
+
raise Error, "Environment '#{@environment}' not found in config" unless env_config
|
|
76
|
+
|
|
77
|
+
env_config
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'rails/generators'
|
|
2
|
+
|
|
3
|
+
module BoringServices
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path('templates', __dir__)
|
|
7
|
+
|
|
8
|
+
def create_config_file
|
|
9
|
+
template 'services.yml.erb', 'config/services.yml'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def show_instructions
|
|
13
|
+
puts "\nā
BoringServices installed!"
|
|
14
|
+
puts "\nš Next steps:"
|
|
15
|
+
puts ' 1. Either:'
|
|
16
|
+
puts ' a) Edit config/services.yml with your service hosts, OR'
|
|
17
|
+
puts ' b) Use Terraform to auto-generate config/services.yml'
|
|
18
|
+
puts ' 2. Deploy services: rails boring_services:setup'
|
|
19
|
+
puts ' 3. Check status: rails boring_services:status'
|
|
20
|
+
puts "\nš Available: Memcached, Redis, HAProxy (SSL), Nginx"
|
|
21
|
+
puts ' See config/services.example.yml in gem for examples'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|