chore: 인프라 마이그레이션 - 로그 로테이션, NeonDB 전환, Vercel CLI 배포 - #388
Conversation
- Docker 컨테이너 로그: json-file 드라이버, max-size 10m, max-file 3 - Loki: 15일 retention 정책 및 compaction 활성화 - Prometheus: 15일 메트릭 보관 기간 설정
📝 Walkthrough둘러보기단일 서버 아키텍처에서 분산 아키텍처로의 마이그레이션을 지원하기 위해 인프라, API 설정, CI/CD 파이프라인을 재구성했습니다. Loki, Prometheus, Grafana, Promtail 모니터링 서비스를 제거하고, TypeORM 비동기 설정을 추가하며, Docker 컴포저 로깅을 설정하고, API 상태 체크 엔드포인트를 추가하고, GitHub Actions 워크플로우를 분리했습니다. 변경 사항
예상 코드 리뷰 노력🎯 3 (중간) | ⏱️ ~20분 시
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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. Comment |
There was a problem hiding this comment.
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-data와loki-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
📒 Files selected for processing (6)
infra/docker-compose-infra.yamlinfra/docker-compose.local.yamlinfra/docker-compose.monitoring.yamlinfra/docker-compose.prod.yamlinfra/loki-config.local.yamlinfra/loki-config.yaml
| environment: | ||
| GF_SECURITY_ADMIN_USER: admin | ||
| GF_SECURITY_ADMIN_PASSWORD: admin |
There was a problem hiding this comment.
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.
| 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 파일이나 배포 환경에서 해당 변수를 정의하도록 설정해 일관성과 보안성을
확보하세요.
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 불필요
There was a problem hiding this comment.
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
📒 Files selected for processing (20)
.github/workflows/deploy-web.yml.github/workflows/deploy.yml.gitignoreapps/api/nest-cli.jsonapps/api/src/app.controller.tsapps/api/src/app.module.tsapps/api/src/configs/typeorm.config.tsapps/api/src/datasource-cli.tsapps/api/src/main.tsapps/web/next.config.tsdocs/infrastructure-migration-guide.mdinfra/docker-compose.local.yamlinfra/docker-compose.prod.yamlinfra/grafana-datasource.yamlinfra/loki-config.local.yamlinfra/loki-config.yamlinfra/prometheus.yamlinfra/promtail-config.yamlnginx.confpackages/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
| 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 }} |
There was a problem hiding this comment.
🧩 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.shcancel-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: falseThis 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
-
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]
- Deploy without assigning production domains:
-
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]
-
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]
-
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:
-
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] -
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.
| 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.
| "$schema": "https://json.schemastore.org/nest-cli", | ||
| "collection": "@nestjs/schematics", | ||
| "sourceRoot": "src", | ||
| "entryFile": "apps/api/src/main", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the actual nest-cli.json file
find . -name "nest-cli.json" -type f | head -20Repository: 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)
sourceRootis 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]entryFileis the entry module filename (without extension) by default in a Nest app it’s typically"main"(i.e.main.ts→main.js). The docs commonly refer to the compiled entry point asdist/main.jsin the default layout. [2]
How the Nest CLI actually resolves the runtime JS path (nest start)
When you run nest start, the CLI:
- Reads
outDirfrom yourtsconfig(falling back to a defaultdist). [3] - Resolves
sourceRootandentryFilefromnest-cli.json(orprojects.<appName>.*in monorepo), with CLI flags overriding config when applicable. This is done viagetValueOrDefault(...). [4] - 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
- e.g.
- Fallback if that file doesn’t exist:
join(outDir, entryFile)- e.g.
dist/main.js
- e.g.
- First try:
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 justnest start). [3]
- If you want the compiled entrypoint to be
dist/main.js, you must configure TypeScript to emit that shape (commonly by ensuring TSrootDiraligns so it doesn’t preserve thesrc/segment), otherwise Nest CLI will still finddist/src/main.jsfirst. (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.jsonRepository: 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 -30Repository: 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 -30Repository: 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.jsonRepository: 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 -20Repository: 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.
| "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, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "datasource-cli.ts" -type fRepository: boostcampwm2025/web11-mmh
Length of output: 101
🏁 Script executed:
cat -n apps/api/src/datasource-cli.ts | head -30Repository: 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
sslobject and also anextrabag of options that are passed to the underlying driver (node-postgres), sorejectUnauthorized: falsecommonly gets wired in there. [4] - In node-postgres, behavior changed in
pg@8:ssl: truestarted using Node’s default TLS settings (which verify certs), and the old “don’t verify” behavior requires explicitly settingrejectUnauthorized: false. [3]
Safer alternatives (recommended for production)
- Verify properly: use
ssl: true(orssl: { 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 viassl/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
-
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=requireif you care about MITM resistance—it encrypts but does not validate the server identity. (docs.aws.amazon.com)
- For libpq-based clients (psql, most native drivers), use:
-
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 usessslmode=verify-fullwith 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)
- Configure your client to trust the AWS RDS root CA (or bundle) via
-
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)
- Java/JDBC: you may need to import the RDS CA into the JVM trust store (
-
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)
- Hostname verification (
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:
- 1: https://www.postgresql.org/docs/current/libpq-ssl.html?utm_source=openai
- 2: https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Security.SSL.html?utm_source=openai
- 3: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Concepts.General.SSL.html?utm_source=openai
- 4: https://aws.amazon.com/blogs/aws/rotate-your-ssl-tls-certificates-now-amazon-rds-and-amazon-aurora-expire-in-2024/?utm_source=openai
- 5: https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Security.SSL.html?utm_source=openai
- 6: https://repost.aws/knowledge-center/rds-connect-ssl-connection?utm_source=openai
- 7: https://www.postgresql.org/docs/current/libpq-ssl.html?utm_source=openai
- 8: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Concepts.General.SSL.html?utm_source=openai
- 9: https://www.postgresql.org/docs/current/libpq-ssl.html?utm_source=openai
- 10: https://aws.amazon.com/blogs/aws/rotate-your-ssl-tls-certificates-now-amazon-rds-and-amazon-aurora-expire-in-2024/?utm_source=openai
- 11: https://repost.aws/knowledge-center/rds-connect-ssl-connection?utm_source=openai
- 12: https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Security.SSL.html?utm_source=openai
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.
| 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.
| // CORS 설정 | ||
| app.enableCors({ | ||
| origin: true, | ||
| credentials: true, | ||
| }); |
There was a problem hiding this comment.
🧩 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
fiRepository: 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
fiRepository: 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 -100Repository: 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’sOriginand 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 blockAccess-Control-Allow-Credentials: truewhenAccess-Control-Allow-Origin: *, so* + credentialsfails;origin: trueavoids*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
originfunction that checks the incoming origin against a list) rather thanorigin: true. (expressjs.com) - Only set
credentials: truewhen you truly need cookies across origins; if you do, also ensure your auth cookies are appropriately scoped (e.g.,HttpOnly,Secure, and a suitableSameSitepolicy) and implement CSRF defenses for state-changing endpoints. (developer.mozilla.org)
Citations:
- 1: https://expressjs.com/en/resources/middleware/cors.html?utm_source=openai
- 2: https://www.zaproxy.org/docs/alerts/40040-2/?utm_source=openai
- 3: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS/Errors/CORSNotSupportingCredentials?utm_source=openai
- 4: https://www.zaproxy.org/docs/alerts/40040-2/?utm_source=openai
- 5: https://expressjs.com/en/resources/middleware/cors.html?utm_source=openai
- 6: https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CORS?utm_source=openai
프로덕션 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-모니터링-전환) |
There was a problem hiding this comment.
목차 링크 앵커가 실제 섹션 헤더와 불일치합니다.
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.
| 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.
Summary
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.yamlinfra/loki-config.yaml,infra/loki-config.local.yaml,infra/promtail-config.yamlTest plan
act push --secret-file .secrets -W .github/workflows/deploy-web.yml로 Vercel 배포 확인Summary by CodeRabbit
릴리스 노트
새로운 기능
Chores
Documentation