deploy-stack 0.9.11 → 0.9.13

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.13",
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');
@@ -72,34 +94,36 @@ export async function generateTemplates(targetDir, config) {
72
94
 
73
95
  // 6. Create .dockerignore to keep images lean and secure
74
96
  const dockerignorePath = path.join(targetDir, '.dockerignore');
97
+ const appendDockerIgnore = '\n# Infrastructure (deploy-stack)\nterraform/\n**/.terraform/\n**/.terraform.*\n**/*.tfstate*\n.env\n';
98
+
75
99
  if (!fsSync.existsSync(dockerignorePath)) {
76
100
  const dockerignoreContent = `
77
101
  .git/
78
102
  .github/
79
- terraform/
80
- **/.terraform/
81
- **/.terraform.*
82
- **/*.tfstate*
83
- .env
84
103
  README.md
85
104
  node_modules/
86
105
  npm-debug.log
87
106
  __pycache__/
88
107
  *.pyc
89
108
  .DS_Store
90
- `.trim();
109
+ `.trim() + appendDockerIgnore;
91
110
  await fs.writeFile(dockerignorePath, dockerignoreContent);
111
+ } else {
112
+ const existingDockerignore = await fs.readFile(dockerignorePath, 'utf-8');
113
+ if (!existingDockerignore.includes('terraform/')) {
114
+ await fs.appendFile(dockerignorePath, appendDockerIgnore);
115
+ }
92
116
  }
93
- }
94
117
 
95
- function getGitignoreContent(framework) {
96
- const baseIgnore = `
118
+ function getGitignoreContent(framework) {
119
+ const baseIgnore = `
97
120
  # Infrastructure (deploy-stack)
98
121
  terraform/.terraform/
99
122
  terraform/*.tfstate
100
123
  terraform/*.tfstate.backup
101
124
  terraform/.terraform.lock.hcl
102
125
  terraform/secret_keys.json
126
+ terraform/*.auto.tfvars
103
127
  .env
104
128
 
105
129
  # OS
@@ -107,17 +131,17 @@ terraform/secret_keys.json
107
131
  Thumbs.db
108
132
  `;
109
133
 
110
- const presets = {
111
- node: '\n# Node.js\nnode_modules/\nnpm-debug.log\nyarn-error.log\n',
112
- nextjs: '\n# Next.js\nnode_modules/\n.next/\nout/\nbuild/\nnext-env.d.ts\n',
113
- nuxt: '\n# Nuxt 3\nnode_modules/\n.nuxt/\n.output/\ndist/\n',
114
- python: '\n# Python\n__pycache__/\n*.py[cod]\n*$py.class\nvenv/\nenv/\n.venv/\n.pytest_cache/\n',
115
- django: '\n# Django\n*.log\n*.pot\n*.pyc\n__pycache__/\ndb.sqlite3\nmedia/\n',
116
- go: '\n# Go\n/bin/\n/pkg/\n/obj/\n*.exe\n*.exe~*\n*.dll\n*.so\n*.dylib\n',
117
- rails: '\n# Rails\n/*.log\n/tmp/\n/log/\n/public/system/\n/public/assets/\n/vendor/bundle/\n',
118
- static: '\n# Static Sites\nnode_modules/\ndist/\nbuild/\nout/\n.output/\n.cache/\npublic/\n'
119
- };
120
-
121
- const frameworkIgnore = presets[framework] || '';
122
- return (baseIgnore + frameworkIgnore).trim();
123
- }
134
+ const presets = {
135
+ node: '\n# Node.js\nnode_modules/\nnpm-debug.log\nyarn-error.log\n',
136
+ nextjs: '\n# Next.js\nnode_modules/\n.next/\nout/\nbuild/\nnext-env.d.ts\n',
137
+ nuxt: '\n# Nuxt 3\nnode_modules/\n.nuxt/\n.output/\ndist/\n',
138
+ python: '\n# Python\n__pycache__/\n*.py[cod]\n*$py.class\nvenv/\nenv/\n.venv/\n.pytest_cache/\n',
139
+ django: '\n# Django\n*.log\n*.pot\n*.pyc\n__pycache__/\ndb.sqlite3\nmedia/\n',
140
+ go: '\n# Go\n/bin/\n/pkg/\n/obj/\n*.exe\n*.exe~*\n*.dll\n*.so\n*.dylib\n',
141
+ rails: '\n# Rails\n/*.log\n/tmp/\n/log/\n/public/system/\n/public/assets/\n/vendor/bundle/\n',
142
+ static: '\n# Static Sites\nnode_modules/\ndist/\nbuild/\nout/\n.output/\n.cache/\npublic/\n'
143
+ };
144
+
145
+ const frameworkIgnore = presets[framework] || '';
146
+ return (baseIgnore + frameworkIgnore).trim();
147
+ }
@@ -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 listed above will
69
+ return an 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]