belt 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/README.md +140 -0
- data/lib/belt/cli/backup_config.rb +122 -0
- data/lib/belt/cli/backup_runner.rb +510 -0
- data/lib/belt/cli/deploy_command.rb +97 -8
- data/lib/belt/cli/generate_command.rb +22 -6
- data/lib/belt/cli/tables_command.rb +128 -85
- data/lib/belt/cli.rb +2 -0
- data/lib/belt/version.rb +1 -1
- data/lib/templates/environment/main.tf.erb +2 -0
- data/lib/templates/environment/terraform.tfvars.erb +7 -0
- data/lib/templates/environment/variables.tf.erb +12 -0
- data/lib/templates/module/variables.tf.erb +12 -0
- data/lib/templates/new_app/config/schema.tf.rb.erb +21 -8
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17b4d2d7497d0961b96e4628ee718fae22f5da62e321652cd3ff252351883836
|
|
4
|
+
data.tar.gz: 155caff17e7ca310097baf72376ed8ad8a1515c2ce397769b2db2d6cb5653a26
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 491589da65264857a3c6964ec522344b2dd222a3cab655235039bb39c48ad84c23a3ed83bc20aeaa31de2da197ba51d71c37dfed9395eeaf66464cebcbce28cb
|
|
7
|
+
data.tar.gz: 9d407911c6bec8492f26ece6bda2f592e67056a44a85ae6c09c99cd877992ffa356893d0a955fea0436684278911164916a1d8066490cb0be80e30368a134837
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Pre-Deploy Backups
|
|
6
|
+
|
|
7
|
+
Belt now runs automated backups before each deploy when configured. No standalone scripts or generators — `belt deploy` owns the entire lifecycle.
|
|
8
|
+
|
|
9
|
+
**New: Backup Config DSL** — configure backups per environment in `infrastructure/<env>/belt.rb`:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
Belt.configure do |config|
|
|
13
|
+
config.backups do
|
|
14
|
+
dynamodb :all # PITR + on-demand snapshot before deploy
|
|
15
|
+
cognito :users, :pool_config # Export users + pool config to backup bucket
|
|
16
|
+
s3 :legal_documents # Sync bucket to backup bucket
|
|
17
|
+
retention snapshots: 90, cognito: 10, s3: 10
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Simple mode: `config.backups = true` enables DynamoDB backups for all tables.
|
|
23
|
+
|
|
24
|
+
**Backup types supported:**
|
|
25
|
+
- **DynamoDB**: Verifies PITR is enabled, creates on-demand snapshots (retained per policy)
|
|
26
|
+
- **Cognito**: Paginated user export + pool configuration → versioned backup bucket
|
|
27
|
+
- **S3**: Syncs configured buckets to a dedicated backup bucket
|
|
28
|
+
|
|
29
|
+
**Deploy lifecycle (updated):**
|
|
30
|
+
1. Validate
|
|
31
|
+
2. Generate route manifests
|
|
32
|
+
3. **Run backups** (if configured for this environment)
|
|
33
|
+
4. `terraform init`
|
|
34
|
+
5. `terraform plan`
|
|
35
|
+
6. Confirm apply
|
|
36
|
+
7. `terraform apply`
|
|
37
|
+
|
|
38
|
+
**New CLI flags:**
|
|
39
|
+
- `belt deploy prod --skip-backup` — skip backup phase (CI re-runs, etc.)
|
|
40
|
+
- `belt deploy prod --backup-only` — run backups without deploying
|
|
41
|
+
|
|
42
|
+
**TablesCommand** — all generated DynamoDB tables now include PITR and deletion protection by default:
|
|
43
|
+
|
|
44
|
+
```hcl
|
|
45
|
+
point_in_time_recovery {
|
|
46
|
+
enabled = var.enable_pitr # true by default
|
|
47
|
+
}
|
|
48
|
+
deletion_protection_enabled = var.deletion_protection # true in prod, false in dev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 0.2.5
|
|
52
|
+
|
|
53
|
+
### Table Generation Separation
|
|
54
|
+
|
|
55
|
+
- Separated table generation from API schema contracts
|
|
56
|
+
|
|
57
|
+
### Schema DSL
|
|
58
|
+
|
|
59
|
+
- Added index support to schema DSL for custom GSIs
|
|
60
|
+
|
|
3
61
|
## 0.1.13
|
|
4
62
|
|
|
5
63
|
### Generator Extension API
|
data/README.md
CHANGED
|
@@ -395,6 +395,146 @@ end
|
|
|
395
395
|
|
|
396
396
|
When `--tables-file` is provided, Belt parses `aws_dynamodb_table` resource blocks from your Terraform files and infers which tables each route accesses based on the resource name in the route path. Routes can also declare tables explicitly in the DSL via `tables: [:posts, :comments]`.
|
|
397
397
|
|
|
398
|
+
## Backups
|
|
399
|
+
|
|
400
|
+
Belt integrates automated pre-deploy backups directly into the deploy lifecycle. No standalone scripts, no generators — `belt deploy` handles everything.
|
|
401
|
+
|
|
402
|
+
### Quick Start Guide
|
|
403
|
+
|
|
404
|
+
This walks you through enabling backups for an existing Belt app from scratch.
|
|
405
|
+
|
|
406
|
+
#### Step 1: Create a backup config
|
|
407
|
+
|
|
408
|
+
Create `infrastructure/<env>/belt.rb` in your environment directory (e.g. `infrastructure/prod/belt.rb`):
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
# infrastructure/prod/belt.rb
|
|
412
|
+
Belt.configure do |config|
|
|
413
|
+
config.backups do
|
|
414
|
+
dynamodb :all
|
|
415
|
+
retention snapshots: 90
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
That's the minimum — every DynamoDB table in this environment will get an on-demand snapshot before each deploy, retained for 90 days. PITR (point-in-time recovery) will be verified as enabled.
|
|
421
|
+
|
|
422
|
+
#### Step 2: Deploy
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
belt deploy prod
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Output:
|
|
429
|
+
|
|
430
|
+
```
|
|
431
|
+
━━━ pre-deploy backup ━━━
|
|
432
|
+
📦 Creating backup bucket: my-app-backups-prod
|
|
433
|
+
✅ Backup bucket created and secured
|
|
434
|
+
🔍 Verifying PITR: my-app-prod-posts → enabled ✓
|
|
435
|
+
📸 Snapshot: my-app-prod-posts → my-app-prod-posts-20260723-170000 ✓
|
|
436
|
+
🧹 Cleanup: 0 expired snapshots removed
|
|
437
|
+
|
|
438
|
+
━━━ terraform init ━━━
|
|
439
|
+
...
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The backup bucket is created automatically on first run (versioned, public access blocked). Subsequent deploys reuse it.
|
|
443
|
+
|
|
444
|
+
#### Step 3: Add more backup types (optional)
|
|
445
|
+
|
|
446
|
+
As your app grows, add Cognito and S3 backups:
|
|
447
|
+
|
|
448
|
+
```ruby
|
|
449
|
+
# infrastructure/prod/belt.rb
|
|
450
|
+
Belt.configure do |config|
|
|
451
|
+
config.backups do
|
|
452
|
+
dynamodb :all # On-demand snapshots + PITR verification
|
|
453
|
+
cognito :users, :pool_config # Export user list + pool settings to S3
|
|
454
|
+
s3 :legal_documents # Sync bucket contents to backup bucket
|
|
455
|
+
retention snapshots: 90, cognito: 10, s3: 10
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
| Backup type | What it does | Retention default |
|
|
461
|
+
|-------------|-------------|-------------------|
|
|
462
|
+
| `dynamodb :all` | PITR check + on-demand snapshot per table | 90 days |
|
|
463
|
+
| `dynamodb :posts, :users` | Same, but only named tables | 90 days |
|
|
464
|
+
| `cognito :users` | Paginated user export → JSON in backup bucket | 10 copies |
|
|
465
|
+
| `cognito :pool_config` | Pool configuration export → JSON | 10 copies |
|
|
466
|
+
| `s3 :bucket_name` | Full sync to backup bucket | 10 copies |
|
|
467
|
+
|
|
468
|
+
#### Step 4: Keep dev lightweight
|
|
469
|
+
|
|
470
|
+
Don't configure backups for dev environments — just omit the file or the backups block:
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
# infrastructure/dev01/belt.rb
|
|
474
|
+
Belt.configure do |config|
|
|
475
|
+
# No backups block = no backups run during deploy
|
|
476
|
+
end
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Or simply don't create `infrastructure/dev01/belt.rb` at all.
|
|
480
|
+
|
|
481
|
+
### Simple mode
|
|
482
|
+
|
|
483
|
+
If you only need DynamoDB backups with all defaults:
|
|
484
|
+
|
|
485
|
+
```ruby
|
|
486
|
+
# infrastructure/prod/belt.rb
|
|
487
|
+
Belt.configure do |config|
|
|
488
|
+
config.backups = true
|
|
489
|
+
end
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
This enables DynamoDB snapshots for all tables with 90-day retention.
|
|
493
|
+
|
|
494
|
+
### CLI Flags
|
|
495
|
+
|
|
496
|
+
| Flag | Description |
|
|
497
|
+
|------|-------------|
|
|
498
|
+
| `--skip-backup` | Skip backup phase (useful for CI re-runs) |
|
|
499
|
+
| `--backup-only` | Run backups without deploying |
|
|
500
|
+
|
|
501
|
+
```bash
|
|
502
|
+
# Normal deploy (runs backups first if configured)
|
|
503
|
+
belt deploy prod
|
|
504
|
+
|
|
505
|
+
# Skip backups this time
|
|
506
|
+
belt deploy prod --skip-backup
|
|
507
|
+
|
|
508
|
+
# Just create a recovery point, don't deploy
|
|
509
|
+
belt deploy prod --backup-only
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### DynamoDB Protection Defaults
|
|
513
|
+
|
|
514
|
+
All DynamoDB tables generated by Belt include safety defaults:
|
|
515
|
+
|
|
516
|
+
```hcl
|
|
517
|
+
point_in_time_recovery {
|
|
518
|
+
enabled = var.enable_pitr # true by default
|
|
519
|
+
}
|
|
520
|
+
deletion_protection_enabled = var.deletion_protection # true in prod, false in dev
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
These are set via Terraform variables in each environment's `terraform.tfvars`. PITR gives you 35 days of continuous recovery regardless of the snapshot schedule.
|
|
524
|
+
|
|
525
|
+
### How It Works
|
|
526
|
+
|
|
527
|
+
1. **Backup bucket** — Belt creates `<app-name>-backups-<env>` on first run (versioned, public access blocked)
|
|
528
|
+
2. **Table discovery** — Table names are read from `terraform output` (requires at least one prior successful deploy)
|
|
529
|
+
3. **DynamoDB** — Verifies PITR is enabled, then creates an on-demand backup named `<table>-<timestamp>`
|
|
530
|
+
4. **Cognito** — Uses `list-users` (paginated) and `describe-user-pool` to export JSON to the backup bucket
|
|
531
|
+
5. **S3** — Runs `aws s3 sync` from source bucket to `s3://<backup-bucket>/s3/<bucket-name>/<timestamp>/`
|
|
532
|
+
6. **Cleanup** — Removes snapshots/copies older than the configured retention
|
|
533
|
+
|
|
534
|
+
### First Deploy Note
|
|
535
|
+
|
|
536
|
+
The backup phase reads table names from Terraform outputs. On a brand-new environment that has never been deployed, there are no outputs yet — Belt will warn and skip the backup phase gracefully. After the first successful deploy, backups run normally on subsequent deploys.
|
|
537
|
+
|
|
398
538
|
## License
|
|
399
539
|
|
|
400
540
|
MIT
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module CLI
|
|
5
|
+
class BackupConfig
|
|
6
|
+
attr_reader :dynamodb_tables, :cognito_exports, :s3_buckets, :retention
|
|
7
|
+
|
|
8
|
+
# Load backup config from infrastructure/<env>/belt.rb
|
|
9
|
+
# Returns nil if no config file exists or backups aren't configured
|
|
10
|
+
def self.load(env, infra_dir: nil)
|
|
11
|
+
infra_dir ||= 'infrastructure'
|
|
12
|
+
config_file = File.join(infra_dir, env, 'belt.rb')
|
|
13
|
+
return nil unless File.exist?(config_file)
|
|
14
|
+
|
|
15
|
+
config = new
|
|
16
|
+
evaluator = ConfigEvaluator.new(config)
|
|
17
|
+
evaluator.evaluate(File.read(config_file), config_file)
|
|
18
|
+
|
|
19
|
+
# Return nil if no backups were configured
|
|
20
|
+
config.any? ? config : nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@dynamodb_tables = nil # nil = not configured, :all = all tables, Array = specific tables
|
|
25
|
+
@cognito_exports = [] # e.g. [:users, :pool_config]
|
|
26
|
+
@s3_buckets = [] # e.g. [:legal_documents]
|
|
27
|
+
@retention = { snapshots: 90, cognito: 10, s3: 10 }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def dynamodb?
|
|
31
|
+
!@dynamodb_tables.nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def cognito?
|
|
35
|
+
@cognito_exports.any?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def any?
|
|
39
|
+
dynamodb? || cognito? || @s3_buckets.any?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Evaluates the belt.rb config file in an isolated context
|
|
43
|
+
# that intercepts Belt.configure calls
|
|
44
|
+
class ConfigEvaluator
|
|
45
|
+
def initialize(config)
|
|
46
|
+
@config = config
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def evaluate(content, filename)
|
|
50
|
+
# The config file calls Belt.configure { |config| ... }
|
|
51
|
+
# We wrap the content to intercept the Belt constant lookup
|
|
52
|
+
# by evaluating in a module where Belt is redefined
|
|
53
|
+
config = @config
|
|
54
|
+
sandbox = Module.new
|
|
55
|
+
# Define a Belt module inside the sandbox that has .configure
|
|
56
|
+
belt_proxy = Module.new do
|
|
57
|
+
define_singleton_method(:configure) do |&block|
|
|
58
|
+
block.call(ConfigDSL.new(config))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
sandbox.const_set(:Belt, belt_proxy)
|
|
62
|
+
|
|
63
|
+
# Evaluate the file content within the sandbox module
|
|
64
|
+
sandbox.module_eval(content, filename)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# DSL yielded to the block inside Belt.configure { |config| ... }
|
|
69
|
+
class ConfigDSL
|
|
70
|
+
def initialize(config)
|
|
71
|
+
@config = config
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def backups(enabled = nil, &block)
|
|
75
|
+
if enabled == true
|
|
76
|
+
# Simple mode: config.backups = true → just DynamoDB for all tables
|
|
77
|
+
@config.instance_variable_set(:@dynamodb_tables, :all)
|
|
78
|
+
elsif block
|
|
79
|
+
backup_dsl = BackupDSL.new(@config)
|
|
80
|
+
backup_dsl.instance_eval(&block)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# DSL for the backups block
|
|
86
|
+
class BackupDSL
|
|
87
|
+
def initialize(config)
|
|
88
|
+
@config = config
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def dynamodb(scope = :all)
|
|
92
|
+
if scope == :all
|
|
93
|
+
@config.instance_variable_set(:@dynamodb_tables, :all)
|
|
94
|
+
else
|
|
95
|
+
tables = Array(scope)
|
|
96
|
+
@config.instance_variable_set(:@dynamodb_tables, tables.map(&:to_s))
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def cognito(*exports)
|
|
101
|
+
@config.instance_variable_set(:@cognito_exports, exports.flatten.map(&:to_sym))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def s3(*buckets)
|
|
105
|
+
@config.instance_variable_set(:@s3_buckets, buckets.flatten.map(&:to_sym))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def retention(opts = {})
|
|
109
|
+
current = @config.instance_variable_get(:@retention)
|
|
110
|
+
merged = current.merge(opts.transform_keys(&:to_sym))
|
|
111
|
+
|
|
112
|
+
# Convert duration objects (90.days) to integer days
|
|
113
|
+
merged.transform_values! do |v|
|
|
114
|
+
v.respond_to?(:to_i) ? v.to_i : v
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@config.instance_variable_set(:@retention, merged)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|