grada-run 0.0.1 → 0.32.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.
Files changed (231) hide show
  1. package/.github/workflows/deploy-docs.yml +37 -0
  2. package/.github/workflows/iac-validation.yml +303 -0
  3. package/.github/workflows/publish.yml +68 -0
  4. package/.github/workflows/sync-bedrock-models.yml +57 -0
  5. package/.github/workflows/test.yml +43 -0
  6. package/.muserules +31 -0
  7. package/LICENSE +21 -0
  8. package/README.md +193 -3
  9. package/apps/docs/.astro/collections/docs.schema.json +644 -0
  10. package/apps/docs/.astro/content-assets.mjs +4 -0
  11. package/apps/docs/.astro/content-modules.mjs +4 -0
  12. package/apps/docs/.astro/content.d.ts +179 -0
  13. package/apps/docs/.astro/data-store.json +1 -0
  14. package/apps/docs/.astro/dev.json +14 -0
  15. package/apps/docs/.astro/settings.json +5 -0
  16. package/apps/docs/.astro/types.d.ts +2 -0
  17. package/apps/docs/astro.config.mjs +97 -0
  18. package/apps/docs/package.json +17 -0
  19. package/apps/docs/src/content/docs/adrs/0001-s3-native-state-locking.md +37 -0
  20. package/apps/docs/src/content/docs/adrs/0002-eject-mechanism-pure-iac.md +39 -0
  21. package/apps/docs/src/content/docs/adrs/0003-sync-ai-context-strategy.md +48 -0
  22. package/apps/docs/src/content/docs/adrs/0004-iac-driven-diagnostic-context.md +37 -0
  23. package/apps/docs/src/content/docs/adrs/0005-ecs-fargate-alb-runtime-target.md +38 -0
  24. package/apps/docs/src/content/docs/adrs/0006-github-oidc-no-stored-keys.md +37 -0
  25. package/apps/docs/src/content/docs/adrs/0007-framework-detection-with-fallback.md +37 -0
  26. package/apps/docs/src/content/docs/adrs/0008-secrets-names-in-git-values-in-aws.md +37 -0
  27. package/apps/docs/src/content/docs/adrs/0009-regenerate-with-backup-on-rerun.md +37 -0
  28. package/apps/docs/src/content/docs/adrs/0010-advisory-only-security-scans.md +37 -0
  29. package/apps/docs/src/content/docs/cli/add.md +84 -0
  30. package/apps/docs/src/content/docs/cli/apply.md +32 -0
  31. package/apps/docs/src/content/docs/cli/db.md +200 -0
  32. package/apps/docs/src/content/docs/cli/destroy.md +31 -0
  33. package/apps/docs/src/content/docs/cli/diagnose.md +37 -0
  34. package/apps/docs/src/content/docs/cli/doctor.md +28 -0
  35. package/apps/docs/src/content/docs/cli/domain.md +57 -0
  36. package/apps/docs/src/content/docs/cli/drift.md +40 -0
  37. package/apps/docs/src/content/docs/cli/eject.md +29 -0
  38. package/apps/docs/src/content/docs/cli/exec.md +49 -0
  39. package/apps/docs/src/content/docs/cli/gc.md +37 -0
  40. package/apps/docs/src/content/docs/cli/init.md +72 -0
  41. package/apps/docs/src/content/docs/cli/logs.md +39 -0
  42. package/apps/docs/src/content/docs/cli/rollback.md +51 -0
  43. package/apps/docs/src/content/docs/cli/secrets.md +73 -0
  44. package/apps/docs/src/content/docs/cli/sleep.md +53 -0
  45. package/apps/docs/src/content/docs/cli/status.md +34 -0
  46. package/apps/docs/src/content/docs/cli/sync-ai.md +27 -0
  47. package/apps/docs/src/content/docs/guides/architecture.md +87 -0
  48. package/apps/docs/src/content/docs/guides/aws-credentials.md +72 -0
  49. package/apps/docs/src/content/docs/guides/background-workers.md +45 -0
  50. package/apps/docs/src/content/docs/guides/cicd-pipeline.md +64 -0
  51. package/apps/docs/src/content/docs/guides/database-connections.md +64 -0
  52. package/apps/docs/src/content/docs/guides/docker-compose.md +37 -0
  53. package/apps/docs/src/content/docs/guides/dockerfiles.md +46 -0
  54. package/apps/docs/src/content/docs/guides/ephemeral-pr-previews.md +39 -0
  55. package/apps/docs/src/content/docs/guides/examples.md +50 -0
  56. package/apps/docs/src/content/docs/guides/frameworks.md +88 -0
  57. package/apps/docs/src/content/docs/guides/headless.md +75 -0
  58. package/apps/docs/src/content/docs/guides/quickstart.md +52 -0
  59. package/apps/docs/src/content/docs/guides/rerun-init.md +43 -0
  60. package/apps/docs/src/content/docs/guides/secrets-management.md +83 -0
  61. package/apps/docs/src/content/docs/guides/understanding-your-bill.md +63 -0
  62. package/apps/docs/src/content/docs/index.mdx +103 -0
  63. package/apps/docs/src/content/docs/migrations/astro-vercel-to-aws.md +55 -0
  64. package/apps/docs/src/content/docs/migrations/heroku-procfile-to-aws.md +41 -0
  65. package/apps/docs/src/content/docs/migrations/nextjs-vercel-to-aws.md +51 -0
  66. package/apps/docs/src/content/docs/migrations/sveltekit-vercel-to-aws.md +63 -0
  67. package/apps/docs/src/content/docs/roadmap.md +97 -0
  68. package/apps/docs/src/content/docs/testing-strategy.md +32 -0
  69. package/apps/docs/src/content.config.ts +7 -0
  70. package/apps/docs/src/custom.css +14 -0
  71. package/apps/docs/tsconfig.json +6 -0
  72. package/bin/cli.js +140 -0
  73. package/package.json +105 -6
  74. package/scripts/sync-bedrock-models.js +22 -0
  75. package/scripts/test-iac.js +261 -0
  76. package/specs/add-redis-sqs-bedrock.md +128 -0
  77. package/specs/add-storage-dynamodb.md +106 -0
  78. package/specs/bedrock-model-catalog.md +131 -0
  79. package/specs/ci-pipeline.md +17 -0
  80. package/specs/cost-transparency.md +115 -0
  81. package/specs/custom-domains-and-ses.md +153 -0
  82. package/specs/database-suite-expansion.md +151 -0
  83. package/specs/db-connect.md +69 -0
  84. package/specs/db-lifecycle-migrations.md +159 -0
  85. package/specs/dependency-aware-init.md +176 -0
  86. package/specs/deploy-stack-to-grada-run-rebrand.md +45 -0
  87. package/specs/deployment-safety.md +170 -0
  88. package/specs/diagnose.md +16 -0
  89. package/specs/docs-hub.md +16 -0
  90. package/specs/dx-polish.md +46 -0
  91. package/specs/exec.md +25 -0
  92. package/specs/finops-cron-drift.md +161 -0
  93. package/specs/gc.md +26 -0
  94. package/specs/integration-suite.md +16 -0
  95. package/specs/logs.md +32 -0
  96. package/specs/rollback-live-polling.md +40 -0
  97. package/specs/secrets-pull-audit.md +51 -0
  98. package/specs/serverless-lambda-target.md +133 -0
  99. package/specs/status.md +31 -0
  100. package/specs/telemetry-and-spawn-hardening.md +69 -0
  101. package/specs/telemetry-hardening.md +35 -0
  102. package/src/commands/add.js +1111 -0
  103. package/src/commands/apply.js +214 -0
  104. package/src/commands/db/backup.js +229 -0
  105. package/src/commands/db/connect.js +304 -0
  106. package/src/commands/db/enable-vector.js +344 -0
  107. package/src/commands/db/import.js +604 -0
  108. package/src/commands/db/migrate.js +477 -0
  109. package/src/commands/db/restore.js +361 -0
  110. package/src/commands/db.js +87 -0
  111. package/src/commands/destroy.js +217 -0
  112. package/src/commands/diagnose.js +460 -0
  113. package/src/commands/doctor.js +109 -0
  114. package/src/commands/domain.js +685 -0
  115. package/src/commands/drift.js +243 -0
  116. package/src/commands/eject.js +127 -0
  117. package/src/commands/exec.js +222 -0
  118. package/src/commands/gc.js +250 -0
  119. package/src/commands/init.js +649 -0
  120. package/src/commands/logs.js +256 -0
  121. package/src/commands/rollback.js +323 -0
  122. package/src/commands/secrets.js +485 -0
  123. package/src/commands/sleep.js +347 -0
  124. package/src/commands/status.js +309 -0
  125. package/src/commands/sync-ai.js +115 -0
  126. package/src/commands/wake.js +337 -0
  127. package/src/core/parser.js +126 -0
  128. package/src/core/telemetry.js +244 -0
  129. package/src/data/bedrock-models.json +896 -0
  130. package/src/utils/addons.js +126 -0
  131. package/src/utils/ai-rules.js +59 -0
  132. package/src/utils/args.js +91 -0
  133. package/src/utils/aws.js +178 -0
  134. package/src/utils/backup.js +69 -0
  135. package/src/utils/bedrock-catalog.js +511 -0
  136. package/src/utils/capabilities.js +500 -0
  137. package/src/utils/command.js +65 -0
  138. package/src/utils/db-tunnel.js +164 -0
  139. package/src/utils/detector.js +298 -0
  140. package/src/utils/dockerCompose.js +65 -0
  141. package/src/utils/domains.js +73 -0
  142. package/src/utils/ecs-runner.js +289 -0
  143. package/src/utils/ecs.js +92 -0
  144. package/src/utils/frameworks.js +55 -0
  145. package/src/utils/generator.js +527 -0
  146. package/src/utils/hcl.js +426 -0
  147. package/src/utils/lambda-ecr.js +185 -0
  148. package/src/utils/prompts.js +278 -0
  149. package/src/utils/rds.js +131 -0
  150. package/src/utils/resolvers.js +174 -0
  151. package/src/utils/sleep-state.js +140 -0
  152. package/src/utils/sleep-targets.js +139 -0
  153. package/src/utils/system.js +42 -0
  154. package/src/utils/terraform.js +70 -0
  155. package/src/utils/visualizer.js +381 -0
  156. package/src/utils/warnings.js +49 -0
  157. package/templates/README.md +150 -0
  158. package/templates/docker/django.Dockerfile +40 -0
  159. package/templates/docker/go.Dockerfile +23 -0
  160. package/templates/docker/nestjs.Dockerfile +33 -0
  161. package/templates/docker/nextjs.Dockerfile +55 -0
  162. package/templates/docker/node.Dockerfile +24 -0
  163. package/templates/docker/nuxt.Dockerfile +47 -0
  164. package/templates/docker/python.Dockerfile +38 -0
  165. package/templates/docker/rails.Dockerfile +59 -0
  166. package/templates/docker/static.Dockerfile +32 -0
  167. package/templates/docker/svelte.Dockerfile +52 -0
  168. package/templates/github/deploy-lambda.yml +120 -0
  169. package/templates/github/deploy.yml +138 -0
  170. package/templates/github/drift.yml +112 -0
  171. package/templates/github/preview-lambda.yml +86 -0
  172. package/templates/github/preview.yml +69 -0
  173. package/templates/github/teardown.yml +43 -0
  174. package/templates/terraform/addons/bedrock.tf +34 -0
  175. package/templates/terraform/addons/cron-lambda.tf +78 -0
  176. package/templates/terraform/addons/cron.tf +101 -0
  177. package/templates/terraform/addons/dynamodb.tf +73 -0
  178. package/templates/terraform/addons/redis.tf +64 -0
  179. package/templates/terraform/addons/s3.tf +143 -0
  180. package/templates/terraform/addons/ses.tf +73 -0
  181. package/templates/terraform/addons/sqs.tf +67 -0
  182. package/templates/terraform/backend.tf +22 -0
  183. package/templates/terraform/cloudfront-lambda.tf +80 -0
  184. package/templates/terraform/cloudfront.tf +80 -0
  185. package/templates/terraform/database-aurora-postgresql.tf +92 -0
  186. package/templates/terraform/database-mysql.tf +72 -0
  187. package/templates/terraform/database.tf +71 -0
  188. package/templates/terraform/main-lambda.tf +229 -0
  189. package/templates/terraform/main.tf +296 -0
  190. package/templates/terraform/network.tf +95 -0
  191. package/templates/terraform/oidc.tf +64 -0
  192. package/templates/terraform/secrets.tf +31 -0
  193. package/templates/terraform/worker.tf +69 -0
  194. package/tests/__snapshots__/generator.test.js.snap +9633 -0
  195. package/tests/add.test.js +2037 -0
  196. package/tests/ai.test.js +94 -0
  197. package/tests/apply.test.js +488 -0
  198. package/tests/args.test.js +86 -0
  199. package/tests/aws.test.js +244 -0
  200. package/tests/capabilities.test.js +307 -0
  201. package/tests/cli.test.js +29 -0
  202. package/tests/command.test.js +100 -0
  203. package/tests/commands-import.test.js +74 -0
  204. package/tests/db.test.js +2704 -0
  205. package/tests/destroy.test.js +391 -0
  206. package/tests/detector.test.js +79 -0
  207. package/tests/diagnose.test.js +779 -0
  208. package/tests/doctor.test.js +202 -0
  209. package/tests/domain.test.js +899 -0
  210. package/tests/drift.test.js +243 -0
  211. package/tests/ecs.test.js +130 -0
  212. package/tests/eject.test.js +65 -0
  213. package/tests/exec.test.js +380 -0
  214. package/tests/gc.test.js +496 -0
  215. package/tests/generator.test.js +794 -0
  216. package/tests/headless.test.js +562 -0
  217. package/tests/lambda-ecr.test.js +185 -0
  218. package/tests/logs.test.js +447 -0
  219. package/tests/parser.test.js +160 -0
  220. package/tests/rds.test.js +244 -0
  221. package/tests/resolvers.test.js +282 -0
  222. package/tests/rollback.test.js +692 -0
  223. package/tests/secrets.test.js +752 -0
  224. package/tests/sleep-wake.test.js +1016 -0
  225. package/tests/status.test.js +370 -0
  226. package/tests/system.test.js +70 -0
  227. package/tests/telemetry.test.js +520 -0
  228. package/tests/terraform.test.js +84 -0
  229. package/tests/visualizer.test.js +496 -0
  230. package/vitest.config.js +9 -0
  231. package/index.js +0 -2
@@ -0,0 +1,381 @@
1
+ import { note, confirm, isCancel, cancel } from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { ADDON_REGISTRY } from './addons.js';
6
+ import { trackEvent, flushTelemetry, trackFailure } from '../core/telemetry.js';
7
+
8
+ export const COST_ESTIMATE_MARKER = 'Estimated Fixed Monthly Baseline:';
9
+ export const LEGACY_COST_ESTIMATE_MARKER = 'Estimated Monthly Cost:';
10
+
11
+ // Cost benchmarks for AWS us-east-2 baseline (Fargate + ALB)
12
+ const PRICING_TABLE = {
13
+ fargate: {
14
+ cpuPerHour: 0.04048, // per vCPU hour
15
+ memoryPerHour: 0.004445 // per GB hour
16
+ },
17
+ alb: {
18
+ basePerHour: 0.0225, // ~$16.43/month base
19
+ lcuPerHour: 0.008 // Baseline ~1 LCU (~$5.84/month)
20
+ },
21
+ rds: {
22
+ microPerHour: 0.016, // ~$11.68/mo for db.t4g.micro
23
+ storagePerMonth: 2.30, // 20GB gp3 storage baseline
24
+ auroraAcuPerHour: 0.12 // Serverless v2 compute when active ($0/mo idle at 0 ACU)
25
+ },
26
+ secretsManagerPerSecret: 0.40 // per secret per month
27
+ };
28
+
29
+ // 1. Parse the local terraform files to extract the actual configuration
30
+ export function parseTerraformConfig(tfDir) {
31
+ const tfvarsPath = path.join(tfDir, 'terraform.tfvars');
32
+ let region = 'us-east-2';
33
+ let cpu = 256;
34
+ let memory = 512;
35
+ let framework = 'Application';
36
+
37
+ // Read rendered cpu/memory from main.tf first so non-micro sizes chosen
38
+ // at init time survive apply previews and README cost syncs.
39
+ const mainTfPath = path.join(tfDir, 'main.tf');
40
+ let computeTarget = 'ecs';
41
+ if (fs.existsSync(mainTfPath)) {
42
+ const mainTf = fs.readFileSync(mainTfPath, 'utf-8');
43
+ const renderedCpu = mainTf.match(/cpu\s*=\s*"(\d+)"/);
44
+ if (renderedCpu) cpu = parseInt(renderedCpu[1], 10);
45
+ const renderedMemory = mainTf.match(/memory\s*=\s*"(\d+)"/);
46
+ if (renderedMemory) memory = parseInt(renderedMemory[1], 10);
47
+ // Lambda projects provision the serverless function instead of ECS.
48
+ if (mainTf.includes('resource "aws_lambda_function"')) computeTarget = 'lambda';
49
+ }
50
+
51
+ // terraform.tfvars overrides rendered values when present; hard defaults
52
+ // apply only when neither source specifies them.
53
+ if (fs.existsSync(tfvarsPath)) {
54
+ const content = fs.readFileSync(tfvarsPath, 'utf-8');
55
+
56
+ // Use regex to pull values out of the HCL format
57
+ const regionMatch = content.match(/aws_region\s*=\s*"([^"]+)"/);
58
+ if (regionMatch) region = regionMatch[1];
59
+
60
+ const cpuMatch = content.match(/container_cpu\s*=\s*(\d+)/);
61
+ if (cpuMatch) cpu = parseInt(cpuMatch[1], 10);
62
+
63
+ const memoryMatch = content.match(/container_memory\s*=\s*(\d+)/);
64
+ if (memoryMatch) memory = parseInt(memoryMatch[1], 10);
65
+ }
66
+
67
+ // Check if database files exist
68
+ const hasDb = fs.existsSync(path.join(tfDir, 'rds.tf')) || fs.existsSync(path.join(tfDir, 'database.tf'));
69
+ const hasWorker = fs.existsSync(path.join(tfDir, 'worker.tf'));
70
+ const hasSecrets = fs.existsSync(path.join(tfDir, 'secrets.tf'));
71
+
72
+ // Detect the provisioned engine from database.tf content (Aurora
73
+ // cluster first, then MySQL; anything else is standard PostgreSQL).
74
+ let dbEngine = 'postgres';
75
+ if (hasDb) {
76
+ const databaseTfPath = path.join(tfDir, 'database.tf');
77
+ if (fs.existsSync(databaseTfPath)) {
78
+ const databaseTf = fs.readFileSync(databaseTfPath, 'utf-8');
79
+ if (databaseTf.includes('resource "aws_rds_cluster"')) {
80
+ dbEngine = 'aurora-postgresql';
81
+ } else if (/engine\s*=\s*"mysql"/.test(databaseTf)) {
82
+ dbEngine = 'mysql';
83
+ }
84
+ }
85
+ }
86
+
87
+ // Registry-driven addon detection: capability keys whose .tf file exists.
88
+ const addons = [];
89
+ for (const [capability, entry] of Object.entries(ADDON_REGISTRY)) {
90
+ if (!entry) continue;
91
+ if (fs.existsSync(path.join(tfDir, entry.file))) addons.push(capability);
92
+ }
93
+
94
+ return { framework, region, cpu, memory, hasDb, dbEngine, hasWorker, hasSecrets, addons, computeTarget };
95
+ }
96
+
97
+ // 2. Calculate itemized monthly costs based on task definition settings
98
+ export function estimateMonthlyCost({ cpu = 256, memory = 512, hasDb = false, dbEngine = 'postgres', hasWorker = false, hasSecrets = false, addons = [], computeTarget = 'ecs' }) {
99
+ const vCpu = cpu / 1024;
100
+ const memGb = memory / 1024;
101
+ const hoursInMonth = 730;
102
+
103
+ // If a worker service exists, we are running a second identical Fargate task.
104
+ // Lambda web functions are scale-to-zero (usage-based, $0 fixed); only a
105
+ // hand-attached Fargate worker would still bill.
106
+ const taskMultiplier = computeTarget === 'lambda' ? (hasWorker ? 1 : 0) : (hasWorker ? 2 : 1);
107
+
108
+ const fargateCost = ((vCpu * PRICING_TABLE.fargate.cpuPerHour) +
109
+ (memGb * PRICING_TABLE.fargate.memoryPerHour)) * hoursInMonth * taskMultiplier;
110
+ // API Gateway HTTP API v2 is usage-based ($1.00 per million requests)
111
+ // with no fixed hourly baseline.
112
+ const albCost = computeTarget === 'lambda'
113
+ ? 0
114
+ : (PRICING_TABLE.alb.basePerHour + PRICING_TABLE.alb.lcuPerHour) * hoursInMonth;
115
+ // Aurora Serverless v2 idles at 0 ACU: $0/mo idle compute baseline
116
+ // (+$0.12/ACU-hr when active); managed instances bill the micro rate.
117
+ const dbCost = !hasDb
118
+ ? 0
119
+ : dbEngine === 'aurora-postgresql'
120
+ ? 0
121
+ : (PRICING_TABLE.rds.microPerHour * hoursInMonth) + PRICING_TABLE.rds.storagePerMonth;
122
+
123
+ // Secrets Manager bills per secret: one for the base app-secrets JSON
124
+ // secret plus one for the RDS managed master password when hasDb is true.
125
+ const secretCount = (hasSecrets ? 1 : 0) + (hasDb ? 1 : 0);
126
+ const secretsCost = secretCount * PRICING_TABLE.secretsManagerPerSecret;
127
+
128
+ // Future addons may carry a fixed monthly fee via cost.monthlyFixed.
129
+ let addonsFixedCost = 0;
130
+ for (const key of addons || []) {
131
+ const entry = ADDON_REGISTRY[key];
132
+ if (!entry) continue;
133
+ if (entry.cost && entry.cost.monthlyFixed > 0) addonsFixedCost += entry.cost.monthlyFixed;
134
+ }
135
+
136
+ const total = fargateCost + albCost + dbCost + secretsCost + addonsFixedCost;
137
+
138
+ return {
139
+ fargateMonthly: fargateCost.toFixed(2),
140
+ albMonthly: albCost.toFixed(2),
141
+ dbMonthly: dbCost.toFixed(2),
142
+ secretsMonthly: secretsCost.toFixed(2),
143
+ totalMonthly: total.toFixed(2)
144
+ };
145
+ }
146
+
147
+ // FinOps savings while an environment sleeps: paused Fargate replicas plus
148
+ // paused RDS compute. Storage, ALB, and Secrets Manager keep billing, so
149
+ // they are excluded here. Aurora Serverless v2 idles at 0 ACU, so its
150
+ // compute savings are $0 (stopping only prevents active wake-ups).
151
+ export function estimateSleepSavings({ cpu = 256, memory = 512, hasDb = false, dbEngine = 'postgres', appReplicas = 1, workerReplicas = 0 } = {}) {
152
+ const vCpu = cpu / 1024;
153
+ const memGb = memory / 1024;
154
+ const hoursInMonth = 730;
155
+ const perReplicaMonthly = ((vCpu * PRICING_TABLE.fargate.cpuPerHour)
156
+ + (memGb * PRICING_TABLE.fargate.memoryPerHour)) * hoursInMonth;
157
+ const fargateMonthly = perReplicaMonthly * (Math.max(0, appReplicas) + Math.max(0, workerReplicas));
158
+ const dbComputeMonthly = !hasDb || dbEngine === 'aurora-postgresql'
159
+ ? 0
160
+ : PRICING_TABLE.rds.microPerHour * hoursInMonth;
161
+ const monthly = fargateMonthly + dbComputeMonthly;
162
+ return {
163
+ fargateMonthly: fargateMonthly.toFixed(2),
164
+ dbComputeMonthly: dbComputeMonthly.toFixed(2),
165
+ monthly: monthly.toFixed(2),
166
+ hourly: (monthly / hoursInMonth).toFixed(3),
167
+ };
168
+ }
169
+
170
+ // Visible width of a styled line (ANSI escapes don't occupy columns).
171
+ function visibleLength(text) {
172
+ return String(text).replace(/\[[0-9;]*m/g, '').length;
173
+ }
174
+
175
+ function formatBaselineLine(totalMonthly, parts, suffix = '') {
176
+ const breakdown = parts.length > 0 && suffix
177
+ ? `${parts.join(', ')} ${suffix}`
178
+ : [...parts, suffix].filter(Boolean).join(', ');
179
+ return `${pc.bold('Fixed Baseline:')} ${pc.green(pc.bold(`~$${totalMonthly}/mo`))} ${pc.dim(`(${breakdown})`)}`;
180
+ }
181
+
182
+ // Builds the Fixed Baseline line, folding Secrets/Addons into a compact
183
+ // `+$X other` part when the full breakdown would exceed 90 visible columns.
184
+ export function buildBaselineLine(totalMonthly, costParts, secretsMonthly = 0, addonsMonthly = 0, suffix = '') {
185
+ const full = formatBaselineLine(totalMonthly, costParts, suffix);
186
+ if (visibleLength(full) <= 90) return full;
187
+
188
+ const folded = (Number(secretsMonthly) || 0) + (Number(addonsMonthly) || 0);
189
+ const compactParts = costParts.filter(
190
+ (part) => !part.startsWith('Secrets: ') && !part.startsWith('Addons: ')
191
+ );
192
+ if (folded > 0) compactParts.push(`+$${folded.toFixed(2)} other`);
193
+ const compact = formatBaselineLine(totalMonthly, compactParts, suffix);
194
+ if (visibleLength(compact) <= 90) return compact;
195
+
196
+ const bare = `${pc.bold('Fixed Baseline:')} ${pc.green(pc.bold(`~$${totalMonthly}/mo`))}`;
197
+ return suffix ? `${bare} ${pc.dim(`(${suffix})`)}` : bare;
198
+ }
199
+
200
+ // Topology line for the managed database, per provisioned engine.
201
+ function dbTopologyLabel(dbEngine) {
202
+ if (dbEngine === 'aurora-postgresql') {
203
+ return `✨ ${pc.yellow('Amazon Aurora PostgreSQL')} (Serverless v2 · 0–2 ACU scale-to-zero)`;
204
+ }
205
+ if (dbEngine === 'mysql') {
206
+ return `🐬 ${pc.yellow('Amazon RDS')} (MySQL managed instance)`;
207
+ }
208
+ return `🐘 ${pc.yellow('Amazon RDS')} (PostgreSQL managed instance)`;
209
+ }
210
+
211
+ // 3. Render the terminal architecture visualization and requests confirmation
212
+ export async function renderDryRunPreview(config, isDryRunFlag = false) {
213
+ const { framework = 'Node.js', region = 'us-east-2', cpu = 256, memory = 512, hasDb = false, dbEngine = 'postgres', hasWorker = false, hasSecrets = false, addons = [], computeTarget = 'ecs' } = config;
214
+ const isLambda = computeTarget === 'lambda';
215
+
216
+ // Fixed the duplicate hasWorker argument
217
+ const cost = estimateMonthlyCost({ cpu, memory, hasDb, dbEngine, hasWorker, hasSecrets, addons, computeTarget });
218
+
219
+ const hourlyRate = (Number(cost.totalMonthly) / 730).toFixed(3); // 730 hours in a month
220
+ const secretCount = (hasSecrets ? 1 : 0) + (hasDb ? 1 : 0);
221
+
222
+ const validAddons = (addons || []).filter((key) => Boolean(ADDON_REGISTRY[key]));
223
+
224
+ let addonsMonthly = 0;
225
+ for (const key of validAddons) {
226
+ addonsMonthly += ADDON_REGISTRY[key]?.cost?.monthlyFixed || 0;
227
+ }
228
+
229
+ // Lambda compute and API Gateway are usage-based with no fixed
230
+ // baseline, so the Lambda breakdown starts at the database.
231
+ const costParts = isLambda
232
+ ? []
233
+ : [`Fargate: $${cost.fargateMonthly}`, `ALB: $${cost.albMonthly}`];
234
+ if (hasDb) costParts.push(`RDS: $${cost.dbMonthly}`);
235
+ const secretsMonthly = Number(cost.secretsMonthly) || 0;
236
+ if (secretsMonthly > 0) costParts.push(`Secrets: $${cost.secretsMonthly}`);
237
+ if (addonsMonthly > 0) costParts.push(`Addons: $${addonsMonthly.toFixed(2)}`);
238
+
239
+ const addonNodes = [];
240
+ if (validAddons.length >= 3) {
241
+ addonNodes.push(` ${pc.gray('├──')} 🧩 [${pc.bold(`Addons (${validAddons.length}): ${validAddons.join(', ')}`)}]`);
242
+ } else {
243
+ for (const key of validAddons) {
244
+ const entry = ADDON_REGISTRY[key];
245
+ addonNodes.push(` ${pc.gray('├──')} 🧩 [${pc.bold(entry.label)}] ${pc.dim(`(${key})`)}`);
246
+ }
247
+ }
248
+ const usageBasedCount = validAddons.filter((key) => (ADDON_REGISTRY[key]?.cost?.monthlyFixed || 0) === 0).length;
249
+ const usageLine = usageBasedCount > 0
250
+ ? ` + Usage-based (${usageBasedCount} addon${usageBasedCount === 1 ? '' : 's'}): $0/mo fixed · per request, storage & egress`
251
+ : '';
252
+
253
+ const baselineLine = buildBaselineLine(cost.totalMonthly, costParts, secretsMonthly, addonsMonthly, isLambda ? '+ API GW & Lambda usage' : '');
254
+
255
+ // Flattened the tree to eliminate nesting and vertical bloat
256
+ const treeOutput = [
257
+ `${pc.bold('Topology')} (${pc.cyan(region)}):`,
258
+ isLambda
259
+ ? ` ${pc.gray('├──')} 🌐 ${pc.bold('API Gateway HTTP API v2')} (Scale-to-zero HTTPS entry)`
260
+ : ` ${pc.gray('├──')} 🌐 ${pc.bold('ALB')} (Public Entry & Health: ${pc.green('200 OK')})`,
261
+ ` ${pc.gray('├──')} 🔒 ${pc.bold('IAM OIDC')} (GitHub Auth) & 🐳 ${pc.bold('ECR')} (Registry)`,
262
+ hasDb ? ` ${pc.gray('├──')} ${dbTopologyLabel(dbEngine)}` : '',
263
+ secretCount > 0 ? ` ${pc.gray('├──')} 🔑 [${pc.bold('Secrets Manager')} (${secretCount === 1 ? '1 secret' : `${secretCount} secrets`})]` : '',
264
+ ...addonNodes,
265
+ isLambda
266
+ ? ` ${pc.gray('└──')} ⚡ ${pc.bold('AWS Lambda Web Service')} 🟢 ${pc.green(framework)} [512 MB · Scale-to-zero]`
267
+ : ` ${pc.gray(hasWorker ? '├──' : '└──')} 📦 ${pc.bold('ECS Web Service')} 🟢 ${pc.green(framework)} [${cpu} CPU / ${memory} MB]`,
268
+ !isLambda && hasWorker ? ` ${pc.gray('└──')} 📦 ${pc.bold('ECS Worker Service')} 🔄 Background Tasks [${cpu} CPU / ${memory} MB]` : '',
269
+ '',
270
+ baselineLine,
271
+ usageLine,
272
+ ` ${pc.dim(`* ~$${hourlyRate}/hr (us-east-2 rates) · Destroy anytime: "npx grada-run destroy"`)}`
273
+ ].filter(Boolean).join('\n');
274
+
275
+ note(treeOutput, 'Cloud Infrastructure Pre-Flight Inspection');
276
+
277
+ // 4. Check if this is a dry run (print & exit) or full apply (prompt & proceed)
278
+ if (isDryRunFlag) {
279
+ return true;
280
+ }
281
+
282
+ const shouldProceed = await confirm({
283
+ message: 'Review completed. Provision this infrastructure to AWS now?',
284
+ initialValue: true
285
+ });
286
+
287
+ if (isCancel(shouldProceed) || !shouldProceed) {
288
+ cancel('Operation canceled. No infrastructure was created.');
289
+ await trackFailure('infrastructure_applied', {
290
+ status: 'cancelled_at_preview',
291
+ ...buildCostTelemetryProps(config, cost),
292
+ });
293
+ process.exit(0);
294
+ }
295
+
296
+ return true;
297
+ }
298
+
299
+ // Shared cost/shape telemetry properties so visualizer.js and apply.js emit
300
+ // the exact same shape. projectName flows into telemetry's hashed distinct_id,
301
+ // keeping per-project funnels joinable without storing raw names.
302
+ export function buildCostTelemetryProps(config = {}, costs = estimateMonthlyCost(config)) {
303
+ return {
304
+ projectName: config.projectName || path.basename(process.cwd()),
305
+ estimated_monthly_usd: Number(costs.totalMonthly),
306
+ compute_target: config.computeTarget || 'ecs',
307
+ cpu: config.cpu ?? 256,
308
+ memory: config.memory ?? 512,
309
+ has_db: Boolean(config.hasDb),
310
+ db_engine: config.hasDb ? (config.dbEngine || 'postgres') : 'none',
311
+ has_worker: Boolean(config.hasWorker),
312
+ addons: config.addons || [],
313
+ addon_count: (config.addons || []).length,
314
+ };
315
+ }
316
+
317
+ const DOC_COST_LINE = (total) =>
318
+ `* **${COST_ESTIMATE_MARKER}** ~$${total}/month (us-east-2 reference rates; excludes variable traffic, ECR/CloudWatch storage, and usage-based addons)`;
319
+
320
+ function buildActiveAddonsSection(addons = []) {
321
+ const lines = [];
322
+ for (const key of addons || []) {
323
+ const entry = ADDON_REGISTRY[key];
324
+ if (!entry) continue;
325
+ lines.push(`- \`${key}\` (${entry.label}): ${entry.cost.summary}`);
326
+ }
327
+ if (lines.length === 0) return '';
328
+ return ['### Active Addons (Usage-Based)', '', ...lines].join('\n');
329
+ }
330
+
331
+ // Refresh the cost baseline (and usage-based addon list) in the generated
332
+ // deployment docs after `grada add`. Checks DEPLOYMENT.md, README.md,
333
+ // GRADA.md, then legacy DEPLOY-STACK.md; no-ops gracefully when no file
334
+ // exists or the user removed the cost marker. Returns the updated file
335
+ // path, or null when untouched.
336
+ export function syncDocCostEstimate(cwd = process.cwd()) {
337
+ const tfDir = path.join(cwd, 'terraform');
338
+ if (!fs.existsSync(path.join(tfDir, 'main.tf'))) return null;
339
+
340
+ let targetPath = null;
341
+ for (const name of ['DEPLOYMENT.md', 'README.md', 'GRADA.md', 'DEPLOY-STACK.md']) {
342
+ const candidate = path.join(cwd, name);
343
+ if (!fs.existsSync(candidate)) continue;
344
+ const content = fs.readFileSync(candidate, 'utf-8');
345
+ if (content.includes(COST_ESTIMATE_MARKER) || content.includes(LEGACY_COST_ESTIMATE_MARKER)) {
346
+ targetPath = candidate;
347
+ break;
348
+ }
349
+ }
350
+ if (!targetPath) return null;
351
+
352
+ const detected = parseTerraformConfig(tfDir);
353
+ const costs = estimateMonthlyCost(detected);
354
+
355
+ const lines = fs.readFileSync(targetPath, 'utf-8').split('\n');
356
+ const markerIdx = lines.findIndex(
357
+ (line) => line.includes(COST_ESTIMATE_MARKER) || line.includes(LEGACY_COST_ESTIMATE_MARKER)
358
+ );
359
+ if (markerIdx === -1) return null;
360
+ lines[markerIdx] = DOC_COST_LINE(costs.totalMonthly);
361
+
362
+ // Drop any previously rendered Active Addons section (heading, blank
363
+ // separators, and our `- \`key\`` bullets) so reruns replace rather than
364
+ // duplicate it. Anything else is user content and stays untouched.
365
+ const sectionIdx = lines.findIndex((line) => line.trim() === '### Active Addons (Usage-Based)');
366
+ if (sectionIdx !== -1) {
367
+ let endIdx = sectionIdx + 1;
368
+ while (endIdx < lines.length && lines[endIdx].trim() === '') endIdx++;
369
+ while (endIdx < lines.length && lines[endIdx].trim().startsWith('- `')) endIdx++;
370
+ lines.splice(sectionIdx, endIdx - sectionIdx);
371
+ if (lines[sectionIdx] === '' && lines[sectionIdx + 1] === '') lines.splice(sectionIdx, 1);
372
+ }
373
+
374
+ const section = buildActiveAddonsSection(detected.addons);
375
+ if (section) {
376
+ lines.splice(markerIdx + 1, 0, '', section);
377
+ }
378
+
379
+ fs.writeFileSync(targetPath, lines.join('\n'));
380
+ return targetPath;
381
+ }
@@ -0,0 +1,49 @@
1
+ import color from 'picocolors';
2
+
3
+ export function getFrameworkWarning(frameworkId) {
4
+ switch (frameworkId) {
5
+ case 'nestjs':
6
+ return (
7
+ color.bgYellow(color.black(' ⚠️ IMPORTANT: NESTJS SETUP REQUIRED ')) +
8
+ color.yellow('\n You must ensure your app binds to 0.0.0.0 to receive traffic in AWS Fargate.') +
9
+ color.yellow('\n In src/main.ts, update your bootstrap function to:') +
10
+ color.green('\n await app.listen(process.env.PORT ?? 3000, \'0.0.0.0\');\n\n')
11
+ );
12
+ case 'nextjs':
13
+ return (
14
+ color.bgYellow(color.black(' ⚠️ IMPORTANT: NEXT.JS SETUP REQUIRED ')) +
15
+ color.yellow('\n You must modify your next.config file and create a health check route before deploying.') +
16
+ color.yellow('\n See the "Critical Application Prerequisites" section in your README.md for copy-paste code.\n\n')
17
+ );
18
+ case 'node':
19
+ return (
20
+ color.bgYellow(color.black(' ⚠️ IMPORTANT: NODE.JS SETUP REQUIRED ')) +
21
+ color.yellow('\n 1. Ensure your package.json has a "start" script (e.g., "start": "node index.js").') +
22
+ color.yellow('\n 2. Your app must listen on 0.0.0.0 (not localhost) to receive traffic in Docker.\n\n')
23
+ );
24
+ case 'python':
25
+ return (
26
+ color.bgYellow(color.black(' ⚠️ IMPORTANT: PYTHON SETUP REQUIRED ')) +
27
+ color.yellow('\n 1. Ensure your requirements.txt includes your web framework (e.g., fastapi, uvicorn).') +
28
+ color.yellow('\n 2. Your app must listen on 0.0.0.0 (not localhost) to receive traffic in Docker.') +
29
+ color.yellow('\n 3. Ensure your app has a health check route returning 200 OK.\n\n')
30
+ );
31
+ case 'rails':
32
+ return (
33
+ color.bgYellow(color.black(' ℹ️ RAILS DOCKERFILE REPLACED ')) +
34
+ color.yellow('\n 1. We safely backed up your default Rails Dockerfile to Dockerfile.bak.') +
35
+ color.yellow('\n 2. We replaced it with an Alpine multi-stage build to guarantee 0 CVEs and AWS ALB compatibility.') +
36
+ color.yellow('\n 3. If you use SQLite locally but provisioned RDS, ensure the "pg" gem is in your Gemfile.\n\n')
37
+ );
38
+ case 'static':
39
+ return (
40
+ color.bgYellow(color.black(' ⚠️ IMPORTANT: STATIC SITE SETUP REQUIRED ')) +
41
+ color.yellow('\n 1. Open your generated Dockerfile.') +
42
+ color.yellow('\n 2. We defaulted your output folder to /app/dist.') +
43
+ color.yellow('\n 3. If your framework uses a different folder (like build/ or out/), change it in the COPY command.') +
44
+ color.yellow('\n 4. Ensure your package.json has a "build" script (e.g., "vite build").\n\n')
45
+ );
46
+ default:
47
+ return '';
48
+ }
49
+ }
@@ -0,0 +1,150 @@
1
+ # {{PROJECT_NAME}} - Cloud Infrastructure
2
+
3
+ > ⚡ **This infrastructure and CI/CD pipeline was auto-generated by [grada](https://github.com/grada-run/grada).**
4
+ >
5
+ > It contains a production-ready AWS ECS Fargate architecture and a zero-secret GitHub Actions deployment pipeline.
6
+
7
+ ## 🏗️ Architecture Overview
8
+
9
+ Your application has been configured for an enterprise-grade AWS deployment. Instead of relying on a black-box PaaS, you now own the underlying infrastructure:
10
+ * **Compute:** Your app is packaged into a Docker container and runs on **AWS ECS Fargate** (Serverless compute, meaning no EC2 instances to manage).
11
+ * **Networking:** Traffic flows through an **Application Load Balancer (ALB)**, which sits inside a custom VPC across multiple Availability Zones for high availability.
12
+ * **Security:** Deployments are handled via GitHub Actions using **AWS IAM OIDC**. This means GitHub securely requests temporary tokens to deploy your code—no long-lived AWS keys are stored anywhere.
13
+ * **State Management:** Terraform state is securely backed by an encrypted S3 bucket with native S3 locking.
14
+ * **Modular Addons:** Extend this stack anytime with `npx grada-run add storage:s3` (private S3 + CDN), `db:dynamodb` (serverless NoSQL), `db:redis` (Valkey caching), `queue:sqs` (SQS queue + DLQ), or `ai:bedrock` (Bedrock AI models). Added resources appear in `terraform/` and your container environment automatically — see `grada add --help`.
15
+
16
+ ## 💰 Cost Estimate & Disclaimer
17
+
18
+ This infrastructure provisions a highly available Application Load Balancer (ALB) and an ECS Fargate container (Size: **{{COMPUTE_TIER}}**).
19
+
20
+ * **Estimated Fixed Monthly Baseline:** ~${{ESTIMATED_COST}}/month (us-east-2 reference rates; excludes variable traffic, ECR/CloudWatch storage, and usage-based addons)
21
+ * *Note: AWS bills by the hour. If you destroy this stack after a few hours of testing, it will cost less than $0.20.*
22
+
23
+ > **⚠️ DISCLAIMER:** This cost is a rough estimate. AWS pricing changes and varies by region. **You are solely responsible for all AWS charges incurred by deploying this infrastructure.** The creators of `grada` are not liable for unexpected cloud costs, compromised credentials, or runaway billing. Always monitor your AWS Billing Dashboard and set up budget alerts.
24
+
25
+ ## 🚀 Deployment Guide
26
+
27
+ 1. **Initial Provisioning:**
28
+ Ensure you have configured your AWS CLI locally, then run the native deploy command:
29
+ ```bash
30
+ npx grada-run apply
31
+ ```
32
+ *(Alternatively, run `cd terraform && terraform init && terraform apply`)*
33
+
34
+ 2. **Push Secrets (Optional):**
35
+ If your application requires environment variables, create a local `.env` file and sync it directly to AWS Secrets Manager:
36
+ ```bash
37
+ npx grada-run secrets push .env
38
+ ```
39
+
40
+ 3. **Automated CI/CD:**
41
+ Push this repository to GitHub. Your deployment pipeline uses the official [Grada GitHub Action](https://github.com/marketplace/actions/deploy-stack-aws-fargate-terraform-deploy). Every push to `{{DEPLOY_BRANCH}}` will automatically run your infrastructure changes, build your container, and deploy your application.
42
+
43
+ ### 📟 Day-2 Operations
44
+ Once live, manage the stack without opening the AWS console:
45
+ * `npx grada-run status` — service health dashboard (auto-runs `diagnose` on degradation)
46
+ * `npx grada-run logs --error` — CloudWatch logs, error-filtered
47
+ * `npx grada-run rollback` — return to the previous task revision
48
+ * `npx grada-run db connect` — local tunnel into private RDS
49
+ * `npx grada-run add <capability>` — attach S3, DynamoDB, Valkey, SQS, or Bedrock later (`--list-models` shows available AI models)
50
+
51
+ ### ⚠️ Troubleshooting: OIDC Provider Already Exists
52
+ If `terraform apply` fails with `EntityAlreadyExists` for the OIDC provider, this AWS account already has a GitHub Actions provider (AWS permits one per account). Set `create_oidc_provider` to `false` in `terraform/oidc.tf`:
53
+ ```hcl
54
+ variable "create_oidc_provider" {
55
+ type = bool
56
+ default = false # <--- Change this from true to false
57
+ }
58
+ ```
59
+ Re-run `npx grada-run apply` to link directly to your existing provider.
60
+
61
+ ## 🛑 Safe Teardown (Destroying the Stack)
62
+
63
+ If you are done testing and want to stop all AWS billing, you must destroy the infrastructure.
64
+
65
+ Run the automated teardown command from the root of your project:
66
+ ```bash
67
+ npx grada-run destroy
68
+ ```
69
+ *Type `yes` when prompted. This will execute a safe Terraform teardown of your Load Balancer, ECS cluster, and networking components. You will then be asked whether to also empty and delete your remote S3 state bucket (kept by default, so you can re-provision later with `npx grada-run apply`).*
70
+
71
+ ## ⚠️ Critical Application Prerequisites
72
+
73
+ Before you push your code to GitHub, ensure your application is configured to run inside a Docker container and respond to AWS Load Balancer health checks.
74
+
75
+ ### 1. The Health Check Route (All Frameworks)
76
+
77
+ AWS constantly pings your container to ensure it is alive. If you configured a custom health check path (e.g., `/api/health`) during the CLI setup, **you must create that route in your application**. If AWS receives a `404 Not Found`, it will assume your app is broken and terminate the container.
78
+
79
+ Make sure your app returns a `200 OK` at your configured path:
80
+
81
+ * **Next.js (App Router):** Create `app/api/health/route.ts` returning a 200 response.
82
+ * **NestJS:** No action required if your health check path is `/` (the default `AppController` handles this). If you configured a custom path, create a specific controller for it returning a 200 response.
83
+ * **Express.js:** Add `app.get('/api/health', (req, res) => res.sendStatus(200));`
84
+ * **FastAPI/Python:** Add `@app.get("/api/health")` returning a 200 status.
85
+ * **Ruby on Rails:** Rails 7.1+ includes a default `/up` health check. Ensure `Rails.application.config.force_ssl = true` isn't blocking HTTP health checks from the ALB.
86
+ * **Django:** Add a simple view in `urls.py` that returns `HttpResponse("OK", status=200)` at your configured path.
87
+ * **Go:** Add a handler to your mux: `http.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })` and listen on your configured port (e.g., `http.ListenAndServe(":{{PORT}}", nil)`).
88
+ * **Nuxt 3:** Create a server route at `server/routes/health.ts` returning `200`.
89
+
90
+ ### 2. Enable Standalone Output (Next.js ONLY)
91
+
92
+ Next.js must be configured in "standalone" mode so it can bundle a minimal Node.js server. Without this, your GitHub Actions Docker build will crash.
93
+
94
+ Open `next.config.js` or `next.config.ts` in your root directory and add `output: 'standalone'`:
95
+
96
+ ```typescript
97
+ import type { NextConfig } from 'next';
98
+
99
+ const nextConfig: NextConfig = {
100
+ output: 'standalone', // <--- Add this exact line
101
+ };
102
+
103
+ export default nextConfig;
104
+ ```
105
+
106
+ ### 3. Container Network Binding (Node & Python)
107
+
108
+ 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.
109
+
110
+ Make sure your app is configured correctly:
111
+ * **NestJS:** Update `src/main.ts` to `await app.listen(process.env.PORT ?? 3000, '0.0.0.0');`
112
+ * **Express.js:** `app.listen(port, '0.0.0.0', () => ...)`
113
+ * **FastAPI:** `uvicorn.run(app, host="0.0.0.0", port=8000)`
114
+ * **Ruby on Rails:** Bound automatically by the CLI's Puma command (`-b tcp://0.0.0.0:{{PORT}}`).
115
+ * **Django:** Bound automatically by the CLI's Gunicorn command (`--bind 0.0.0.0:{{PORT}}`).
116
+ * **Go:** Ensure your `ListenAndServe` string looks like this: `http.ListenAndServe(":8080", nil)` or `http.ListenAndServe("0.0.0.0:8080", nil)`.
117
+ * **Nuxt 3:** Bound automatically via the `NITRO_HOST=0.0.0.0` environment variable injected by the CLI Dockerfile.
118
+
119
+ ### 4. Static Sites (Vite, Astro, React, Vue, SvelteKit)
120
+
121
+ If you are deploying a static site, your application is served via a highly optimized, unprivileged Nginx container.
122
+ * **Zero-Config Build:** The CLI automatically detected your framework's output folder (`dist`, `build`, etc.) and pre-configured your Dockerfile.
123
+ * **Health Checks:** You do not need to configure a custom `/health` route. Nginx will automatically return a `200 OK` when AWS pings the root `/` index page.
124
+
125
+ ### 5. Database Connections (Backend Frameworks Only)
126
+ If you opted to include a managed AWS RDS PostgreSQL database, the infrastructure automatically creates the database in isolated private subnets and injects the credentials into your container's environment variables.
127
+
128
+ Ensure your backend framework (Rails, Django, Go, etc.) is configured to consume these variables at runtime:
129
+ * `DB_HOST`: The AWS RDS endpoint URL
130
+ * `DB_PORT`: `5432`
131
+ * `DB_NAME`: The auto-formatted database name
132
+ * `DB_USER`: The hardcoded master username (dbadmin)
133
+ * `DB_PASSWORD`: The securely injected master password (sourced from AWS Secrets Manager)
134
+
135
+ ### 6. Framework Adapters (SvelteKit & Astro ONLY)
136
+
137
+ SvelteKit and Astro must build for Node.js, not Vercel — otherwise your Docker build will fail.
138
+ * **SvelteKit:** Use `@sveltejs/adapter-node` in `svelte.config.js` (replace `adapter-vercel` or `adapter-auto`).
139
+ * **Astro:** Use `@astrojs/node` as the adapter in `astro.config.mjs` (replace `@astrojs/vercel`).
140
+
141
+ ## 🛡️ Security Scanning
142
+
143
+ This pipeline includes automated DevSecOps guardrails using [Trivy](https://trivy.dev/).
144
+ Every time you push code, the pipeline will scan both your Docker container and your
145
+ Terraform configurations for vulnerabilities and misconfigurations.
146
+
147
+ To view the security reports:
148
+ 1. Navigate to the **Actions** tab in GitHub.
149
+ 2. Click on the latest deployment run.
150
+ 3. Scroll down the **Summary** page to view the generated vulnerability tables.
@@ -0,0 +1,40 @@
1
+ FROM python:3.12-alpine
2
+
3
+ # --- DevSecOps Patch: Upgrade Alpine system packages to clear OS-level CVEs ---
4
+ RUN apk upgrade --no-cache
5
+
6
+ # Prevent Python from writing .pyc files and buffer stdout for cleaner logs
7
+ ENV PYTHONDONTWRITEBYTECODE=1
8
+ ENV PYTHONUNBUFFERED=1
9
+
10
+ WORKDIR /app
11
+
12
+ # Create unprivileged user (Alpine syntax)
13
+ RUN addgroup -g 1001 appgroup && \
14
+ adduser -u 1001 -G appgroup -s /bin/sh -D appuser
15
+
16
+ # Install runtime libraries and temporary build tools
17
+ RUN apk update && \
18
+ apk add --no-cache libpq && \
19
+ apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev
20
+
21
+ COPY requirements.txt .
22
+
23
+ # Upgrade pip, install dependencies, clear caches, and NUKE package managers
24
+ RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
25
+ pip install --no-cache-dir -r requirements.txt gunicorn && \
26
+ find / -type d -name "ensurepip" -exec rm -rf {} + || true && \
27
+ rm -rf /root/.cache/pip && \
28
+ pip uninstall -y setuptools wheel pip
29
+
30
+ # Remove the build tools to shrink the image and reduce attack surface
31
+ RUN apk del .build-deps
32
+
33
+ # Copy code with explicit ownership
34
+ COPY --chown=appuser:appgroup . .
35
+
36
+ USER appuser
37
+ EXPOSE {{PORT}}
38
+
39
+ # Run Gunicorn using the dynamically injected WSGI module
40
+ CMD ["gunicorn", "--bind", "0.0.0.0:{{PORT}}", "--workers", "3", "{{DJANGO_WSGI}}:application"]
@@ -0,0 +1,23 @@
1
+ # Stage 1: Compile
2
+ FROM golang:1.26-alpine AS builder
3
+ WORKDIR /app
4
+ COPY go.mod go.sum* ./
5
+ RUN go mod download
6
+ COPY . .
7
+ RUN CGO_ENABLED=0 GOOS=linux go build -o /main .
8
+
9
+ # Stage 2: Production runner
10
+ FROM alpine:3.20
11
+ WORKDIR /app
12
+
13
+ # Create unprivileged user
14
+ RUN addgroup -g 1001 -S appgroup && \
15
+ adduser -S appuser -u 1001 -G appgroup
16
+
17
+ # Copy compiled binary with explicit ownership
18
+ COPY --from=builder --chown=appuser:appgroup /main /main
19
+
20
+ USER appuser
21
+ EXPOSE {{PORT}}
22
+
23
+ CMD ["/main"]