From e5e8a9ec3de9fb6c8b5ac7f714aeaf5fe1f1775e Mon Sep 17 00:00:00 2001 From: ifLov Date: Tue, 1 Sep 2026 10:51:58 +0900 Subject: [PATCH] chore: move the AWS infrastructure out to barebones-infra This is a scaffold, and a scaffold can only ship templates. The foundation half of infra/terraform is an instance: there is exactly one VPC, ALB and ECS cluster per environment, a second copy is a mistake rather than a feature, and its consumers include repositories that were never scaffolded from here. The service half only runs against a foundation, so keeping it alone would leave a half that cannot deploy. The line drawn last commit by change rate and blast radius turns out to be the same line you get from asking what is reusable, which is why both halves leave together. What remains is the container boundary: Dockerfile and Compose say how to run this service, .env.example and src/config/env.validation.ts say what configuration it requires. Both are cloud-agnostic. Moved with git subtree split, so the three commits that shaped the configuration went with it. docs/terraform-repo-boundary.md stays here as the record of the decision, since it explains a change to this repository. Also drops the terraform CI workflow, the paths-ignore that existed only to keep terraform-only changes off this pipeline, and the terraform ignore rules. 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 ----- .gitignore | 5 - AGENTS.md | 2 +- README.md | 28 +-- docs/terraform-repo-boundary.md | 17 ++ infra/terraform/.gitignore | 22 --- infra/terraform/.terraform.lock.hcl | 26 --- infra/terraform/README.md | 89 --------- infra/terraform/backend.hcl.example | 23 --- infra/terraform/backend.tf | 10 -- infra/terraform/compute.tf | 218 ----------------------- infra/terraform/data.tf | 64 ------- infra/terraform/network.tf | 167 ----------------- infra/terraform/outputs.tf | 84 --------- infra/terraform/terraform.tfvars.example | 31 ---- infra/terraform/variables.tf | 169 ------------------ infra/terraform/versions.tf | 22 --- 18 files changed, 26 insertions(+), 1007 deletions(-) delete mode 100644 .github/workflows/terraform.yml delete mode 100644 infra/terraform/.gitignore delete mode 100644 infra/terraform/.terraform.lock.hcl delete mode 100644 infra/terraform/README.md delete mode 100644 infra/terraform/backend.hcl.example delete mode 100644 infra/terraform/backend.tf delete mode 100644 infra/terraform/compute.tf delete mode 100644 infra/terraform/data.tf delete mode 100644 infra/terraform/network.tf delete mode 100644 infra/terraform/outputs.tf delete mode 100644 infra/terraform/terraform.tfvars.example delete mode 100644 infra/terraform/variables.tf delete mode 100644 infra/terraform/versions.tf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1632012..e264546 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,19 +1,9 @@ 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 deleted file mode 100644 index 8165b7f..0000000 --- a/.github/workflows/terraform.yml +++ /dev/null @@ -1,46 +0,0 @@ -# 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/.gitignore b/.gitignore index d6de416..bb97f57 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,6 @@ coverage .env.local .DS_Store pnpm-debug.log -*.tfstate -*.tfstate.* -.terraform/ -infra/terraform/backend.hcl -infra/terraform/terraform.tfvars # Agent runtime state는 제외하고, 프로젝트 스킬은 Claude/Codex 각각 추적한다. /.claude/* diff --git a/AGENTS.md b/AGENTS.md index 571e9b3..9620168 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,7 +67,7 @@ adapters/in ## Infrastructure - 로컬 의존성은 Docker Compose에서 실행한다. sql.js 같은 인메모리 RDB 대체물을 사용하지 않는다. -- AWS Terraform은 기본 배포 시작점이며 state backend와 secret 값은 환경별로 분리한다. +- 배포 인프라는 이 저장소에 없다. 컨테이너 경계(`Dockerfile`, Compose)까지가 이 저장소의 범위다. - app 시작 순서에는 Docker healthcheck를 사용한다. - 선택된 필수 의존성이 실행 중 실패하면 readiness는 `503`이다. diff --git a/README.md b/README.md index 6a3bc61..c568da8 100644 --- a/README.md +++ b/README.md @@ -107,28 +107,16 @@ Matt Pocock의 안정 `engineering`/`productivity` 스킬을 두 에이전트에 두 디렉터리는 symlink로 공유하지 않습니다. 각 에이전트가 해당 디렉터리만 읽습니다. 스킬은 사용자가 이름을 명시했을 때만 실행합니다. -## AWS Terraform +## 배포 -`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입니다. +이 저장소의 범위는 **컨테이너 경계까지**입니다 — `Dockerfile`과 Compose가 "이 서비스를 +어떻게 돌리나"를 정의하고, `.env.example`과 `src/config/env.validation.ts`가 "앱이 무슨 +설정을 요구하나"를 정의합니다. 둘 다 클라우드에 무관합니다. -```bash -cd infra/terraform -cp terraform.tfvars.example terraform.tfvars # cors_origins는 필수입니다 -terraform init -backend-config=backend.hcl -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에 없습니다. +AWS 인프라는 [iflov/barebones-infra](https://github.com/iflov/barebones-infra)에 있습니다. +2026-09-01에 분리했습니다 — VPC·ALB·ECS 클러스터는 환경당 정확히 하나뿐인 **인스턴스**라 +스캐폴드가 실을 수 없고, 그 소비자에는 이 스캐폴드에서 나오지 않은 저장소도 포함됩니다. +근거와 검증 과정은 [결정 문서](./docs/terraform-repo-boundary.md)에 있습니다. ## 검증 diff --git a/docs/terraform-repo-boundary.md b/docs/terraform-repo-boundary.md index ab4d537..d4ffd53 100644 --- a/docs/terraform-repo-boundary.md +++ b/docs/terraform-repo-boundary.md @@ -4,6 +4,23 @@ > `infra/terraform`을 앱 저장소 밖으로 뺄 것인가. 3라운드 교차검증 결과와 내린 결정. > 조사 2026-08-31 · 결정 2026-09-01 > +> **✅ 종결 (2026-09-01): `infra/terraform`은 [iflov/barebones-infra](https://github.com/iflov/barebones-infra)로 +> 나갔다.** 이 문서는 그 결정의 기록으로 barebones에 남는다. +> +> 최종 근거는 저장소 경계도 CI 비용도 아니었다 — **barebones는 스캐폴드이고 스캐폴드는 +> 템플릿만 실을 수 있는데, foundation은 환경당 하나뿐인 인스턴스다.** VPC·ALB·ECS +> 클러스터는 복제하면 사고이지 기능이 아니고, 그 소비자에는 이 스캐폴드에서 나오지 +> 않은 저장소(frontend 등)도 포함된다. 아래 "2026-09-01 재검토"에서 변경 빈도와 +> blast radius로 그은 foundation/service 선이, 재사용 가능성으로 그으면 나오는 +> "인스턴스 / 템플릿" 선과 같았다. +> +> 남은 배포 경계는 컨테이너까지다 — `Dockerfile`, Compose, `.env.example`, +> `src/config/env.validation.ts`. 전부 클라우드에 무관하다. +> +> 이전은 `git subtree split`으로 했고 이 구성을 만든 커밋 3개가 함께 갔다. +> +> --- +> > **⚠ 아래 "추천과 그 근거"는 2026-09-01에 폐기됐다.** 그 추천은 "terraform 사본 하나당 > 소비자 하나"를 전제했는데, 사용자가 그 전제가 틀렸다고 정정했다 — frontend와 다른 > 서비스가 **같은 인프라를 나눠 쓴다.** 갱신된 판단은 아래 "2026-09-01 재검토"에 있다. diff --git a/infra/terraform/.gitignore b/infra/terraform/.gitignore deleted file mode 100644 index 545c206..0000000 --- a/infra/terraform/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -# 이 디렉터리는 앱 저장소 밖으로 나갈 수 있으므로 무시 규칙을 자기 안에 둔다. -# 저장소 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/.terraform.lock.hcl b/infra/terraform/.terraform.lock.hcl deleted file mode 100644 index 308cc90..0000000 --- a/infra/terraform/.terraform.lock.hcl +++ /dev/null @@ -1,26 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/aws" { - version = "6.60.0" - constraints = "~> 6.0" - hashes = [ - "h1:BDSQlnJzI6J7VA7JmDk1gfgwVlEBWhTSVljOsxmEpOo=", - "zh:1a29660d9038c38e9e306194bd3e7de7017148f3491af7fdfa434d7d8c82b3d4", - "zh:268d711a0a9c18459e09a837405244e5d459bb4ebb5d34a91c4fcbabe3e25d26", - "zh:2d7058c285484e7cccf4f5b1973c10b404ae77532f77ea95115915dd341fbf5b", - "zh:2e1cddc362497b569ba528f013d0cf770465047b8185e8180f4ccabafa082577", - "zh:5210c99d5946dd4985a6cff7460422e257037679b35df0a54982e3f675d1a8d5", - "zh:60703aa30b23ed5e5cfabf2614aeb465cf2aa4b848eb8afb2723b6b9d358ccd0", - "zh:904f0651e9bfbf08b16f4d13c85e7841c2d1b5a670087cb11f8d4ad06249cf0c", - "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:b4268c98208f725ea28afb38dde6cb4b7536de577c36ee919fa44c3d8d5faf70", - "zh:caf78d1713cc323383facae063c61584758ea697428f328600227902f3a9980f", - "zh:cb349f8a43653bfc78867909d6342c56073f9805bdbd9dc32a8e1d18302257d4", - "zh:e087e5e6e3130980838552022e022a62a52a851cf93a0029cb5d410a34f6106b", - "zh:e94318f9ebf29d06aa821da73a7fc7a637d7d13e535e40585c6242cea4477262", - "zh:effc513566df2f5b5921a5df884817b1de57790db1c485eece566c5d4c55c4ee", - "zh:f2be12b976d10a91289835056ff962721e65f47f8e52b368211c4bd3ce9e20e6", - "zh:fb7ef7e405b05789cec32c8ea06a353789c6e3a336b876508cf2ac98984f0558", - ] -} diff --git a/infra/terraform/README.md b/infra/terraform/README.md deleted file mode 100644 index 012a672..0000000 --- a/infra/terraform/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# AWS Terraform starter - -기본 프로필은 2개 AZ VPC, ALB, ECS Fargate, RDS, 선택적 ElastiCache Valkey를 만듭니다. -MongoDB는 DocumentDB를 동일 제품처럼 취급하지 않으며, Atlas 등 실제 MongoDB URI를 AWS Secrets -Manager ARN으로 주입합니다. SQS/DLQ는 `enable_sqs`로 켜며 기본은 꺼짐입니다. - -```bash -cp terraform.tfvars.example terraform.tfvars -terraform init -backend-config=backend.hcl -terraform plan -terraform apply -``` - -## foundation과 service - -한 root 안에 두 층이 있고 이름 축이 갈려 있습니다. 아직 state는 하나지만, 층 구분은 -코드에 이미 반영돼 있습니다. - -| 층 | 이름 | 무엇 | -| -------------- | -------------------------------- | ----------------------------------------------------------------------------------- | -| **foundation** | `project_name`-`environment` | VPC, subnet, route table, ALB + listener, ALB SG, ECS 클러스터, DB subnet group | -| **service** | foundation 이름 + `service_name` | task definition, ECS service, target group, task SG, data SG, IAM, RDS, Valkey, SQS | - -foundation은 여러 서비스가 나눠 쓰도록 설계했습니다. **데이터 플레인(RDS/Valkey)은 -서비스별입니다** — 공유하려면 `hashicorp/aws`만으로는 안 되고 별도 SQL provider와 -VPC 안에서 도는 특권 runner가 필요한데, 서울 리전 `db.t4g.micro` + `cache.t4g.micro`가 -월 $32 남짓이라 그 운영 표면을 사는 거래가 성립하지 않습니다. 자세한 근거는 -[결정 문서](../../docs/terraform-repo-boundary.md)에 있습니다. - -`outputs.tf`의 foundation 계약(VPC ID, subnet IDs, ALB SG ID, listener ARN, -cluster ARN)은 지금 아무도 쓰지 않지만, 층이 갈릴 때 service root가 읽어야 하는 -값들입니다. 전달은 `terraform_remote_state`가 아니라 SSM Parameter Store를 씁니다 — -전자는 output만 읽는 것처럼 보여도 state 스냅샷 전체에 대한 읽기 권한을 요구합니다. - -보안 그룹 rule은 inline `ingress`가 아니라 별도 `aws_vpc_security_group_*_rule` -리소스입니다. inline 블록은 authoritative라 층이 갈리면 foundation apply가 -service가 붙인 rule을 지웁니다. - -## 소비자가 반드시 넘겨야 하는 값 - -| 변수 | 기본값 | 왜 여기 있는가 | -| ----------------- | ------ | -------------------------------------------------------------------------------------------- | -| `container_image` | 없음 | 배포할 이미지 URI. ECR build/push 경로는 아직 CI에 없습니다. | -| `cors_origins` | 없음 | 브라우저 Origin 목록. 스킴 필수, `*` 금지 — 앱의 production 검증을 plan 시점으로 당겼습니다. | - -## 토폴로지가 정하는 값 - -| 변수 | 기본값 | 근거 | -| ------------------ | ------- | ------------------------------------------------------------------------------------------------------- | -| `service_name` | `api` | foundation 안에서의 정체성. 리소스 이름과 Redis 네임스페이스가 여기서 유도됩니다. | -| `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`을 사용합니다. - -`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과 승인 경로가 정해지지 않았습니다. - -## 아직 없는 것 - -- **listener rule이 없습니다.** 지금은 listener의 default action이 이 서비스의 - target group을 직접 forward합니다. 서비스가 둘이 되면 default는 고정 404가 되고 - 각 service가 host/path 기반 `aws_lb_listener_rule`을 소유해야 합니다. rule - priority는 listener 안에서 유일해야 하므로(중복 시 `PriorityInUse`) 대역 배정 - 규약이 함께 필요합니다. host-based냐 path-based냐는 미결정입니다. -- **한 번도 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.hcl.example b/infra/terraform/backend.hcl.example deleted file mode 100644 index 6f9b076..0000000 --- a/infra/terraform/backend.hcl.example +++ /dev/null @@ -1,23 +0,0 @@ -# 이 파일은 커밋하지 않는다. `backend.hcl`로 복사해 실제 값을 채운다. - -bucket = "replace-with-terraform-state-bucket" -region = "ap-northeast-2" - -# key 규약 — foundation과 service를 **처음부터 다른 key로** 둔다. -# -# //foundation/terraform.tfstate -# //service//terraform.tfstate -# -# 지금은 한 root라 key도 하나지만, 이 규약을 지금 정해두는 이유는 state 경계가 -# 저장소가 아니라 **backend key**로 정해지기 때문이다. 저장소를 옮겨도 key가 같으면 -# 같은 state이고, 저장소를 안 옮겨도 key를 나누면 state는 나뉜다. -# -# key를 나누면 apply role도 key 단위로 자를 수 있다 — service role에는 자기 -# key의 GetObject/PutObject와 lockfile 권한만 준다. foundation state에는 RDS -# master secret ARN과 IAM 정책이 들어 있으므로, service가 그걸 읽을 이유가 없다. -key = "barebones/dev/service/api/terraform.tfstate" - -encrypt = true - -# S3 native locking (Terraform 1.10+). DynamoDB 테이블이 필요 없다. -use_lockfile = true diff --git a/infra/terraform/backend.tf b/infra/terraform/backend.tf deleted file mode 100644 index 4f04361..0000000 --- a/infra/terraform/backend.tf +++ /dev/null @@ -1,10 +0,0 @@ -# 이 블록은 **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 deleted file mode 100644 index df13628..0000000 --- a/infra/terraform/compute.tf +++ /dev/null @@ -1,218 +0,0 @@ -resource "aws_cloudwatch_log_group" "app" { - name = "/ecs/${local.service_name}" - retention_in_days = var.environment == "prod" ? 30 : 7 -} - -# 클러스터는 foundation이다. 여러 서비스가 같은 클러스터에 뜬다. -resource "aws_ecs_cluster" "this" { - name = local.foundation_name -} - -resource "aws_iam_role" "ecs_execution" { - name = "${local.service_name}-ecs-execution" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { Service = "ecs-tasks.amazonaws.com" } - }] - }) -} - -resource "aws_iam_role_policy_attachment" "ecs_execution" { - policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" - role = aws_iam_role.ecs_execution.name -} - -resource "aws_iam_role_policy" "ecs_secrets" { - name = "secrets" - role = aws_iam_role.ecs_execution.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Action = ["secretsmanager:GetSecretValue"] - Effect = "Allow" - Resource = compact([aws_db_instance.this.master_user_secret[0].secret_arn, var.mongodb_secret_arn]) - }] - }) -} - -resource "aws_iam_role" "ecs_task" { - name = "${local.service_name}-ecs-task" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { Service = "ecs-tasks.amazonaws.com" } - }] - }) -} - -resource "aws_iam_role_policy" "messages" { - count = var.enable_sqs ? 1 : 0 - - name = "messages" - role = aws_iam_role.ecs_task.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Action = [ - "sqs:ChangeMessageVisibility", - "sqs:DeleteMessage", - "sqs:GetQueueAttributes", - "sqs:GetQueueUrl", - "sqs:ReceiveMessage", - "sqs:SendMessage" - ] - Effect = "Allow" - Resource = aws_sqs_queue.messages[0].arn - }] - }) -} - -locals { - 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) }, - { name = "DB_DATABASE", value = var.db_name }, - { name = "DB_USERNAME", value = var.db_username }, - { name = "REDIS_ENABLED", value = tostring(var.enable_redis) }, - { 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) }, - # 앱은 이 세 축을 이미 갖고 있는데 terraform이 넘기지 않았다. 기본값이 - # cache `app:` / BullMQ `app` / db 0이라 서비스 둘이 같은 Valkey를 쓰면 - # 그대로 충돌한다. 서비스 이름에서 유도해 그 사고를 없앤다. - # > src/config/env.validation.ts:85-89 · cache.config.ts:29 · redis.config.ts:29 - { name = "REDIS_DB", value = tostring(var.redis_db) }, - { name = "REDIS_KEY_PREFIX", value = "${var.service_name}:" }, - { name = "BULLMQ_PREFIX", value = var.service_name }, - { name = "MONGODB_ENABLED", value = tostring(local.mongodb_on) }, - { name = "PROMETHEUS_ENABLED", value = "true" }, - { 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( - [{ - name = "DB_PASSWORD" - valueFrom = "${aws_db_instance.this.master_user_secret[0].secret_arn}:password::" - }], - var.mongodb_secret_arn == null ? [] : [{ - name = "MONGODB_URI" - valueFrom = var.mongodb_secret_arn - }] - ) -} - -resource "aws_ecs_task_definition" "app" { - family = local.service_name - cpu = 512 - memory = 1024 - network_mode = "awsvpc" - requires_compatibilities = ["FARGATE"] - execution_role_arn = aws_iam_role.ecs_execution.arn - task_role_arn = aws_iam_role.ecs_task.arn - - container_definitions = jsonencode([{ - name = "app" - image = var.container_image - essential = true - environment = local.app_environment - secrets = local.app_secrets - portMappings = [{ containerPort = var.container_port, protocol = "tcp" }] - logConfiguration = { - logDriver = "awslogs" - options = { - awslogs-group = aws_cloudwatch_log_group.app.name - awslogs-region = var.aws_region - awslogs-stream-prefix = "app" - } - } - }]) -} - -# ALB는 foundation이다. 여러 서비스가 listener rule로 나눠 쓴다. -resource "aws_lb" "this" { - name = local.foundation_name - internal = false - load_balancer_type = "application" - security_groups = [aws_security_group.alb.id] - subnets = aws_subnet.public[*].id - - # 이전에는 `substr(..., 0, 32)`로 **조용히 잘랐다.** 이름이 긴 서비스 둘이 - # 같은 32자로 잘리면 이름이 충돌하는데, 그 사실이 apply 시점에야 드러난다. - lifecycle { - precondition { - condition = length(local.foundation_name) <= 32 - error_message = "ALB name '${local.foundation_name}' exceeds 32 characters. Shorten project_name or environment." - } - } -} - -resource "aws_lb_target_group" "app" { - name = local.service_name - port = var.container_port - protocol = "HTTP" - target_type = "ip" - vpc_id = aws_vpc.this.id - - health_check { - healthy_threshold = 2 - interval = 30 - path = "/v1/system/health" - unhealthy_threshold = 3 - } - - lifecycle { - precondition { - condition = length(local.service_name) <= 32 - error_message = "Target group name '${local.service_name}' exceeds 32 characters. Shorten project_name, environment, or service_name." - } - } -} - -resource "aws_lb_listener" "http" { - load_balancer_arn = aws_lb.this.arn - port = 80 - protocol = "HTTP" - - default_action { - target_group_arn = aws_lb_target_group.app.arn - type = "forward" - } -} - -resource "aws_ecs_service" "app" { - name = local.service_name - cluster = aws_ecs_cluster.this.id - task_definition = aws_ecs_task_definition.app.arn - desired_count = var.desired_count - launch_type = "FARGATE" - - network_configuration { - assign_public_ip = true - security_groups = [aws_security_group.app.id] - subnets = aws_subnet.public[*].id - } - - load_balancer { - container_name = "app" - container_port = var.container_port - target_group_arn = aws_lb_target_group.app.arn - } - - depends_on = [aws_lb_listener.http] -} diff --git a/infra/terraform/data.tf b/infra/terraform/data.tf deleted file mode 100644 index 067cd20..0000000 --- a/infra/terraform/data.tf +++ /dev/null @@ -1,64 +0,0 @@ -# subnet group은 "DB가 들어갈 subnet 목록"일 뿐이라 foundation이다. -resource "aws_db_subnet_group" "this" { - name = local.foundation_name - subnet_ids = aws_subnet.private[*].id -} - -resource "aws_db_instance" "this" { - allocated_storage = 20 - backup_retention_period = var.environment == "prod" ? 7 : 1 - db_name = var.db_name - db_subnet_group_name = aws_db_subnet_group.this.name - deletion_protection = var.environment == "prod" - engine = var.db_engine - identifier = local.service_name - instance_class = var.db_instance_class - manage_master_user_password = true - multi_az = var.environment == "prod" - port = local.db_port - publicly_accessible = false - skip_final_snapshot = var.environment != "prod" - storage_encrypted = true - username = var.db_username - vpc_security_group_ids = [aws_security_group.data.id] -} - -resource "aws_elasticache_subnet_group" "this" { - count = var.enable_redis ? 1 : 0 - - name = local.foundation_name - subnet_ids = aws_subnet.private[*].id -} - -resource "aws_elasticache_replication_group" "this" { - count = var.enable_redis ? 1 : 0 - - at_rest_encryption_enabled = true - description = "${local.service_name} cache and BullMQ backend" - engine = "valkey" - node_type = var.redis_node_type - num_cache_clusters = var.environment == "prod" ? 2 : 1 - port = 6379 - replication_group_id = local.service_name - security_group_ids = [aws_security_group.data.id] - subnet_group_name = aws_elasticache_subnet_group.this[0].name -} - -resource "aws_sqs_queue" "dead_letter" { - count = var.enable_sqs ? 1 : 0 - - name = "${local.service_name}-dlq" - message_retention_seconds = 1209600 -} - -resource "aws_sqs_queue" "messages" { - count = var.enable_sqs ? 1 : 0 - - name = "${local.service_name}-messages" - visibility_timeout_seconds = 60 - - redrive_policy = jsonencode({ - deadLetterTargetArn = aws_sqs_queue.dead_letter[0].arn - maxReceiveCount = 5 - }) -} diff --git a/infra/terraform/network.tf b/infra/terraform/network.tf deleted file mode 100644 index 13d5288..0000000 --- a/infra/terraform/network.tf +++ /dev/null @@ -1,167 +0,0 @@ -data "aws_availability_zones" "available" { - state = "available" -} - -locals { - # foundation — 여러 서비스가 나눠 쓴다. VPC, subnet, ECS 클러스터, ALB. - foundation_name = "${var.project_name}-${var.environment}" - - # service — 이 서비스만의 것. task definition, target group, RDS, 큐. - service_name = "${local.foundation_name}-${var.service_name}" - - selected_azs = slice(data.aws_availability_zones.available.names, 0, 2) - db_port = var.db_engine == "postgres" ? 5432 : 3306 - db_driver = var.db_engine - mongodb_on = var.mongodb_secret_arn != null - public_cidrs = ["10.20.0.0/24", "10.20.1.0/24"] - private_cidrs = ["10.20.10.0/24", "10.20.11.0/24"] -} - -resource "aws_vpc" "this" { - cidr_block = "10.20.0.0/16" - enable_dns_hostnames = true - enable_dns_support = true - - tags = { Name = local.foundation_name } -} - -resource "aws_internet_gateway" "this" { - vpc_id = aws_vpc.this.id - tags = { Name = local.foundation_name } -} - -resource "aws_subnet" "public" { - count = 2 - - availability_zone = local.selected_azs[count.index] - cidr_block = local.public_cidrs[count.index] - map_public_ip_on_launch = true - vpc_id = aws_vpc.this.id - - tags = { Name = "${local.foundation_name}-public-${count.index + 1}" } -} - -resource "aws_subnet" "private" { - count = 2 - - availability_zone = local.selected_azs[count.index] - cidr_block = local.private_cidrs[count.index] - vpc_id = aws_vpc.this.id - - tags = { Name = "${local.foundation_name}-private-${count.index + 1}" } -} - -resource "aws_route_table" "public" { - vpc_id = aws_vpc.this.id - - route { - cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.this.id - } - - tags = { Name = "${local.foundation_name}-public" } -} - -resource "aws_route_table_association" "public" { - count = 2 - - route_table_id = aws_route_table.public.id - subnet_id = aws_subnet.public[count.index].id -} - -# ──────────────────────────────────────────────────────────────────────────── -# 보안 그룹 -# -# rule을 `aws_security_group`의 inline `ingress`/`egress`로 쓰지 않는다. -# inline 블록은 **authoritative**라 terraform이 구성에 없는 rule을 지운다. -# 그러면 foundation과 service가 갈렸을 때, service root가 공유 SG에 붙인 rule을 -# foundation apply가 조용히 삭제한다. 별도 rule 리소스는 소유자를 rule 단위로 -# 나눌 수 있어서, 각 root가 자기가 만든 rule만 관리한다. -# ──────────────────────────────────────────────────────────────────────────── - -# foundation. 여러 서비스의 task SG가 이걸 source로 참조한다. -resource "aws_security_group" "alb" { - name_prefix = "${local.foundation_name}-alb-" - vpc_id = aws_vpc.this.id - - tags = { Name = "${local.foundation_name}-alb" } -} - -resource "aws_vpc_security_group_ingress_rule" "alb_http" { - description = "Public HTTP" - security_group_id = aws_security_group.alb.id - - cidr_ipv4 = "0.0.0.0/0" - from_port = 80 - to_port = 80 - ip_protocol = "tcp" -} - -resource "aws_vpc_security_group_egress_rule" "alb_all" { - description = "ALB to targets" - security_group_id = aws_security_group.alb.id - - cidr_ipv4 = "0.0.0.0/0" - ip_protocol = "-1" -} - -# service. 이 서비스의 task만 담는다. -resource "aws_security_group" "app" { - name_prefix = "${local.service_name}-app-" - vpc_id = aws_vpc.this.id - - tags = { Name = "${local.service_name}-app" } -} - -resource "aws_vpc_security_group_ingress_rule" "app_from_alb" { - description = "ALB to this service" - security_group_id = aws_security_group.app.id - - referenced_security_group_id = aws_security_group.alb.id - from_port = var.container_port - to_port = var.container_port - ip_protocol = "tcp" -} - -resource "aws_vpc_security_group_egress_rule" "app_all" { - description = "Task egress" - security_group_id = aws_security_group.app.id - - cidr_ipv4 = "0.0.0.0/0" - ip_protocol = "-1" -} - -# service. 이 서비스의 RDS/Valkey를 담는다. -# 데이터 플레인을 서비스끼리 공유하게 되면 이 SG는 foundation으로 올라가고, -# 아래 rule들은 각 service root가 자기 것을 만들게 된다 — 그때 inline이 아닌 -# 것이 결정적으로 중요해진다. -resource "aws_security_group" "data" { - name_prefix = "${local.service_name}-data-" - vpc_id = aws_vpc.this.id - - tags = { Name = "${local.service_name}-data" } -} - -resource "aws_vpc_security_group_ingress_rule" "data_from_app" { - description = "Service tasks to RDB" - security_group_id = aws_security_group.data.id - - referenced_security_group_id = aws_security_group.app.id - from_port = local.db_port - to_port = local.db_port - ip_protocol = "tcp" -} - -resource "aws_vpc_security_group_ingress_rule" "data_redis_from_app" { - count = var.enable_redis ? 1 : 0 - - description = "Service tasks to Valkey" - security_group_id = aws_security_group.data.id - - referenced_security_group_id = aws_security_group.app.id - from_port = 6379 - to_port = 6379 - ip_protocol = "tcp" -} - -# data SG에 egress rule은 없다. RDS와 ElastiCache는 연결을 시작하지 않는다. diff --git a/infra/terraform/outputs.tf b/infra/terraform/outputs.tf deleted file mode 100644 index b381f50..0000000 --- a/infra/terraform/outputs.tf +++ /dev/null @@ -1,84 +0,0 @@ -# ──────────────────────────────────────────────────────────────────────────── -# foundation 계약 -# -# 이 값들은 이 root가 foundation과 service로 갈릴 때 **service root가 읽어야 하는 -# 것들**이다. 지금은 한 root라 아무도 안 쓰지만, 계약을 지금 고정해두면 분리할 때 -# "무엇이 공개 인터페이스였나"를 다시 발굴하지 않아도 된다. -# -# 분리 후 전달은 SSM Parameter Store를 쓴다. `terraform_remote_state`는 output만 -# 읽는 것처럼 보여도 state 스냅샷 전체에 대한 읽기 권한을 요구하고, 그 안에는 -# RDS master secret ARN과 IAM 정책이 함께 들어 있다. `sensitive = true`는 출력 -# 표시를 가릴 뿐 권한 경계가 아니다. -# ──────────────────────────────────────────────────────────────────────────── - -output "vpc_id" { - description = "Foundation VPC" - value = aws_vpc.this.id -} - -output "public_subnet_ids" { - description = "Foundation public subnets (ALB, and currently ECS tasks)" - value = aws_subnet.public[*].id -} - -output "private_subnet_ids" { - description = "Foundation private subnets. No route table or NAT is attached yet." - value = aws_subnet.private[*].id -} - -output "alb_security_group_id" { - description = "Foundation ALB security group. Service task SGs allow ingress from this." - value = aws_security_group.alb.id -} - -output "alb_listener_arn" { - description = "Foundation HTTP listener. Services attach their own aws_lb_listener_rule here." - value = aws_lb_listener.http.arn -} - -output "alb_dns_name" { - description = "Foundation ALB DNS name, for the DNS records services point at" - value = aws_lb.this.dns_name -} - -output "ecs_cluster_arn" { - description = "Foundation ECS cluster that service tasks run in" - value = aws_ecs_cluster.this.arn -} - -output "ecs_cluster_name" { - value = aws_ecs_cluster.this.name -} - -output "db_subnet_group_name" { - description = "Foundation DB subnet group. Per-service RDS instances place themselves here." - value = aws_db_subnet_group.this.name -} - -# ──────────────────────────────────────────────────────────────────────────── -# service 출력 -# ──────────────────────────────────────────────────────────────────────────── - -output "application_url" { - value = "http://${aws_lb.this.dns_name}" -} - -output "rds_endpoint" { - value = aws_db_instance.this.endpoint -} - -# ⚠ 이것은 **master** credential이다. 지금은 task가 직접 쓴다(compute.tf). -# 데이터 플레인을 서비스끼리 공유하게 되면 이 값을 앱에 주는 것은 성립하지 않는다 — -# 서비스별 database/user/grant가 먼저 필요하고, 그건 hashicorp/aws만으로는 안 된다. -output "rds_master_secret_arn" { - value = aws_db_instance.this.master_user_secret[0].secret_arn - sensitive = true -} - -output "message_queue_url" { - value = var.enable_sqs ? aws_sqs_queue.messages[0].url : null -} - -output "redis_endpoint" { - value = var.enable_redis ? aws_elasticache_replication_group.this[0].primary_endpoint_address : null -} diff --git a/infra/terraform/terraform.tfvars.example b/infra/terraform/terraform.tfvars.example deleted file mode 100644 index 6a3fd0e..0000000 --- a/infra/terraform/terraform.tfvars.example +++ /dev/null @@ -1,31 +0,0 @@ -aws_region = "ap-northeast-2" -project_name = "barebones" -environment = "dev" - -# foundation 안에서 이 서비스의 정체성. task definition, target group, RDS, -# 큐 이름과 Redis 네임스페이스가 전부 여기서 유도된다. -# `project_name`-`environment`가 foundation 이름(VPC, ECS 클러스터, ALB)이고 -# 여기에 이 값을 붙인 것이 service 이름이다. -service_name = "api" -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 - -# Redis logical database. 데이터 플레인이 서비스별이면 0으로 둔다. -# redis_db = 0 - -# 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 deleted file mode 100644 index 0f55e14..0000000 --- a/infra/terraform/variables.tf +++ /dev/null @@ -1,169 +0,0 @@ -variable "aws_region" { - description = "AWS region" - type = string - default = "ap-northeast-2" -} - -variable "project_name" { - description = "Resource name prefix" - type = string - default = "barebones" -} - -variable "environment" { - description = "Environment name" - type = string - default = "dev" -} - -# 이름 축이 둘로 갈린다. `project_name`-`environment`가 **foundation** 이름이고 -# (VPC, ECS 클러스터, ALB — 여러 서비스가 나눠 쓴다), 여기에 `service_name`을 -# 붙인 것이 **service** 이름이다(task definition, target group, RDS). -# -# 이전에는 `local.name` 하나가 VPC부터 target group까지 전부의 이름이었다. -# 서비스가 둘이면 ECS 클러스터 이름부터 충돌한다. -variable "service_name" { - description = "Service identity within the foundation (e.g. api, web, worker)" - type = string - default = "api" - - validation { - condition = can(regex("^[a-z][a-z0-9-]*[a-z0-9]$", var.service_name)) - error_message = "service_name must be lowercase alphanumeric with hyphens, starting with a letter." - } -} - -variable "container_image" { - description = "Deployable application image URI" - 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 -} - -variable "desired_count" { - type = number - 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 - default = "postgres" - - validation { - condition = contains(["postgres", "mysql", "mariadb"], var.db_engine) - error_message = "db_engine must be postgres, mysql, or mariadb." - } -} - -variable "db_name" { - type = string - default = "app" -} - -variable "db_username" { - type = string - default = "app" -} - -variable "db_instance_class" { - type = string - default = "db.t4g.micro" -} - -variable "enable_redis" { - type = bool - 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" -} - -# Redis logical database 번호. 데이터 플레인이 서비스별이면 0으로 두면 된다. -# Valkey 하나를 여러 서비스가 나눠 쓰게 되면 여기서 갈라야 한다 — 단 번호는 16개뿐이고 -# key prefix와 달리 권한 경계도 아니다. 진짜 격리는 ElastiCache RBAC인데, 그건 -# transit encryption과 앱의 username/TLS 지원을 함께 요구한다. 지금 앱의 -# `buildRedisOptions`는 password만 받는다. -# > src/config/redis.config.ts:20-32 -variable "redis_db" { - description = "Redis logical database index for this service" - type = number - default = 0 - - validation { - condition = var.redis_db >= 0 && var.redis_db <= 15 && floor(var.redis_db) == var.redis_db - error_message = "redis_db must be an integer between 0 and 15." - } -} - -variable "mongodb_secret_arn" { - description = "Optional Secrets Manager ARN whose value is the MongoDB URI" - type = string - default = null - nullable = true -} diff --git a/infra/terraform/versions.tf b/infra/terraform/versions.tf deleted file mode 100644 index e5f9e5b..0000000 --- a/infra/terraform/versions.tf +++ /dev/null @@ -1,22 +0,0 @@ -terraform { - required_version = ">= 1.10.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 6.0" - } - } -} - -provider "aws" { - region = var.aws_region - - default_tags { - tags = { - Environment = var.environment - ManagedBy = "terraform" - Project = var.project_name - } - } -}