deploy-stack 0.2.1 β 0.2.3
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/README.md +8 -5
- package/package.json +22 -1
- package/src/commands/init.js +41 -156
- package/src/utils/aws.js +38 -0
- package/src/utils/generator.js +56 -0
- package/src/utils/warnings.js +21 -0
- package/templates/README.md +9 -1
- package/templates/docker/node.Dockerfile +17 -2
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
## The Problem
|
|
@@ -118,11 +120,12 @@ npx deploy-stack --no-telemetry
|
|
|
118
120
|
|
|
119
121
|
## πΊοΈ Roadmap
|
|
120
122
|
|
|
121
|
-
- [x] **
|
|
122
|
-
- [x] **
|
|
123
|
-
- [
|
|
124
|
-
- [ ] **
|
|
125
|
-
- [ ] **
|
|
123
|
+
- [x] **Core MVP:** Interactive CLI, ECS Fargate + ALB Terraform generation, CI/CD, and Secrets sync.
|
|
124
|
+
- [x] **Production Readiness:** CloudFront distribution, S3 State locking, and OIDC security.
|
|
125
|
+
- [x] **Smart Experience:** Zero-config framework auto-discovery for Next.js, Express, and FastAPI.
|
|
126
|
+
- [ ] **State & Storage:** Automated RDS PostgreSQL provisioning and persistent volumes.
|
|
127
|
+
- [ ] **Advanced Networking:** Custom domains (Route 53), ACM SSL configuration, and environment workspaces (staging/prod).
|
|
128
|
+
- [ ] **Security Hardening:** Private VPC subnets with NAT gateways, and AWS WAF integration.
|
|
126
129
|
|
|
127
130
|
---
|
|
128
131
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deploy-stack",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,27 @@
|
|
|
14
14
|
"type": "git",
|
|
15
15
|
"url": "git+https://github.com/anton-codes-iac/deploy-stack.git"
|
|
16
16
|
},
|
|
17
|
+
"homepage": "https://github.com/anton-codes-iac/deploy-stack#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/anton-codes-iac/deploy-stack/issues"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"aws",
|
|
23
|
+
"terraform",
|
|
24
|
+
"github-actions",
|
|
25
|
+
"ci-cd",
|
|
26
|
+
"ecs",
|
|
27
|
+
"fargate",
|
|
28
|
+
"nextjs-deployment",
|
|
29
|
+
"infrastructure-as-code",
|
|
30
|
+
"oidc",
|
|
31
|
+
"deploy-stack",
|
|
32
|
+
"expressjs",
|
|
33
|
+
"fastapi",
|
|
34
|
+
"nextjs",
|
|
35
|
+
"static-site",
|
|
36
|
+
"nginx"
|
|
37
|
+
],
|
|
17
38
|
"license": "MIT",
|
|
18
39
|
"dependencies": {
|
|
19
40
|
"@aws-sdk/client-s3": "3.1115.0",
|
package/src/commands/init.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
|
2
|
-
import { S3Client, CreateBucketCommand, PutBucketVersioningCommand } from '@aws-sdk/client-s3';
|
|
3
1
|
import fsSync from 'fs';
|
|
4
2
|
import fs from 'fs/promises';
|
|
5
3
|
import path from 'path';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
4
|
import { intro, outro, group, text, select, spinner, cancel, confirm, log } from '@clack/prompts';
|
|
8
5
|
import color from 'picocolors';
|
|
9
6
|
|
|
10
7
|
import { checkDependency } from '../utils/system.js';
|
|
11
8
|
import { detectFramework } from '../utils/detector.js';
|
|
12
9
|
import { trackEvent } from '../core/telemetry.js';
|
|
10
|
+
import { getFrameworkWarning } from '../utils/warnings.js';
|
|
11
|
+
import { provisionStateBucket } from '../utils/aws.js';
|
|
12
|
+
import { generateTemplates } from '../utils/generator.js';
|
|
13
13
|
|
|
14
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -111,19 +111,6 @@ export async function mainStack() {
|
|
|
111
111
|
placeholder: defaultPort,
|
|
112
112
|
defaultValue: defaultPort,
|
|
113
113
|
}),
|
|
114
|
-
framework: () => {
|
|
115
|
-
if (detectedFramework) return undefined;
|
|
116
|
-
|
|
117
|
-
return select({
|
|
118
|
-
message: 'Which framework preset should we configure?',
|
|
119
|
-
options: [
|
|
120
|
-
{ value: 'node', label: 'Node.js / Express' },
|
|
121
|
-
{ value: 'nextjs', label: 'Next.js (Standalone)' },
|
|
122
|
-
{ value: 'python', label: 'Python FastAPI' },
|
|
123
|
-
{ value: 'static', label: 'Static Site (Gatsby, React, plain HTML via Nginx)' },
|
|
124
|
-
],
|
|
125
|
-
});
|
|
126
|
-
},
|
|
127
114
|
size: () =>
|
|
128
115
|
select({
|
|
129
116
|
message: 'Select your Fargate compute size:',
|
|
@@ -200,138 +187,38 @@ export async function mainStack() {
|
|
|
200
187
|
s.start('Provisioning infrastructure...');
|
|
201
188
|
|
|
202
189
|
// 6. Provision S3 bucket for Terraform state & enable versioning
|
|
203
|
-
|
|
204
|
-
let awsAccountId;
|
|
190
|
+
let awsAccountId, stateBucketName;
|
|
205
191
|
try {
|
|
206
|
-
const
|
|
207
|
-
awsAccountId =
|
|
192
|
+
const bucketData = await provisionStateBucket(project.region, actualProjectName);
|
|
193
|
+
awsAccountId = bucketData.awsAccountId;
|
|
194
|
+
stateBucketName = bucketData.stateBucketName;
|
|
208
195
|
} catch (error) {
|
|
209
|
-
s.stop('β Failed to authenticate with AWS.');
|
|
210
|
-
console.error(color.red(`AWS Error:
|
|
211
|
-
|
|
212
|
-
trackEvent('cli-error', { step: 'aws_sts_auth', error_code: error.name });
|
|
196
|
+
s.stop('β Failed to provision remote state or authenticate with AWS.');
|
|
197
|
+
console.error(color.red(`AWS Error: ${error.message}`));
|
|
198
|
+
trackEvent('cli-error', { step: 'aws_provisioning', error_code: error.name || 'UNKNOWN' });
|
|
213
199
|
process.exit(1);
|
|
214
200
|
}
|
|
215
201
|
|
|
216
|
-
let stateBucketName = `${actualProjectName}-tfstate-${awsAccountId}`.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
217
|
-
if (stateBucketName.length > 63) {
|
|
218
|
-
stateBucketName = stateBucketName.substring(0, 63).replace(/-$/, '');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const s3Client = new S3Client({ region: project.region });
|
|
222
|
-
try {
|
|
223
|
-
await s3Client.send(new CreateBucketCommand({
|
|
224
|
-
Bucket: stateBucketName,
|
|
225
|
-
CreateBucketConfiguration: project.region === 'us-east-1' ? undefined : { LocationConstraint: project.region }
|
|
226
|
-
}));
|
|
227
|
-
|
|
228
|
-
await s3Client.send(new PutBucketVersioningCommand({
|
|
229
|
-
Bucket: stateBucketName,
|
|
230
|
-
VersioningConfiguration: { Status: 'Enabled' }
|
|
231
|
-
}));
|
|
232
|
-
} catch (error) {
|
|
233
|
-
if (error.name !== 'BucketAlreadyOwnedByYou') {
|
|
234
|
-
s.stop('β Failed to provision remote state.');
|
|
235
|
-
console.error(color.red(`AWS S3 Error: ${error.message}`));
|
|
236
|
-
|
|
237
|
-
trackEvent('cli-error', { step: 's3_bucket_creation', error_code: error.name });
|
|
238
|
-
process.exit(1);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
202
|
s.message('Synthesizing Terraform templates...');
|
|
243
203
|
|
|
244
|
-
// 7.
|
|
245
|
-
await
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
let tfMain = await fs.readFile(tfMainPath, 'utf-8');
|
|
262
|
-
let tfNetwork = await fs.readFile(tfNetworkPath, 'utf-8');
|
|
263
|
-
let tfSecrets = await fs.readFile(tfSecretsPath, 'utf-8');
|
|
264
|
-
let tfOidc = await fs.readFile(tfOidcPath, 'utf-8');
|
|
265
|
-
let tfBackend = await fs.readFile(tfBackendPath, 'utf-8');
|
|
266
|
-
let tfCloudfront = await fs.readFile(tfCloudfrontPath, 'utf-8');
|
|
267
|
-
let dockerContent = await fs.readFile(dockerTemplatePath, 'utf-8');
|
|
268
|
-
let githubAction = await fs.readFile(githubActionPath, 'utf-8');
|
|
269
|
-
let readmeContent = await fs.readFile(readmePath, 'utf-8');
|
|
270
|
-
let gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
271
|
-
|
|
272
|
-
// 9. Update the variables
|
|
273
|
-
const injectVariables = (content) => {
|
|
274
|
-
return content
|
|
275
|
-
.replace(/{{PROJECT_NAME}}/g, actualProjectName)
|
|
276
|
-
.replace(/{{REGION}}/g, project.region)
|
|
277
|
-
.replace(/{{PORT}}/g, project.port)
|
|
278
|
-
.replace(/{{CPU}}/g, cpu)
|
|
279
|
-
.replace(/{{MEMORY}}/g, memory)
|
|
280
|
-
.replace(/{{COMPUTE_TIER}}/g, computeTier)
|
|
281
|
-
.replace(/{{ESTIMATED_COST}}/g, estimatedCost)
|
|
282
|
-
.replace(/{{STATE_BUCKET}}/g, stateBucketName)
|
|
283
|
-
.replace(/{{AWS_ACCOUNT_ID}}/g, awsAccountId)
|
|
284
|
-
.replace(/{{HEALTH_CHECK_PATH}}/g, healthCheckPath)
|
|
285
|
-
.replace(/{{DESIRED_COUNT}}/g, desiredCount)
|
|
286
|
-
.replace(/{{DEPLOY_BRANCH}}/g, deployBranch);
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
tfMain = injectVariables(tfMain);
|
|
290
|
-
tfNetwork = injectVariables(tfNetwork);
|
|
291
|
-
tfSecrets = injectVariables(tfSecrets);
|
|
292
|
-
tfOidc = injectVariables(tfOidc);
|
|
293
|
-
tfBackend = injectVariables(tfBackend);
|
|
294
|
-
tfCloudfront = injectVariables(tfCloudfront);
|
|
295
|
-
dockerContent = injectVariables(dockerContent);
|
|
296
|
-
githubAction = injectVariables(githubAction);
|
|
297
|
-
readmeContent = injectVariables(readmeContent);
|
|
298
|
-
|
|
299
|
-
// 10. Write the finalized files
|
|
300
|
-
await fs.writeFile(path.join(targetDir, 'terraform', 'main.tf'), tfMain);
|
|
301
|
-
await fs.writeFile(path.join(targetDir, 'terraform', 'network.tf'), tfNetwork);
|
|
302
|
-
await fs.writeFile(path.join(targetDir, 'terraform', 'secrets.tf'), tfSecrets);
|
|
303
|
-
await fs.writeFile(path.join(targetDir, 'terraform', 'oidc.tf'), tfOidc);
|
|
304
|
-
await fs.writeFile(path.join(targetDir, 'terraform', 'backend.tf'), tfBackend);
|
|
305
|
-
await fs.writeFile(path.join(targetDir, 'terraform', 'cloudfront.tf'), tfCloudfront);
|
|
306
|
-
await fs.writeFile(path.join(targetDir, 'Dockerfile'), dockerContent);
|
|
307
|
-
await fs.writeFile(path.join(targetDir, '.github', 'workflows', 'deploy.yml'), githubAction);
|
|
308
|
-
await fs.writeFile(path.join(targetDir, 'README.md'), readmeContent);
|
|
309
|
-
await fs.writeFile(path.join(targetDir, 'terraform', 'secret_keys.json'), "[]");
|
|
310
|
-
|
|
311
|
-
// 10.5. Ensure .gitignore exists and contains necessary Terraform ignores
|
|
312
|
-
const targetGitignore = path.join(targetDir, '.gitignore');
|
|
313
|
-
if (!fsSync.existsSync(targetGitignore)) {
|
|
314
|
-
// No gitignore exists? Give them the full template (Terraform + Node + Python)
|
|
315
|
-
await fs.writeFile(targetGitignore, gitignoreContent);
|
|
316
|
-
} else {
|
|
317
|
-
// File exists? Only inject the Terraform rules to prevent duplicates
|
|
318
|
-
const existingGitignore = await fs.readFile(targetGitignore, 'utf-8');
|
|
319
|
-
|
|
320
|
-
if (!existingGitignore.includes('terraform/.terraform/')) {
|
|
321
|
-
const terraformIgnores = `
|
|
322
|
-
# Added by deploy-stack (Terraform)
|
|
323
|
-
terraform/.terraform/
|
|
324
|
-
terraform/*.tfstate
|
|
325
|
-
terraform/*.tfstate.backup
|
|
326
|
-
terraform/.terraform.lock.hcl
|
|
327
|
-
terraform/secret_keys.json
|
|
328
|
-
terraform/.terraform.*
|
|
329
|
-
`;
|
|
330
|
-
await fs.appendFile(targetGitignore, '\n' + terraformIgnores);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
204
|
+
// 7. Generate all templates and directories
|
|
205
|
+
await generateTemplates(targetDir, {
|
|
206
|
+
PROJECT_NAME: actualProjectName,
|
|
207
|
+
REGION: project.region,
|
|
208
|
+
PORT: project.port,
|
|
209
|
+
CPU: cpu,
|
|
210
|
+
MEMORY: memory,
|
|
211
|
+
COMPUTE_TIER: computeTier,
|
|
212
|
+
ESTIMATED_COST: estimatedCost,
|
|
213
|
+
STATE_BUCKET: stateBucketName,
|
|
214
|
+
AWS_ACCOUNT_ID: awsAccountId,
|
|
215
|
+
HEALTH_CHECK_PATH: healthCheckPath,
|
|
216
|
+
DESIRED_COUNT: desiredCount,
|
|
217
|
+
DEPLOY_BRANCH: deployBranch,
|
|
218
|
+
finalFramework: finalFramework
|
|
219
|
+
});
|
|
333
220
|
|
|
334
|
-
//
|
|
221
|
+
// 8. Track the event in telemetry
|
|
335
222
|
trackEvent('project_provisioned', {
|
|
336
223
|
projectName: actualProjectName,
|
|
337
224
|
framework: finalFramework,
|
|
@@ -344,27 +231,25 @@ export async function mainStack() {
|
|
|
344
231
|
|
|
345
232
|
s.stop('Infrastructure provisioned successfully!');
|
|
346
233
|
|
|
347
|
-
//
|
|
348
|
-
|
|
234
|
+
// 9. Provide the Outro, Framework Warnings and Next Steps
|
|
235
|
+
const frameworkWarnings = getFrameworkWarning(finalFramework);
|
|
349
236
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
color.yellow('\n You must modify your next.config file and create a health check route before deploying.') +
|
|
354
|
-
color.yellow('\n See the "Critical Application Prerequisites" section in your README.md for copy-paste code.\n\n');
|
|
355
|
-
}
|
|
237
|
+
const cdStep = projectName === '.' ? '' : `1. cd ${projectName}\n `;
|
|
238
|
+
const deployStepNum = projectName === '.' ? '1' : '2';
|
|
239
|
+
const gitStepNum = projectName === '.' ? '2' : '3';
|
|
356
240
|
|
|
357
241
|
outro(`
|
|
358
242
|
${color.green('β
Project provisioned successfully!')}
|
|
243
|
+
|
|
244
|
+
${frameworkWarnings}
|
|
359
245
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
${color.cyan('Once deployed, Terraform will output your new https://*.cloudfront.net URL.')}
|
|
246
|
+
Next steps:
|
|
247
|
+
${cdStep}${deployStepNum}. Deploy infrastructure:
|
|
248
|
+
cd terraform && terraform init && terraform apply
|
|
249
|
+
${gitStepNum}. Push to GitHub:
|
|
250
|
+
git init && git add . && git commit -m "Initial commit"
|
|
251
|
+
|
|
252
|
+
${color.cyan('Once deployed, Terraform will output your new https://*.cloudfront.net URL.')}
|
|
368
253
|
|
|
369
254
|
${color.magenta('π Infrastructure ready! Need help or have feedback? Grab 15 mins with Anton:')}
|
|
370
255
|
${color.underline('https://calendly.com/anton-codes-iac/15min')}
|
package/src/utils/aws.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
|
2
|
+
import { S3Client, CreateBucketCommand, PutBucketVersioningCommand } from '@aws-sdk/client-s3';
|
|
3
|
+
|
|
4
|
+
export async function provisionStateBucket(region, projectName) {
|
|
5
|
+
const stsClient = new STSClient({ region });
|
|
6
|
+
let awsAccountId;
|
|
7
|
+
|
|
8
|
+
// 1. Get AWS Account ID
|
|
9
|
+
const { Account } = await stsClient.send(new GetCallerIdentityCommand({}));
|
|
10
|
+
awsAccountId = Account;
|
|
11
|
+
|
|
12
|
+
// 2. Format Bucket Name
|
|
13
|
+
let stateBucketName = `${projectName}-tfstate-${awsAccountId}`.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
14
|
+
if (stateBucketName.length > 63) {
|
|
15
|
+
stateBucketName = stateBucketName.substring(0, 63).replace(/-$/, '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 3. Create S3 Bucket & Enable Versioning
|
|
19
|
+
const s3Client = new S3Client({ region });
|
|
20
|
+
try {
|
|
21
|
+
await s3Client.send(new CreateBucketCommand({
|
|
22
|
+
Bucket: stateBucketName,
|
|
23
|
+
CreateBucketConfiguration: region === 'us-east-1' ? undefined : { LocationConstraint: region }
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
await s3Client.send(new PutBucketVersioningCommand({
|
|
27
|
+
Bucket: stateBucketName,
|
|
28
|
+
VersioningConfiguration: { Status: 'Enabled' }
|
|
29
|
+
}));
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// Ignore if the bucket already exists and is owned by the user
|
|
32
|
+
if (error.name !== 'BucketAlreadyOwnedByYou') {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { awsAccountId, stateBucketName };
|
|
38
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export async function generateTemplates(targetDir, config) {
|
|
10
|
+
// 1. Create directories
|
|
11
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
12
|
+
await fs.mkdir(path.join(targetDir, 'terraform'), { recursive: true });
|
|
13
|
+
await fs.mkdir(path.join(targetDir, '.github', 'workflows'), { recursive: true });
|
|
14
|
+
|
|
15
|
+
// 2. Define paths
|
|
16
|
+
const templatesDir = path.join(__dirname, '../../templates');
|
|
17
|
+
const filesToProcess = [
|
|
18
|
+
{ src: 'terraform/main.tf', dest: 'terraform/main.tf' },
|
|
19
|
+
{ src: 'terraform/network.tf', dest: 'terraform/network.tf' },
|
|
20
|
+
{ src: 'terraform/secrets.tf', dest: 'terraform/secrets.tf' },
|
|
21
|
+
{ src: 'terraform/oidc.tf', dest: 'terraform/oidc.tf' },
|
|
22
|
+
{ src: 'terraform/backend.tf', dest: 'terraform/backend.tf' },
|
|
23
|
+
{ src: 'terraform/cloudfront.tf', dest: 'terraform/cloudfront.tf' },
|
|
24
|
+
{ src: `docker/${config.finalFramework}.Dockerfile`, dest: 'Dockerfile' },
|
|
25
|
+
{ src: 'github/deploy.yml', dest: '.github/workflows/deploy.yml' },
|
|
26
|
+
{ src: 'README.md', dest: 'README.md' }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// 3. Process standard files
|
|
30
|
+
for (const file of filesToProcess) {
|
|
31
|
+
let content = await fs.readFile(path.join(templatesDir, file.src), 'utf-8');
|
|
32
|
+
|
|
33
|
+
// Inject variables
|
|
34
|
+
for (const [key, value] of Object.entries(config)) {
|
|
35
|
+
content = content.replace(new RegExp(`{{${key}}}`, 'g'), value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await fs.writeFile(path.join(targetDir, file.dest), content);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 4. Create empty secrets file
|
|
42
|
+
await fs.writeFile(path.join(targetDir, 'terraform', 'secret_keys.json'), "[]");
|
|
43
|
+
|
|
44
|
+
// 5. Handle .gitignore appending cleanly
|
|
45
|
+
const targetGitignore = path.join(targetDir, '.gitignore');
|
|
46
|
+
const gitignoreContent = await fs.readFile(path.join(templatesDir, '_gitignore'), 'utf-8');
|
|
47
|
+
|
|
48
|
+
if (!fsSync.existsSync(targetGitignore)) {
|
|
49
|
+
await fs.writeFile(targetGitignore, gitignoreContent);
|
|
50
|
+
} else {
|
|
51
|
+
const existingGitignore = await fs.readFile(targetGitignore, 'utf-8');
|
|
52
|
+
if (!existingGitignore.includes('terraform/.terraform/')) {
|
|
53
|
+
await fs.appendFile(targetGitignore, '\n# Added by deploy-stack (Terraform)\nterraform/.terraform/\nterraform/*.tfstate\nterraform/*.tfstate.backup\nterraform/.terraform.lock.hcl\nterraform/secret_keys.json\nterraform/.terraform.*\n');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import color from 'picocolors';
|
|
2
|
+
|
|
3
|
+
export function getFrameworkWarning(frameworkId) {
|
|
4
|
+
switch (frameworkId) {
|
|
5
|
+
case 'nextjs':
|
|
6
|
+
return (
|
|
7
|
+
color.bgYellow(color.black(' β οΈ IMPORTANT: NEXT.JS SETUP REQUIRED ')) +
|
|
8
|
+
color.yellow('\n You must modify your next.config file and create a health check route before deploying.') +
|
|
9
|
+
color.yellow('\n See the "Critical Application Prerequisites" section in your README.md for copy-paste code.\n\n')
|
|
10
|
+
);
|
|
11
|
+
case 'node':
|
|
12
|
+
return (
|
|
13
|
+
color.bgYellow(color.black(' β οΈ IMPORTANT: NODE.JS SETUP REQUIRED ')) +
|
|
14
|
+
color.yellow('\n 1. Ensure your package.json has a "start" script (e.g., "start": "node index.js").') +
|
|
15
|
+
color.yellow('\n 2. Your app must listen on 0.0.0.0 (not localhost) to receive traffic in Docker.\n\n')
|
|
16
|
+
);
|
|
17
|
+
// Python / FastAPI warnings can be added here easily!
|
|
18
|
+
default:
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
}
|
package/templates/README.md
CHANGED
|
@@ -82,4 +82,12 @@ const nextConfig: NextConfig = {
|
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
export default nextConfig;
|
|
85
|
-
```
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. Container Network Binding (Node & Python)
|
|
88
|
+
|
|
89
|
+
When running inside a Docker container, your server must bind to all network interfaces (`0.0.0.0`), not just `localhost` or `127.0.0.1`. If you bind to localhost, the AWS Load Balancer will not be able to route traffic to your application.
|
|
90
|
+
|
|
91
|
+
Make sure your app is configured correctly:
|
|
92
|
+
* **Express.js:** `app.listen(port, '0.0.0.0', () => ...)`
|
|
93
|
+
* **FastAPI:** `uvicorn.run(app, host="0.0.0.0", port=8000)`
|
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
FROM node:
|
|
1
|
+
FROM node:20-alpine
|
|
2
|
+
|
|
3
|
+
# 1. Set production environment (optimizes Node and prevents dev dependencies)
|
|
4
|
+
ENV NODE_ENV=production
|
|
5
|
+
|
|
2
6
|
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
# 2. Copy only dependency files first to cache the npm install layer
|
|
3
9
|
COPY package*.json ./
|
|
4
|
-
|
|
10
|
+
|
|
11
|
+
# 3. Use npm ci for strict, deterministic, and faster CI/CD installs
|
|
12
|
+
RUN npm ci
|
|
13
|
+
|
|
14
|
+
# 4. Copy the rest of the application source code
|
|
5
15
|
COPY . .
|
|
16
|
+
|
|
17
|
+
# 5. DevSecOps best practice: do not run the container as root
|
|
18
|
+
USER node
|
|
19
|
+
|
|
6
20
|
EXPOSE {{PORT}}
|
|
21
|
+
|
|
7
22
|
CMD ["npm", "start"]
|