Skip to content

chore: 인프라 마이그레이션 - 로그 로테이션, NeonDB 전환, Vercel CLI 배포 - #388

Open
kimjihyo wants to merge 5 commits into
developfrom
chore/add-log-rotation
Open

chore: 인프라 마이그레이션 - 로그 로테이션, NeonDB 전환, Vercel CLI 배포#388
kimjihyo wants to merge 5 commits into
developfrom
chore/add-log-rotation

Conversation

@kimjihyo

@kimjihyo kimjihyo commented Mar 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • 로그 로테이션 설정 추가 (Docker json-file 드라이버)
  • NeonDB 마이그레이션 및 인프라 전환 가이드 문서 추가
  • 모니터링 스택(Prometheus, Grafana, Loki, Promtail) 제거 — t3.micro 메모리 절약
  • Vercel CLI 기반 Next.js 배포 워크플로우(deploy-web.yml) 추가
  • next.config.ts에서 output: "standalone" 제거 (Vercel 자체 빌드 사용)
  • 불필요한 nginx.conf 삭제 (EC2 호스트에서 직접 관리)

변경 내역

신규

  • .github/workflows/deploy-web.yml: main push 시 Vercel CLI로 Next.js 프로덕션 배포
  • docs/infrastructure-migration-guide.md: 전체 마이그레이션 가이드 (NeonDB, EC2, Vercel, CI/CD)

수정

  • .github/workflows/deploy.yml: Web 빌드 제거, 모니터링 환경변수/파일 제거
  • apps/web/next.config.ts: output: "standalone" 제거
  • apps/api/src/configs/typeorm.config.ts: SSL 옵션 및 synchronize: false 적용
  • apps/api/src/main.ts: CORS 프로덕션 지원
  • apps/api/src/app.controller.ts: /health 엔드포인트 추가
  • infra/docker-compose.prod.yaml: DB/Web/Nginx/모니터링 서비스 제거
  • .gitignore: .env*.local 패턴 추가

삭제

  • nginx.conf, infra/prometheus.yaml, infra/grafana-datasource.yaml
  • infra/loki-config.yaml, infra/loki-config.local.yaml, infra/promtail-config.yaml

Test plan

  • act push --secret-file .secrets -W .github/workflows/deploy-web.yml로 Vercel 배포 확인
  • 배포된 사이트에서 API 통신 정상 동작 확인
  • deploy.yml로 API 배포 정상 동작 확인

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 헬스 체크 엔드포인트 추가
    • 웹 애플리케이션 자동 배포 워크플로우 추가
  • Chores

    • 인프라를 분산 아키텍처로 마이그레이션 (데이터베이스 외부화, API 배포 단순화)
    • 모니터링 시스템 제거 (Prometheus, Grafana, Loki, Promtail)
    • 데이터베이스 SSL 연결 지원 추가
    • CORS 설정 변경
    • 로깅 구성 표준화
  • Documentation

    • 상세한 인프라 마이그레이션 가이드 추가

- Docker 컨테이너 로그: json-file 드라이버, max-size 10m, max-file 3
- Loki: 15일 retention 정책 및 compaction 활성화
- Prometheus: 15일 메트릭 보관 기간 설정
@coderabbitai

coderabbitai Bot commented Mar 1, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

둘러보기

단일 서버 아키텍처에서 분산 아키텍처로의 마이그레이션을 지원하기 위해 인프라, API 설정, CI/CD 파이프라인을 재구성했습니다. Loki, Prometheus, Grafana, Promtail 모니터링 서비스를 제거하고, TypeORM 비동기 설정을 추가하며, Docker 컴포저 로깅을 설정하고, API 상태 체크 엔드포인트를 추가하고, GitHub Actions 워크플로우를 분리했습니다.

변경 사항

Cohort / File(s) 요약
Docker 인프라 로깅 설정
infra/docker-compose-infra.yaml, infra/docker-compose.local.yaml, infra/docker-compose.prod.yaml
각 Docker Compose 파일에 기본 로깅 앵커를 추가하고, 여러 서비스에 적용했습니다. prod 환경에서는 데이터베이스 연결 설정을 환경 변수로 변경하고(DB_HOST, DB_USERNAME, DB_NAME).
모니터링 서비스 제거
infra/docker-compose.local.yaml, infra/docker-compose.prod.yaml, infra/loki-config.local.yaml, infra/loki-config.yaml, infra/grafana-datasource.yaml, infra/prometheus.yaml, infra/promtail-config.yaml
Loki, Prometheus, Grafana, Promtail 서비스와 관련 설정 파일 전체를 제거했습니다.
NestJS API 설정 업데이트
apps/api/nest-cli.json, apps/api/src/app.controller.ts, apps/api/src/app.module.ts, apps/api/src/configs/typeorm.config.ts, apps/api/src/datasource-cli.ts, apps/api/src/main.ts
TypeORM 모듈을 동기식에서 비동기식으로 변경하고, ConfigService를 통해 데이터베이스 설정을 관리하도록 업데이트했습니다. 상태 확인 엔드포인트를 추가하고, CORS를 무조건적으로 활성화하며, SSL 옵션을 추가했습니다.
다음 웹 설정 및 워크플로우
apps/web/next.config.ts, .github/workflows/deploy-web.yml
Next.js의 standalone 출력 옵션을 제거하고, Vercel에 배포하기 위한 새로운 GitHub Actions 워크플로우를 추가했습니다.
배포 워크플로우 재구성
.github/workflows/deploy.yml
웹 이미지 빌드 단계를 제거하고 API 중심으로 변경했습니다. 환경 변수를 데이터베이스 연결 설정으로 업데이트하고, 모니터링 관련 변수를 제거했습니다.
인프라 마이그레이션 가이드 및 기타
docs/infrastructure-migration-guide.md, .gitignore, nginx.conf, packages/types/tsconfig.tsbuildinfo
1073줄의 상세한 마이그레이션 가이드 문서를 추가했습니다. nginx.conf 파일을 제거하고, .gitignore에 .env*.local 패턴을 추가하며, TypeScript 빌드 아티팩트를 제거했습니다.

예상 코드 리뷰 노력

🎯 3 (중간) | ⏱️ ~20분

🐰 거대한 모니터링 옛 방식은 퇴장하고,

환경 변수와 비동기 설정의 신세계 도래!

단일 서버에서 구름 위로 날아올라,

건강한 API와 로그의 흐름을 따라,

새로운 아키텍처, 마이그레이션의 노래~! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 PR의 주요 변경사항(로그 로테이션, NeonDB 전환, Vercel CLI 배포)을 정확하게 요약하고 있으며, 변경 내용과 명확하게 관련되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/add-log-rotation

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
infra/loki-config.yaml (1)

38-41: retention_delete_worker_count: 150은 단일 노드 환경에서 과도할 수 있습니다.

Loki의 기본값은 10입니다. 워커 수가 많으면 compaction 중 CPU 스파이크가 발생할 수 있습니다. 단일 노드 배포 환경이라면 10~30 정도로 낮추는 것을 권장합니다.

🔧 워커 수 조정 제안
   retention_enabled: true
   compaction_interval: 10m
   retention_delete_delay: 2h
-  retention_delete_worker_count: 150
+  retention_delete_worker_count: 30
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infra/loki-config.yaml` around lines 38 - 41, The
retention_delete_worker_count is set to 150 which is excessive for a single-node
Loki deployment; change the retention_delete_worker_count key value to a lower
number (recommended 10–30, e.g., 20) to avoid CPU spikes during compaction while
keeping retention_enabled, compaction_interval, and retention_delete_delay
unchanged; update the value for retention_delete_worker_count in the loki config
block so the single-node deployment uses the reduced worker count.
infra/docker-compose.monitoring.yaml (1)

44-55: Loki 서비스에 데이터 영속성을 위한 볼륨이 없습니다.

docker-compose.prod.yaml에서는 loki-dataloki-wal 볼륨을 마운트하여 데이터를 영속화하고 있습니다. 모니터링 스택에서도 컨테이너 재시작 시 로그 데이터가 유지되어야 한다면 볼륨 추가를 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infra/docker-compose.monitoring.yaml` around lines 44 - 55, The loki service
currently has no persistent volumes; add persistent volume mounts to the loki
service (service name "loki") by mounting the same named volumes used in
production (e.g., "loki-data" and "loki-wal") to the container paths that Loki
expects (the same target paths you used in docker-compose.prod.yaml), and
declare those named volumes at the top-level volumes section so data persists
across restarts. Ensure the volume names match ("loki-data", "loki-wal") and
that the service's volumes array maps them to the correct in-container
directories and appropriate access mode.
infra/loki-config.local.yaml (1)

41-44: prod 설정과 동일하게 retention_delete_worker_count가 높게 설정되어 있습니다.

로컬 환경에서는 리소스가 더 제한적일 수 있으므로, 워커 수를 더 낮게 설정하는 것이 적합할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infra/loki-config.local.yaml` around lines 41 - 44, The local Loki config
sets retention_delete_worker_count very high; reduce this in the retention
configuration (the
retention_enabled/compaction_interval/retention_delete_delay/retention_delete_worker_count
block) by lowering retention_delete_worker_count from 150 to a much smaller
number appropriate for local dev (e.g., 5–20) to avoid exhausting local
resources.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@infra/docker-compose.monitoring.yaml`:
- Around line 32-34: 환경에 하드코딩된 Grafana 관리자 자격증명(GF_SECURITY_ADMIN_USER: admin,
GF_SECURITY_ADMIN_PASSWORD: admin)을 환경변수 참조로 바꾸세요: docker-compose의 environment
블록에서 리터럴 문자열 대신 ${GF_SECURITY_ADMIN_USER}와 ${GF_SECURITY_ADMIN_PASSWORD}로 교체하고
.env 파일이나 배포 환경에서 해당 변수를 정의하도록 설정해 일관성과 보안성을 확보하세요.

---

Nitpick comments:
In `@infra/docker-compose.monitoring.yaml`:
- Around line 44-55: The loki service currently has no persistent volumes; add
persistent volume mounts to the loki service (service name "loki") by mounting
the same named volumes used in production (e.g., "loki-data" and "loki-wal") to
the container paths that Loki expects (the same target paths you used in
docker-compose.prod.yaml), and declare those named volumes at the top-level
volumes section so data persists across restarts. Ensure the volume names match
("loki-data", "loki-wal") and that the service's volumes array maps them to the
correct in-container directories and appropriate access mode.

In `@infra/loki-config.local.yaml`:
- Around line 41-44: The local Loki config sets retention_delete_worker_count
very high; reduce this in the retention configuration (the
retention_enabled/compaction_interval/retention_delete_delay/retention_delete_worker_count
block) by lowering retention_delete_worker_count from 150 to a much smaller
number appropriate for local dev (e.g., 5–20) to avoid exhausting local
resources.

In `@infra/loki-config.yaml`:
- Around line 38-41: The retention_delete_worker_count is set to 150 which is
excessive for a single-node Loki deployment; change the
retention_delete_worker_count key value to a lower number (recommended 10–30,
e.g., 20) to avoid CPU spikes during compaction while keeping retention_enabled,
compaction_interval, and retention_delete_delay unchanged; update the value for
retention_delete_worker_count in the loki config block so the single-node
deployment uses the reduced worker count.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c0decc and ecbf2c0.

📒 Files selected for processing (6)
  • infra/docker-compose-infra.yaml
  • infra/docker-compose.local.yaml
  • infra/docker-compose.monitoring.yaml
  • infra/docker-compose.prod.yaml
  • infra/loki-config.local.yaml
  • infra/loki-config.yaml

Comment thread infra/docker-compose.monitoring.yaml Outdated
Comment on lines +32 to +34
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Grafana 관리자 자격 증명이 하드코딩되어 있습니다.

다른 compose 파일들(docker-compose.local.yaml, docker-compose.prod.yaml)에서는 환경 변수(${GF_SECURITY_ADMIN_USER}, ${GF_SECURITY_ADMIN_PASSWORD})를 사용하고 있습니다. 일관성과 보안을 위해 환경 변수를 사용하는 것이 좋습니다.

🔒 환경 변수 사용 제안
     environment:
-      GF_SECURITY_ADMIN_USER: admin
-      GF_SECURITY_ADMIN_PASSWORD: admin
+      GF_SECURITY_ADMIN_USER: ${GF_SECURITY_ADMIN_USER:-admin}
+      GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD:-admin}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin
environment:
GF_SECURITY_ADMIN_USER: ${GF_SECURITY_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD:-admin}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infra/docker-compose.monitoring.yaml` around lines 32 - 34, 환경에 하드코딩된 Grafana
관리자 자격증명(GF_SECURITY_ADMIN_USER: admin, GF_SECURITY_ADMIN_PASSWORD: admin)을 환경변수
참조로 바꾸세요: docker-compose의 environment 블록에서 리터럴 문자열 대신 ${GF_SECURITY_ADMIN_USER}와
${GF_SECURITY_ADMIN_PASSWORD}로 교체하고 .env 파일이나 배포 환경에서 해당 변수를 정의하도록 설정해 일관성과 보안성을
확보하세요.

@rwaeng rwaeng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 빠른 대응 감사합니다!

kimjihyo added 4 commits March 5, 2026 19:58
Phase 1 - NeonDB 연결 설정:
- TypeORM 설정을 forRoot에서 forRootAsync(ConfigService)로 변경하여
  .env.local/.env 로드 순서 문제 해결
- NeonDB pooler 연결 시 search_path가 비어있는 문제를 schema: 'public' 명시로 해결
- SSL 연결 지원 추가 (DB_SSL=true 환경변수로 제어)
- synchronize: true → false로 변경 (외부 관리형 DB 안전성)
- datasource-cli.ts에도 동일한 SSL 옵션 적용

버그 수정:
- nest-cli.json에 entryFile 추가하여 dist/main을 찾지 못하는 기존 버그 수정

문서:
- 인프라 마이그레이션 가이드 추가 (docs/infrastructure-migration-guide.md)
  - Next.js → Vercel (Hobby), NestJS → EC2 + Docker + Nginx, DB → NeonDB
  - 6단계 Phase별 상세 절차, 환경변수 총정리, 롤백 계획, 비용 추정
서버 리소스 제약으로 인해 모니터링 스택(Prometheus, Grafana, Loki, Promtail)을
전면 제거하고, NeonDB 전환 및 API 전용 배포 파이프라인으로 정리

## 모니터링 스택 제거
- docker-compose.prod.yaml: prometheus, grafana 서비스 제거
- docker-compose.local.yaml: prometheus, grafana, loki, promtail 서비스 및
  promtail-positions 볼륨 제거
- docker-compose.monitoring.yaml: 모니터링 전용 compose 파일 삭제
- prometheus.yaml, grafana-datasource.yaml 삭제
- loki-config.yaml, loki-config.local.yaml, promtail-config.yaml 삭제
- deploy.yml: 모니터링 설정 파일 SCP 전송 및 GF_SECURITY 환경변수 제거

## NeonDB 전환
- docker-compose.prod.yaml: 로컬 PostgreSQL 컨테이너 제거, NeonDB 환경변수로 전환
  (DB_HOST, DB_USERNAME, DB_NAME을 변수화, DB_SSL 활성화)
- deploy.yml: DB_HOST, DB_USERNAME, DB_NAME 환경변수 추가

## 배포 파이프라인 정리
- deploy.yml: API 전용 빌드로 변경 (Web 이미지 빌드 제거)
- deploy.yml: path 필터 추가 (apps/api/**, packages/**, infra/**)
- deploy.yml: 불필요한 QEMU 설정 제거

## API 변경
- health check 엔드포인트 추가 (GET /health)
- CORS 설정을 개발환경 제한에서 전체 허용으로 변경

## 빌드 수정
- packages/types/tsconfig.tsbuildinfo를 git 추적에서 제거
  (incremental 빌드 캐시가 Docker 빌드 시 dist 미생성 문제 유발)

로그 로테이션 설정(x-logging)은 모든 compose 파일에서 유지
- .github/workflows/deploy-web.yml 신규 생성
  - main 브랜치 push 시 apps/web/** 및 packages/** 변경 감지
  - Vercel CLI로 pull → build → deploy 순서로 프로덕션 배포
  - pnpm corepack 설치 단계 포함 (act 호환)

- apps/web/next.config.ts
  - output: "standalone" 제거 (Vercel 자체 빌드 시스템 사용)

- docs/infrastructure-migration-guide.md
  - Phase 3: Vercel GitHub App → CLI 기반 배포로 변경 (vercel link, GitHub Secrets 가이드)
  - Phase 4: deploy.yml에서 모니터링 관련 파일 SCP 및 환경변수 제거
  - Phase 5: Prometheus/Grafana 제거 반영, Docker 로그 직접 확인 방식으로 전환
  - Phase 6: 삭제 대상에 prometheus.yaml, grafana-datasource.yaml 추가
  - Phase 6: 유지 파일에 deploy-web.yml 추가
  - 환경변수 총정리: VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID 추가
  - 아키텍처 다이어그램에서 Prometheus/Grafana 제거
  - 체크리스트 업데이트

- .gitignore
  - .env*.local 패턴 추가 (vercel env pull 결과 파일 제외)
EC2 호스트에서 Nginx를 직접 관리하므로 레포 내 nginx.conf 불필요
@kimjihyo kimjihyo changed the title chore: 로그 로테이션 설정 추가 chore: 인프라 마이그레이션 - 로그 로테이션, NeonDB 전환, Vercel CLI 배포 Mar 6, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/deploy.yml (1)

38-40: ⚠️ Potential issue | 🟠 Major

배포 이미지가 커밋 SHA로 고정되지 않습니다.

라인 40에서 ${{ github.sha }} 태그로 이미지를 빌드하지만, docker-compose.prod.yaml에서는 :latest 태그만 사용하고 있습니다. 배포 단계에서 docker compose pull을 실행할 때 항상 가장 최신의 mutable 태그를 받아오게 되므로, 정확히 이 빌드에서 만든 이미지를 배포할 수 없습니다. 연속 push나 워크플로우 재실행 상황에서는 다른 커밋의 이미지가 배포될 수 있습니다.

🔧 제안 수정

docker-compose.prod.yaml에서 latest 대신 환경 변수를 사용하고, 워크플로우에서 그 값을 전달해야 합니다:

      - name: Deploy via SSH
         uses: appleboy/ssh-action@v1.0.3
         with:
           host: ${{ secrets.SSH_HOST }}
           username: ${{ secrets.SSH_USERNAME }}
           key: ${{ secrets.SSH_PRIVATE_KEY }}
-          envs: DOCKERHUB_USERNAME,DB_HOST,DB_USERNAME,DB_PASSWORD,DB_NAME,GEMINI_API_KEY,NCLOUD_CLOVA_SPEECH_INVOKE_URL,NCLOUD_CLOVA_SPEECH_SECRET_KEY,NCLOUD_OBJECT_STORAGE_ENDPOINT,NCLOUD_OBJECT_STORAGE_REGION,NCLOUD_OBJECT_STORAGE_ACCESS_KEY,NCLOUD_OBJECT_STORAGE_SECRET_KEY,NCLOUD_OBJECT_STORAGE_BUCKET,STT_CALLBACK_URL,JWT_SECRET,MAIL_HOST,MAIL_PORT,MAIL_USER,MAIL_PASSWORD,MAIL_FROM,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GOOGLE_CALLBACK_URL,FRONTEND_URL
+          envs: DOCKERHUB_USERNAME,IMAGE_TAG,DB_HOST,DB_USERNAME,DB_PASSWORD,DB_NAME,GEMINI_API_KEY,NCLOUD_CLOVA_SPEECH_INVOKE_URL,NCLOUD_CLOVA_SPEECH_SECRET_KEY,NCLOUD_OBJECT_STORAGE_ENDPOINT,NCLOUD_OBJECT_STORAGE_REGION,NCLOUD_OBJECT_STORAGE_ACCESS_KEY,NCLOUD_OBJECT_STORAGE_SECRET_KEY,NCLOUD_OBJECT_STORAGE_BUCKET,STT_CALLBACK_URL,JWT_SECRET,MAIL_HOST,MAIL_PORT,MAIL_USER,MAIL_PASSWORD,MAIL_FROM,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GOOGLE_CALLBACK_URL,FRONTEND_URL
         env:
           DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
+          IMAGE_TAG: ${{ github.sha }}

그리고 docker-compose.prod.yaml의 api 서비스 image를 다음과 같이 변경합니다:

   api:
     container_name: malmanhae-api
-    image: ${DOCKERHUB_USERNAME}/malmanhae-api:latest
+    image: ${DOCKERHUB_USERNAME}/malmanhae-api:${IMAGE_TAG}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/deploy.yml around lines 38 - 40, The workflow currently
tags images with ${{ github.sha }} but docker-compose.prod.yaml only pulls
:latest, so deployments can pull a different mutable image; fix by introducing a
stable tag variable (e.g., IMAGE_TAG) and use it consistently: set IMAGE_TAG to
${{ github.sha }} in the deploy workflow step (or export it into the
environment), change the tags block to push both ${{ env.API_IMAGE }}:latest and
${{ env.API_IMAGE }}:${{ env.IMAGE_TAG }}, and update docker-compose.prod.yaml’s
api service image to use the interpolated tag (e.g., image:
"${API_IMAGE}:${IMAGE_TAG}" or similar) so the deploy step pulls the exact
SHA-tagged image; ensure the deploy job receives/passes IMAGE_TAG to the compose
command or env file used during docker compose pull/up.
infra/docker-compose.prod.yaml (1)

39-50: ⚠️ Potential issue | 🟠 Major

CORS_ALLOWED_ORIGINS 환경 변수가 누락되었습니다.

마이그레이션 가이드(Line 417)와 PR 목표에 따르면, Vercel(프론트엔드)과 EC2(백엔드) 간 크로스 오리진 통신을 위해 CORS_ALLOWED_ORIGINS 설정이 필요합니다. 현재 main.ts의 CORS 설정이 이 환경 변수를 참조하므로, 누락 시 프로덕션에서 CORS 에러가 발생합니다.

🐛 수정 제안
       JWT_SECRET: ${JWT_SECRET}
+      CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
       # Mail
       MAIL_HOST: ${MAIL_HOST}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infra/docker-compose.prod.yaml` around lines 39 - 50, Add the missing
CORS_ALLOWED_ORIGINS environment variable to the production docker-compose
service so the backend can read it at runtime; specifically, update the
environment block that currently lists JWT_SECRET, MAIL_*, GOOGLE_* and
FRONTEND_URL to include CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS} so main.ts
(the CORS configuration that reads process.env.CORS_ALLOWED_ORIGINS) will
receive the correct value in production.
🧹 Nitpick comments (3)
.gitignore (1)

47-47: 환경 변수 ignore 규칙은 한 섹션으로 묶어 두는 편이 좋습니다.

.env*.local가 이미 .env.local, .env.development.local, .env.test.local, .env.production.local를 포괄하므로, 현재처럼 파일 하단에 따로 두면 규칙이 중복되고 나중에 관리 포인트가 분산됩니다. 이 패턴을 # Local env files 섹션으로 올리고 기존 개별 .env.*.local 항목은 정리하는 쪽이 더 읽기 쉽습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 47, 현재 `.env*.local` 패턴이 파일 하단에 따로 있어 중복 관리되고 있으니
`.env.local`, `.env.development.local`, `.env.test.local`,
`.env.production.local` 같은 개별 항목들을 삭제하고 `.env*.local` 항목을 `# Local env files`
섹션으로 옮겨 한 곳에서 관리되도록 정리하세요; 대상 식별자로는 `.env*.local` 패턴과 기존 개별 `.env.local`,
`.env.development.local`, `.env.test.local`, `.env.production.local` 항목 및 `#
Local env files` 섹션을 찾아 변경하면 됩니다.
apps/api/src/configs/typeorm.config.ts (1)

18-21: SSL 인증서 검증 비활성화에 대한 참고 사항입니다.

rejectUnauthorized: false는 인증서 검증을 건너뜁니다. NeonDB와 같은 관리형 서비스에서는 일반적으로 허용되지만, 엄격한 보안이 필요한 환경에서는 인증서 검증 활성화를 고려하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/configs/typeorm.config.ts` around lines 18 - 21, The ssl config
currently forces rejectUnauthorized: false when DB_SSL is 'true', which disables
certificate validation; update the ssl logic in the typeorm config (the ssl
property that uses configService.get<string>('DB_SSL')) to enable certificate
validation by default and only set rejectUnauthorized: false when an explicit
opt-in variable (e.g., DB_SSL_INSECURE='true') is present, or better yet support
providing a CA bundle via an env var (e.g., DB_SSL_CA) and pass it into the ssl
object so you can set { ca: <CA>, rejectUnauthorized: true } for secure
connections and only fallback to { rejectUnauthorized: false } when explicitly
requested.
docs/infrastructure-migration-guide.md (1)

24-24: 코드 블록에 언어 식별자 추가를 권장합니다.

Markdown 린터 경고: 여러 코드 블록(Line 24, 46, 554, 675, 679, 705)에 언어 식별자가 없습니다. 다이어그램이나 텍스트 기반 내용에는 text 또는 plaintext를 사용하면 됩니다.

📝 예시
-```
+```text
 현재 구조                         새 구조
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/infrastructure-migration-guide.md` at line 24, 문서의 여러 코드 블록(``` 백틱으로
시작하는 블록들)에 언어 식별자가 누락되어 마크다운 린터 경고가 발생하므로,
docs/infrastructure-migration-guide.md 내 모든 코드 펜스(``` ... ```)에 적절한 언어
식별자(다이어그램/텍스트 기반이면 `text` 또는 `plaintext`)를 추가하여 수정하세요; 각 코드 블록 시작 백틱 뒤에 언어를 명시하면
린터 경고가 해소됩니다.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/deploy-web.yml:
- Around line 1-40: The workflow lacks concurrency controls causing race
conditions; add a concurrency block to the deploy job (job name "deploy") to
ensure only one run for the same branch/env proceeds and to cancel in-progress
runs: insert a concurrency: group: expression like "deploy-web-${{ github.ref
}}-production" and cancel-in-progress: true under the deploy job definition so
overlapping pushes cancel earlier runs and prevent slower deployments from
reassigning the production alias.

In `@apps/api/nest-cli.json`:
- Line 5: Update the JSON key entryFile in the nest-cli configuration to use the
filename only: change entryFile from "apps/api/src/main" to "main"; keep
sourceRoot set to "src" as-is so Nest CLI resolves the compiled output
correctly.

In `@apps/api/src/datasource-cli.ts`:
- Line 15: The current ssl config forces rejectUnauthorized: false when DB_SSL
is 'true' (the ssl property assignment), which disables server cert
verification; change the logic so that when DB_SSL === 'true' you enable TLS
with certificate validation by default (ssl: true or an object with
rejectUnauthorized: true) and introduce a new explicit env var (e.g.,
DB_SSL_REJECT_UNAUTHORIZED or DB_SSL_INSECURE) that, only when set to 'false' or
'true' respectively, will disable validation; update the ssl assignment to read
DB_SSL and the new env var (keep the ssl property name and environment var names
DB_SSL and the new DB_SSL_REJECT_UNAUTHORIZED/DB_SSL_INSECURE) so verification
remains enabled unless explicitly opted out.

In `@apps/api/src/main.ts`:
- Around line 39-43: The CORS config using app.enableCors({ origin: true,
credentials: true }) allows reflected origins with credentials in production;
change it to use an allowlist based on the existing CORS_ALLOWED_ORIGINS env
pattern (as used in object-storage.service.ts): read CORS_ALLOWED_ORIGINS (or
NODE_ENV) and, if in development/local, keep origin: true, otherwise set origin
to a function that checks the request Origin against the allowlist and blocks
unknown origins; keep credentials as required but only enable credentialed
responses for allowed origins. Ensure this logic is implemented where
app.enableCors is called (refer to app.enableCors) and that the allowlist
parsing matches the pattern used in object-storage.service.ts.

In `@docs/infrastructure-migration-guide.md`:
- Line 14: The table of contents anchor "[Phase 5: 모니터링 전환](`#6-phase-5-모니터링-전환`)"
does not match the actual section header "## 6. Phase 5: 모니터링" (header at line
856); update the TOC link or the header so they match exactly—either change the
TOC entry to "[Phase 5: 모니터링](`#6-phase-5-모니터링`)" or rename the header to "## 6.
Phase 5: 모니터링 전환" so the fragment identifier (anchor) and text align and the
link resolves correctly.

---

Outside diff comments:
In @.github/workflows/deploy.yml:
- Around line 38-40: The workflow currently tags images with ${{ github.sha }}
but docker-compose.prod.yaml only pulls :latest, so deployments can pull a
different mutable image; fix by introducing a stable tag variable (e.g.,
IMAGE_TAG) and use it consistently: set IMAGE_TAG to ${{ github.sha }} in the
deploy workflow step (or export it into the environment), change the tags block
to push both ${{ env.API_IMAGE }}:latest and ${{ env.API_IMAGE }}:${{
env.IMAGE_TAG }}, and update docker-compose.prod.yaml’s api service image to use
the interpolated tag (e.g., image: "${API_IMAGE}:${IMAGE_TAG}" or similar) so
the deploy step pulls the exact SHA-tagged image; ensure the deploy job
receives/passes IMAGE_TAG to the compose command or env file used during docker
compose pull/up.

In `@infra/docker-compose.prod.yaml`:
- Around line 39-50: Add the missing CORS_ALLOWED_ORIGINS environment variable
to the production docker-compose service so the backend can read it at runtime;
specifically, update the environment block that currently lists JWT_SECRET,
MAIL_*, GOOGLE_* and FRONTEND_URL to include CORS_ALLOWED_ORIGINS:
${CORS_ALLOWED_ORIGINS} so main.ts (the CORS configuration that reads
process.env.CORS_ALLOWED_ORIGINS) will receive the correct value in production.

---

Nitpick comments:
In @.gitignore:
- Line 47: 현재 `.env*.local` 패턴이 파일 하단에 따로 있어 중복 관리되고 있으니 `.env.local`,
`.env.development.local`, `.env.test.local`, `.env.production.local` 같은 개별 항목들을
삭제하고 `.env*.local` 항목을 `# Local env files` 섹션으로 옮겨 한 곳에서 관리되도록 정리하세요; 대상 식별자로는
`.env*.local` 패턴과 기존 개별 `.env.local`, `.env.development.local`,
`.env.test.local`, `.env.production.local` 항목 및 `# Local env files` 섹션을 찾아 변경하면
됩니다.

In `@apps/api/src/configs/typeorm.config.ts`:
- Around line 18-21: The ssl config currently forces rejectUnauthorized: false
when DB_SSL is 'true', which disables certificate validation; update the ssl
logic in the typeorm config (the ssl property that uses
configService.get<string>('DB_SSL')) to enable certificate validation by default
and only set rejectUnauthorized: false when an explicit opt-in variable (e.g.,
DB_SSL_INSECURE='true') is present, or better yet support providing a CA bundle
via an env var (e.g., DB_SSL_CA) and pass it into the ssl object so you can set
{ ca: <CA>, rejectUnauthorized: true } for secure connections and only fallback
to { rejectUnauthorized: false } when explicitly requested.

In `@docs/infrastructure-migration-guide.md`:
- Line 24: 문서의 여러 코드 블록(``` 백틱으로 시작하는 블록들)에 언어 식별자가 누락되어 마크다운 린터 경고가 발생하므로,
docs/infrastructure-migration-guide.md 내 모든 코드 펜스(``` ... ```)에 적절한 언어
식별자(다이어그램/텍스트 기반이면 `text` 또는 `plaintext`)를 추가하여 수정하세요; 각 코드 블록 시작 백틱 뒤에 언어를 명시하면
린터 경고가 해소됩니다.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5a751280-4ce6-4bf4-8034-c635bd461375

📥 Commits

Reviewing files that changed from the base of the PR and between ecbf2c0 and 5655c59.

📒 Files selected for processing (20)
  • .github/workflows/deploy-web.yml
  • .github/workflows/deploy.yml
  • .gitignore
  • apps/api/nest-cli.json
  • apps/api/src/app.controller.ts
  • apps/api/src/app.module.ts
  • apps/api/src/configs/typeorm.config.ts
  • apps/api/src/datasource-cli.ts
  • apps/api/src/main.ts
  • apps/web/next.config.ts
  • docs/infrastructure-migration-guide.md
  • infra/docker-compose.local.yaml
  • infra/docker-compose.prod.yaml
  • infra/grafana-datasource.yaml
  • infra/loki-config.local.yaml
  • infra/loki-config.yaml
  • infra/prometheus.yaml
  • infra/promtail-config.yaml
  • nginx.conf
  • packages/types/tsconfig.tsbuildinfo
💤 Files with no reviewable changes (8)
  • apps/web/next.config.ts
  • infra/promtail-config.yaml
  • infra/loki-config.yaml
  • infra/prometheus.yaml
  • infra/grafana-datasource.yaml
  • nginx.conf
  • infra/loki-config.local.yaml
  • packages/types/tsconfig.tsbuildinfo

Comment on lines +1 to +40
name: Deploy Web

on:
push:
branches: [main]
paths:
- 'apps/web/**'
- 'packages/**'

env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

jobs:
deploy:
name: Deploy to Vercel
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install pnpm
run: corepack enable && corepack prepare pnpm@10.24.0 --activate

- name: Install Vercel CLI
run: npm install -g vercel

- name: Pull Vercel Environment
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

- name: Build
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}

- name: Deploy
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

GitHub Actions concurrency block cancel-in-progress workflow deployment

💡 Result:

To make a GitHub Actions deployment workflow “single-file” (only one deploy at a time per target) and optionally cancel older runs, use concurrency with cancel-in-progress.

1) Typical: only the latest deploy to an environment (cancel older in-progress)

name: Deploy

on:
  push:
    branches: [main]

concurrency:
  group: deploy-prod          # any string; same group => mutually exclusive
  cancel-in-progress: true    # cancel older runs when a new one starts

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - run: ./deploy.sh

cancel-in-progress: true cancels any currently running workflow/job in the same concurrency group, ensuring only the newest run proceeds. [1]

2) Safer for production: queue deploys (don’t cancel in-progress)

concurrency:
  group: deploy-prod
  cancel-in-progress: false

This prevents overlapping deployments but lets an in-flight production deploy finish. (New runs will wait their turn.) [1]

3) Concurrency per environment name (same workflow deploys to dev/stage/prod)

jobs:
  deploy:
    concurrency:
      group: deploy-${{ inputs.env }}   # or ${{ github.ref_name }}, etc.
      cancel-in-progress: true
    environment: ${{ inputs.env }}

You can set concurrency at the workflow level or job level; job-level is common when only the deploy job must be serialized. [1]

4) If you use GitHub Environments for deployments

GitHub explicitly recommends using concurrency to limit deployments so an environment has at most one deployment in progress (and one pending), and notes you can also use cancel-in-progress. [2]

5) Conditional cancellation (example: cancel PR deploys, don’t cancel main/prod)

cancel-in-progress can be an expression:

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

[3]

Practical caution

Canceling in-progress deployments can leave partial state if your deploy tooling needs cleanup; consider cancel-in-progress: false for production if that risk matters. [1]

Sources: GitHub Docs on concurrency and cancelation [1], GitHub Docs on controlling deployments with environments and concurrency [2], GitHub Community example using an expression for cancel-in-progress [3].


🌐 Web query:

Vercel deploy --prod concurrent runs race condition overwrite

💡 Result:

Concurrent vercel deploy --prod runs don’t “overwrite” the deployment artifact, but they race on the production alias/domain assignment: each run creates its own production-target deployment, and whichever finishes last will (by default) get your production domain(s) aliased to it (“last writer wins”). Vercel’s alias behavior is explicitly “move the alias from the old deployment to the new one.” [1]

How to avoid the race/overwrite

  1. Stage the production deployment, then promote exactly one

    • Deploy without assigning production domains:
      vercel deploy --prod --skip-domain [2]
    • Then promote the chosen deployment:
      vercel promote <deployment-url-or-id> [2][3]
  2. Disable auto-assignment of production domains (so production deploys stay “Staged” until you promote)

    • Turn off Auto-assign Custom Production Domains in project settings; then promote when ready. Vercel documents the resulting states (“Staged / Promoted / Current”). [3]
  3. If you’re doing this via API/automation, rely on alias assignment semantics

    • The Alias API will remove an alias from the previously assigned deployment and assign it to the new one, so your production domain always points to only one deployment—but your pipeline must ensure only the intended deployment performs that step. [1][4]
  4. Optionally add Deployment Checks to block auto-promotion

    • With Deployment Checks, production deployments can be created but won’t be assigned to production domains until checks pass, reducing accidental early promotion (still pair with “promote exactly one” in CI). [5]

Practical CI pattern (prevents concurrent prod promotions)

  • Allow concurrent builds if you want, but enforce single “promote” (e.g., one job allowed to run at a time per environment), and have all other runs only create staged deployments (--prod --skip-domain). [2][3]

Sources:
[1] Vercel CLI vercel alias docs (auto-apply domains; preferred --skip-domain/promote)
[2] Vercel CLI vercel deploy docs (--prod, --skip-domain)
[3] Vercel docs: Promoting Deployments (staged/promoted/current; disable auto-assign)
[4] Vercel REST API: Assign an Alias (reassigns alias from old deployment to new)
[5] Vercel docs: Deployment Checks (block production aliasing until checks pass)


🌐 Web query:

Vercel deployment alias production how does it handle concurrent deployments

💡 Result:

On Vercel, your production domain(s) are effectively an alias pointer to exactly one deployment: the deployment marked Current. Vercel either auto-assigns the production domains on deploys from your production branch (often main), or you can manually change which deployment is Current via Promote/Rollback, which works by assigning the domain(s) to an existing deployment (no rebuild) and immediately marking it Current. [1]

What happens with concurrent deployments?

There are two different “concurrency” concerns:

  1. Build concurrency (can two builds run at once?)
    Vercel limits how many builds can run concurrently on your account/plan (e.g., Hobby vs Pro). Extra builds will queue once you hit the limit. [2]

  2. Production alias concurrency (which deployment ends up on the production domain?)
    Because the production domain serves the single deployment that is Current (aliased to your domain), each promotion/auto-assignment overwrites the previous one—so if multiple production deployments are in-flight, the one that gets promoted/auto-assigned last is the one that ends up Current and therefore served on the production domain. This follows directly from Vercel’s model that “Current” = the deployment aliased to your domain, and “promote” = “make this deployment current.” [1][3]

How to make it deterministic / avoid races

  • Disable auto-assignment of custom production domains, let deployments land as Staged, and then manually Promote the exact deployment you want to become Current. [1]
  • In CI, explicitly promote a known deployment using vercel promote [deployment] rather than relying on “whatever finished last.” [3]

Sources: Promoting Deployments docs (auto-promotion, Staged/Current states, domain assignment, instant promotion/rollback) [1]; Limits (Concurrent Builds) [2]; Vercel CLI docs (vercel promote) [3].


concurrent push 시 이전 배포가 production을 덮어쓸 수 있습니다.

이 workflow는 concurrency 제어가 없어서 main에 연속 push가 들어올 때 race condition이 발생합니다. Vercel은 production alias를 가장 늦게 완료된 deployment로 이동시키므로, 느리게 끝난 이전 run의 배포가 최신 배포를 덮어쓸 수 있습니다.

제안 수정
 name: Deploy Web
 
 on:
   push:
     branches: [main]
     paths:
       - 'apps/web/**'
       - 'packages/**'

+concurrency:
+  group: deploy-web-production
+  cancel-in-progress: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name: Deploy Web
on:
push:
branches: [main]
paths:
- 'apps/web/**'
- 'packages/**'
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
jobs:
deploy:
name: Deploy to Vercel
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pnpm
run: corepack enable && corepack prepare pnpm@10.24.0 --activate
- name: Install Vercel CLI
run: npm install -g vercel
- name: Pull Vercel Environment
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
name: Deploy Web
on:
push:
branches: [main]
paths:
- 'apps/web/**'
- 'packages/**'
concurrency:
group: deploy-web-production
cancel-in-progress: true
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
jobs:
deploy:
name: Deploy to Vercel
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pnpm
run: corepack enable && corepack prepare pnpm@10.24.0 --activate
- name: Install Vercel CLI
run: npm install -g vercel
- name: Pull Vercel Environment
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/deploy-web.yml around lines 1 - 40, The workflow lacks
concurrency controls causing race conditions; add a concurrency block to the
deploy job (job name "deploy") to ensure only one run for the same branch/env
proceeds and to cancel in-progress runs: insert a concurrency: group: expression
like "deploy-web-${{ github.ref }}-production" and cancel-in-progress: true
under the deploy job definition so overlapping pushes cancel earlier runs and
prevent slower deployments from reassigning the production alias.

Comment thread apps/api/nest-cli.json
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"entryFile": "apps/api/src/main",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's check the actual nest-cli.json file
find . -name "nest-cli.json" -type f | head -20

Repository: boostcampwm2025/web11-mmh

Length of output: 93


🌐 Web query:

Nest CLI nest-cli.json entryFile sourceRoot configuration how does path resolution work

💡 Result:

What entryFile + sourceRoot mean (conceptually)

  • sourceRoot is the root directory of your source code for the (default) project (e.g. "src" in a standard app, or "apps/api/src" in a monorepo). The Nest docs describe it that way for standard vs monorepo layouts. [1]
  • entryFile is the entry module filename (without extension) by default in a Nest app it’s typically "main" (i.e. main.tsmain.js). The docs commonly refer to the compiled entry point as dist/main.js in the default layout. [2]

How the Nest CLI actually resolves the runtime JS path (nest start)

When you run nest start, the CLI:

  1. Reads outDir from your tsconfig (falling back to a default dist). [3]
  2. Resolves sourceRoot and entryFile from nest-cli.json (or projects.<appName>.* in monorepo), with CLI flags overriding config when applicable. This is done via getValueOrDefault(...). [4]
  3. Builds the path to the compiled JS entrypoint like this (in order): [3]
    • First try: join(outDir, sourceRoot, entryFile) and then checks for + '.js'
      • e.g. dist/src/main.js
      • e.g. dist/apps/api/src/main.js
    • Fallback if that file doesn’t exist: join(outDir, entryFile)
      • e.g. dist/main.js

So, in “default starter” shape (sourceRoot: "src", entryFile: "main", outDir: "dist"), nest start prefers dist/src/main.js, and only falls back to dist/main.js if dist/src/main.js doesn’t exist. [3]

Why this often confuses people

Whether your compiled file ends up at dist/main.js vs dist/src/main.js is primarily determined by TypeScript’s rootDir / outDir behavior, not by nest-cli.json alone. Nest CLI just tries dist/<sourceRoot>/<entryFile>.js first, then dist/<entryFile>.js. [3]

Practical rules of thumb

  • If your build output contains dist/src/main.js, keep:
    • sourceRoot: "src"
    • entryFile: "main"
    • and run node dist/src/main.js (or just nest start). [3]
  • If you want the compiled entrypoint to be dist/main.js, you must configure TypeScript to emit that shape (commonly by ensuring TS rootDir aligns so it doesn’t preserve the src/ segment), otherwise Nest CLI will still find dist/src/main.js first. (The CLI’s lookup order won’t “flatten” the output by itself.) [3]

Sources: Nest monorepo/CLI docs for sourceRoot [1]; Nest deployment docs for the typical dist/main.js entry point [2]; Nest CLI implementation showing the exact path resolution (outDir/sourceRoot/entryFile then fallback) [3]; helper showing how per-project config/CLI flags override config values [4].


🏁 Script executed:

# Check the actual content of the nest-cli.json file
cat -n apps/api/nest-cli.json

Repository: boostcampwm2025/web11-mmh

Length of output: 338


🏁 Script executed:

# Check the directory structure to understand if sourceRoot should be "src" or "apps/api/src"
ls -la apps/api/ | grep -E "^d.*src"
cat -n apps/api/tsconfig.json 2>/dev/null | head -30

Repository: boostcampwm2025/web11-mmh

Length of output: 1067


🏁 Script executed:

# Check the actual source file structure to confirm the entry point
ls -la apps/api/src/ | grep -E "main\.(ts|js)"
# Also check the root tsconfig to understand the monorepo structure
cat -n tsconfig.json | head -30

Repository: boostcampwm2025/web11-mmh

Length of output: 189


🏁 Script executed:

# Check if there's a monorepo root tsconfig or tsconfig references
find . -maxdepth 2 -name "tsconfig*.json" -type f | head -10
# Also check the actual build output structure
ls -la apps/api/ | grep -E "dist|build"

Repository: boostcampwm2025/web11-mmh

Length of output: 138


🏁 Script executed:

# Check if there's a root monorepo configuration or if other apps have similar setup
find apps -name "nest-cli.json" -type f | xargs cat -n
# Also verify the build output structure expectations
cat -n apps/api/tsconfig.build.json

Repository: boostcampwm2025/web11-mmh

Length of output: 463


🏁 Script executed:

# Check if there's a root-level nest-cli.json that might define project-specific settings
find . -maxdepth 1 -name "nest-cli.json" -type f -exec cat -n {} \;
# Also check package.json to understand the build setup
cat -n package.json | grep -A 5 -B 5 "scripts"

Repository: boostcampwm2025/web11-mmh

Length of output: 412


🏁 Script executed:

# Verify how the build actually resolves by checking if there's a dist directory
find apps/api -name "dist" -type d 2>/dev/null
# Check the actual TypeScript compiler output to see where main.js ends up
cat -n apps/api/tsconfig.json | grep -E "rootDir|outDir|baseUrl"

Repository: boostcampwm2025/web11-mmh

Length of output: 127


🏁 Script executed:

# Let's trace through what the actual resolved paths would be
# Check if there's any build documentation or if we can find how other monorepo apps are configured
find . -path "*/apps/*/nest-cli.json" -type f 2>/dev/null
# Also check what the intended build output would be
cat -n apps/api/tsconfig.json | head -20

Repository: boostcampwm2025/web11-mmh

Length of output: 802


entryFile 값을 "main"으로 수정하세요.

sourceRoot: "src"가 이미 설정되어 있을 때 entryFile: "apps/api/src/main"은 부정확한 경로 해석을 발생시킵니다. Nest CLI는 먼저 dist/src/apps/api/src/main.js를 찾으려 하지만, 실제 컴파일된 파일은 dist/src/main.js에 위치합니다. entryFile"main"으로 변경하면 경로가 정확히 해석됩니다.

-  "entryFile": "apps/api/src/main",
+  "entryFile": "main",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"entryFile": "apps/api/src/main",
"entryFile": "main",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/nest-cli.json` at line 5, Update the JSON key entryFile in the
nest-cli configuration to use the filename only: change entryFile from
"apps/api/src/main" to "main"; keep sourceRoot set to "src" as-is so Nest CLI
resolves the compiled output correctly.

entities: ['src/**/*.entity.ts'],
migrations: ['migrations/*.ts'],
synchronize: false,
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "datasource-cli.ts" -type f

Repository: boostcampwm2025/web11-mmh

Length of output: 101


🏁 Script executed:

cat -n apps/api/src/datasource-cli.ts | head -30

Repository: boostcampwm2025/web11-mmh

Length of output: 729


🌐 Web query:

TypeORM 0.3 node-postgres PostgreSQL SSL rejectUnauthorized false security implications

💡 Result:

Setting ssl: { rejectUnauthorized: false } (whether directly in node-postgres or passed through TypeORM 0.3 via ssl / extra) turns off TLS certificate verification—i.e., the connection is encrypted, but the client does not authenticate the server’s certificate. In Node.js TLS terms, when rejectUnauthorized is false, the peer certificate is not required to validate against trusted CAs. [1]

Security implications

  • MITM / server impersonation risk: Any attacker who can intercept traffic (compromised Wi‑Fi, corporate proxy, poisoned DNS/BGP, compromised VPC routing, etc.) can present any certificate and your app will accept it, enabling credential/session capture and data tampering despite “SSL being on”. [1][2]
  • You lose the main security property of TLS (authentication): This effectively becomes “encryption without identity verification,” which is why node-postgres labels this mode as no-verify. [2]

Why this often shows up with TypeORM + Postgres

  • TypeORM’s Postgres driver supports an ssl object and also an extra bag of options that are passed to the underlying driver (node-postgres), so rejectUnauthorized: false commonly gets wired in there. [4]
  • In node-postgres, behavior changed in pg@8: ssl: true started using Node’s default TLS settings (which verify certs), and the old “don’t verify” behavior requires explicitly setting rejectUnauthorized: false. [3]

Safer alternatives (recommended for production)

  • Verify properly: use ssl: true (or ssl: { rejectUnauthorized: true }) with a certificate chain that validates (public CA or your org CA). [1][3]
  • If the server uses a private/self-signed CA: configure trust by supplying the CA (e.g., ssl: { ca: <CA PEM> }) rather than disabling verification. (TypeORM passes this through via ssl / extra.) [4][1]

Rule of thumb: rejectUnauthorized: false is generally only acceptable for short-lived local/dev troubleshooting in a fully trusted network path—not for internet-facing or production database connections. [1][2]

Sources: Node.js TLS docs on rejectUnauthorized [1]; node-postgres SSL “no-verify” behavior [2]; node-postgres pg@8 change requiring explicit rejectUnauthorized: false for old behavior [3]; TypeORM docs on ssl and extra options passed to the driver [4].


🌐 Web query:

PostgreSQL driver SSL certificate verification best practices managed databases AWS RDS

💡 Result:

Best practices for SSL/TLS certificate verification when connecting to Amazon RDS for PostgreSQL

  1. Require TLS and verify the server certificate + hostname

    • For libpq-based clients (psql, most native drivers), use:
      • sslmode=verify-full (verifies CA chain and that the cert matches the hostname). (postgresql.org)
    • Avoid sslmode=require if you care about MITM resistance—it encrypts but does not validate the server identity. (docs.aws.amazon.com)
  2. Trust the right CA: use the current AWS RDS CA bundle (and manage rotation)

    • Configure your client to trust the AWS RDS root CA (or bundle) via sslrootcert=... and keep it updated. AWS’s RDS PostgreSQL SSL example uses sslmode=verify-full with an AWS CA file. (docs.aws.amazon.com)
    • Plan CA rotations ahead of time (update client trust stores before/with DB CA changes). AWS provides guidance and CA identifiers used for rotations. (aws.amazon.com)
    • If you have mixed/unknown regions or multiple RDS CAs, using the combined CA bundle is a common approach referenced by AWS services/docs. (docs.aws.amazon.com)
  3. Ensure the driver actually uses your trust store

    • Java/JDBC: you may need to import the RDS CA into the JVM trust store (cacerts) (or explicitly point the driver at a trust store), otherwise verification may fail or be bypassed depending on configuration. (repost.aws)
  4. Use the RDS endpoint hostname (not raw IPs)

    • Hostname verification (verify-full) only works as intended when the client connects using the DNS name that matches the certificate (typically the RDS endpoint). (postgresql.org)

Source index

  • [1] Amazon RDS User Guide – Using SSL with a PostgreSQL DB instance (docs.aws.amazon.com)
  • [2] PostgreSQL docs – libpq SSL support / sslmode (verify-full, CA files, hostname verification) (postgresql.org)
  • [3] AWS News Blog – Rotate your SSL/TLS certificates (RDS/Aurora CA rotation guidance & identifiers) (aws.amazon.com)
  • [4] AWS re:Post – Use SSL to connect to your RDS instance (JDBC trust store note) (repost.aws)
  • [5] AWS DMS docs – RDS combined CA bundle + meaning of require/verify- (docs.aws.amazon.com)

Citations:


DB_SSL 활성화 시 TLS 인증서 검증이 기본으로 비활성화됩니다.

Line 15에서 DB_SSL=true인 모든 환경에 rejectUnauthorized: false를 강제하면, 연결이 암호화되더라도 서버 인증서 검증이 빠져 중간자(MITM) 공격에 노출됩니다. TLS의 주요 보안 특성인 서버 인증이 제거되므로, 인프라 손상이나 DNS 변조 환경에서 공격자의 임의 인증서를 수용하게 됩니다. 기본값으로 검증을 유지하고, 검증 비활성화가 필수인 경우만 별도 환경 변수로 명시적으로 활성화하는 것이 안전합니다.

제안 방향
-  ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
+  ssl:
+    process.env.DB_SSL === 'true'
+      ? {
+          rejectUnauthorized:
+            process.env.DB_SSL_ALLOW_INVALID_CERT !== 'true',
+        }
+      : false,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
ssl:
process.env.DB_SSL === 'true'
? {
rejectUnauthorized:
process.env.DB_SSL_ALLOW_INVALID_CERT !== 'true',
}
: false,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/datasource-cli.ts` at line 15, The current ssl config forces
rejectUnauthorized: false when DB_SSL is 'true' (the ssl property assignment),
which disables server cert verification; change the logic so that when DB_SSL
=== 'true' you enable TLS with certificate validation by default (ssl: true or
an object with rejectUnauthorized: true) and introduce a new explicit env var
(e.g., DB_SSL_REJECT_UNAUTHORIZED or DB_SSL_INSECURE) that, only when set to
'false' or 'true' respectively, will disable validation; update the ssl
assignment to read DB_SSL and the new env var (keep the ssl property name and
environment var names DB_SSL and the new
DB_SSL_REJECT_UNAUTHORIZED/DB_SSL_INSECURE) so verification remains enabled
unless explicitly opted out.

Comment thread apps/api/src/main.ts
Comment on lines +39 to +43
// CORS 설정
app.enableCors({
origin: true,
credentials: true,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the actual code at the specified location
if [ -f "apps/api/src/main.ts" ]; then
  echo "=== apps/api/src/main.ts (lines 35-50) ==="
  sed -n '35,50p' apps/api/src/main.ts
else
  echo "File not found, searching for main.ts files"
  find . -name "main.ts" -type f 2>/dev/null | head -20
fi

Repository: boostcampwm2025/web11-mmh

Length of output: 442


🏁 Script executed:

# Check if the referenced documentation exists
if [ -f "docs/infrastructure-migration-guide.md" ]; then
  echo "=== Checking docs/infrastructure-migration-guide.md around lines 283-303 ==="
  sed -n '280,310p' docs/infrastructure-migration-guide.md
else
  echo "Documentation file not found"
  find . -path "*/docs/*" -name "*.md" -type f 2>/dev/null | head -20
fi

Repository: boostcampwm2025/web11-mmh

Length of output: 805


🏁 Script executed:

# Search for existing CORS configuration patterns in the codebase
echo "=== Searching for CORS configuration patterns ==="
rg "enableCors|CORS_ALLOWED|cors\(" --type ts --type js -B 2 -A 5 | head -100

Repository: boostcampwm2025/web11-mmh

Length of output: 1862


🌐 Web query:

NestJS Express CORS origin: true credentials: true security implications

💡 Result:

In NestJS (Express platform), app.enableCors({ origin: true, credentials: true }) delegates to the Express cors middleware. With origin: true, the server reflects whatever Origin the browser sends (effectively “allow any origin, dynamically”), and with credentials: true it tells the browser it may include cookies / HTTP auth / client TLS certs and let the calling page read the response. (expressjs.com)

Security implications (why this is risky)

  • Cross-site data exposure if you use cookie-based sessions (or other ambient credentials). Any website a logged-in user visits can run JS that sends fetch(..., { credentials: "include" }) to your API; if your API reflects that site’s Origin and allows credentials, the attacker’s page can read sensitive API responses as the victim (account data, PII, etc.). This is a well-known CORS misconfiguration class. (zaproxy.org)
  • “It’s not *, so it works” is exactly the problem. Browsers block Access-Control-Allow-Credentials: true when Access-Control-Allow-Origin: *, so * + credentials fails; origin: true avoids * by echoing a specific origin, which makes credentialed cross-origin reads succeed—potentially for any origin. (developer.mozilla.org)
  • CSRF is still possible even without CORS, but permissive credentialed CORS can turn some attacks from “can trigger requests” into “can trigger requests and read the results,” which is often much more damaging (data exfiltration). (zaproxy.org)

What to do instead (typical production guidance)

  • Use an explicit allowlist (or an origin function that checks the incoming origin against a list) rather than origin: true. (expressjs.com)
  • Only set credentials: true when you truly need cookies across origins; if you do, also ensure your auth cookies are appropriately scoped (e.g., HttpOnly, Secure, and a suitable SameSite policy) and implement CSRF defenses for state-changing endpoints. (developer.mozilla.org)

Citations:


프로덕션 CORS가 모든 Origin의 크레덴셜 기반 요청을 허용합니다.

Line 41의 origin: true는 요청 Origin을 그대로 반사합니다. 여기에 credentials: true까지 켜져 있으면, 임의 사이트에서 인증된 응답을 읽을 수 있게 됩니다. 사용자가 로그인한 상태에서 악의적 사이트를 방문할 경우, 그 사이트는 쿠키/인증정보를 포함한 API 요청을 보내고 응답을 읽을 수 있어 사용자 데이터가 노출될 수 있습니다. docs/infrastructure-migration-guide.md에서 권장하는 대로 개발/로컬에서만 true를 쓰고, 프로덕션은 allowlist로 제한하세요. 코드베이스는 이미 object-storage.service.ts에서 CORS_ALLOWED_ORIGINS 환경변수 패턴을 사용 중입니다.

🔧 제안 수정
-  // CORS 설정
-  app.enableCors({
-    origin: true,
-    credentials: true,
-  });
+  const corsOrigins =
+    process.env.CORS_ALLOWED_ORIGINS?.split(',')
+      .map((origin) => origin.trim())
+      .filter(Boolean) ?? [];
+
+  app.enableCors({
+    origin: nodeEnv === 'development' || nodeEnv === 'local' ? true : corsOrigins,
+    credentials: true,
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/main.ts` around lines 39 - 43, The CORS config using
app.enableCors({ origin: true, credentials: true }) allows reflected origins
with credentials in production; change it to use an allowlist based on the
existing CORS_ALLOWED_ORIGINS env pattern (as used in
object-storage.service.ts): read CORS_ALLOWED_ORIGINS (or NODE_ENV) and, if in
development/local, keep origin: true, otherwise set origin to a function that
checks the request Origin against the allowlist and blocks unknown origins; keep
credentials as required but only enable credentialed responses for allowed
origins. Ensure this logic is implemented where app.enableCors is called (refer
to app.enableCors) and that the allowlist parsing matches the pattern used in
object-storage.service.ts.

3. [Phase 2: EC2에 NestJS + Nginx 배포](#3-phase-2-ec2에-nestjs--nginx-배포)
4. [Phase 3: Vercel로 Next.js 배포](#4-phase-3-vercel로-nextjs-배포)
5. [Phase 4: CI/CD 파이프라인 개편](#5-phase-4-cicd-파이프라인-개편)
6. [Phase 5: 모니터링 전환](#6-phase-5-모니터링-전환)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

목차 링크 앵커가 실제 섹션 헤더와 불일치합니다.

Line 14의 #6-phase-5-모니터링-전환이 Line 856의 실제 헤더 ## 6. Phase 5: 모니터링와 일치하지 않아 링크가 작동하지 않습니다.

🔗 수정 제안
-6. [Phase 5: 모니터링 전환](`#6-phase-5-모니터링-전환`)
+6. [Phase 5: 모니터링](`#6-phase-5-모니터링`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
6. [Phase 5: 모니터링 전환](#6-phase-5-모니터링-전환)
6. [Phase 5: 모니터링](`#6-phase-5-모니터링`)
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 14-14: Link fragments should be valid

(MD051, link-fragments)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/infrastructure-migration-guide.md` at line 14, The table of contents
anchor "[Phase 5: 모니터링 전환](`#6-phase-5-모니터링-전환`)" does not match the actual
section header "## 6. Phase 5: 모니터링" (header at line 856); update the TOC link
or the header so they match exactly—either change the TOC entry to "[Phase 5:
모니터링](`#6-phase-5-모니터링`)" or rename the header to "## 6. Phase 5: 모니터링 전환" so the
fragment identifier (anchor) and text align and the link resolves correctly.

@kimjihyo kimjihyo self-assigned this Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants