grada-run 0.0.2 → 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.
- package/.github/workflows/deploy-docs.yml +37 -0
- package/.github/workflows/iac-validation.yml +303 -0
- package/.github/workflows/publish.yml +68 -0
- package/.github/workflows/sync-bedrock-models.yml +57 -0
- package/.github/workflows/test.yml +43 -0
- package/.muserules +31 -0
- package/LICENSE +21 -0
- package/README.md +193 -3
- package/apps/docs/.astro/collections/docs.schema.json +644 -0
- package/apps/docs/.astro/content-assets.mjs +4 -0
- package/apps/docs/.astro/content-modules.mjs +4 -0
- package/apps/docs/.astro/content.d.ts +179 -0
- package/apps/docs/.astro/data-store.json +1 -0
- package/apps/docs/.astro/dev.json +14 -0
- package/apps/docs/.astro/settings.json +5 -0
- package/apps/docs/.astro/types.d.ts +2 -0
- package/apps/docs/astro.config.mjs +97 -0
- package/apps/docs/package.json +17 -0
- package/apps/docs/src/content/docs/adrs/0001-s3-native-state-locking.md +37 -0
- package/apps/docs/src/content/docs/adrs/0002-eject-mechanism-pure-iac.md +39 -0
- package/apps/docs/src/content/docs/adrs/0003-sync-ai-context-strategy.md +48 -0
- package/apps/docs/src/content/docs/adrs/0004-iac-driven-diagnostic-context.md +37 -0
- package/apps/docs/src/content/docs/adrs/0005-ecs-fargate-alb-runtime-target.md +38 -0
- package/apps/docs/src/content/docs/adrs/0006-github-oidc-no-stored-keys.md +37 -0
- package/apps/docs/src/content/docs/adrs/0007-framework-detection-with-fallback.md +37 -0
- package/apps/docs/src/content/docs/adrs/0008-secrets-names-in-git-values-in-aws.md +37 -0
- package/apps/docs/src/content/docs/adrs/0009-regenerate-with-backup-on-rerun.md +37 -0
- package/apps/docs/src/content/docs/adrs/0010-advisory-only-security-scans.md +37 -0
- package/apps/docs/src/content/docs/cli/add.md +84 -0
- package/apps/docs/src/content/docs/cli/apply.md +32 -0
- package/apps/docs/src/content/docs/cli/db.md +200 -0
- package/apps/docs/src/content/docs/cli/destroy.md +31 -0
- package/apps/docs/src/content/docs/cli/diagnose.md +37 -0
- package/apps/docs/src/content/docs/cli/doctor.md +28 -0
- package/apps/docs/src/content/docs/cli/domain.md +57 -0
- package/apps/docs/src/content/docs/cli/drift.md +40 -0
- package/apps/docs/src/content/docs/cli/eject.md +29 -0
- package/apps/docs/src/content/docs/cli/exec.md +49 -0
- package/apps/docs/src/content/docs/cli/gc.md +37 -0
- package/apps/docs/src/content/docs/cli/init.md +72 -0
- package/apps/docs/src/content/docs/cli/logs.md +39 -0
- package/apps/docs/src/content/docs/cli/rollback.md +51 -0
- package/apps/docs/src/content/docs/cli/secrets.md +73 -0
- package/apps/docs/src/content/docs/cli/sleep.md +53 -0
- package/apps/docs/src/content/docs/cli/status.md +34 -0
- package/apps/docs/src/content/docs/cli/sync-ai.md +27 -0
- package/apps/docs/src/content/docs/guides/architecture.md +87 -0
- package/apps/docs/src/content/docs/guides/aws-credentials.md +72 -0
- package/apps/docs/src/content/docs/guides/background-workers.md +45 -0
- package/apps/docs/src/content/docs/guides/cicd-pipeline.md +64 -0
- package/apps/docs/src/content/docs/guides/database-connections.md +64 -0
- package/apps/docs/src/content/docs/guides/docker-compose.md +37 -0
- package/apps/docs/src/content/docs/guides/dockerfiles.md +46 -0
- package/apps/docs/src/content/docs/guides/ephemeral-pr-previews.md +39 -0
- package/apps/docs/src/content/docs/guides/examples.md +50 -0
- package/apps/docs/src/content/docs/guides/frameworks.md +88 -0
- package/apps/docs/src/content/docs/guides/headless.md +75 -0
- package/apps/docs/src/content/docs/guides/quickstart.md +52 -0
- package/apps/docs/src/content/docs/guides/rerun-init.md +43 -0
- package/apps/docs/src/content/docs/guides/secrets-management.md +83 -0
- package/apps/docs/src/content/docs/guides/understanding-your-bill.md +63 -0
- package/apps/docs/src/content/docs/index.mdx +103 -0
- package/apps/docs/src/content/docs/migrations/astro-vercel-to-aws.md +55 -0
- package/apps/docs/src/content/docs/migrations/heroku-procfile-to-aws.md +41 -0
- package/apps/docs/src/content/docs/migrations/nextjs-vercel-to-aws.md +51 -0
- package/apps/docs/src/content/docs/migrations/sveltekit-vercel-to-aws.md +63 -0
- package/apps/docs/src/content/docs/roadmap.md +97 -0
- package/apps/docs/src/content/docs/testing-strategy.md +32 -0
- package/apps/docs/src/content.config.ts +7 -0
- package/apps/docs/src/custom.css +14 -0
- package/apps/docs/tsconfig.json +6 -0
- package/bin/cli.js +140 -0
- package/package.json +105 -7
- package/scripts/sync-bedrock-models.js +22 -0
- package/scripts/test-iac.js +261 -0
- package/specs/add-redis-sqs-bedrock.md +128 -0
- package/specs/add-storage-dynamodb.md +106 -0
- package/specs/bedrock-model-catalog.md +131 -0
- package/specs/ci-pipeline.md +17 -0
- package/specs/cost-transparency.md +115 -0
- package/specs/custom-domains-and-ses.md +153 -0
- package/specs/database-suite-expansion.md +151 -0
- package/specs/db-connect.md +69 -0
- package/specs/db-lifecycle-migrations.md +159 -0
- package/specs/dependency-aware-init.md +176 -0
- package/specs/deploy-stack-to-grada-run-rebrand.md +45 -0
- package/specs/deployment-safety.md +170 -0
- package/specs/diagnose.md +16 -0
- package/specs/docs-hub.md +16 -0
- package/specs/dx-polish.md +46 -0
- package/specs/exec.md +25 -0
- package/specs/finops-cron-drift.md +161 -0
- package/specs/gc.md +26 -0
- package/specs/integration-suite.md +16 -0
- package/specs/logs.md +32 -0
- package/specs/rollback-live-polling.md +40 -0
- package/specs/secrets-pull-audit.md +51 -0
- package/specs/serverless-lambda-target.md +133 -0
- package/specs/status.md +31 -0
- package/specs/telemetry-and-spawn-hardening.md +69 -0
- package/specs/telemetry-hardening.md +35 -0
- package/src/commands/add.js +1111 -0
- package/src/commands/apply.js +214 -0
- package/src/commands/db/backup.js +229 -0
- package/src/commands/db/connect.js +304 -0
- package/src/commands/db/enable-vector.js +344 -0
- package/src/commands/db/import.js +604 -0
- package/src/commands/db/migrate.js +477 -0
- package/src/commands/db/restore.js +361 -0
- package/src/commands/db.js +87 -0
- package/src/commands/destroy.js +217 -0
- package/src/commands/diagnose.js +460 -0
- package/src/commands/doctor.js +109 -0
- package/src/commands/domain.js +685 -0
- package/src/commands/drift.js +243 -0
- package/src/commands/eject.js +127 -0
- package/src/commands/exec.js +222 -0
- package/src/commands/gc.js +250 -0
- package/src/commands/init.js +649 -0
- package/src/commands/logs.js +256 -0
- package/src/commands/rollback.js +323 -0
- package/src/commands/secrets.js +485 -0
- package/src/commands/sleep.js +347 -0
- package/src/commands/status.js +309 -0
- package/src/commands/sync-ai.js +115 -0
- package/src/commands/wake.js +337 -0
- package/src/core/parser.js +126 -0
- package/src/core/telemetry.js +244 -0
- package/src/data/bedrock-models.json +896 -0
- package/src/utils/addons.js +126 -0
- package/src/utils/ai-rules.js +59 -0
- package/src/utils/args.js +91 -0
- package/src/utils/aws.js +178 -0
- package/src/utils/backup.js +69 -0
- package/src/utils/bedrock-catalog.js +511 -0
- package/src/utils/capabilities.js +500 -0
- package/src/utils/command.js +65 -0
- package/src/utils/db-tunnel.js +164 -0
- package/src/utils/detector.js +298 -0
- package/src/utils/dockerCompose.js +65 -0
- package/src/utils/domains.js +73 -0
- package/src/utils/ecs-runner.js +289 -0
- package/src/utils/ecs.js +92 -0
- package/src/utils/frameworks.js +55 -0
- package/src/utils/generator.js +527 -0
- package/src/utils/hcl.js +426 -0
- package/src/utils/lambda-ecr.js +185 -0
- package/src/utils/prompts.js +278 -0
- package/src/utils/rds.js +131 -0
- package/src/utils/resolvers.js +174 -0
- package/src/utils/sleep-state.js +140 -0
- package/src/utils/sleep-targets.js +139 -0
- package/src/utils/system.js +42 -0
- package/src/utils/terraform.js +70 -0
- package/src/utils/visualizer.js +381 -0
- package/src/utils/warnings.js +49 -0
- package/templates/README.md +150 -0
- package/templates/docker/django.Dockerfile +40 -0
- package/templates/docker/go.Dockerfile +23 -0
- package/templates/docker/nestjs.Dockerfile +33 -0
- package/templates/docker/nextjs.Dockerfile +55 -0
- package/templates/docker/node.Dockerfile +24 -0
- package/templates/docker/nuxt.Dockerfile +47 -0
- package/templates/docker/python.Dockerfile +38 -0
- package/templates/docker/rails.Dockerfile +59 -0
- package/templates/docker/static.Dockerfile +32 -0
- package/templates/docker/svelte.Dockerfile +52 -0
- package/templates/github/deploy-lambda.yml +120 -0
- package/templates/github/deploy.yml +138 -0
- package/templates/github/drift.yml +112 -0
- package/templates/github/preview-lambda.yml +86 -0
- package/templates/github/preview.yml +69 -0
- package/templates/github/teardown.yml +43 -0
- package/templates/terraform/addons/bedrock.tf +34 -0
- package/templates/terraform/addons/cron-lambda.tf +78 -0
- package/templates/terraform/addons/cron.tf +101 -0
- package/templates/terraform/addons/dynamodb.tf +73 -0
- package/templates/terraform/addons/redis.tf +64 -0
- package/templates/terraform/addons/s3.tf +143 -0
- package/templates/terraform/addons/ses.tf +73 -0
- package/templates/terraform/addons/sqs.tf +67 -0
- package/templates/terraform/backend.tf +22 -0
- package/templates/terraform/cloudfront-lambda.tf +80 -0
- package/templates/terraform/cloudfront.tf +80 -0
- package/templates/terraform/database-aurora-postgresql.tf +92 -0
- package/templates/terraform/database-mysql.tf +72 -0
- package/templates/terraform/database.tf +71 -0
- package/templates/terraform/main-lambda.tf +229 -0
- package/templates/terraform/main.tf +296 -0
- package/templates/terraform/network.tf +95 -0
- package/templates/terraform/oidc.tf +64 -0
- package/templates/terraform/secrets.tf +31 -0
- package/templates/terraform/worker.tf +69 -0
- package/tests/__snapshots__/generator.test.js.snap +9633 -0
- package/tests/add.test.js +2037 -0
- package/tests/ai.test.js +94 -0
- package/tests/apply.test.js +488 -0
- package/tests/args.test.js +86 -0
- package/tests/aws.test.js +244 -0
- package/tests/capabilities.test.js +307 -0
- package/tests/cli.test.js +29 -0
- package/tests/command.test.js +100 -0
- package/tests/commands-import.test.js +74 -0
- package/tests/db.test.js +2704 -0
- package/tests/destroy.test.js +391 -0
- package/tests/detector.test.js +79 -0
- package/tests/diagnose.test.js +779 -0
- package/tests/doctor.test.js +202 -0
- package/tests/domain.test.js +899 -0
- package/tests/drift.test.js +243 -0
- package/tests/ecs.test.js +130 -0
- package/tests/eject.test.js +65 -0
- package/tests/exec.test.js +380 -0
- package/tests/gc.test.js +496 -0
- package/tests/generator.test.js +794 -0
- package/tests/headless.test.js +562 -0
- package/tests/lambda-ecr.test.js +185 -0
- package/tests/logs.test.js +447 -0
- package/tests/parser.test.js +160 -0
- package/tests/rds.test.js +244 -0
- package/tests/resolvers.test.js +282 -0
- package/tests/rollback.test.js +692 -0
- package/tests/secrets.test.js +752 -0
- package/tests/sleep-wake.test.js +1016 -0
- package/tests/status.test.js +370 -0
- package/tests/system.test.js +70 -0
- package/tests/telemetry.test.js +520 -0
- package/tests/terraform.test.js +84 -0
- package/tests/visualizer.test.js +496 -0
- package/vitest.config.js +9 -0
- 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
|
+
}
|