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,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}}
|