콘텐츠로 이동

ADR 0004: STG runtime 설정과 Canonical DB 전환 경계

상태: Accepted 작성일: 2026-09-03 범위: Vercel Next BFF, Azure Gateway·VM runtime, PostgreSQL baseline, STG 배포 gate

Context

STG 배포 전에 애플리케이션 기능뿐 아니라 실행 환경의 연결 계약을 고정해야 한다. 현재 저장소에서 확인된 위험은 다음과 같다.

  • Next BFF의 Backend origin이 세 파일에 중복되어 있고, 배포 환경에서 PROCEDURE_API_BASE가 누락돼도 localhost:8005로 조용히 fallback한다.
  • Azure CD가 STG_ENV 또는 PROD_ENV를 Docker build context에 만들고, Dockerfile이 그 .env를 image layer에 복사한다.
  • Backend runtime은 단일 Alembic baseline과 PostgreSQL data snapshot만 사용하지만, 현재 snapshot은 STG 직전에 다시 만들어야 하는 중간본이다.
  • 기존 STG DB의 Alembic history는 새 단일 baseline과 호환되지 않을 수 있다. 짧은 점검 중단은 허용되므로 dual write, legacy fallback DB, 병렬 runtime은 만들 필요가 없다.
  • Gateway의 실제 route·write 차단 설정, VM의 deploy.sh, Vercel의 현재 환경변수 값은 이 저장소만으로 확인할 수 없다.

이 결정은 API URL·request·response 구조와 데이터 의미를 바꾸지 않는다. 배포 환경별 연결과 비밀값 전달 방식을 정리하는 작업이다.

Decision

1. 실행 topology

Browser
  └─ same-origin /api/*
      └─ Vercel Next Route Handler
          └─ PROCEDURE_API_BASE (server-only origin)
              └─ Azure Gateway /procedure-hub/*
                  └─ Azure VM procedure-hub container
                      └─ DATABASE_URL
                          └─ Azure PostgreSQL canonical DB
  • Browser는 지금처럼 같은 origin의 Next /api/*만 호출한다.
  • Backend origin은 browser bundle에 넣지 않는다. NEXT_PUBLIC_ 환경변수로 노출하지 않는다.
  • Gateway path /procedure-hub와 API path /api/v1은 환경값이 아니라 공개 API 계약이므로 코드의 단일 상수로 유지한다.

2. Next BFF 환경 설정

PROCEDURE_API_BASE는 scheme과 host만 포함한 origin으로 정의한다.

허용: https://api-stg-csp.medisolveai.com
정규화 후 허용: https://api-stg-csp.medisolveai.com/
금지: https://api-stg-csp.medisolveai.com/procedure-hub

마지막 /는 정규화할 수 있지만 path, query, fragment가 포함된 값은 설정 오류로 처리한다. Next server 설정 모듈 한 곳만 process.env.PROCEDURE_API_BASE를 읽고 모든 BFF route가 이를 사용한다.

  • 모든 실행 환경에서 origin을 명시한다. 로컬 주소는 Compose·env에서 지정한다.
  • 미지정·잘못된 URL이면 localhost로 fallback하지 않고 명시적인 설정 오류로 실패한다.

PROCEDURE_API_BASE는 server runtime 값으로 읽는다. NEXT_PUBLIC_ 값은 build 시 browser bundle에 고정되므로 Backend routing에 사용하지 않는다. 과거 버전 배지 전용 NEXT_PUBLIC_APP_ENV와 NEXT_PUBLIC_APP_VERSION은 미사용 배지 제거와 함께 코드에서 제거했다.

3. 환경별 설정 계약

소유 위치 Local STG PROD
Next PROCEDURE_API_BASE http://127.0.0.1:8005 https://api-stg-csp.medisolveai.com https://api-csp.medisolveai.com
Backend API_PREFIX /procedure-hub /procedure-hub /procedure-hub
Backend DATABASE_URL 공용 로컬 Postgres의 procedure_hub DB STG PostgreSQL TLS DSN PROD PostgreSQL TLS DSN
Backend DATABASE_ECHO 필요 시 명시 false false
Backend CORS_ORIGINS Local FE origin 승인된 STG browser origin 승인된 PROD browser origin

CORS_ORIGINS는 인증 수단이 아니다. Server-to-server 소비자는 CORS의 적용 대상이 아니며, write 인증·인가는 계속 Gateway가 소유한다.

4. Backend 비밀값은 runtime에만 주입

Docker image는 환경과 무관한 동일 runtime 산출물로 만든다.

  • CD build job은 backend/envs/.env를 만들거나 환경 secret을 읽지 않는다.
  • Dockerfile은 .env를 복사하지 않는다.
  • .dockerignore는 실제 .env 파일이 build context에 들어가는 것을 차단한다.
  • 인프라 담당자가 VM의 /opt/procedure-hub/config/{environment}.env를 직접 생성·관리한다. CD는 이 파일을 전달하거나 덮어쓰지 않으며, deploy script는 container 시작 시 --env-file로 연결한다.
  • 파일은 deploy 계정 소유, mode 600으로 둔다. 로그와 image metadata에 값이 나타나지 않게 한다.

첫 전환에서는 Key Vault, sidecar, 별도 secret sync agent를 추가하지 않는다. VM runtime env-file로 현재 문제를 해소하고, 다수 VM·자동 rotation이 실제 요구가 될 때 별도 인프라 작업으로 승격한다. Procedure Hub CD는 STG_ENV·PROD_ENV 같은 애플리케이션 env bundle secret에 의존하지 않는다.

5. DB는 점검 중단을 허용한 단일 전환

STG에서는 legacy DB와 canonical DB를 병행 운영하지 않는다.

  1. 현재 STG DB와 현재 runtime image digest를 rollback 단위로 기록한다.
  2. 현재 STG DB를 custom-format으로 한 번 백업한다.
  3. STG DB를 빈 상태로 재구성하고 b202609030001 baseline을 적용한다.
  4. 최종 procedure_hub_data.dump를 --data-only로 한 번 복원한다.
  5. Alembic revision, manifest, 핵심 row count를 확인한다.
  6. 새 runtime을 시작하고 기존 API·화면과 Knowledge Retrieval을 smoke한다.

Dockerfile CMD의 alembic upgrade head는 이후 동일 baseline 내 배포를 위한 기동 절차로 유지한다. seed/importer/data-init image와 JSON fallback은 다시 만들지 않는다.

Rollback은 ${environment}-rollback runtime image와 전환 직전 DB backup을 항상 함께 복원한다. 새 DB에 과거 runtime만 연결하거나 새 runtime에 과거 DB만 연결하지 않는다.

6. Publication gate는 배포와 분리

DB와 API를 배포한다고 clinical release를 자동 publish하지 않는다.

  • 게시 release가 없을 때 /knowledge/retrieve의 status=unavailable은 정상적인 fail-closed 결과다.
  • 관련 item을 반환하는 STG 기능 검증은 피부과 의사가 승인한 release를 명시적으로 발행한 뒤 수행한다.
  • 기술 배포 성공과 임상 content 승인 상태를 하나의 자동화 단계로 합치지 않는다.

7. CI와 배포 gate

Backend test 정의를 복제하지 않는다. 기존 Backend Test workflow를 CD에서도 재사용하고, Docker build는 그 성공 뒤에만 실행한다. stg·prod 직접 push로 검증을 우회하지 못하도록 protected branch의 required check로 같은 workflow를 지정한다.

Frontend는 Vercel build가 통과한 commit만 STG 대상으로 삼고, 배포 뒤 same-origin BFF smoke로 실제 PROCEDURE_API_BASE 주입을 확인한다. Vercel 설정 화면의 값 존재만으로 완료 판정하지 않는다.

8. Gateway와 write 경계

  • /procedure-hub/health, /procedure-hub/api-docs, /procedure-hub/openapi.json, /procedure-hub/api/v1/*의 route 결과를 STG에서 확인한다.
  • Knowledge Retrieval의 HTTP POST는 read이고, glossary apply·rollback과 terminology PUT·DELETE만 semantic write라는 기존 분류를 유지한다.
  • write는 Gateway의 미인증 거부와 인증 허용을 각각 한 번 확인한다.
  • X-Uploader는 감사값이며 인증 수단이 아니다. 신뢰할 수 있는 사용자 identity 전달 경로가 확정되기 전에는 browser가 보낸 값을 그대로 신뢰하지 않고 null 기록을 허용한다.

Deployment-critical hardcode 판정

현재 위치 판정 처리
세 Next server route의 PROCEDURE_API_BASE + localhost fallback 배포 위험 server 설정 모듈 하나로 통합, non-development fallback 제거
/procedure-hub/api/v1 공개 계약 단일 상수로 유지
Backend CLI의 http://127.0.0.1:8005 기본값 로컬 실행 기본 유지
문서의 STG·PROD URL 운영 문서 유지하되 실제 smoke 결과와 대조
Dockerfile의 .env COPY 보안 결함 제거하고 VM runtime env-file로 전환

Implementation tasks

Task 6-1. Next Backend origin 단일화

목표: Next BFF가 환경별 Backend origin을 server 설정 경계 하나에서만 읽도록 변경한다.

예상 결과:

  • process.env.PROCEDURE_API_BASE 직접 참조가 server 설정 모듈 한 곳에만 존재함
  • STG·PROD에서 누락된 설정이 localhost 요청으로 바뀌지 않음
  • 기존 BFF path·status·response body 계약이 유지됨

최소 gate:

  • 설정의 local 허용값, production 누락, path 포함 URL 세 경우만 집중 검증한다.
  • 대표 일반 GET, glossary binary relay, knowledge POST를 실제 local Backend와 한 번 smoke한다.

Task 6-2. Runtime secret 전환

목표: Backend 설정을 image build가 아니라 VM의 container start 시점에 주입한다.

예상 결과:

  • runtime image filesystem과 history에 .env가 없음
  • CD build가 STG_ENV·PROD_ENV 내용을 소비하지 않음
  • VM deploy script가 환경별 runtime env-file을 사용함

최소 gate:

  • image 한 개를 검사해 /app/.env 부재와 환경변수 없이 DB 접속이 실패하는 것을 확인한다.
  • STG container의 마스킹된 /health/config와 실제 DB 연결을 확인한다.

Task 6-3. STG DB baseline 전환 준비

목표: 최종 data snapshot과 단일 baseline을 STG의 빈 PostgreSQL에 적용할 수 있는 상태로 고정한다.

예상 결과:

  • STG 역할이 pg_trgm extension을 사용할 수 있음
  • 최종 dump의 manifest·핵심 row count가 local 승인본과 일치함
  • runtime image와 DB backup의 결합 rollback 정보가 기록됨

최소 gate:

  • 빈 STG DB에 baseline과 snapshot을 한 번 적용한다.
  • 전체 table 전수 비교를 새로 만들지 않고 기존 manifest와 대표 API 결과를 재사용한다.

Task 6-4. Gateway·Vercel preflight

목표: 저장소 밖 연결 설정이 위 topology와 권한 경계를 만족하는지 확정한다.

예상 결과:

  • Vercel STG의 Backend origin이 origin-only 값으로 설정됨
  • Gateway의 health·docs·read·write route 판정이 기록됨
  • 미인증 write 거부와 인증 write 허용이 각각 확인됨

최소 gate:

  • route 종류별 대표 요청만 사용한다. 모든 endpoint의 auth test를 복제하지 않는다.

Task 6-5. STG cutover와 사용자 검토

목표: 점검 중단을 열고 canonical DB와 새 runtime을 STG에 한 번 전환한 뒤 사용자 검토 상태로 둔다.

예상 결과:

  • 기존 공개 API contract diff가 0임
  • 핵심 화면과 BFF smoke failure가 0임
  • publication 상태에 맞는 Knowledge Retrieval 결과가 확인됨
  • rollback 명령, image digest, DB backup 위치가 기록됨

최소 gate:

  • 기존 Backend contract suite를 배포 전에 한 번 실행한다.
  • 배포 뒤 브라우저 핵심 흐름과 외부 소비 API만 smoke한다.
  • 실제 rollback은 STG에서 한 번 수행해 복구 결과를 남긴다.

Change impact

저장소 변경 예상 범위:

  • src/lib/server/: Backend origin 설정 모듈
  • src/lib/server/procedureApiProxy.ts
  • src/app/api/v1/glossary/[...path]/route.ts
  • src/app/api/v1/knowledge/retrieve/route.ts
  • backend/Dockerfile, .dockerignore
  • .github/workflows/backend-test.yml, .github/workflows/csp-azure-deploy.yml
  • 배포·Task 문서

저장소 밖 변경 예상 범위:

  • Vercel STG·PROD environment variables
  • Azure VM의 runtime env-file과 deploy.sh
  • Azure PostgreSQL baseline 적용과 pg_trgm 허용 여부
  • Gateway route·write 인증 설정
  • GitHub stg·prod branch protection

변경하지 않는 범위:

  • 공개 API request·response schema와 의미
  • Browser의 same-origin /api/* 호출 방식
  • Canonical entity·retrieval ranking·0.90 threshold
  • 임상 content 승인 상태
  • Key Vault·Kubernetes·무중단 dual runtime 도입

확인된 STG preflight와 남은 결정

2026-09-04 read-only preflight 결과는 다음과 같다.

  1. 중앙 deploy.sh는 Bastion에서 Procedure Hub VM으로 접속해 /opt/procedure-hub/config/stg.env를 --env-file로 연결한다. INFRA PR #22·#23으로 env 연결과 필수 DATABASE_URL 사전 검증을 반영했고 Bastion sync를 확인했다.
  2. Vercel STG는 PROCEDURE_API_BASE=https://api-stg-csp.medisolveai.com origin을 사용한다.
  3. Gateway의 /procedure-hub/* route는 현재 needsAuth: false다. Backend도 write 인증을 직접 수행하지 않으므로 terminology PUT·DELETE, glossary apply·rollback이 공개 route를 통과할 수 있다. 2026-09-04 수동 전환에서는 STG에 한해 이를 허용했고 PROD 승격 전 별도 write 보호가 필요하다.
  4. Azure PostgreSQL server는 여러 STG DB가 공유한다. azure.extensions allowlist에 기존 VECTOR를 유지하며 PG_TRGM을 추가하고, 실제 CREATE EXTENSION은 procedure_hub_stg에서만 실행했다. 확인 결과 Procedure Hub는 pg_trgm=1, charty_stg·linky_stg·say_stg·watch_stg는 모두 pg_trgm=0이다.

확인되지 않았다는 이유로 legacy fallback을 추가하지 않는다. 특히 write 인증 문제를 runtime·DB 전환 코드에 섞지 않고 Gateway 또는 별도 인증 task로 분리한다.

Rejected alternatives

대안 기각 이유
Backend URL을 NEXT_PUBLIC_로 browser에 주입 build-time에 고정되고 Gateway origin을 불필요하게 노출함
배포 환경에서도 localhost fallback 유지 환경변수 누락이 명확한 설정 실패가 아니라 원인 불명의 502로 보임
.env를 Docker image에 계속 포함 image 접근 권한이 곧 환경 secret 접근 권한이 됨
기존 DB와 새 DB를 dual write·shadow 운영 짧은 중단을 허용하는 현재 요구보다 코드·검증·운영 비용이 큼
startup seed 또는 data-init image 복원 DB를 source of truth로 만든 현재 경계를 다시 흐림
최초 전환부터 Key Vault 도입 단일 VM 배포의 현재 문제를 푸는 데 필요한 범위를 넘음