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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploy-stack",
3
- "version": "0.9.11",
3
+ "version": "0.9.12",
4
4
  "description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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('⚠️ deploy-stack configurations already exist. What would you like to do?'),
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: 'cancel', label: 'Cancel', hint: 'Exit without making changes' },
24
- { value: 'backup', label: 'Backup & Regenerate', hint: 'Move old configs to .bak (ignored by Git) and regenerate' }
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
 
@@ -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
- config.DB_SECRETS = `
38
- { "name": "DB_USER", "valueFrom": "\${aws_db_instance.postgres.master_user_secret[0].secret_arn}:username::" },
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
- FROM ruby:3.2-slim
1
+ # Stage 1: Build the Rails application
2
+ FROM ruby:3.3-alpine AS builder
2
3
  WORKDIR /app
3
4
 
4
- RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs && rm -rf /var/lib/apt/lists/*
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
- # Create unprivileged user
7
- RUN groupadd -g 1001 appgroup && \
8
- useradd -u 1001 -g appgroup -s /bin/sh -m appuser
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 Gemfile Gemfile.lock ./
11
- RUN bundle install --without development test
18
+ COPY . .
12
19
 
13
- # Copy code with explicit ownership
14
- COPY --chown=appuser:appgroup . .
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
- RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile
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 appuser
47
+ USER railsuser
19
48
  EXPOSE {{PORT}}
20
49
 
21
- CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
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 = "⚠️ Your infrastructure is up, but these URLs will return 503 errors until you push your code to GitHub and the Actions pipeline deploys your container."
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
- {{DB_SECRETS}}
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 "website_url" {
202
- description = "The public URL of your load balancer"
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]