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,143 @@
1
+ # grada addon: storage:s3
2
+ # Generated by `grada add storage:s3`. Do not edit by hand unless
3
+ # you know what you are doing — re-running with --force overwrites this file.
4
+ #
5
+ # Billing: no fixed hourly instance cost (usage-billed). S3 storage
6
+ # ($0.023/GB-mo), S3 PUT/GET requests, and CloudFront data transfer out
7
+ # are billed by usage on top of the stack's fixed baseline.
8
+
9
+ data "aws_caller_identity" "current" {}
10
+
11
+ # --- Private S3 Bucket ---
12
+ resource "aws_s3_bucket" "storage" {
13
+ # Truncate and sanitize the prefix so the full account ID suffix always fits
14
+ # within S3's 63-character limit. Underscores are replaced because S3 names
15
+ # must be DNS-compatible (lowercase, hyphens only).
16
+ bucket = "${trimsuffix(substr(replace(lower(local.app_name), "_", "-"), 0, 42), "-")}-storage-${data.aws_caller_identity.current.account_id}"
17
+ }
18
+
19
+ resource "aws_s3_bucket_public_access_block" "storage" {
20
+ bucket = aws_s3_bucket.storage.id
21
+ block_public_acls = true
22
+ block_public_policy = true
23
+ ignore_public_acls = true
24
+ restrict_public_buckets = true
25
+ }
26
+
27
+ resource "aws_s3_bucket_server_side_encryption_configuration" "storage" {
28
+ bucket = aws_s3_bucket.storage.id
29
+
30
+ rule {
31
+ apply_server_side_encryption_by_default {
32
+ sse_algorithm = "AES256"
33
+ }
34
+ }
35
+ }
36
+
37
+ resource "aws_s3_bucket_cors_configuration" "storage" {
38
+ bucket = aws_s3_bucket.storage.id
39
+
40
+ cors_rule {
41
+ allowed_methods = ["GET", "PUT", "POST", "DELETE", "HEAD"]
42
+ # NOTE: intentionally open for zero-config presigned browser uploads.
43
+ # Restrict to your app domain (e.g. ["https://example.com"]) once known.
44
+ allowed_origins = ["*"]
45
+ allowed_headers = ["*"]
46
+ expose_headers = ["ETag"]
47
+ max_age_seconds = 3000
48
+ }
49
+ }
50
+
51
+ # --- CloudFront Distribution with Origin Access Control ---
52
+ resource "aws_cloudfront_origin_access_control" "storage_oac" {
53
+ name = "${local.app_name}-storage-oac"
54
+ origin_access_control_origin_type = "s3"
55
+ signing_behavior = "always"
56
+ signing_protocol = "sigv4"
57
+ }
58
+
59
+ resource "aws_cloudfront_distribution" "storage_cdn" {
60
+ enabled = true
61
+ is_ipv6_enabled = true
62
+ wait_for_deployment = false
63
+
64
+ origin {
65
+ domain_name = aws_s3_bucket.storage.bucket_regional_domain_name
66
+ origin_id = "S3Origin"
67
+ origin_access_control_id = aws_cloudfront_origin_access_control.storage_oac.id
68
+ }
69
+
70
+ default_cache_behavior {
71
+ allowed_methods = ["GET", "HEAD", "OPTIONS"]
72
+ cached_methods = ["GET", "HEAD"]
73
+ target_origin_id = "S3Origin"
74
+ viewer_protocol_policy = "redirect-to-https"
75
+
76
+ forwarded_values {
77
+ query_string = false
78
+
79
+ cookies {
80
+ forward = "none"
81
+ }
82
+ }
83
+ }
84
+
85
+ restrictions {
86
+ geo_restriction {
87
+ restriction_type = "none"
88
+ }
89
+ }
90
+
91
+ viewer_certificate {
92
+ cloudfront_default_certificate = true
93
+ }
94
+ }
95
+
96
+ resource "aws_s3_bucket_policy" "storage_oac_policy" {
97
+ bucket = aws_s3_bucket.storage.id
98
+ policy = jsonencode({
99
+ Version = "2012-10-17"
100
+ Statement = [
101
+ {
102
+ Sid = "AllowCloudFrontOAC"
103
+ Effect = "Allow"
104
+ Principal = { Service = "cloudfront.amazonaws.com" }
105
+ Action = "s3:GetObject"
106
+ Resource = "${aws_s3_bucket.storage.arn}/*"
107
+ Condition = {
108
+ StringEquals = { "AWS:SourceArn" = aws_cloudfront_distribution.storage_cdn.arn }
109
+ }
110
+ }
111
+ ]
112
+ })
113
+ }
114
+
115
+ # --- Least-Privilege IAM Policy for the ECS Task Role ---
116
+ resource "aws_iam_role_policy" "storage_s3_access" {
117
+ name = "${local.app_name}-storage-s3-access"
118
+ role = aws_iam_role.task_role.id
119
+ policy = jsonencode({
120
+ Version = "2012-10-17"
121
+ Statement = [
122
+ {
123
+ Effect = "Allow"
124
+ Action = ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"]
125
+ Resource = "${aws_s3_bucket.storage.arn}/*"
126
+ },
127
+ {
128
+ Effect = "Allow"
129
+ Action = ["s3:ListBucket"]
130
+ Resource = aws_s3_bucket.storage.arn
131
+ }
132
+ ]
133
+ })
134
+ }
135
+
136
+ # --- Outputs ---
137
+ output "s3_bucket_name" {
138
+ value = aws_s3_bucket.storage.id
139
+ }
140
+
141
+ output "s3_cdn_domain" {
142
+ value = aws_cloudfront_distribution.storage_cdn.domain_name
143
+ }
@@ -0,0 +1,73 @@
1
+ # grada addon: email:ses
2
+ # Generated by `grada add email:ses`. Do not edit by hand unless
3
+ # you know what you are doing — re-running with --force overwrites this file.
4
+ #
5
+ # Billing: no fixed hourly instance cost (usage-billed at $0.10 per 1,000
6
+ # emails sent). New AWS accounts start in the SES sandbox (verified
7
+ # recipients only) — request production access in the SES console to send
8
+ # to unverified addresses.
9
+
10
+ # --- SES Domain Identity ---
11
+ # Account-wide singletons per domain/region: scoped to the default
12
+ # (production) workspace so PR preview teardowns never delete them.
13
+ resource "aws_ses_domain_identity" "ses" {
14
+ count = terraform.workspace == "default" ? 1 : 0
15
+ domain = "{{SES_DOMAIN}}"
16
+ }
17
+
18
+ resource "aws_ses_domain_dkim" "ses" {
19
+ count = terraform.workspace == "default" ? 1 : 0
20
+ domain = aws_ses_domain_identity.ses[0].domain
21
+ }
22
+
23
+ resource "aws_ses_domain_mail_from" "ses" {
24
+ count = terraform.workspace == "default" ? 1 : 0
25
+ domain = aws_ses_domain_identity.ses[0].domain
26
+ mail_from_domain = "mail.{{SES_DOMAIN}}"
27
+ behavior_on_mx_failure = "UseDefaultValue"
28
+ }
29
+
30
+ {{SES_ROUTE53_RECORDS_BLOCK}}
31
+ # --- Least-Privilege IAM Policy for the ECS Task Role ---
32
+ resource "aws_iam_role_policy" "ses_send" {
33
+ name = "${local.app_name}-ses-send"
34
+ role = aws_iam_role.task_role.id
35
+ policy = jsonencode({
36
+ Version = "2012-10-17"
37
+ Statement = [
38
+ {
39
+ Effect = "Allow"
40
+ Action = ["ses:SendEmail", "ses:SendRawEmail"]
41
+ Resource = "*"
42
+ Condition = {
43
+ StringLike = { "ses:FromAddress" = ["*@{{SES_DOMAIN}}", "{{SES_FROM_EMAIL}}"] }
44
+ }
45
+ }
46
+ ]
47
+ })
48
+ }
49
+
50
+ # --- Outputs ---
51
+ # In external-DNS mode (no --zone-id), create these records at your DNS
52
+ # provider: the verification TXT, the three DKIM CNAMEs
53
+ # (`<token>._domainkey.{{SES_DOMAIN}}` -> `<token>.dkim.amazonses.com`),
54
+ # the MAIL FROM MX/SPF at `mail.{{SES_DOMAIN}}`, and the DMARC TXT.
55
+ output "ses_domain_identity_arn" {
56
+ value = terraform.workspace == "default" ? aws_ses_domain_identity.ses[0].arn : null
57
+ }
58
+
59
+ output "ses_verification_token" {
60
+ value = terraform.workspace == "default" ? aws_ses_domain_identity.ses[0].verification_token : null
61
+ }
62
+
63
+ output "ses_dkim_tokens" {
64
+ value = terraform.workspace == "default" ? aws_ses_domain_dkim.ses[0].dkim_tokens : []
65
+ }
66
+
67
+ output "ses_spf_record" {
68
+ value = "v=spf1 include:amazonses.com ~all"
69
+ }
70
+
71
+ output "ses_dmarc_record" {
72
+ value = "v=DMARC1; p=none;"
73
+ }
@@ -0,0 +1,67 @@
1
+ # grada addon: queue:sqs
2
+ # Generated by `grada add queue:sqs`. Do not edit by hand unless
3
+ # you know what you are doing — re-running with --force overwrites this file.
4
+ #
5
+ # Billing: no fixed hourly instance cost (usage-billed). The first 1M SQS
6
+ # requests/mo are free, then $0.40 per million requests.
7
+
8
+ # --- Dead-Letter Queue ---
9
+ resource "aws_sqs_queue" "dlq" {
10
+ name = "${local.app_name}-dlq"
11
+ message_retention_seconds = 1209600
12
+ sqs_managed_sse_enabled = true
13
+ }
14
+
15
+ # --- Primary Queue ---
16
+ resource "aws_sqs_queue" "main" {
17
+ name = "${local.app_name}-queue"
18
+ visibility_timeout_seconds = 30
19
+ message_retention_seconds = 345600
20
+ receive_wait_time_seconds = 20
21
+ sqs_managed_sse_enabled = true
22
+ redrive_policy = jsonencode({
23
+ deadLetterTargetArn = aws_sqs_queue.dlq.arn
24
+ maxReceiveCount = 3
25
+ })
26
+ }
27
+
28
+ # --- Least-Privilege IAM Policy for the ECS Task Role ---
29
+ resource "aws_iam_role_policy" "queue_sqs_access" {
30
+ name = "${local.app_name}-queue-sqs-access"
31
+ role = aws_iam_role.task_role.id
32
+ policy = jsonencode({
33
+ Version = "2012-10-17"
34
+ Statement = [
35
+ {
36
+ Effect = "Allow"
37
+ Action = [
38
+ "sqs:SendMessage",
39
+ "sqs:ReceiveMessage",
40
+ "sqs:DeleteMessage",
41
+ "sqs:ChangeMessageVisibility",
42
+ "sqs:GetQueueAttributes",
43
+ "sqs:GetQueueUrl"
44
+ ]
45
+ Resource = [
46
+ aws_sqs_queue.main.arn,
47
+ aws_sqs_queue.dlq.arn
48
+ ]
49
+ }
50
+ ]
51
+ })
52
+ }
53
+
54
+ {{WORKER_AUTOSCALING_BLOCK}}
55
+
56
+ # --- Outputs ---
57
+ output "sqs_queue_url" {
58
+ value = aws_sqs_queue.main.id
59
+ }
60
+
61
+ output "sqs_queue_arn" {
62
+ value = aws_sqs_queue.main.arn
63
+ }
64
+
65
+ output "sqs_dlq_url" {
66
+ value = aws_sqs_queue.dlq.id
67
+ }
@@ -0,0 +1,22 @@
1
+ terraform {
2
+ required_version = ">= 1.5.0"
3
+
4
+ required_providers {
5
+ aws = {
6
+ source = "hashicorp/aws"
7
+ version = "~> 5.0"
8
+ }
9
+ tls = {
10
+ source = "hashicorp/tls"
11
+ version = "~> 4.0"
12
+ }
13
+ }
14
+
15
+ backend "s3" {
16
+ bucket = "{{STATE_BUCKET}}"
17
+ key = "state/terraform.tfstate"
18
+ region = "{{REGION}}"
19
+ encrypt = true
20
+ use_lockfile = true
21
+ }
22
+ }
@@ -0,0 +1,80 @@
1
+ # Fetch the AWS Managed Cache Policy (Optimized for standard web traffic)
2
+ data "aws_cloudfront_cache_policy" "optimized" {
3
+ name = "Managed-CachingOptimized"
4
+ }
5
+
6
+ # Fetch the AWS Managed Origin Request Policy (Passes query strings/cookies to your app)
7
+ data "aws_cloudfront_origin_request_policy" "all_viewer" {
8
+ name = "Managed-AllViewerExceptHostHeader"
9
+ }
10
+
11
+ # trivy:ignore:AVD-AWS-0011 - WAF is omitted by default to prevent unexpected monthly costs for users
12
+ resource "aws_cloudfront_distribution" "cdn" {
13
+ enabled = true
14
+ is_ipv6_enabled = true
15
+ wait_for_deployment = false # Prevents Terraform from hanging for 10+ minutes
16
+
17
+ origin {
18
+ # Point the CDN at the API Gateway HTTP API provisioned in main.tf
19
+ domain_name = replace(aws_apigatewayv2_api.main.api_endpoint, "https://", "")
20
+ origin_id = "ApiGatewayOrigin"
21
+
22
+ custom_origin_config {
23
+ http_port = 80
24
+ https_port = 443
25
+ # API Gateway HTTP API endpoints are HTTPS-only
26
+ origin_protocol_policy = "https-only"
27
+ origin_ssl_protocols = ["TLSv1.2"]
28
+ }
29
+ }
30
+
31
+ default_cache_behavior {
32
+ # Allow all HTTP methods so API POST/PUT requests still work
33
+ allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
34
+ cached_methods = ["GET", "HEAD"]
35
+ target_origin_id = "ApiGatewayOrigin"
36
+
37
+ viewer_protocol_policy = "redirect-to-https"
38
+
39
+ # Attach the Managed Policies
40
+ cache_policy_id = data.aws_cloudfront_cache_policy.optimized.id
41
+ origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer.id
42
+ }
43
+
44
+ restrictions {
45
+ geo_restriction {
46
+ restriction_type = "none"
47
+ }
48
+ }
49
+
50
+ viewer_certificate {
51
+ # Gives the user a free HTTPS *.cloudfront.net domain out of the box
52
+ cloudfront_default_certificate = true
53
+ }
54
+ }
55
+
56
+ output "cloudfront_url" {
57
+ description = "Your globally cached, HTTPS-secured application URL"
58
+ value = "https://${aws_cloudfront_distribution.cdn.domain_name}"
59
+ }
60
+
61
+ output "z_NEXT_STEP_REQUIRED" {
62
+ value = <<EOT
63
+
64
+ ====================================================================
65
+ 🚀 INFRASTRUCTURE PROVISIONED SUCCESSFULLY!
66
+ ====================================================================
67
+
68
+ Your AWS environment is ready. However, your URLs listed above will
69
+ return an error until your application code is actually deployed.
70
+
71
+ TO DEPLOY YOUR APP:
72
+ 1. git add .
73
+ 2. git commit -m "ci: configure grada"
74
+ 3. git push origin main
75
+
76
+ Once the GitHub Action completes, your site will be live at the
77
+ CloudFront URL!
78
+ ====================================================================
79
+ EOT
80
+ }
@@ -0,0 +1,80 @@
1
+ # Fetch the AWS Managed Cache Policy (Optimized for standard web traffic)
2
+ data "aws_cloudfront_cache_policy" "optimized" {
3
+ name = "Managed-CachingOptimized"
4
+ }
5
+
6
+ # Fetch the AWS Managed Origin Request Policy (Passes query strings/cookies to your app)
7
+ data "aws_cloudfront_origin_request_policy" "all_viewer" {
8
+ name = "Managed-AllViewerExceptHostHeader"
9
+ }
10
+
11
+ # trivy:ignore:AVD-AWS-0011 - WAF is omitted by default to prevent unexpected monthly costs for users
12
+ resource "aws_cloudfront_distribution" "cdn" {
13
+ enabled = true
14
+ is_ipv6_enabled = true
15
+ wait_for_deployment = false # Prevents Terraform from hanging for 10+ minutes
16
+
17
+ origin {
18
+ # Point the CDN at the Load Balancer we provision in main.tf
19
+ domain_name = aws_lb.main.dns_name
20
+ origin_id = "ALBOrigin"
21
+
22
+ custom_origin_config {
23
+ http_port = 80
24
+ https_port = 443
25
+ # Because this is the free tier without a custom SSL domain, we route internally via HTTP
26
+ origin_protocol_policy = "http-only"
27
+ origin_ssl_protocols = ["TLSv1.2"]
28
+ }
29
+ }
30
+
31
+ default_cache_behavior {
32
+ # Allow all HTTP methods so API POST/PUT requests still work
33
+ allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
34
+ cached_methods = ["GET", "HEAD"]
35
+ target_origin_id = "ALBOrigin"
36
+
37
+ viewer_protocol_policy = "redirect-to-https"
38
+
39
+ # Attach the Managed Policies
40
+ cache_policy_id = data.aws_cloudfront_cache_policy.optimized.id
41
+ origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer.id
42
+ }
43
+
44
+ restrictions {
45
+ geo_restriction {
46
+ restriction_type = "none"
47
+ }
48
+ }
49
+
50
+ viewer_certificate {
51
+ # Gives the user a free HTTPS *.cloudfront.net domain out of the box
52
+ cloudfront_default_certificate = true
53
+ }
54
+ }
55
+
56
+ output "cloudfront_url" {
57
+ description = "Your globally cached, HTTPS-secured application URL"
58
+ value = "https://${aws_cloudfront_distribution.cdn.domain_name}"
59
+ }
60
+
61
+ output "z_NEXT_STEP_REQUIRED" {
62
+ value = <<EOT
63
+
64
+ ====================================================================
65
+ 🚀 INFRASTRUCTURE PROVISIONED SUCCESSFULLY!
66
+ ====================================================================
67
+
68
+ Your AWS environment is ready. However, your URLs listed above will
69
+ return an error until your application code is actually deployed.
70
+
71
+ TO DEPLOY YOUR APP:
72
+ 1. git add .
73
+ 2. git commit -m "ci: configure grada"
74
+ 3. git push origin main
75
+
76
+ Once the GitHub Action completes, your site will be live at the
77
+ CloudFront URL!
78
+ ====================================================================
79
+ EOT
80
+ }
@@ -0,0 +1,92 @@
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 Aurora PostgreSQL Serverless v2 Cluster (scale-to-zero)
33
+ resource "aws_rds_cluster" "postgres" {
34
+ cluster_identifier = "${local.app_name}-db-cluster"
35
+ engine = "aurora-postgresql"
36
+
37
+ # engine_version is intentionally omitted: AWS retires Aurora minor versions
38
+ # (e.g. 16.4 in us-east-2), so new clusters take the regional default.
39
+ # Strip dashes for the database name: Aurora database_name must begin with a
40
+ # letter and contain only alphanumeric characters (e.g. my-project -> myproject)
41
+ database_name = replace(local.app_name, "-", "")
42
+ master_username = "dbadmin"
43
+
44
+ # AWS automatically creates and manages the secret in Secrets Manager!
45
+ manage_master_user_password = true
46
+
47
+ db_subnet_group_name = aws_db_subnet_group.main.name
48
+ vpc_security_group_ids = [aws_security_group.rds.id]
49
+
50
+ skip_final_snapshot = true
51
+ storage_encrypted = true
52
+
53
+ serverlessv2_scaling_configuration {
54
+ min_capacity = 0
55
+ max_capacity = 2
56
+ seconds_until_auto_pause = 300
57
+ }
58
+
59
+ lifecycle {
60
+ # RDS auto-assigns AZ ordering beyond our 2-subnet group; never treat
61
+ # that drift as a change on subsequent applies.
62
+ ignore_changes = [availability_zones]
63
+ }
64
+ }
65
+
66
+ resource "aws_rds_cluster_instance" "postgres" {
67
+ identifier = "${local.app_name}-db-instance-1"
68
+ cluster_identifier = aws_rds_cluster.postgres.id
69
+ instance_class = "db.serverless"
70
+ engine = aws_rds_cluster.postgres.engine
71
+ engine_version = aws_rds_cluster.postgres.engine_version
72
+
73
+ publicly_accessible = false
74
+ }
75
+
76
+ # 4. IAM Permission for RDS Master Password Secret
77
+ resource "aws_iam_role_policy" "rds_secret_access" {
78
+ name = "${local.app_name}-rds-secret-policy"
79
+ role = aws_iam_role.execution_role.id
80
+ policy = jsonencode({
81
+ Version = "2012-10-17"
82
+ Statement = [
83
+ {
84
+ Effect = "Allow"
85
+ Action = ["secretsmanager:GetSecretValue"]
86
+ Resource = [
87
+ aws_rds_cluster.postgres.master_user_secret[0].secret_arn
88
+ ]
89
+ }
90
+ ]
91
+ })
92
+ }
@@ -0,0 +1,72 @@
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 = 3306
26
+ to_port = 3306
27
+ protocol = "tcp"
28
+ security_groups = [aws_security_group.ecs_tasks.id]
29
+ }
30
+ }
31
+
32
+ # 3. The MySQL Instance
33
+ resource "aws_db_instance" "postgres" {
34
+ identifier = "${local.app_name}-db"
35
+ engine = "mysql"
36
+ engine_version = "8.0"
37
+ instance_class = "db.t4g.micro"
38
+ allocated_storage = 20
39
+ storage_encrypted = true
40
+
41
+ # Strip dashes for the database name: RDS MySQL db_name must begin with a
42
+ # letter and contain only alphanumeric characters (e.g. my-project -> myproject)
43
+ db_name = replace(local.app_name, "-", "")
44
+ username = "dbadmin"
45
+
46
+ # AWS automatically creates and manages the secret in Secrets Manager!
47
+ manage_master_user_password = true
48
+
49
+ db_subnet_group_name = aws_db_subnet_group.main.name
50
+ vpc_security_group_ids = [aws_security_group.rds.id]
51
+
52
+ skip_final_snapshot = true
53
+ publicly_accessible = false
54
+ }
55
+
56
+ # 4. IAM Permission for RDS Master Password Secret
57
+ resource "aws_iam_role_policy" "rds_secret_access" {
58
+ name = "${local.app_name}-rds-secret-policy"
59
+ role = aws_iam_role.execution_role.id
60
+ policy = jsonencode({
61
+ Version = "2012-10-17"
62
+ Statement = [
63
+ {
64
+ Effect = "Allow"
65
+ Action = ["secretsmanager:GetSecretValue"]
66
+ Resource = [
67
+ aws_db_instance.postgres.master_user_secret[0].secret_arn
68
+ ]
69
+ }
70
+ ]
71
+ })
72
+ }