deploy-stack 0.12.2 → 0.13.0

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 CHANGED
@@ -30,6 +30,7 @@ You retain complete ownership of your infrastructure code without relying on bla
30
30
  **🚀 Zero-Config Deployments**
31
31
  * **Framework Agnostic:** Tailored container presets for Next.js, Express.js, FastAPI, Go, Django, Rails, Nuxt 3, and Static Sites (React, Vue, SvelteKit, Astro).
32
32
  * **Smart Discovery:** Automatically detects build output directories and generates highly optimized, multi-stage Dockerfiles.
33
+ * **PaaS Migration Engine:** Natively parses Heroku `Procfile` configurations to automatically translate web and background worker processes (like Celery or Sidekiq) into multi-container AWS Fargate architectures.
33
34
  * **Database Scaffolding:** Automatically provisions fully isolated, zero-trust AWS RDS PostgreSQL databases for backend monoliths.
34
35
 
35
36
  **🛡️ DevSecOps & Security**
@@ -83,7 +84,7 @@ The interactive wizard will analyze your codebase, detect your framework, estima
83
84
  Strips all `deploy-stack` metadata and management tags from your project, leaving behind pure, standard Terraform and GitHub Actions files. You retain 100% ownership.
84
85
 
85
86
  * **`npx deploy-stack --headless`**
86
- Bypasses the interactive wizard for fully programmatic execution. Perfect for CI/CD pipelines, custom scripts, or AI agent integration. Accepts flags like `--framework=static`, `--region=us-east-1`, and `--size=micro`.
87
+ Bypasses the interactive wizard for fully programmatic execution. Perfect for CI/CD pipelines, custom scripts, or AI agent integration. Accepts flags like `--framework=static`, `--region=us-east-2`, and `--size=micro`.
87
88
 
88
89
  ---
89
90
 
@@ -123,6 +124,7 @@ your-project/
123
124
  * **[Ruby on Rails](https://github.com/anton-codes-iac/deploy-stack-rails-example):** A production Rails 7+ setup featuring an auto-provisioned PostgreSQL database and secure `.auto.tfvars` Master Key injection.
124
125
  * **[Nuxt 3 (SSR)](https://github.com/anton-codes-iac/deploy-stack-nuxt-example):** Demonstrates a fully server-side rendered Nuxt application using Nitro's optimized Node output.
125
126
  * **[Django / Python](https://github.com/anton-codes-iac/deploy-stack-django-example):** A secure Gunicorn/WSGI implementation with PostgreSQL and unprivileged container adapters.
127
+ * **[Heroku to AWS Migration (Django)](https://github.com/anton-codes-iac/deploy-stack-heroku-django-example):** A classic Heroku-style monolith migrated via the Procfile Importer, demonstrating a multi-container Web and Celery Worker architecture deployed from a single codebase.
126
128
  * **[Go / Fiber](https://github.com/anton-codes-iac/deploy-stack-go-example):** A distroless, compiled Go binary deployment demonstrating ultra-low memory footprints and instant boot times.
127
129
  ---
128
130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploy-stack",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -111,7 +111,7 @@ export async function applyStack(options = {}) {
111
111
  // 6. Run terraform apply
112
112
  s.start('Provisioning AWS infrastructure (this may take 3–5 minutes)...');
113
113
  await runTerraformCommand(['apply', '-auto-approve'], tfDir, s, 'Provisioning');
114
- s.stop('AWS infrastructure provisioned successfully!');
114
+ s.stop('Cloud resources provisioned.');
115
115
 
116
116
  // 7. Extract and print the outputs
117
117
  const outputs = await getTerraformOutputs(tfDir);
@@ -77,7 +77,7 @@ export async function destroyStack() {
77
77
  const regionMatch = backendContent.match(/region\s*=\s*"([^"]+)"/);
78
78
 
79
79
  const bucketName = bucketMatch ? bucketMatch[1] : null;
80
- const region = regionMatch ? regionMatch[1] : 'us-east-1';
80
+ const region = regionMatch ? regionMatch[1] : 'us-east-2';
81
81
 
82
82
  // 2. Execute Terraform Destroy
83
83
  s.start('Destroying AWS compute resources (this takes a few minutes)...');
@@ -6,7 +6,7 @@ import color from 'picocolors';
6
6
  import { execSync } from 'child_process';
7
7
 
8
8
  import { checkDependency } from '../utils/system.js';
9
- import { detectFramework } from '../utils/detector.js';
9
+ import { detectFramework, parseProcfile } from '../utils/detector.js';
10
10
  import { trackEvent, flushTelemetry } from '../core/telemetry.js';
11
11
  import { getFrameworkWarning } from '../utils/warnings.js';
12
12
  import { provisionStateBucket } from '../utils/aws.js';
@@ -35,6 +35,7 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
35
35
  let disableDefaultCI = false;
36
36
  let project = {};
37
37
  let currentGitBranch = 'main';
38
+ let procfile = null;
38
39
 
39
40
  if (isHeadless) {
40
41
  // --- HEADLESS MODE ---
@@ -43,10 +44,11 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
43
44
  targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
44
45
 
45
46
  detectedFramework = detectFramework(targetDir);
47
+ procfile = parseProcfile(targetDir);
46
48
  finalFramework = getFlag('framework', detectedFramework ? detectedFramework.id : 'static');
47
49
 
48
50
  project = {
49
- region: getFlag('region', 'us-east-1'),
51
+ region: getFlag('region', 'us-east-2'),
50
52
  port: getFlag('port', finalFramework === 'static' ? '8080' : '3000'),
51
53
  size: getFlag('size', 'micro'),
52
54
  healthCheckPath: getFlag('healthCheckPath', '/'),
@@ -85,7 +87,13 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
85
87
  log.success(`Auto-detected framework: ${detectedFramework.name}`);
86
88
  }
87
89
 
88
- // 2.6 Resolve the framework
90
+ // 2.6 Run the Procfile Parser
91
+ procfile = parseProcfile(targetDir);
92
+ if (procfile && procfile.web) {
93
+ log.success(`Auto-detected Procfile (web command: ${procfile.web.join(' ')})`);
94
+ }
95
+
96
+ // 2.7 Resolve the framework
89
97
  finalFramework = detectedFramework ? detectedFramework.id : null;
90
98
 
91
99
  if (!finalFramework) {
@@ -109,15 +117,32 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
109
117
  }
110
118
  }
111
119
 
112
- // --- DJANGO SPECIFIC PROMPT ---
120
+ // 2.8 Check if framework is Django and resolve wsgi.py path
113
121
  djangoWsgi = 'core.wsgi';
114
122
  if (finalFramework === 'django') {
115
- djangoWsgi = await text({
116
- message: 'What is the Python module path to your Django wsgi.py?',
117
- placeholder: 'core.wsgi',
118
- initialValue: 'core.wsgi',
119
- });
120
- if (typeof djangoWsgi === 'symbol') process.exit(0);
123
+ let extractedWsgi = null;
124
+
125
+ // Check if Procfile already specifies the WSGI module
126
+ if (procfile && procfile.web) {
127
+ const webCommand = procfile.web.join(' ');
128
+ // Matches patterns like "gunicorn my_app.wsgi" or "my_app.wsgi:application"
129
+ const wsgiMatch = webCommand.match(/([a-zA-Z0-9_]+)\.wsgi/);
130
+ if (wsgiMatch) {
131
+ extractedWsgi = `${wsgiMatch[1]}.wsgi`;
132
+ }
133
+ }
134
+
135
+ if (extractedWsgi) {
136
+ djangoWsgi = extractedWsgi;
137
+ log.success(`Auto-detected Django WSGI from Procfile: ${color.cyan(djangoWsgi)}`);
138
+ } else {
139
+ djangoWsgi = await text({
140
+ message: 'What is the Python module path to your Django wsgi.py?',
141
+ placeholder: 'core.wsgi',
142
+ initialValue: 'core.wsgi',
143
+ });
144
+ if (typeof djangoWsgi === 'symbol') process.exit(0);
145
+ }
121
146
  }
122
147
 
123
148
  // 3. Prompt for Setup Mode
@@ -309,7 +334,8 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
309
334
  finalFramework: finalFramework,
310
335
  NEEDS_DATABASE: needsDatabase,
311
336
  DJANGO_WSGI: djangoWsgi,
312
- DISABLE_DEFAULT_CI: disableDefaultCI
337
+ DISABLE_DEFAULT_CI: disableDefaultCI,
338
+ PROCFILE: procfile
313
339
  });
314
340
 
315
341
  // 11. Track the event in telemetry
@@ -336,8 +362,8 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
336
362
 
337
363
  const needsCd = projectName && projectName !== '.';
338
364
  const applyStep = needsCd
339
- ? `cd ${projectName} && npx deploy-stack apply`
340
- : 'npx deploy-stack apply';
365
+ ? `cd ${projectName} && npx --yes deploy-stack apply`
366
+ : 'npx --yes deploy-stack apply';
341
367
 
342
368
  const gitInstructions = isGitInitialized
343
369
  ? `git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git push`
package/src/utils/aws.js CHANGED
@@ -21,7 +21,7 @@ export async function provisionStateBucket(region, projectName) {
21
21
  try {
22
22
  await s3Client.send(new CreateBucketCommand({
23
23
  Bucket: stateBucketName,
24
- CreateBucketConfiguration: region === 'us-east-1' ? undefined : { LocationConstraint: region }
24
+ CreateBucketConfiguration: region === 'us-east-2' ? undefined : { LocationConstraint: region }
25
25
  }));
26
26
 
27
27
  await s3Client.send(new PutBucketTaggingCommand({
@@ -1,6 +1,7 @@
1
1
  import fsSync from 'fs';
2
2
  import path from 'path';
3
3
 
4
+ // Detects the framework based on the presence of framework-specific files.
4
5
  export function detectFramework(targetDir) {
5
6
  const packageJsonPath = path.join(targetDir, 'package.json');
6
7
  const requirementsTxtPath = path.join(targetDir, 'requirements.txt');
@@ -62,4 +63,35 @@ export function detectFramework(targetDir) {
62
63
 
63
64
  // 5. Fallback
64
65
  return null;
66
+ }
67
+
68
+ // Parses a Heroku/Render Procfile and formats the commands for Terraform ECS.
69
+ export function parseProcfile(targetDir) {
70
+ const procfilePath = path.join(targetDir, 'Procfile');
71
+
72
+ if (!fsSync.existsSync(procfilePath)) return null;
73
+
74
+ const content = fsSync.readFileSync(procfilePath, 'utf-8');
75
+ const processes = {};
76
+
77
+ // Match lines like "web: gunicorn myapp.wsgi"
78
+ const lines = content.split('\n');
79
+ const procRegex = /^([A-Za-z0-9_-]+):\s*(.+)$/;
80
+
81
+ for (const line of lines) {
82
+ const match = line.trim().match(procRegex);
83
+ if (match) {
84
+ const type = match[1].toLowerCase();
85
+ const rawCommand = match[2].trim();
86
+
87
+ // Terraform requires the command as a JSON array of strings
88
+ // This splits by spaces but respects single and double quotes
89
+ const commandArray = rawCommand.match(/[^\s"']+|"([^"]*)"|'([^']*)'/g)
90
+ .map(str => str.replace(/^["']|["']$/g, '')); // Strip the quotes
91
+
92
+ processes[type] = commandArray;
93
+ }
94
+ }
95
+
96
+ return Object.keys(processes).length > 0 ? processes : null;
65
97
  }
@@ -26,8 +26,28 @@ export async function generateTemplates(targetDir, config) {
26
26
  { src: 'README.md', dest: 'README.md' }
27
27
  ];
28
28
 
29
+ // 3. Configure Compute Commands & Environment Variables
29
30
  let secretsArray = [];
30
31
 
32
+ // 3.1. Format the ECS Task Command (Heroku Procfile Support)
33
+ // If a Procfile exists and has a 'web' process, override the Docker CMD
34
+ if (config.PROCFILE && config.PROCFILE.web) {
35
+ // e.g., ["gunicorn", "myproject.wsgi"] -> JSON string for Terraform
36
+ config.TASK_COMMAND = `command = ${JSON.stringify(config.PROCFILE.web)}`;
37
+ } else {
38
+ // Fallback to the default Dockerfile CMD
39
+ config.TASK_COMMAND = '';
40
+ }
41
+
42
+ if (config.PROCFILE && config.PROCFILE.worker) {
43
+ config.WORKER_COMMAND = `command = ${JSON.stringify(config.PROCFILE.worker)}`;
44
+ // Dynamically add worker.tf to the generation list
45
+ filesToProcess.push({ src: 'terraform/worker.tf', dest: 'terraform/worker.tf' });
46
+ } else {
47
+ config.WORKER_COMMAND = '';
48
+ }
49
+
50
+ // 3.2. Inject Managed Database Variables
31
51
  if (config.NEEDS_DATABASE) {
32
52
  filesToProcess.push({ src: 'terraform/database.tf', dest: 'terraform/database.tf' });
33
53
 
@@ -42,10 +62,11 @@ export async function generateTemplates(targetDir, config) {
42
62
  config.DB_ENV_VARS = '';
43
63
  }
44
64
 
65
+ // 4. Configure AWS Secrets Manager Integration
45
66
  // Build the initial HCL map for AWS Secrets Manager
46
67
  let initialSecretMap = `{\n EXAMPLE_API_KEY = "replace_me_in_aws_console"`;
47
68
 
48
- // Inject Rails Master Key if applicable
69
+ // 4.1. Inject Rails Master Key if applicable
49
70
  if (config.finalFramework === 'rails') {
50
71
  secretsArray.push(`{ "name": "RAILS_MASTER_KEY", "valueFrom": "\${aws_secretsmanager_secret.app_secrets.arn}:RAILS_MASTER_KEY::" }`);
51
72
 
@@ -64,7 +85,11 @@ export async function generateTemplates(targetDir, config) {
64
85
  config.TASK_SECRETS = secretsArray.join(',\n ');
65
86
  config.INITIAL_SECRET_MAP = initialSecretMap;
66
87
 
67
- // 3. Process standard files
88
+ config.SAFE_ALB_NAME = config.PROJECT_NAME.length > 27
89
+ ? config.PROJECT_NAME.substring(0, 27).replace(/-$/, '') // Remove trailing hyphens
90
+ : config.PROJECT_NAME;
91
+
92
+ // 5. Process standard files
68
93
  for (const file of filesToProcess) {
69
94
  let content = await fs.readFile(path.join(templatesDir, file.src), 'utf-8');
70
95
 
@@ -76,10 +101,10 @@ export async function generateTemplates(targetDir, config) {
76
101
  await fs.writeFile(path.join(targetDir, file.dest), content);
77
102
  }
78
103
 
79
- // 4. Create empty secrets file
104
+ // 6. Create empty secrets file
80
105
  await fs.writeFile(path.join(targetDir, 'terraform', 'secret_keys.json'), "[]");
81
106
 
82
- // 5. Handle .gitignore dynamically based on framework
107
+ // 7. Handle .gitignore dynamically based on framework
83
108
  const targetGitignore = path.join(targetDir, '.gitignore');
84
109
 
85
110
  if (!fsSync.existsSync(targetGitignore)) {
@@ -92,7 +117,7 @@ export async function generateTemplates(targetDir, config) {
92
117
  }
93
118
  }
94
119
 
95
- // 6. Create .dockerignore to keep images lean and secure
120
+ // 8. Create .dockerignore to keep images lean and secure
96
121
  const dockerignorePath = path.join(targetDir, '.dockerignore');
97
122
  const appendDockerIgnore = '\n# Infrastructure (deploy-stack)\nterraform/\n**/.terraform/\n**/.terraform.*\n**/*.tfstate*\n.env\n';
98
123
 
@@ -146,7 +171,7 @@ Thumbs.db
146
171
  return (baseIgnore + frameworkIgnore).trim();
147
172
  }
148
173
 
149
- // 7. Framework-specific cleanup
174
+ // 9. Framework-specific cleanup
150
175
  // Disable default workflows that crash in isolated CI environments
151
176
  if (config.finalFramework === 'rails') {
152
177
  const railsCiPath = path.join(targetDir, '.github', 'workflows', 'ci.yml');
@@ -3,7 +3,7 @@ import pc from 'picocolors';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
5
 
6
- // Cost benchmarks for AWS us-east-1 baseline (Fargate + ALB)
6
+ // Cost benchmarks for AWS us-east-2 baseline (Fargate + ALB)
7
7
  const PRICING_TABLE = {
8
8
  fargate: {
9
9
  cpuPerHour: 0.04048, // per vCPU hour
@@ -22,7 +22,7 @@ const PRICING_TABLE = {
22
22
  // 1. Parse the local terraform files to extract the actual configuration
23
23
  export function parseTerraformConfig(tfDir) {
24
24
  const tfvarsPath = path.join(tfDir, 'terraform.tfvars');
25
- let region = 'us-east-1';
25
+ let region = 'us-east-2';
26
26
  let cpu = 256;
27
27
  let memory = 512;
28
28
  let framework = 'Application';
@@ -43,18 +43,22 @@ export function parseTerraformConfig(tfDir) {
43
43
 
44
44
  // Check if database files exist
45
45
  const hasDb = fs.existsSync(path.join(tfDir, 'rds.tf')) || fs.existsSync(path.join(tfDir, 'database.tf'));
46
+ const hasWorker = fs.existsSync(path.join(tfDir, 'worker.tf'));
46
47
 
47
- return { framework, region, cpu, memory, hasDb };
48
+ return { framework, region, cpu, memory, hasDb, hasWorker };
48
49
  }
49
50
 
50
51
  // 2. Calculate itemized monthly costs based on task definition settings
51
- export function estimateMonthlyCost({ cpu = 256, memory = 512, hasDb = false }) {
52
+ export function estimateMonthlyCost({ cpu = 256, memory = 512, hasDb = false, hasWorker = false }) {
52
53
  const vCpu = cpu / 1024;
53
54
  const memGb = memory / 1024;
54
55
  const hoursInMonth = 730;
55
56
 
57
+ // If a worker service exists, we are running a second identical Fargate task
58
+ const taskMultiplier = hasWorker ? 2 : 1;
59
+
56
60
  const fargateCost = ((vCpu * PRICING_TABLE.fargate.cpuPerHour) +
57
- (memGb * PRICING_TABLE.fargate.memoryPerHour)) * hoursInMonth;
61
+ (memGb * PRICING_TABLE.fargate.memoryPerHour)) * hoursInMonth * taskMultiplier;
58
62
  const albCost = (PRICING_TABLE.alb.basePerHour + PRICING_TABLE.alb.lcuPerHour) * hoursInMonth;
59
63
  const dbCost = hasDb ? (PRICING_TABLE.rds.microPerHour * hoursInMonth) + PRICING_TABLE.rds.storagePerMonth : 0;
60
64
 
@@ -70,17 +74,21 @@ export function estimateMonthlyCost({ cpu = 256, memory = 512, hasDb = false })
70
74
 
71
75
  // 3. Render the terminal architecture visualization and requests confirmation
72
76
  export async function renderDryRunPreview(config, isDryRunFlag = false) {
73
- const { framework = 'Node.js', region = 'us-east-1', cpu = 256, memory = 512, hasDb = false } = config;
74
- const cost = estimateMonthlyCost({ cpu, memory, hasDb });
77
+ const { framework = 'Node.js', region = 'us-east-2', cpu = 256, memory = 512, hasDb = false, hasWorker = false } = config;
78
+
79
+ // Fixed the duplicate hasWorker argument
80
+ const cost = estimateMonthlyCost({ cpu, memory, hasDb, hasWorker });
75
81
 
76
82
  const hourlyRate = (cost.totalMonthly / 730).toFixed(3); // 730 hours in a month
77
83
 
84
+ // Flattened the tree to eliminate nesting and vertical bloat
78
85
  const treeOutput = [
79
86
  `${pc.bold('Topology')} (${pc.cyan(region)}):`,
80
87
  ` ${pc.gray('├──')} 🌐 ${pc.bold('ALB')} (Public Entry & Health: ${pc.green('200 OK')})`,
81
88
  ` ${pc.gray('├──')} 🔒 ${pc.bold('IAM OIDC')} (GitHub Auth) & 🐳 ${pc.bold('ECR')} (Registry)`,
82
- ` ${pc.gray('└──')} 📦 ${pc.bold('ECS Fargate Cluster')} 🟢 ${pc.green(framework)} [${cpu} CPU / ${memory} MB]`,
83
- hasDb ? ` └── 🛢️ ${pc.yellow('Amazon RDS')} (PostgreSQL managed instance)` : '',
89
+ hasDb ? ` ${pc.gray('├──')} 🐘 ${pc.yellow('Amazon RDS')} (PostgreSQL managed instance)` : '',
90
+ ` ${pc.gray(hasWorker ? '├──' : '└──')} 📦 ${pc.bold('ECS Web Service')} 🟢 ${pc.green(framework)} [${cpu} CPU / ${memory} MB]`,
91
+ hasWorker ? ` ${pc.gray('└──')} 📦 ${pc.bold('ECS Worker Service')} 🔄 Background Tasks [${cpu} CPU / ${memory} MB]` : '',
84
92
  '',
85
93
  `${pc.bold('Est. Monthly Cost:')} ${pc.green(pc.bold(`~$${cost.totalMonthly}`))} ${pc.dim(`(ALB: $${cost.albMonthly}, Fargate: $${cost.fargateMonthly}${hasDb ? `, RDS: $${cost.dbMonthly}` : ''})`)}`,
86
94
  ` ${pc.dim(`* Hourly billing: ~$${hourlyRate}/hr. Destroy anytime with "npx deploy-stack destroy --yes"`)}`
@@ -1,5 +1,8 @@
1
1
  FROM python:3.12-alpine
2
2
 
3
+ # --- DevSecOps Patch: Upgrade Alpine system packages to clear OS-level CVEs ---
4
+ RUN apk upgrade --no-cache
5
+
3
6
  # Prevent Python from writing .pyc files and buffer stdout for cleaner logs
4
7
  ENV PYTHONDONTWRITEBYTECODE=1
5
8
  ENV PYTHONUNBUFFERED=1
@@ -2,6 +2,10 @@
2
2
  FROM node:22-alpine AS builder
3
3
  WORKDIR /app
4
4
 
5
+ # FIX 1: Upgrade Alpine system packages & upgrade global NPM to patch 'tar' and 'pacote' CVEs
6
+ RUN apk upgrade --no-cache && \
7
+ npm install -g npm@latest
8
+
5
9
  # Copy package files and install dependencies
6
10
  COPY package.json package-lock.json* ./
7
11
  RUN npm ci
@@ -17,6 +21,18 @@ RUN npm run build
17
21
  FROM node:22-alpine AS runner
18
22
  WORKDIR /app
19
23
 
24
+ # FIX 2: Upgrade Alpine packages and absolutely destroy npm, yarn, and corepack
25
+ # to eliminate all remaining Critical/High container vulnerabilities.
26
+ RUN apk upgrade --no-cache && \
27
+ rm -rf /usr/local/lib/node_modules/npm \
28
+ /usr/local/bin/npm \
29
+ /usr/local/bin/npx \
30
+ /opt/yarn-* \
31
+ /usr/local/bin/yarn \
32
+ /usr/local/bin/yarnpkg \
33
+ /usr/local/lib/node_modules/corepack \
34
+ /usr/local/bin/corepack
35
+
20
36
  ENV NODE_ENV=production
21
37
  ENV PORT={{PORT}}
22
38
  ENV HOSTNAME="0.0.0.0"
@@ -99,6 +99,8 @@ resource "aws_ecs_task_definition" "app" {
99
99
  ]
100
100
  )
101
101
 
102
+ {{TASK_COMMAND}}
103
+
102
104
  portMappings = [
103
105
  {
104
106
  containerPort = {{PORT}}
@@ -122,7 +124,7 @@ resource "aws_ecs_task_definition" "app" {
122
124
  # --- Application Load Balancer ---
123
125
  # trivy:ignore:AVD-AWS-0053 - This ALB is intended to be publicly facing behind CloudFront
124
126
  resource "aws_lb" "main" {
125
- name = "{{PROJECT_NAME}}-alb"
127
+ name = "{{SAFE_ALB_NAME}}-alb"
126
128
  load_balancer_type = "application"
127
129
  security_groups = [aws_security_group.alb.id]
128
130
  subnets = aws_subnet.public[*].id
@@ -130,7 +132,7 @@ resource "aws_lb" "main" {
130
132
  }
131
133
 
132
134
  resource "aws_lb_target_group" "app" {
133
- name = "{{PROJECT_NAME}}-tg"
135
+ name = "{{SAFE_ALB_NAME}}-tg"
134
136
  port = {{PORT}}
135
137
  protocol = "HTTP"
136
138
  vpc_id = aws_vpc.main.id
@@ -0,0 +1,62 @@
1
+ # --- Worker ECS Task Definition ---
2
+ resource "aws_ecs_task_definition" "worker" {
3
+ family = "{{PROJECT_NAME}}-worker-task"
4
+ network_mode = "awsvpc"
5
+ requires_compatibilities = ["FARGATE"]
6
+ cpu = "{{CPU}}"
7
+ memory = "{{MEMORY}}"
8
+ execution_role_arn = aws_iam_role.execution_role.arn
9
+ task_role_arn = aws_iam_role.task_role.arn
10
+
11
+ container_definitions = jsonencode([
12
+ {
13
+ name = "{{PROJECT_NAME}}-worker-container"
14
+ image = "${aws_ecr_repository.app.repository_url}:latest"
15
+ essential = true
16
+
17
+ environment = [
18
+ { "name": "NODE_ENV", "value": "production" },
19
+ {{DB_ENV_VARS}}
20
+ ]
21
+
22
+ secrets = concat(
23
+ [
24
+ for key in local.secret_keys : {
25
+ name = key
26
+ valueFrom = "${aws_secretsmanager_secret.app_secrets.arn}:${key}::"
27
+ }
28
+ ],
29
+ [
30
+ {{TASK_SECRETS}}
31
+ ]
32
+ )
33
+
34
+ {{WORKER_COMMAND}}
35
+
36
+ logConfiguration = {
37
+ logDriver = "awslogs"
38
+ options = {
39
+ "awslogs-group" = aws_cloudwatch_log_group.app_logs.name
40
+ "awslogs-region" = "{{REGION}}"
41
+ "awslogs-stream-prefix" = "worker" # Isolates worker logs from web logs
42
+ }
43
+ }
44
+ }
45
+ ])
46
+ }
47
+
48
+ # --- Worker ECS Service ---
49
+ # Notice there is NO load_balancer block. This service is strictly private.
50
+ resource "aws_ecs_service" "worker" {
51
+ name = "{{PROJECT_NAME}}-worker-service"
52
+ cluster = aws_ecs_cluster.main.id
53
+ task_definition = aws_ecs_task_definition.worker.arn
54
+ launch_type = "FARGATE"
55
+ desired_count = 1
56
+
57
+ network_configuration {
58
+ subnets = aws_subnet.public[*].id
59
+ security_groups = [aws_security_group.ecs_tasks.id]
60
+ assign_public_ip = true
61
+ }
62
+ }