From 7e703f51b73bcf8e35e6cc1cf5f00f30be78d9b4 Mon Sep 17 00:00:00 2001 From: ifLov Date: Tue, 1 Sep 2026 09:08:42 +0900 Subject: [PATCH] feat(terraform): make the ECS starter apply-ready under model B Derived projects will reference a versioned module rather than copy the starter (docs/terraform-repo-boundary.md). A referenced module cannot be edited by its consumer, so every value a copy would have edited has to become an input. - backend.tf: add the empty `backend "s3" {}` block. Without it, `-backend-config=backend.hcl` warned and initialized a local backend. It lives in its own file because a child module cannot declare a backend, so it moves to the consumer root on extraction. - cors_origins: required input replacing the hardcoded "https://replace-me.example", which *passed* the app's production validation (has a scheme, not a wildcard) so the task booted fine and only browsers were blocked. Validation mirrors the app's rules at plan time instead of at task startup. - trust_proxy_hops: inject TRUST_PROXY_HOPS, default 1 for the single ALB this config builds. It was never injected, so the app defaulted to 0 and ThrottlerGuard bucketed every user under the ALB IP. - enable_sqs: gate the queues, DLQ, IAM policy and MESSAGE_QUEUE_URL, default false. The app's messaging root is BullMQ/Redis and nothing reads MESSAGE_QUEUE_URL. Kept as an input rather than deleted because a consumer of a referenced module has no other way to turn it on. - infra/terraform/.gitignore: the root ignore rules are root-relative and would not follow the directory out of this repo. - terraform.yml: fmt/init -backend=false/validate, scoped to infra/terraform/**. ci.yml gets paths-ignore so terraform-only changes stop running the app pipeline. CI first, then the split, so terraform is never left unchecked. plan/apply stay out of CI: the backend block now requires backend initialization, and no OIDC role or approval path exists yet. This config has still never been applied. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DrsDBRXt7sYCYxt5gmFMVy --- .github/workflows/ci.yml | 10 ++++ .github/workflows/terraform.yml | 46 ++++++++++++++++++ README.md | 15 ++++-- docs/terraform-repo-boundary.md | 36 ++++++++++++-- infra/terraform/.gitignore | 22 +++++++++ infra/terraform/README.md | 47 +++++++++++++++++-- infra/terraform/backend.tf | 10 ++++ infra/terraform/compute.tf | 16 +++++-- infra/terraform/data.tf | 6 ++- infra/terraform/outputs.tf | 2 +- infra/terraform/terraform.tfvars.example | 14 ++++++ infra/terraform/variables.tf | 60 ++++++++++++++++++++++++ 12 files changed, 264 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/terraform.yml create mode 100644 infra/terraform/.gitignore create mode 100644 infra/terraform/backend.tf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e264546..1632012 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,19 @@ name: CI +# terraform-only 변경은 이 파이프라인을 태우지 않는다 — `terraform.yml`이 대신 돈다. +# `pull_request`와 `push`는 별개 이벤트라 양쪽에 모두 필요하다. +# +# ⚠ main에 branch protection이나 ruleset을 걸 때 이 job을 required check로 +# 지정하면, skip된 실행이 Pending으로 남아 terraform-only PR이 영구히 막힌다. +# 그때는 required check를 두지 말고 별도 gate job을 쓴다. on: pull_request: + paths-ignore: + - 'infra/terraform/**' push: branches: [main] + paths-ignore: + - 'infra/terraform/**' permissions: contents: read diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml new file mode 100644 index 0000000..8165b7f --- /dev/null +++ b/.github/workflows/terraform.yml @@ -0,0 +1,46 @@ +# terraform 전용 검사. `ci.yml`에 job으로 얹지 않고 파일을 분리한 이유는 +# 이 구성이 결국 자기 저장소로 나가기 때문이다 — 그때 이 파일이 함께 이동한다. +# +# 자격증명이 필요 없는 검사만 여기서 돈다. backend 블록이 생긴 뒤로 +# `plan`은 backend 초기화를 요구하므로 `-backend=false`로 끊는다. +# plan/apply는 OIDC role이 붙은 별도 워크플로의 몫이며 아직 없다. +name: Terraform + +on: + pull_request: + paths: + - 'infra/terraform/**' + - '.github/workflows/terraform.yml' + push: + branches: [main] + paths: + - 'infra/terraform/**' + - '.github/workflows/terraform.yml' + +permissions: + contents: read + +defaults: + run: + working-directory: infra/terraform + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.16.0 + terraform_wrapper: false + + - name: fmt + run: terraform fmt -check -recursive -diff + + # provider 스키마만 받고 backend는 건드리지 않는다. + # `.terraform.lock.hcl`이 커밋돼 있으므로 provider 버전이 고정된다. + - name: init + run: terraform init -backend=false -input=false + + - name: validate + run: terraform validate diff --git a/README.md b/README.md index b9fe0bc..6a3bc61 100644 --- a/README.md +++ b/README.md @@ -109,13 +109,14 @@ Matt Pocock의 안정 `engineering`/`productivity` 스킬을 두 에이전트에 ## AWS Terraform -`infra/terraform`은 ECS Fargate, ALB, RDS(PostgreSQL/MySQL/MariaDB), 선택형 ElastiCache Valkey, -SQS/DLQ의 시작 구성입니다. MongoDB는 DocumentDB로 가정하지 않고 외부 관리형 Mongo URI를 Secrets -Manager로 주입할 수 있게 둡니다. +`infra/terraform`은 ECS Fargate, ALB, RDS(PostgreSQL/MySQL/MariaDB), 선택형 ElastiCache Valkey의 +시작 구성입니다. MongoDB는 DocumentDB로 가정하지 않고 외부 관리형 Mongo URI를 Secrets Manager로 +주입할 수 있게 둡니다. SQS/DLQ는 `enable_sqs`로 끌 수 있으며 기본은 꺼짐입니다 — 이 앱의 messaging +composition root는 BullMQ/Redis입니다. ```bash cd infra/terraform -cp terraform.tfvars.example terraform.tfvars +cp terraform.tfvars.example terraform.tfvars # cors_origins는 필수입니다 terraform init -backend-config=backend.hcl terraform plan ``` @@ -123,6 +124,12 @@ terraform plan 실제 backend와 secret 값은 커밋하지 않습니다. 처음에는 `backend.hcl.example`을 환경별 파일로 복사하고, state bucket/versioning/lock 설정을 먼저 준비합니다. +이 구성은 [별도 저장소로 분리해 버전 모듈로 참조](./docs/terraform-repo-boundary.md)하는 방향으로 +갑니다. 그래서 파생 프로젝트가 고쳤을 값은 사본을 편집하는 대신 input으로 노출합니다. + +terraform-only 변경은 앱 CI를 태우지 않고 `.github/workflows/terraform.yml`이 fmt/init/validate를 +돌립니다. `plan`/`apply`는 아직 CI에 없습니다. + ## 검증 ```bash diff --git a/docs/terraform-repo-boundary.md b/docs/terraform-repo-boundary.md index 2b0f476..9df2451 100644 --- a/docs/terraform-repo-boundary.md +++ b/docs/terraform-repo-boundary.md @@ -1,8 +1,14 @@ # Terraform 저장소 경계 -> **상태: 미결정 — 내일 이어서** -> `infra/terraform`을 앱 저장소 밖으로 뺄 것인가. 3라운드 교차검증 결과와 아직 내리지 않은 결정. -> 2026-08-31 +> **상태: 모델 B 선택됨 (2026-09-01). 분리는 아직 실행하지 않았다.** +> `infra/terraform`을 앱 저장소 밖으로 뺄 것인가. 3라운드 교차검증 결과와 내린 결정. +> 조사 2026-08-31 · 결정 2026-09-01 +> +> **결정: 모델 B — 파생 프로젝트는 버전 모듈을 참조한다.** 따라서 별도 repo가 최종 목적지다. +> 다만 아래 "추천과 그 근거"의 순서 제약은 그대로 유효해서, 지금 옮기지 않는다 — +> 모듈 인터페이스는 한 번 apply해서 무엇이 입력이고 무엇이 프로젝트별인지 드러난 뒤에 정한다. +> B는 대신 그 사이 작업의 **내용**을 바꿨다: 파생 프로젝트가 사본에서 고쳤을 값이 +> 전부 input이 되어야 한다. 참조 모듈은 편집할 수 없기 때문이다. ## 먼저 정할 것은 repo가 아니다 @@ -91,6 +97,26 @@ Deployment/Service/Ingress까지 붙어 2~4배로 추정된다. 검증 중에 드러난 것들이다. **같은 저장소에 있는데도 이 계약들이 이미 어긋나 있다**는 게 요점 — 별도 repo로 옮긴다고 정렬되지 않고, 발견만 늦어진다. +> **2026-09-01 현재 상태.** 아래 4개 중 3개를 고쳤다. 남은 하나(이미지 전달 경로)는 +> AWS 계정 쪽 결정(OIDC role, apply 승인 주체)이 먼저다. +> +> | 결함 | 상태 | 어떻게 | +> | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- | +> | 소비자 없는 SQS | 고침 | 지우지 않고 `var.enable_sqs`(기본 `false`)로 선택지화. 모델 B에서 소비자가 고칠 수 없으므로 문을 남겼다. | +> | `TRUST_PROXY_HOPS` | 고침 | `var.trust_proxy_hops`(기본 `1` = ALB 하나), 0–10 정수 validation이 앱 Joi와 같은 범위. | +> | backend 블록 부재 | 고침 | `backend.tf`에 `backend "s3" {}`. 별도 파일인 이유는 backend가 하위 모듈에 존재할 수 없어서 — 모듈화 시 소비 root로 이동한다. | +> | 이미지 전달 경로 | **남음** | ECR build/push, 태그 전달, apply 승인 주체 미정. | +> +> 함께 한 것: `CORS_ORIGINS`가 `"https://replace-me.example"` 하드코딩이었는데 앱의 production +> 검증을 **통과**해서(스킴 있음·와일드카드 아님) task는 뜨고 브라우저만 조용히 막혔다. +> 기본값 없는 `var.cors_origins`로 올리고 앱의 규칙을 plan 시점 validation으로 당겼다. +> `infra/terraform/.gitignore`도 추가했다 — root의 `infra/terraform/...` 규칙은 경로가 +> root 기준이라 디렉터리가 나가면 따라가지 못한다. +> +> CI: `.github/workflows/terraform.yml`이 `fmt -check`/`init -backend=false`/`validate`를 돌리고, +> `ci.yml`에 `paths-ignore`를 넣어 terraform-only 변경이 앱 파이프라인을 태우지 않게 했다. +> 순서는 아래 4번대로 CI를 먼저 만든 뒤 끊었다. + ### 소비자 없는 SQS terraform이 SQS + DLQ + IAM을 만들고 `MESSAGE_QUEUE_URL`을 ECS task에 주입하는데, @@ -152,8 +178,8 @@ apply하지 않는다 — apply하는 건 파생된 각 프로젝트가 자기 ## 내일 시작점 -1. **모델 A냐 B냐를 먼저 정한다.** - 복제냐 버전 모듈 참조냐. 이게 저장소 경계를 결정한다. 나머지 단계는 여기에 종속된다. +1. ~~**모델 A냐 B냐를 먼저 정한다.**~~ → **B(버전 모듈 참조)로 정했다 (2026-09-01).** + 저장소 경계는 별도 repo로 결정됐다. 다만 실행은 3번 이후다. 2. **ECS starter를 실행 가능한 단위로 완성한다.** `backend "s3"` 블록 추가 + `infra/terraform/.gitignore`, `TRUST_PROXY_HOPS` 주입, diff --git a/infra/terraform/.gitignore b/infra/terraform/.gitignore new file mode 100644 index 0000000..545c206 --- /dev/null +++ b/infra/terraform/.gitignore @@ -0,0 +1,22 @@ +# 이 디렉터리는 앱 저장소 밖으로 나갈 수 있으므로 무시 규칙을 자기 안에 둔다. +# 저장소 root의 `infra/terraform/...` 규칙은 경로가 root 기준이라 함께 따라가지 못한다. + +# provider 바이너리와 backend 메타데이터. `.terraform.lock.hcl`은 반대로 **커밋한다**. +.terraform/ + +# state는 backend에만 둔다. local backend로 잘못 init하면 여기 생긴다. +*.tfstate +*.tfstate.* + +# 실제 값. `*.example`만 커밋한다. +backend.hcl +terraform.tfvars +*.auto.tfvars +*.auto.tfvars.json + +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json diff --git a/infra/terraform/README.md b/infra/terraform/README.md index 8c42175..2dab829 100644 --- a/infra/terraform/README.md +++ b/infra/terraform/README.md @@ -1,8 +1,8 @@ # AWS Terraform starter -기본 프로필은 2개 AZ VPC, ALB, ECS Fargate, RDS, 선택적 ElastiCache Valkey, SQS와 DLQ를 만듭니다. +기본 프로필은 2개 AZ VPC, ALB, ECS Fargate, RDS, 선택적 ElastiCache Valkey를 만듭니다. MongoDB는 DocumentDB를 동일 제품처럼 취급하지 않으며, Atlas 등 실제 MongoDB URI를 AWS Secrets -Manager ARN으로 주입합니다. +Manager ARN으로 주입합니다. SQS/DLQ는 `enable_sqs`로 켜며 기본은 꺼짐입니다. ```bash cp terraform.tfvars.example terraform.tfvars @@ -11,9 +11,48 @@ terraform plan terraform apply ``` +## 소비자가 반드시 넘겨야 하는 값 + +| 변수 | 기본값 | 왜 여기 있는가 | +| ----------------- | ------ | -------------------------------------------------------------------------------------------- | +| `container_image` | 없음 | 배포할 이미지 URI. ECR build/push 경로는 아직 CI에 없습니다. | +| `cors_origins` | 없음 | 브라우저 Origin 목록. 스킴 필수, `*` 금지 — 앱의 production 검증을 plan 시점으로 당겼습니다. | + +## 토폴로지가 정하는 값 + +| 변수 | 기본값 | 근거 | +| ------------------ | ------- | ------------------------------------------------------------------------------------------------------- | +| `trust_proxy_hops` | `1` | 이 구성은 ALB 하나입니다. CloudFront를 얹으면 2. 실제보다 크게 잡으면 XFF 위조로 rate limit이 뚫립니다. | +| `enable_redis` | `true` | BullMQ와 캐시의 backend입니다. | +| `enable_sqs` | `false` | 앱에 `MESSAGE_QUEUE_URL` 소비자가 없습니다. 켜려면 `MessageQueuePort`의 SQS adapter를 먼저 만듭니다. | + +## backend + +`backend.tf`의 `backend "s3" {}`는 빈 블록이지만 지우면 안 됩니다. partial configuration도 +backend **type**은 구성 안에서 정해야 하고, 없으면 `-backend-config=backend.hcl`이 +`Warning: Missing backend configuration`을 내고 local backend로 초기화됩니다. + 상태 버킷은 이 구성과 별도 bootstrap에서 만들고 versioning을 켜십시오. `backend.hcl`은 커밋하지 -않으며 예제의 S3 `use_lockfile`을 사용합니다. 운영에서는 HTTPS listener/ACM, WAF, private ECS -subnet+NAT/VPC endpoints, autoscaling과 secret rotation을 환경 요구에 맞게 추가해야 합니다. +않으며 예제의 S3 `use_lockfile`을 사용합니다. + +`backend.tf`와 `versions.tf`의 `provider` 블록은 **root 전용**입니다. 이 구성을 재사용 모듈로 +빼면 둘 다 소비 프로젝트의 root로 이동합니다 — 하위 모듈은 backend를 가질 수 없고, provider를 +가지면 그 모듈에 `count`/`for_each`/`depends_on`을 쓸 수 없습니다. + +## CI + +`.github/workflows/terraform.yml`이 `infra/terraform/**` 변경에만 반응해 +`fmt -check` / `init -backend=false` / `validate`를 돌립니다. 자격증명이 필요 없는 검사만 +있으며 `plan`/`apply`는 아직 없습니다 — OIDC role과 승인 경로가 정해지지 않았습니다. + +## 아직 없는 것 + +- **한 번도 apply된 적이 없습니다.** 이 구성이 실제로 뜨는지 미검증입니다. +- ECR build/push와 이미지 태그 전달 경로. `Dockerfile`의 `EXPOSE 3000`도 + `var.container_port`와 무관하게 고정입니다. +- HTTPS listener/ACM, WAF, private ECS subnet + NAT/VPC endpoints, autoscaling, secret rotation. + 현재 ECS task는 public subnet에 `assign_public_ip = true`로 뜹니다 — private subnet에는 + route table도 NAT도 없습니다. AWS 자격증명을 `.tf`나 `.tfvars`에 넣지 마십시오. AWS CLI profile, 환경변수 또는 CI의 OIDC role을 사용합니다. diff --git a/infra/terraform/backend.tf b/infra/terraform/backend.tf new file mode 100644 index 0000000..4f04361 --- /dev/null +++ b/infra/terraform/backend.tf @@ -0,0 +1,10 @@ +# 이 블록은 **root 전용**이다. backend는 하위 모듈에 존재할 수 없으므로, +# 이 구성을 재사용 모듈로 빼면 이 파일은 소비 프로젝트의 root로 이동한다. +# +# 빈 블록이지만 지워선 안 된다. partial configuration도 backend **type**은 +# 구성 안에서 정해야 하고, 이 블록이 없으면 `-backend-config=backend.hcl`은 +# `Warning: Missing backend configuration`을 내고 local backend로 초기화된다. +# 나머지 값(bucket/key/region)은 `backend.hcl`이 채운다. +terraform { + backend "s3" {} +} diff --git a/infra/terraform/compute.tf b/infra/terraform/compute.tf index 4cf6ad2..af1e5cf 100644 --- a/infra/terraform/compute.tf +++ b/infra/terraform/compute.tf @@ -53,6 +53,8 @@ resource "aws_iam_role" "ecs_task" { } resource "aws_iam_role_policy" "messages" { + count = var.enable_sqs ? 1 : 0 + name = "messages" role = aws_iam_role.ecs_task.id @@ -68,15 +70,16 @@ resource "aws_iam_role_policy" "messages" { "sqs:SendMessage" ] Effect = "Allow" - Resource = aws_sqs_queue.messages.arn + Resource = aws_sqs_queue.messages[0].arn }] }) } locals { - app_environment = [ + app_environment = concat([ { name = "NODE_ENV", value = "production" }, { name = "APP_PORT", value = tostring(var.container_port) }, + { name = "TRUST_PROXY_HOPS", value = tostring(var.trust_proxy_hops) }, { name = "DB_TYPE", value = local.db_driver }, { name = "DB_HOST", value = aws_db_instance.this.address }, { name = "DB_PORT", value = tostring(local.db_port) }, @@ -86,10 +89,13 @@ locals { { name = "REDIS_HOST", value = var.enable_redis ? aws_elasticache_replication_group.this[0].primary_endpoint_address : "localhost" }, { name = "BULLMQ_ENABLED", value = tostring(var.enable_redis) }, { name = "MONGODB_ENABLED", value = tostring(local.mongodb_on) }, - { name = "MESSAGE_QUEUE_URL", value = aws_sqs_queue.messages.url }, { name = "PROMETHEUS_ENABLED", value = "true" }, - { name = "CORS_ORIGINS", value = "https://replace-me.example" } - ] + { name = "CORS_ORIGINS", value = join(",", var.cors_origins) } + ], + # 앱은 allowUnknown이라 이 변수가 남아도 부팅은 되지만, 큐가 없으면 + # 가리킬 URL도 없다. 켰을 때만 주입한다. + var.enable_sqs ? [{ name = "MESSAGE_QUEUE_URL", value = aws_sqs_queue.messages[0].url }] : [] + ) app_secrets = concat( [{ diff --git a/infra/terraform/data.tf b/infra/terraform/data.tf index 05d4597..459fc95 100644 --- a/infra/terraform/data.tf +++ b/infra/terraform/data.tf @@ -44,16 +44,20 @@ resource "aws_elasticache_replication_group" "this" { } resource "aws_sqs_queue" "dead_letter" { + count = var.enable_sqs ? 1 : 0 + name = "${local.name}-dlq" message_retention_seconds = 1209600 } resource "aws_sqs_queue" "messages" { + count = var.enable_sqs ? 1 : 0 + name = "${local.name}-messages" visibility_timeout_seconds = 60 redrive_policy = jsonencode({ - deadLetterTargetArn = aws_sqs_queue.dead_letter.arn + deadLetterTargetArn = aws_sqs_queue.dead_letter[0].arn maxReceiveCount = 5 }) } diff --git a/infra/terraform/outputs.tf b/infra/terraform/outputs.tf index 5b423a9..400c903 100644 --- a/infra/terraform/outputs.tf +++ b/infra/terraform/outputs.tf @@ -12,7 +12,7 @@ output "rds_master_secret_arn" { } output "message_queue_url" { - value = aws_sqs_queue.messages.url + value = var.enable_sqs ? aws_sqs_queue.messages[0].url : null } output "redis_endpoint" { diff --git a/infra/terraform/terraform.tfvars.example b/infra/terraform/terraform.tfvars.example index 771ac5e..33b4d6b 100644 --- a/infra/terraform/terraform.tfvars.example +++ b/infra/terraform/terraform.tfvars.example @@ -4,5 +4,19 @@ environment = "dev" container_image = "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/barebones:latest" db_engine = "postgres" +# 필수. 기본값이 없다 — 브라우저가 보내는 Origin과 정확히 일치해야 하고 +# 스킴을 포함해야 한다. production에서 "*"는 앱이 부팅을 거부한다. +cors_origins = ["https://app.example.com"] + +# 신뢰할 프록시 홉 수. 이 구성은 ALB 하나이므로 1이 기본값이다. +# CloudFront를 앞에 얹으면 2. 실제 토폴로지보다 크게 잡으면 클라이언트가 +# X-Forwarded-For를 위조해 rate limit을 우회할 수 있다. +# trust_proxy_hops = 1 + +# SQS 메인 큐/DLQ와 task IAM 권한. 기본은 false다 — 이 앱의 messaging root는 +# BullMQ/Redis이고 MESSAGE_QUEUE_URL을 읽는 코드가 없다. +# 켜려면 MessageQueuePort의 SQS adapter를 먼저 만든다. +# enable_sqs = false + # MongoDB Atlas 등 외부 MongoDB URI를 담은 Secrets Manager ARN. 사용하지 않으면 생략한다. # mongodb_secret_arn = "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:mongodb-uri" diff --git a/infra/terraform/variables.tf b/infra/terraform/variables.tf index 5e3fdc8..81b1e53 100644 --- a/infra/terraform/variables.tf +++ b/infra/terraform/variables.tf @@ -21,6 +21,34 @@ variable "container_image" { type = string } +# 기본값이 없는 것은 의도다. 이전에는 `compute.tf`에 "https://replace-me.example"이 +# 박혀 있었고, 앱의 production 검증(스킴 있음·와일드카드 아님)을 **통과**하기 때문에 +# task는 멀쩡히 뜨고 브라우저 요청만 조용히 막혔다. 복제 모델에서는 사본을 고치면 +# 됐지만 참조 모델에서는 고칠 방법이 없으므로 소비자가 반드시 넘겨야 한다. +# +# 아래 validation은 앱의 규칙을 plan 시점으로 당긴 것이다 — 같은 실패를 +# ECS task 기동이 아니라 `terraform plan`에서 본다. +# > src/config/env.validation.ts:136 · src/config/cors.config.ts:14 +variable "cors_origins" { + description = "Allowed browser origins, each with a scheme (e.g. https://app.example.com)" + type = list(string) + + validation { + condition = length(var.cors_origins) > 0 + error_message = "cors_origins must list at least one origin." + } + + validation { + condition = !contains(var.cors_origins, "*") + error_message = "cors_origins must not contain \"*\" - the app rejects it in production." + } + + validation { + condition = alltrue([for o in var.cors_origins : can(regex("^[a-z][a-z0-9+.-]*://", o))]) + error_message = "Each cors_origins entry must include a scheme. Browsers send Origin as scheme://host[:port]." + } +} + variable "container_port" { type = number default = 3000 @@ -31,6 +59,25 @@ variable "desired_count" { default = 1 } +# 앱은 이 값으로 Express `trust proxy`를 설정한다. 0은 "프록시 없음"이라 +# `req.ip`가 ALB IP가 되고 ThrottlerGuard가 전 사용자를 한 버킷에 넣는다. +# 이 구성은 ALB 하나를 세우므로 기본값이 1이다. CloudFront를 앞에 얹는 +# 소비자는 2로 올린다. `true`(전부 신뢰)는 XFF 위조로 throttle을 우회시키므로 +# 앱이 숫자만 받는다. +# +# 상한 10은 앱의 Joi 스키마와 같은 값이다 — 여기서 넘기면 task가 부팅에 실패한다. +# > src/config/env.validation.ts:64 · src/app.setup.ts:75 +variable "trust_proxy_hops" { + description = "Number of proxy hops the app should trust for X-Forwarded-For" + type = number + default = 1 + + validation { + condition = var.trust_proxy_hops >= 0 && var.trust_proxy_hops <= 10 && floor(var.trust_proxy_hops) == var.trust_proxy_hops + error_message = "trust_proxy_hops must be an integer between 0 and 10." + } +} + variable "db_engine" { description = "Materialized RDB engine" type = string @@ -62,6 +109,19 @@ variable "enable_redis" { default = true } +# 기본값 false다. 이 앱의 messaging composition root는 BullMQ/Redis이고 +# (src/infra/queue/queue.module.ts) `MESSAGE_QUEUE_URL`을 읽는 코드가 없다. +# 켜면 소비자 없는 큐·DLQ·IAM 권한이 그대로 생기므로 기본은 끈다. +# +# 지우지 않고 input으로 남긴 이유: 모델 B에서 소비 프로젝트는 이 구성을 +# 참조만 하고 고칠 수 없다. SQS가 필요한 소비자에게 남기는 유일한 문이다. +# 켜는 쪽은 앱에 `MessageQueuePort`의 SQS adapter를 함께 만들어야 한다. +variable "enable_sqs" { + description = "Create the SQS main/dead-letter queues and grant the task access" + type = bool + default = false +} + variable "redis_node_type" { type = string default = "cache.t4g.micro"