deploy-stack 0.9.11 → 0.9.12
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/package.json
CHANGED
package/src/utils/backup.js
CHANGED
|
@@ -17,11 +17,16 @@ export async function handleExistingFiles(targetDir) {
|
|
|
17
17
|
return; // Clean slate
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const foundFiles = [];
|
|
21
|
+
if (tfExists) foundFiles.push('terraform/');
|
|
22
|
+
if (dockerfileExists) foundFiles.push('Dockerfile');
|
|
23
|
+
if (wfExists) foundFiles.push('.github/workflows/deploy.yml');
|
|
24
|
+
|
|
20
25
|
const overwriteDecision = await select({
|
|
21
|
-
message: color.yellow(
|
|
26
|
+
message: color.yellow(`⚠️ Conflicting files found (${foundFiles.join(', ')}). To guarantee a secure, 0-CVE deployment, we must use our optimized configurations.`),
|
|
22
27
|
options: [
|
|
23
|
-
{ value: '
|
|
24
|
-
{ value: '
|
|
28
|
+
{ value: 'backup', label: 'Backup & Regenerate', hint: 'Move old configs to .bak and generate secure templates' },
|
|
29
|
+
{ value: 'cancel', label: 'Cancel', hint: 'Exit without making changes' }
|
|
25
30
|
]
|
|
26
31
|
});
|
|
27
32
|
|
package/src/utils/generator.js
CHANGED
|
@@ -26,6 +26,8 @@ export async function generateTemplates(targetDir, config) {
|
|
|
26
26
|
{ src: 'README.md', dest: 'README.md' }
|
|
27
27
|
];
|
|
28
28
|
|
|
29
|
+
let secretsArray = [];
|
|
30
|
+
|
|
29
31
|
if (config.NEEDS_DATABASE) {
|
|
30
32
|
filesToProcess.push({ src: 'terraform/database.tf', dest: 'terraform/database.tf' });
|
|
31
33
|
|
|
@@ -34,14 +36,34 @@ export async function generateTemplates(targetDir, config) {
|
|
|
34
36
|
{ "name": "DB_PORT", "value": "5432" },
|
|
35
37
|
{ "name": "DB_NAME", "value": "\${aws_db_instance.postgres.db_name}" }`;
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
{ "name": "
|
|
39
|
-
{ "name": "DB_PASSWORD", "valueFrom": "\${aws_db_instance.postgres.master_user_secret[0].secret_arn}:password::" }`;
|
|
39
|
+
secretsArray.push(`{ "name": "DB_USER", "valueFrom": "\${aws_db_instance.postgres.master_user_secret[0].secret_arn}:username::" }`);
|
|
40
|
+
secretsArray.push(`{ "name": "DB_PASSWORD", "valueFrom": "\${aws_db_instance.postgres.master_user_secret[0].secret_arn}:password::" }`);
|
|
40
41
|
} else {
|
|
41
42
|
config.DB_ENV_VARS = '';
|
|
42
|
-
config.DB_SECRETS = '';
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Build the initial HCL map for AWS Secrets Manager
|
|
46
|
+
let initialSecretMap = `{\n EXAMPLE_API_KEY = "replace_me_in_aws_console"`;
|
|
47
|
+
|
|
48
|
+
// Inject Rails Master Key if applicable
|
|
49
|
+
if (config.finalFramework === 'rails') {
|
|
50
|
+
secretsArray.push(`{ "name": "RAILS_MASTER_KEY", "valueFrom": "\${aws_secretsmanager_secret.app_secrets.arn}:RAILS_MASTER_KEY::" }`);
|
|
51
|
+
|
|
52
|
+
initialSecretMap += `,\n RAILS_MASTER_KEY = var.rails_master_key`;
|
|
53
|
+
|
|
54
|
+
const masterKeyPath = path.join(targetDir, 'config', 'master.key');
|
|
55
|
+
if (fsSync.existsSync(masterKeyPath)) {
|
|
56
|
+
const realKey = fsSync.readFileSync(masterKeyPath, 'utf-8').trim();
|
|
57
|
+
const tfvarsPath = path.join(targetDir, 'terraform', 'secrets.auto.tfvars');
|
|
58
|
+
fsSync.writeFileSync(tfvarsPath, `rails_master_key = "${realKey}"\n`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
initialSecretMap += `\n }`;
|
|
63
|
+
|
|
64
|
+
config.TASK_SECRETS = secretsArray.join(',\n ');
|
|
65
|
+
config.INITIAL_SECRET_MAP = initialSecretMap;
|
|
66
|
+
|
|
45
67
|
// 3. Process standard files
|
|
46
68
|
for (const file of filesToProcess) {
|
|
47
69
|
let content = await fs.readFile(path.join(templatesDir, file.src), 'utf-8');
|
|
@@ -100,6 +122,7 @@ terraform/*.tfstate
|
|
|
100
122
|
terraform/*.tfstate.backup
|
|
101
123
|
terraform/.terraform.lock.hcl
|
|
102
124
|
terraform/secret_keys.json
|
|
125
|
+
terraform/*.auto.tfvars
|
|
103
126
|
.env
|
|
104
127
|
|
|
105
128
|
# OS
|
|
@@ -1,21 +1,50 @@
|
|
|
1
|
-
|
|
1
|
+
# Stage 1: Build the Rails application
|
|
2
|
+
FROM ruby:3.3-alpine AS builder
|
|
2
3
|
WORKDIR /app
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
# Install native build tools required to compile C extensions (like pg or nokogiri)
|
|
6
|
+
RUN apk update && \
|
|
7
|
+
apk add --no-cache build-base postgresql-dev tzdata nodejs yarn
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
COPY Gemfile Gemfile.lock* ./
|
|
10
|
+
|
|
11
|
+
# Use ENV to guarantee Bundler skips dev/test gems
|
|
12
|
+
ENV BUNDLE_WITHOUT="development:test"
|
|
13
|
+
|
|
14
|
+
# Install deps and remove cache
|
|
15
|
+
RUN bundle install && \
|
|
16
|
+
rm -rf /usr/local/bundle/cache/*.gem
|
|
9
17
|
|
|
10
|
-
COPY
|
|
11
|
-
RUN bundle install --without development test
|
|
18
|
+
COPY . .
|
|
12
19
|
|
|
13
|
-
#
|
|
14
|
-
|
|
20
|
+
# Precompile assets (we use a dummy key since it's just for building)
|
|
21
|
+
RUN SECRET_KEY_BASE=dummy bundle exec rails assets:precompile || true
|
|
22
|
+
|
|
23
|
+
# Stage 2: Production runner
|
|
24
|
+
FROM ruby:3.3-alpine AS runner
|
|
25
|
+
WORKDIR /app
|
|
26
|
+
|
|
27
|
+
ENV RAILS_ENV=production \
|
|
28
|
+
RAILS_SERVE_STATIC_FILES=true \
|
|
29
|
+
RAILS_LOG_TO_STDOUT=true \
|
|
30
|
+
BUNDLE_WITHOUT="development:test" \
|
|
31
|
+
PORT={{PORT}}
|
|
32
|
+
|
|
33
|
+
# Upgrade Alpine OS to patch system CVEs and install runtime libs
|
|
34
|
+
# The pure Ruby Alpine image has no NPM/Yarn ghosts to vaporize!
|
|
35
|
+
RUN apk update && apk upgrade --no-cache && \
|
|
36
|
+
apk add --no-cache postgresql-libs tzdata && \
|
|
37
|
+
rm -rf /var/cache/apk/*
|
|
38
|
+
|
|
39
|
+
# Create unprivileged user
|
|
40
|
+
RUN addgroup -g 1001 -S railsgroup && \
|
|
41
|
+
adduser -S railsuser -u 1001 -G railsgroup
|
|
15
42
|
|
|
16
|
-
|
|
43
|
+
# Copy built gems and application code
|
|
44
|
+
COPY --from=builder --chown=railsuser:railsgroup /usr/local/bundle /usr/local/bundle
|
|
45
|
+
COPY --from=builder --chown=railsuser:railsgroup /app /app
|
|
17
46
|
|
|
18
|
-
USER
|
|
47
|
+
USER railsuser
|
|
19
48
|
EXPOSE {{PORT}}
|
|
20
49
|
|
|
21
|
-
CMD ["bundle", "exec", "puma", "-
|
|
50
|
+
CMD ["bundle", "exec", "puma", "-b", "tcp://0.0.0.0:{{PORT}}"]
|
|
@@ -59,5 +59,22 @@ output "cloudfront_url" {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
output "z_NEXT_STEP_REQUIRED" {
|
|
62
|
-
value =
|
|
62
|
+
value = <<EOT
|
|
63
|
+
|
|
64
|
+
====================================================================
|
|
65
|
+
🚀 INFRASTRUCTURE PROVISIONED SUCCESSFULLY!
|
|
66
|
+
====================================================================
|
|
67
|
+
|
|
68
|
+
Your AWS environment is ready. However, your URLs will return an
|
|
69
|
+
error until your application code is actually deployed.
|
|
70
|
+
|
|
71
|
+
TO DEPLOY YOUR APP:
|
|
72
|
+
1. git add .
|
|
73
|
+
2. git commit -m "ci: configure deploy-stack"
|
|
74
|
+
3. git push origin main
|
|
75
|
+
|
|
76
|
+
Once the GitHub Action completes, your site will be live at the
|
|
77
|
+
CloudFront URL!
|
|
78
|
+
====================================================================
|
|
79
|
+
EOT
|
|
63
80
|
}
|
|
@@ -95,7 +95,7 @@ resource "aws_ecs_task_definition" "app" {
|
|
|
95
95
|
}
|
|
96
96
|
],
|
|
97
97
|
[
|
|
98
|
-
{{
|
|
98
|
+
{{TASK_SECRETS}}
|
|
99
99
|
]
|
|
100
100
|
)
|
|
101
101
|
|
|
@@ -198,8 +198,8 @@ resource "aws_iam_role_policy" "secrets_policy" {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
# --- Outputs ---
|
|
201
|
-
output "
|
|
202
|
-
description = "
|
|
201
|
+
output "alb_direct_url" {
|
|
202
|
+
description = "Direct Load Balancer URL (Bypasses CloudFront/CDN)"
|
|
203
203
|
value = "http://${aws_lb.main.dns_name}"
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -5,12 +5,16 @@ resource "aws_secretsmanager_secret" "app_secrets" {
|
|
|
5
5
|
recovery_window_in_days = 0 # Allows instant deletion for dev/POC environments
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
# Fallback dummy key for CI/CD environments where the real key isn't present
|
|
9
|
+
variable "rails_master_key" {
|
|
10
|
+
type = string
|
|
11
|
+
default = "1234567890abcdef1234567890abcdef"
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
# Initial placeholder secret so the ECS task doesn't fail on first boot
|
|
9
15
|
resource "aws_secretsmanager_secret_version" "app_secrets_initial" {
|
|
10
16
|
secret_id = aws_secretsmanager_secret.app_secrets.id
|
|
11
|
-
secret_string = jsonencode({
|
|
12
|
-
EXAMPLE_API_KEY = "replace_me_in_aws_console_or_cli"
|
|
13
|
-
})
|
|
17
|
+
secret_string = jsonencode({{INITIAL_SECRET_MAP}})
|
|
14
18
|
|
|
15
19
|
lifecycle {
|
|
16
20
|
ignore_changes = [secret_string]
|