grada-run 0.0.2 → 0.33.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 (242) hide show
  1. package/.github/workflows/deploy-docs.yml +37 -0
  2. package/.github/workflows/e2e.yml +73 -0
  3. package/.github/workflows/iac-validation.yml +303 -0
  4. package/.github/workflows/publish.yml +68 -0
  5. package/.github/workflows/sync-bedrock-models.yml +57 -0
  6. package/.github/workflows/test.yml +43 -0
  7. package/.muserules +31 -0
  8. package/LICENSE +21 -0
  9. package/README.md +193 -3
  10. package/apps/docs/.astro/collections/docs.schema.json +644 -0
  11. package/apps/docs/.astro/content-assets.mjs +4 -0
  12. package/apps/docs/.astro/content-modules.mjs +4 -0
  13. package/apps/docs/.astro/content.d.ts +179 -0
  14. package/apps/docs/.astro/data-store.json +1 -0
  15. package/apps/docs/.astro/dev.json +14 -0
  16. package/apps/docs/.astro/settings.json +5 -0
  17. package/apps/docs/.astro/types.d.ts +2 -0
  18. package/apps/docs/astro.config.mjs +97 -0
  19. package/apps/docs/package.json +17 -0
  20. package/apps/docs/src/content/docs/adrs/0001-s3-native-state-locking.md +37 -0
  21. package/apps/docs/src/content/docs/adrs/0002-eject-mechanism-pure-iac.md +39 -0
  22. package/apps/docs/src/content/docs/adrs/0003-sync-ai-context-strategy.md +48 -0
  23. package/apps/docs/src/content/docs/adrs/0004-iac-driven-diagnostic-context.md +37 -0
  24. package/apps/docs/src/content/docs/adrs/0005-ecs-fargate-alb-runtime-target.md +38 -0
  25. package/apps/docs/src/content/docs/adrs/0006-github-oidc-no-stored-keys.md +37 -0
  26. package/apps/docs/src/content/docs/adrs/0007-framework-detection-with-fallback.md +37 -0
  27. package/apps/docs/src/content/docs/adrs/0008-secrets-names-in-git-values-in-aws.md +37 -0
  28. package/apps/docs/src/content/docs/adrs/0009-regenerate-with-backup-on-rerun.md +37 -0
  29. package/apps/docs/src/content/docs/adrs/0010-advisory-only-security-scans.md +37 -0
  30. package/apps/docs/src/content/docs/cli/add.md +85 -0
  31. package/apps/docs/src/content/docs/cli/apply.md +35 -0
  32. package/apps/docs/src/content/docs/cli/db.md +200 -0
  33. package/apps/docs/src/content/docs/cli/destroy.md +35 -0
  34. package/apps/docs/src/content/docs/cli/diagnose.md +37 -0
  35. package/apps/docs/src/content/docs/cli/doctor.md +28 -0
  36. package/apps/docs/src/content/docs/cli/domain.md +57 -0
  37. package/apps/docs/src/content/docs/cli/drift.md +40 -0
  38. package/apps/docs/src/content/docs/cli/eject.md +33 -0
  39. package/apps/docs/src/content/docs/cli/exec.md +49 -0
  40. package/apps/docs/src/content/docs/cli/gc.md +37 -0
  41. package/apps/docs/src/content/docs/cli/init.md +72 -0
  42. package/apps/docs/src/content/docs/cli/logs.md +39 -0
  43. package/apps/docs/src/content/docs/cli/rollback.md +51 -0
  44. package/apps/docs/src/content/docs/cli/secrets.md +73 -0
  45. package/apps/docs/src/content/docs/cli/sleep.md +53 -0
  46. package/apps/docs/src/content/docs/cli/status.md +34 -0
  47. package/apps/docs/src/content/docs/cli/sync-ai.md +27 -0
  48. package/apps/docs/src/content/docs/guides/architecture.md +87 -0
  49. package/apps/docs/src/content/docs/guides/aws-credentials.md +72 -0
  50. package/apps/docs/src/content/docs/guides/background-workers.md +45 -0
  51. package/apps/docs/src/content/docs/guides/cicd-pipeline.md +64 -0
  52. package/apps/docs/src/content/docs/guides/database-connections.md +64 -0
  53. package/apps/docs/src/content/docs/guides/docker-compose.md +37 -0
  54. package/apps/docs/src/content/docs/guides/dockerfiles.md +46 -0
  55. package/apps/docs/src/content/docs/guides/ephemeral-pr-previews.md +39 -0
  56. package/apps/docs/src/content/docs/guides/examples.md +50 -0
  57. package/apps/docs/src/content/docs/guides/frameworks.md +88 -0
  58. package/apps/docs/src/content/docs/guides/headless.md +87 -0
  59. package/apps/docs/src/content/docs/guides/quickstart.md +52 -0
  60. package/apps/docs/src/content/docs/guides/rerun-init.md +43 -0
  61. package/apps/docs/src/content/docs/guides/secrets-management.md +83 -0
  62. package/apps/docs/src/content/docs/guides/understanding-your-bill.md +63 -0
  63. package/apps/docs/src/content/docs/index.mdx +103 -0
  64. package/apps/docs/src/content/docs/migrations/astro-vercel-to-aws.md +55 -0
  65. package/apps/docs/src/content/docs/migrations/heroku-procfile-to-aws.md +41 -0
  66. package/apps/docs/src/content/docs/migrations/nextjs-vercel-to-aws.md +51 -0
  67. package/apps/docs/src/content/docs/migrations/sveltekit-vercel-to-aws.md +63 -0
  68. package/apps/docs/src/content/docs/roadmap.md +99 -0
  69. package/apps/docs/src/content/docs/testing-strategy.md +37 -0
  70. package/apps/docs/src/content.config.ts +7 -0
  71. package/apps/docs/src/custom.css +14 -0
  72. package/apps/docs/tsconfig.json +6 -0
  73. package/bin/cli.js +140 -0
  74. package/package.json +107 -7
  75. package/scripts/sync-bedrock-models.js +22 -0
  76. package/scripts/test-iac.js +261 -0
  77. package/specs/add-redis-sqs-bedrock.md +128 -0
  78. package/specs/add-storage-dynamodb.md +106 -0
  79. package/specs/bedrock-model-catalog.md +131 -0
  80. package/specs/ci-pipeline.md +17 -0
  81. package/specs/cost-transparency.md +115 -0
  82. package/specs/custom-domains-and-ses.md +153 -0
  83. package/specs/database-suite-expansion.md +151 -0
  84. package/specs/db-connect.md +69 -0
  85. package/specs/db-lifecycle-migrations.md +159 -0
  86. package/specs/dependency-aware-init.md +176 -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/e2e-testing.md +50 -0
  92. package/specs/exec.md +25 -0
  93. package/specs/finops-cron-drift.md +161 -0
  94. package/specs/gc.md +26 -0
  95. package/specs/integration-suite.md +16 -0
  96. package/specs/logs.md +32 -0
  97. package/specs/rollback-live-polling.md +40 -0
  98. package/specs/secrets-pull-audit.md +51 -0
  99. package/specs/serverless-lambda-target.md +133 -0
  100. package/specs/status.md +31 -0
  101. package/specs/telemetry-and-spawn-hardening.md +69 -0
  102. package/specs/telemetry-hardening.md +35 -0
  103. package/specs/test-suite-deduplication.md +42 -0
  104. package/src/commands/add.js +1111 -0
  105. package/src/commands/apply.js +214 -0
  106. package/src/commands/db/backup.js +229 -0
  107. package/src/commands/db/connect.js +304 -0
  108. package/src/commands/db/enable-vector.js +344 -0
  109. package/src/commands/db/import.js +604 -0
  110. package/src/commands/db/migrate.js +477 -0
  111. package/src/commands/db/restore.js +361 -0
  112. package/src/commands/db.js +87 -0
  113. package/src/commands/destroy.js +217 -0
  114. package/src/commands/diagnose.js +460 -0
  115. package/src/commands/doctor.js +109 -0
  116. package/src/commands/domain.js +685 -0
  117. package/src/commands/drift.js +243 -0
  118. package/src/commands/eject.js +130 -0
  119. package/src/commands/exec.js +222 -0
  120. package/src/commands/gc.js +250 -0
  121. package/src/commands/init.js +649 -0
  122. package/src/commands/logs.js +256 -0
  123. package/src/commands/rollback.js +323 -0
  124. package/src/commands/secrets.js +485 -0
  125. package/src/commands/sleep.js +347 -0
  126. package/src/commands/status.js +309 -0
  127. package/src/commands/sync-ai.js +115 -0
  128. package/src/commands/wake.js +337 -0
  129. package/src/core/parser.js +128 -0
  130. package/src/core/telemetry.js +244 -0
  131. package/src/data/bedrock-models.json +896 -0
  132. package/src/utils/addons.js +126 -0
  133. package/src/utils/ai-rules.js +59 -0
  134. package/src/utils/args.js +91 -0
  135. package/src/utils/aws.js +178 -0
  136. package/src/utils/backup.js +69 -0
  137. package/src/utils/bedrock-catalog.js +511 -0
  138. package/src/utils/capabilities.js +500 -0
  139. package/src/utils/command.js +78 -0
  140. package/src/utils/db-tunnel.js +164 -0
  141. package/src/utils/detector.js +298 -0
  142. package/src/utils/dockerCompose.js +65 -0
  143. package/src/utils/domains.js +73 -0
  144. package/src/utils/ecs-runner.js +289 -0
  145. package/src/utils/ecs.js +92 -0
  146. package/src/utils/frameworks.js +55 -0
  147. package/src/utils/generator.js +527 -0
  148. package/src/utils/hcl.js +426 -0
  149. package/src/utils/lambda-ecr.js +185 -0
  150. package/src/utils/prompts.js +278 -0
  151. package/src/utils/rds.js +131 -0
  152. package/src/utils/resolvers.js +174 -0
  153. package/src/utils/sleep-state.js +140 -0
  154. package/src/utils/sleep-targets.js +139 -0
  155. package/src/utils/system.js +42 -0
  156. package/src/utils/terraform.js +70 -0
  157. package/src/utils/visualizer.js +381 -0
  158. package/src/utils/warnings.js +49 -0
  159. package/templates/README.md +150 -0
  160. package/templates/docker/django.Dockerfile +40 -0
  161. package/templates/docker/go.Dockerfile +23 -0
  162. package/templates/docker/nestjs.Dockerfile +33 -0
  163. package/templates/docker/nextjs.Dockerfile +55 -0
  164. package/templates/docker/node.Dockerfile +24 -0
  165. package/templates/docker/nuxt.Dockerfile +47 -0
  166. package/templates/docker/python.Dockerfile +38 -0
  167. package/templates/docker/rails.Dockerfile +59 -0
  168. package/templates/docker/static.Dockerfile +32 -0
  169. package/templates/docker/svelte.Dockerfile +52 -0
  170. package/templates/github/deploy-lambda.yml +120 -0
  171. package/templates/github/deploy.yml +138 -0
  172. package/templates/github/drift.yml +112 -0
  173. package/templates/github/preview-lambda.yml +86 -0
  174. package/templates/github/preview.yml +69 -0
  175. package/templates/github/teardown.yml +43 -0
  176. package/templates/terraform/addons/bedrock.tf +34 -0
  177. package/templates/terraform/addons/cron-lambda.tf +78 -0
  178. package/templates/terraform/addons/cron.tf +101 -0
  179. package/templates/terraform/addons/dynamodb.tf +73 -0
  180. package/templates/terraform/addons/redis.tf +64 -0
  181. package/templates/terraform/addons/s3.tf +143 -0
  182. package/templates/terraform/addons/ses.tf +73 -0
  183. package/templates/terraform/addons/sqs.tf +67 -0
  184. package/templates/terraform/backend.tf +22 -0
  185. package/templates/terraform/cloudfront-lambda.tf +80 -0
  186. package/templates/terraform/cloudfront.tf +80 -0
  187. package/templates/terraform/database-aurora-postgresql.tf +92 -0
  188. package/templates/terraform/database-mysql.tf +72 -0
  189. package/templates/terraform/database.tf +71 -0
  190. package/templates/terraform/main-lambda.tf +229 -0
  191. package/templates/terraform/main.tf +296 -0
  192. package/templates/terraform/network.tf +95 -0
  193. package/templates/terraform/oidc.tf +64 -0
  194. package/templates/terraform/secrets.tf +31 -0
  195. package/templates/terraform/worker.tf +69 -0
  196. package/tests/__snapshots__/generator.test.js.snap +9633 -0
  197. package/tests/add.test.js +2018 -0
  198. package/tests/ai.test.js +91 -0
  199. package/tests/apply.test.js +446 -0
  200. package/tests/args.test.js +86 -0
  201. package/tests/aws.test.js +244 -0
  202. package/tests/capabilities.test.js +304 -0
  203. package/tests/cli.test.js +29 -0
  204. package/tests/command.test.js +99 -0
  205. package/tests/commands-import.test.js +74 -0
  206. package/tests/db.test.js +2673 -0
  207. package/tests/destroy.test.js +365 -0
  208. package/tests/detector.test.js +79 -0
  209. package/tests/diagnose.test.js +765 -0
  210. package/tests/doctor.test.js +195 -0
  211. package/tests/domain.test.js +882 -0
  212. package/tests/drift.test.js +228 -0
  213. package/tests/e2e/helpers.js +122 -0
  214. package/tests/e2e/tier0.e2e.test.js +142 -0
  215. package/tests/e2e/tier1.live.e2e.test.js +118 -0
  216. package/tests/ecs.test.js +130 -0
  217. package/tests/eject.test.js +65 -0
  218. package/tests/exec.test.js +366 -0
  219. package/tests/gc.test.js +466 -0
  220. package/tests/generator.test.js +794 -0
  221. package/tests/headless.test.js +491 -0
  222. package/tests/helpers/clack.js +103 -0
  223. package/tests/helpers/console.js +41 -0
  224. package/tests/helpers/telemetry.js +37 -0
  225. package/tests/helpers/tmpdir.js +36 -0
  226. package/tests/lambda-ecr.test.js +185 -0
  227. package/tests/logs.test.js +436 -0
  228. package/tests/parser.test.js +160 -0
  229. package/tests/rds.test.js +244 -0
  230. package/tests/resolvers.test.js +279 -0
  231. package/tests/rollback.test.js +670 -0
  232. package/tests/secrets.test.js +729 -0
  233. package/tests/sleep-wake.test.js +998 -0
  234. package/tests/status.test.js +356 -0
  235. package/tests/system.test.js +70 -0
  236. package/tests/telemetry.test.js +520 -0
  237. package/tests/terraform.test.js +84 -0
  238. package/tests/visualizer.test.js +480 -0
  239. package/vitest.config.js +10 -0
  240. package/vitest.e2e.tier0.config.js +8 -0
  241. package/vitest.e2e.tier1.config.js +8 -0
  242. package/index.js +0 -2
@@ -0,0 +1,71 @@
1
+ # 1. Isolated Subnets (No Internet Gateway routing)
2
+ resource "aws_subnet" "db_isolated" {
3
+ count = 2
4
+ vpc_id = aws_vpc.main.id
5
+ cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 10)
6
+ availability_zone = data.aws_availability_zones.available.names[count.index]
7
+
8
+ tags = {
9
+ Name = "${local.app_name}-db-isolated-${count.index}"
10
+ }
11
+ }
12
+
13
+ resource "aws_db_subnet_group" "main" {
14
+ name = "${local.app_name}-db-subnet-group"
15
+ subnet_ids = aws_subnet.db_isolated[*].id
16
+ }
17
+
18
+ # 2. Database Security Group
19
+ resource "aws_security_group" "rds" {
20
+ name = "${local.app_name}-rds-sg"
21
+ vpc_id = aws_vpc.main.id
22
+
23
+ # ONLY allow inbound traffic from the ECS Fargate tasks
24
+ ingress {
25
+ from_port = 5432
26
+ to_port = 5432
27
+ protocol = "tcp"
28
+ security_groups = [aws_security_group.ecs_tasks.id]
29
+ }
30
+ }
31
+
32
+ # 3. The PostgreSQL Instance
33
+ resource "aws_db_instance" "postgres" {
34
+ identifier = "${local.app_name}-db"
35
+ engine = "postgres"
36
+ engine_version = "16"
37
+ instance_class = "db.t4g.micro"
38
+ allocated_storage = 20
39
+ storage_encrypted = true
40
+
41
+ # Clean up dashes for the database name (e.g. my-project -> my_project)
42
+ db_name = replace(local.app_name, "-", "_")
43
+ username = "dbadmin"
44
+
45
+ # AWS automatically creates and manages the secret in Secrets Manager!
46
+ manage_master_user_password = true
47
+
48
+ db_subnet_group_name = aws_db_subnet_group.main.name
49
+ vpc_security_group_ids = [aws_security_group.rds.id]
50
+
51
+ skip_final_snapshot = true
52
+ publicly_accessible = false
53
+ }
54
+
55
+ # 4. IAM Permission for RDS Master Password Secret
56
+ resource "aws_iam_role_policy" "rds_secret_access" {
57
+ name = "${local.app_name}-rds-secret-policy"
58
+ role = aws_iam_role.execution_role.id
59
+ policy = jsonencode({
60
+ Version = "2012-10-17"
61
+ Statement = [
62
+ {
63
+ Effect = "Allow"
64
+ Action = ["secretsmanager:GetSecretValue"]
65
+ Resource = [
66
+ aws_db_instance.postgres.master_user_secret[0].secret_arn
67
+ ]
68
+ }
69
+ ]
70
+ })
71
+ }
@@ -0,0 +1,229 @@
1
+ # grada generated infrastructure (Lambda target)
2
+ # Scale-to-zero web service: container image + Lambda Web Adapter, fronted by
3
+ # API Gateway HTTP API v2 and CloudFront. See templates/terraform/main.tf for
4
+ # the ECS Fargate equivalent.
5
+ provider "aws" {
6
+ region = "{{REGION}}"
7
+
8
+ default_tags {
9
+ tags = {
10
+ ManagedBy = "grada"
11
+ }
12
+ }
13
+ }
14
+
15
+ # Alias for us-east-1-only services (CloudFront ACM certificates). Lives here
16
+ # — not in domain.tf — so removing a custom domain never orphans the provider
17
+ # configuration Terraform needs to destroy the certificate.
18
+ # tflint-ignore: terraform_unused_declarations
19
+ provider "aws" {
20
+ alias = "us_east_1"
21
+ region = "us-east-1"
22
+
23
+ default_tags {
24
+ tags = {
25
+ ManagedBy = "grada"
26
+ }
27
+ }
28
+ }
29
+
30
+ locals {
31
+ # Workspace suffix for PR preview environments (e.g., "-pr-123"). Empty for production.
32
+ env_suffix = terraform.workspace == "default" ? "" : "-${terraform.workspace}"
33
+
34
+ # Dynamic naming variable to prevent collisions
35
+ app_name = "{{PROJECT_NAME}}${local.env_suffix}"
36
+ }
37
+
38
+ # --- CloudWatch Logs ---
39
+ resource "aws_cloudwatch_log_group" "app" {
40
+ name = "/aws/lambda/${local.app_name}-fn"
41
+ retention_in_days = 30
42
+ }
43
+
44
+ # --- ECR Repository (Shared across workspaces) ---
45
+ resource "aws_ecr_repository" "app" {
46
+ count = terraform.workspace == "default" ? 1 : 0
47
+ name = "{{PROJECT_NAME}}-repo"
48
+ # trivy:ignore:AVD-AWS-0031 - Mutable tags allow the CI/CD pipeline to reuse the 'latest' tag for simplified deployments
49
+ image_tag_mutability = "MUTABLE"
50
+ force_delete = true
51
+
52
+ image_scanning_configuration {
53
+ scan_on_push = true
54
+ }
55
+ }
56
+
57
+ # In PR workspaces, fetch the existing production repository
58
+ data "aws_ecr_repository" "existing_app" {
59
+ count = terraform.workspace != "default" ? 1 : 0
60
+ name = "{{PROJECT_NAME}}-repo"
61
+ }
62
+
63
+ locals {
64
+ ecr_url = terraform.workspace == "default" ? aws_ecr_repository.app[0].repository_url : data.aws_ecr_repository.existing_app[0].repository_url
65
+ }
66
+
67
+ # --- IAM: Execution Role ---
68
+ # Kept for addon compatibility (same name as the ECS target). The Lambda
69
+ # function itself assumes the task role below.
70
+ data "aws_iam_policy_document" "ecs_trust" {
71
+ statement {
72
+ actions = ["sts:AssumeRole"]
73
+ principals {
74
+ type = "Service"
75
+ identifiers = ["ecs-tasks.amazonaws.com"]
76
+ }
77
+ }
78
+ }
79
+
80
+ resource "aws_iam_role" "execution_role" {
81
+ name = "${local.app_name}-execution-role"
82
+ assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
83
+ }
84
+
85
+ resource "aws_iam_role_policy_attachment" "execution_role_policy" {
86
+ role = aws_iam_role.execution_role.name
87
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
88
+ }
89
+
90
+ # --- IAM: Task Role (Lambda function role) ---
91
+ # Allows your application code running INSIDE the container to access AWS services.
92
+ # Trusted by both Lambda and ECS task principals so every addon
93
+ # `aws_iam_role_policy` attachment targeting this role works on both targets.
94
+ data "aws_iam_policy_document" "lambda_task_trust" {
95
+ statement {
96
+ actions = ["sts:AssumeRole"]
97
+ principals {
98
+ type = "Service"
99
+ identifiers = ["lambda.amazonaws.com", "ecs-tasks.amazonaws.com"]
100
+ }
101
+ }
102
+ }
103
+
104
+ resource "aws_iam_role" "task_role" {
105
+ name = "${local.app_name}-task-role"
106
+ assume_role_policy = data.aws_iam_policy_document.lambda_task_trust.json
107
+ }
108
+
109
+ # VPC ENI management plus CloudWatch Logs delivery (this single managed
110
+ # policy includes the AWSLambdaBasicExecutionRole log permissions).
111
+ resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
112
+ role = aws_iam_role.task_role.name
113
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
114
+ }
115
+
116
+ # Allows application code to read the shared Secrets Manager vault at
117
+ # runtime via the APP_SECRETS_ARN environment variable (non-VPC functions
118
+ # reach Secrets Manager directly; VPC-attached functions should prefer
119
+ # plain environment variables injected with `grada add`).
120
+ resource "aws_iam_role_policy" "app_secrets_access" {
121
+ name = "${local.app_name}-app-secrets-policy"
122
+ role = aws_iam_role.task_role.id
123
+ policy = jsonencode({
124
+ Version = "2012-10-17"
125
+ Statement = [
126
+ {
127
+ Effect = "Allow"
128
+ Action = ["secretsmanager:GetSecretValue"]
129
+ Resource = [local.secret_arn]
130
+ }
131
+ ]
132
+ })
133
+ }
134
+
135
+ # --- Lambda Function (container image + Web Adapter) ---
136
+ resource "aws_lambda_function" "app" {
137
+ function_name = "${local.app_name}-fn"
138
+ role = aws_iam_role.task_role.arn
139
+ package_type = "Image"
140
+ # Lambda requires an image in a private same-account ECR repository.
141
+ # On Day 0 `grada apply` seeds a minimal placeholder under :latest
142
+ # (see src/utils/lambda-ecr.js); every CI push then deploys the real image
143
+ # via `aws lambda update-function-code`, which Terraform intentionally
144
+ # ignores here so code deploys and `terraform apply` never fight.
145
+ image_uri = "${local.ecr_url}:latest"
146
+ architectures = ["x86_64"]
147
+ memory_size = 512
148
+ timeout = 30
149
+
150
+ {{LAMBDA_IMAGE_CONFIG}}
151
+ {{LAMBDA_VPC_CONFIG}}
152
+ environment {
153
+ variables = {
154
+ PORT = "{{PORT}}"
155
+ AWS_LWA_PORT = "{{PORT}}"
156
+ APP_SECRETS_ARN = local.secret_arn
157
+ {{DB_ENV_VARS}}{{COMPOSE_WEB_ENV_VARS}}
158
+ }
159
+ }
160
+
161
+ lifecycle {
162
+ ignore_changes = [image_uri]
163
+ }
164
+
165
+ depends_on = [aws_cloudwatch_log_group.app]
166
+ }
167
+
168
+ # --- API Gateway HTTP API v2 ---
169
+ resource "aws_apigatewayv2_api" "main" {
170
+ name = "${local.app_name}-http-api"
171
+ protocol_type = "HTTP"
172
+ }
173
+
174
+ resource "aws_apigatewayv2_integration" "lambda" {
175
+ api_id = aws_apigatewayv2_api.main.id
176
+ integration_type = "AWS_PROXY"
177
+ integration_uri = aws_lambda_function.app.invoke_arn
178
+ payload_format_version = "2.0"
179
+ }
180
+
181
+ # The $default open route is intentional: authentication (if any) lives in the
182
+ # application, matching the public-ALB posture of the ECS target.
183
+ resource "aws_apigatewayv2_route" "default" {
184
+ api_id = aws_apigatewayv2_api.main.id
185
+ route_key = "$default"
186
+ target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
187
+ }
188
+
189
+ resource "aws_apigatewayv2_stage" "default" {
190
+ api_id = aws_apigatewayv2_api.main.id
191
+ name = "$default"
192
+ auto_deploy = true
193
+ }
194
+
195
+ resource "aws_lambda_permission" "apigw" {
196
+ statement_id = "AllowAPIGatewayInvoke"
197
+ action = "lambda:InvokeFunction"
198
+ function_name = aws_lambda_function.app.function_name
199
+ principal = "apigateway.amazonaws.com"
200
+ source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*"
201
+ }
202
+
203
+ # --- Outputs ---
204
+ output "api_gateway_url" {
205
+ description = "Direct API Gateway HTTP API URL (Bypasses CloudFront/CDN)"
206
+ value = aws_apigatewayv2_api.main.api_endpoint
207
+ }
208
+
209
+ output "ecr_repository_url" {
210
+ description = "The URL of the ECR repository"
211
+ value = local.ecr_url
212
+ }
213
+
214
+ # --- CloudWatch Alarms ---
215
+ resource "aws_cloudwatch_metric_alarm" "lambda_errors" {
216
+ alarm_name = "${local.app_name}-high-lambda-errors"
217
+ comparison_operator = "GreaterThanThreshold"
218
+ evaluation_periods = "2"
219
+ metric_name = "Errors"
220
+ namespace = "AWS/Lambda"
221
+ period = "60"
222
+ statistic = "Sum"
223
+ threshold = "10"
224
+ alarm_description = "Triggers if the Lambda function reports more than 10 errors in 2 minutes."
225
+
226
+ dimensions = {
227
+ FunctionName = aws_lambda_function.app.function_name
228
+ }
229
+ }
@@ -0,0 +1,296 @@
1
+ # grada generated infrastructure
2
+ provider "aws" {
3
+ region = "{{REGION}}"
4
+
5
+ default_tags {
6
+ tags = {
7
+ ManagedBy = "grada"
8
+ }
9
+ }
10
+ }
11
+
12
+ # Alias for us-east-1-only services (CloudFront ACM certificates). Lives here
13
+ # — not in domain.tf — so removing a custom domain never orphans the provider
14
+ # configuration Terraform needs to destroy the certificate.
15
+ # tflint-ignore: terraform_unused_declarations
16
+ provider "aws" {
17
+ alias = "us_east_1"
18
+ region = "us-east-1"
19
+
20
+ default_tags {
21
+ tags = {
22
+ ManagedBy = "grada"
23
+ }
24
+ }
25
+ }
26
+
27
+ locals {
28
+ # Safely read the keys generated by the CLI, default to empty list if file doesn't exist
29
+ secret_keys = fileexists("${path.module}/secret_keys.json") ? jsondecode(file("${path.module}/secret_keys.json")) : []
30
+
31
+ # Workspace suffix for PR preview environments (e.g., "-pr-123"). Empty for production.
32
+ env_suffix = terraform.workspace == "default" ? "" : "-${terraform.workspace}"
33
+
34
+ # Dynamic naming variable to prevent collisions
35
+ app_name = "{{PROJECT_NAME}}${local.env_suffix}"
36
+ }
37
+
38
+ # --- CloudWatch Logs ---
39
+ resource "aws_cloudwatch_log_group" "app_logs" {
40
+ name = "/ecs/${local.app_name}"
41
+ retention_in_days = 14
42
+ }
43
+
44
+ # --- ECR Repository (Shared across workspaces) ---
45
+ resource "aws_ecr_repository" "app" {
46
+ count = terraform.workspace == "default" ? 1 : 0
47
+ name = "{{PROJECT_NAME}}-repo"
48
+ # trivy:ignore:AVD-AWS-0031 - Mutable tags allow the CI/CD pipeline to reuse the 'latest' tag for simplified deployments
49
+ image_tag_mutability = "MUTABLE"
50
+ force_delete = true
51
+
52
+ image_scanning_configuration {
53
+ scan_on_push = true
54
+ }
55
+ }
56
+
57
+ # In PR workspaces, fetch the existing production repository
58
+ data "aws_ecr_repository" "existing_app" {
59
+ count = terraform.workspace != "default" ? 1 : 0
60
+ name = "{{PROJECT_NAME}}-repo"
61
+ }
62
+
63
+ locals {
64
+ ecr_url = terraform.workspace == "default" ? aws_ecr_repository.app[0].repository_url : data.aws_ecr_repository.existing_app[0].repository_url
65
+ }
66
+
67
+ # --- IAM: Execution Role ---
68
+ # Allows the underlying AWS Fargate agent to pull images and push logs
69
+ data "aws_iam_policy_document" "ecs_trust" {
70
+ statement {
71
+ actions = ["sts:AssumeRole"]
72
+ principals {
73
+ type = "Service"
74
+ identifiers = ["ecs-tasks.amazonaws.com"]
75
+ }
76
+ }
77
+ }
78
+
79
+ resource "aws_iam_role" "execution_role" {
80
+ name = "${local.app_name}-execution-role"
81
+ assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
82
+ }
83
+
84
+ resource "aws_iam_role_policy_attachment" "execution_role_policy" {
85
+ role = aws_iam_role.execution_role.name
86
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
87
+ }
88
+
89
+ # --- IAM: Task Role ---
90
+ # Allows your application code running INSIDE the container to access AWS services
91
+ resource "aws_iam_role" "task_role" {
92
+ name = "${local.app_name}-task-role"
93
+ assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
94
+ }
95
+
96
+ # --- IAM: ECS Exec (SSM) permissions for the task role ---
97
+ # Required for `npx grada-run exec` — lets Fargate open an SSM session.
98
+ resource "aws_iam_role_policy" "ecs_exec" {
99
+ name = "${local.app_name}-ecs-exec-policy"
100
+ role = aws_iam_role.task_role.id
101
+ policy = jsonencode({
102
+ Version = "2012-10-17"
103
+ Statement = [
104
+ {
105
+ Effect = "Allow"
106
+ Action = [
107
+ "ssmmessages:CreateControlChannel",
108
+ "ssmmessages:CreateDataChannel",
109
+ "ssmmessages:OpenControlChannel",
110
+ "ssmmessages:OpenDataChannel"
111
+ ]
112
+ Resource = "*"
113
+ }
114
+ ]
115
+ })
116
+ }
117
+
118
+ # --- ECS Cluster ---
119
+ resource "aws_ecs_cluster" "main" {
120
+ name = "${local.app_name}-cluster"
121
+ }
122
+
123
+ # --- ECS Task Definition ---
124
+ resource "aws_ecs_task_definition" "app" {
125
+ family = "${local.app_name}-task"
126
+ network_mode = "awsvpc"
127
+ requires_compatibilities = ["FARGATE"]
128
+ cpu = "{{CPU}}"
129
+ memory = "{{MEMORY}}"
130
+ execution_role_arn = aws_iam_role.execution_role.arn
131
+ task_role_arn = aws_iam_role.task_role.arn
132
+
133
+ container_definitions = jsonencode([
134
+ {
135
+ name = "${local.app_name}-container"
136
+ image = "${local.ecr_url}:${terraform.workspace == "default" ? "latest" : terraform.workspace}"
137
+ essential = true
138
+
139
+ environment = [
140
+ {{COMPOSE_WEB_ENV_VARS}}
141
+ {{DB_ENV_VARS}}
142
+ ]
143
+
144
+ # Dynamically map every secret key found in the local JSON file
145
+ secrets = concat(
146
+ [
147
+ for key in local.secret_keys : {
148
+ name = key
149
+ valueFrom = "${local.secret_arn}:${key}::"
150
+ }
151
+ ],
152
+ [
153
+ {{TASK_SECRETS}}
154
+ ]
155
+ )
156
+
157
+ {{TASK_COMMAND}}
158
+
159
+ portMappings = [
160
+ {
161
+ containerPort = {{PORT}}
162
+ hostPort = {{PORT}}
163
+ protocol = "tcp"
164
+ }
165
+ ]
166
+
167
+ logConfiguration = {
168
+ logDriver = "awslogs"
169
+ options = {
170
+ "awslogs-group" = aws_cloudwatch_log_group.app_logs.name
171
+ "awslogs-region" = "{{REGION}}"
172
+ "awslogs-stream-prefix" = "ecs"
173
+ }
174
+ }
175
+ }{{EXTRA_CONTAINERS}}
176
+ ])
177
+ }
178
+
179
+ # --- Application Load Balancer ---
180
+ # trivy:ignore:AVD-AWS-0053 - This ALB is intended to be publicly facing behind CloudFront
181
+ resource "aws_lb" "main" {
182
+ name = substr("${local.app_name}-alb", 0, 32)
183
+ load_balancer_type = "application"
184
+ security_groups = [aws_security_group.alb.id]
185
+ subnets = aws_subnet.public[*].id
186
+ drop_invalid_header_fields = true
187
+ }
188
+
189
+ resource "aws_lb_target_group" "app" {
190
+ name = substr("${local.app_name}-tg", 0, 32)
191
+ port = {{PORT}}
192
+ protocol = "HTTP"
193
+ vpc_id = aws_vpc.main.id
194
+ target_type = "ip"
195
+
196
+ health_check {
197
+ path = "{{HEALTH_CHECK_PATH}}"
198
+ matcher = "200-399"
199
+ interval = 30
200
+ timeout = 5
201
+ healthy_threshold = 2
202
+ unhealthy_threshold = 3
203
+ }
204
+ }
205
+
206
+ # trivy:ignore:AVD-AWS-0054 - CloudFront handles HTTPS edge termination; ALB uses HTTP to avoid complex ACM DNS validation for users
207
+ resource "aws_lb_listener" "http" {
208
+ load_balancer_arn = aws_lb.main.arn
209
+ port = "80"
210
+ protocol = "HTTP"
211
+
212
+ default_action {
213
+ type = "forward"
214
+ target_group_arn = aws_lb_target_group.app.arn
215
+ }
216
+ }
217
+
218
+ # --- ECS Service ---
219
+ resource "aws_ecs_service" "app" {
220
+ name = "${local.app_name}-service"
221
+ cluster = aws_ecs_cluster.main.id
222
+ task_definition = aws_ecs_task_definition.app.arn
223
+ launch_type = "FARGATE"
224
+ desired_count = {{DESIRED_COUNT}}
225
+ enable_execute_command = true
226
+
227
+ deployment_circuit_breaker {
228
+ enable = true
229
+ rollback = true
230
+ }
231
+
232
+ # Code-only deploys register new task definition revisions outside
233
+ # Terraform (see .github/workflows/deploy.yml). Without this, the next
234
+ # `terraform apply` would revert the service to the revision in state.
235
+ lifecycle {
236
+ ignore_changes = [task_definition]
237
+ }
238
+
239
+ network_configuration {
240
+ subnets = aws_subnet.public[*].id
241
+ security_groups = [aws_security_group.ecs_tasks.id]
242
+ assign_public_ip = true
243
+ }
244
+
245
+ load_balancer {
246
+ target_group_arn = aws_lb_target_group.app.arn
247
+ container_name = "${local.app_name}-container"
248
+ container_port = {{PORT}}
249
+ }
250
+
251
+ depends_on = [aws_lb_listener.http]
252
+ }
253
+
254
+ # Allow the ECS agent to read the specific secret from Secrets Manager
255
+ resource "aws_iam_role_policy" "secrets_policy" {
256
+ name = "{{PROJECT_NAME}}-secrets-policy"
257
+ role = aws_iam_role.execution_role.id
258
+ policy = jsonencode({
259
+ Version = "2012-10-17"
260
+ Statement = [
261
+ {
262
+ Effect = "Allow"
263
+ Action = ["secretsmanager:GetSecretValue"]
264
+ Resource = [local.secret_arn]
265
+ }
266
+ ]
267
+ })
268
+ }
269
+
270
+ # --- Outputs ---
271
+ output "alb_direct_url" {
272
+ description = "Direct Load Balancer URL (Bypasses CloudFront/CDN)"
273
+ value = "http://${aws_lb.main.dns_name}"
274
+ }
275
+
276
+ output "ecr_repository_url" {
277
+ description = "The URL of the ECR repository"
278
+ value = local.ecr_url
279
+ }
280
+
281
+ # --- CloudWatch Alarms ---
282
+ resource "aws_cloudwatch_metric_alarm" "alb_5xx_errors" {
283
+ alarm_name = "${local.app_name}-high-5xx-errors"
284
+ comparison_operator = "GreaterThanThreshold"
285
+ evaluation_periods = "2"
286
+ metric_name = "HTTPCode_Target_5XX_Count"
287
+ namespace = "AWS/ApplicationELB"
288
+ period = "60"
289
+ statistic = "Sum"
290
+ threshold = "10"
291
+ alarm_description = "Triggers if the ALB receives more than 10 5XX errors in 2 minutes."
292
+
293
+ dimensions = {
294
+ LoadBalancer = aws_lb.main.arn_suffix
295
+ }
296
+ }
@@ -0,0 +1,95 @@
1
+ # --- VPC & Networking ---
2
+ resource "aws_vpc" "main" {
3
+ cidr_block = "10.0.0.0/16"
4
+ enable_dns_hostnames = true
5
+ enable_dns_support = true
6
+
7
+ tags = {
8
+ Name = "${local.app_name}-vpc"
9
+ }
10
+ }
11
+
12
+ # Fetch available zones in the current region
13
+ data "aws_availability_zones" "available" {
14
+ state = "available"
15
+ }
16
+
17
+ resource "aws_internet_gateway" "main" {
18
+ vpc_id = aws_vpc.main.id
19
+ }
20
+
21
+ # Create 2 Public Subnets
22
+ # trivy:ignore:AVD-AWS-0164 - Public subnets are used to avoid the $30/mo cost of a NAT Gateway for outbound container traffic
23
+ resource "aws_subnet" "public" {
24
+ count = 2
25
+ vpc_id = aws_vpc.main.id
26
+ cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
27
+ availability_zone = data.aws_availability_zones.available.names[count.index]
28
+ map_public_ip_on_launch = true
29
+
30
+ tags = {
31
+ Name = "${local.app_name}-public-subnet-${count.index + 1}"
32
+ }
33
+ }
34
+
35
+ # Route internet traffic through the Gateway
36
+ resource "aws_route_table" "public" {
37
+ vpc_id = aws_vpc.main.id
38
+ route {
39
+ cidr_block = "0.0.0.0/0"
40
+ gateway_id = aws_internet_gateway.main.id
41
+ }
42
+ }
43
+
44
+ resource "aws_route_table_association" "public" {
45
+ count = 2
46
+ subnet_id = aws_subnet.public[count.index].id
47
+ route_table_id = aws_route_table.public.id
48
+ }
49
+
50
+ # --- Security Groups ---
51
+ # ALB SG: Allow public internet access on HTTP
52
+ resource "aws_security_group" "alb" {
53
+ name = "${local.app_name}-alb-sg"
54
+ description = "Allow inbound HTTP to ALB"
55
+ vpc_id = aws_vpc.main.id
56
+
57
+ ingress {
58
+ from_port = 80
59
+ to_port = 80
60
+ protocol = "tcp"
61
+ cidr_blocks = ["0.0.0.0/0"]
62
+ }
63
+
64
+ egress {
65
+ from_port = 0
66
+ to_port = 0
67
+ protocol = "-1"
68
+ # trivy:ignore:AVD-AWS-0104 - Allow ALB to route out to standard AWS services
69
+ cidr_blocks = ["0.0.0.0/0"]
70
+ }
71
+ }
72
+
73
+ # ECS SG: Allow traffic ONLY from the ALB on the app's specific port
74
+ resource "aws_security_group" "ecs_tasks" {
75
+ name = "${local.app_name}-ecs-sg"
76
+ description = "Allow inbound access from the ALB only"
77
+ vpc_id = aws_vpc.main.id
78
+
79
+ ingress {
80
+ from_port = {{PORT}}
81
+ to_port = {{PORT}}
82
+ protocol = "tcp"
83
+ security_groups = [aws_security_group.alb.id]
84
+ }
85
+
86
+ egress {
87
+ from_port = 0
88
+ to_port = 0
89
+ protocol = "-1"
90
+ # trivy:ignore:AVD-AWS-0104 - Allow containers to pull images and hit external APIs
91
+ cidr_blocks = ["0.0.0.0/0"]
92
+ }
93
+ }
94
+
95
+ {{VERCEL_EDGE_ROUTING}}