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
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');
|
|
@@ -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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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 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
|
-
{{
|
|
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]
|