배포 가이드¶
STG 최초 전환의 topology, 환경변수, runtime secret, DB cutover와 완료 gate는 ADR 0004: STG runtime 설정과 Canonical DB 전환을 먼저 따른다. 이 문서는 승인된 설계를 실제 실행할 때 사용하는 명령과 운영 절차를 소유한다.
브랜치 전략¶
feat/* → release/v1.0.0 → stg (STG 자동 배포)
→ prod (PROD 자동 배포)
CD 자동화¶
배포 현황 — 2026-09-07¶
현재 STG는 이미 b202609030001 baseline이다. 이번 변경에 DB 초기화·dump 재복원을
적용하지 않는다. 아래의 최초 전환 절차는 과거 baseline에서 처음 옮길 때만 사용한다.
| 대상 | 실제 확인 결과 | 이번 배포 준비 |
|---|---|---|
| Vercel | procedure-hub 한 프로젝트에서 PROD와 STG Preview를 함께 운영, Node 24 |
CI도 Node 24 사용 |
| STG 도메인 | stg-procedure-hub.medisolveai.com → stg 브랜치 |
연결 유지 |
| STG 환경변수 | Preview의 stg 전용 PROCEDURE_API_BASE가 STG Gateway origin과 일치 |
값 변경 불필요 |
| Vercel Root Directory | frontend로 전환 완료 |
다음 릴리스의 STG·PROD 빌드에 공통 적용 |
| Vercel 함수 리전 | STG·PROD 모두 서울 icn1 |
실제 배포와 API 응답에서 확인 |
| Frontend 코드 | STG fe8b4ba, PROD 64b15d2 |
PROD는 기존 버전을 유지한 리전 변경 재배포 |
| Azure runtime | procedure-hub-api:stg-5b058a84033a8872ddf7185dfba23227ef1436e6 |
실행 이미지와 불변 태그 digest 일치 확인 |
| STG DB | procedure_hub_stg, TLS, baseline 일치, 제품 59·사전 158건, pg_trgm 설치 |
기존 데이터 유지, 같은 baseline의 migration 실행 |
| runtime env | /opt/procedure-hub/config/stg.env, azureuser:azureuser, 600 |
기존 파일 유지 |
| Bastion 배포 | env 사전 확인 → image pull → 컨테이너 교체 → Up 확인 |
Workflow에서 Gateway health·제품 API까지 검증 |
Vercel의 Root Directory는 프로젝트 공통 설정이다. 다음 PROD 배포도 새 폴더 구조를 사용하기로
결정해 frontend로 전환했다. 과거 루트 구조의 커밋을 재배포할 때는 해당 버전의 빌드 경로를
별도로 맞춰야 한다. 2026-09-07 PROD 리전 변경 시에는 기존 64b15d2를 저장소 루트에서
빌드한 뒤 공통 설정을 frontend로 복원했다. 신규 코드의 PROD 승격은 별도 작업이다.
STG 반영은 후보를 release/v1.2.0에 fast-forward하고 CI 통과 후 stg도 같은 커밋으로
fast-forward한다. 현재 GitHub FE CI는 통과하지만 Vercel은 Git 작성자 권한 연결 문제로
자동 배포를 BLOCKED 처리한다. 최신 STG는 로그인된 Vercel 계정으로 같은 stg 커밋을
명시해 배포했으며, 자동 배포 복구에는 작성자의 팀·프로젝트 접근 권한 연결 확인이 필요하다.
flowchart LR
A[후보 커밋 CI 통과] --> B[stg 반영]
B --> C[Vercel Preview 빌드]
B --> D[Azure 검증·이미지 빌드]
D --> E[Bastion 컨테이너 교체]
E --> F[Gateway health·제품 API 확인]
C --> G[STG 화면·BFF 확인]
F --> G
두 배포는 독립적으로 실행된다. 현재 GitHub stg에는 branch protection·ruleset이 없으므로
CI 성공을 강제하는 상태는 아니다. 실제 반영 전 후보 커밋의 FE·BE CI 결과를 확인한다.
배포 시 현재 runtime digest와 DB revision을 다시 기록하고, 완료 후 실제 실행 이미지가 후보
이미지인지 확인한다. 재배포 실패 시 같은 baseline의 직전 runtime으로 되돌리며 DB를 비우지 않는다.
prod에 직접 push하지 않는다. Backend 자동 CD는 stg/prod push 중
backend/**, .dockerignore 또는 .github/workflows/csp-azure-deploy.yml 변경에 반응한다.
실제 트리거는 해당 workflow가 소유한다.
| 대상 | 브랜치 | 환경 | 배포 경로 |
|---|---|---|---|
| Frontend | stg |
STG | Vercel procedure-hub 프로젝트의 Preview Git 연동 CD |
| Backend | stg push |
STG | GitHub Actions → ACR → Bastion → VM Docker |
| Frontend | prod |
PROD | Vercel production 배포 |
| Backend | prod push |
PROD | GitHub Actions → ACR → Bastion → VM Docker |
| Backend 수동 | workflow_dispatch | STG 또는 PROD | GitHub Actions에서 환경 선택 |
배포 흐름¶
브랜치 push → GitHub Actions → Docker 빌드 → ACR push → Bastion SSH → VM 배포
Frontend는 저장소의 Backend CD workflow가 배포하지 않는다. stg 브랜치의 Vercel Preview가
stg-procedure-hub.medisolveai.com으로 연결되며, 같은 프로젝트의 production branch는 prod다.
Frontend 앱 루트¶
Next.js 앱은 frontend/에 있고 Vercel Root Directory도 frontend로 설정되어 있다.
설치·빌드 명령은 그 디렉터리에서 실행하며,
기존 환경변수와 도메인 연결은 유지한다. 로컬 CLI도 frontend/에서 실행하고 프로젝트를
연결한다. 파일 이동과 Vercel 프로젝트 설정은 별개이며, 현재 설정 변경은 2026-09-07에 적용했다.
로컬 검증은 npm --prefix frontend run build로 실행한다. Backend 전용 Azure workflow의
빌드 context와 실행 경로는 이번 Frontend 이동 때문에 변경하지 않는다.
인프라 정보¶
| 항목 | STG | PROD |
|---|---|---|
| VM IP | 10.1.2.12 | 10.3.2.7 |
| 호스트 포트 | 10000 | 10000 |
| 컨테이너 포트 | 8000 | 8000 |
| ACR 이미지 | procedure-hub-api:stg |
procedure-hub-api:prod |
| Gateway 경로 | /procedure-hub/* |
/procedure-hub/* |
| 프론트 (Vercel) | stg-procedure-hub.medisolveai.com |
procedure-hub.medisolveai.com |
SSH 접속¶
# STG
ssh azure-centurion-vm-procedure-hub-stg
# PROD
ssh azure-centurion-vm-procedure-hub-prod
# 컨테이너 상태 확인
ssh azure-centurion-vm-procedure-hub-stg "docker ps"
# 컨테이너 로그
ssh azure-centurion-vm-procedure-hub-stg "docker logs procedure-hub --tail 50"
수동 배포 (GitHub Actions)¶
- GitHub → Actions → "Azure CD - Procedure Hub"
- "Run workflow" 클릭
- Branch + environment 선택
- "Run workflow" 실행
VM runtime 환경변수 최초 설정¶
Procedure Hub Backend는 App Service가 아니라 VM의 Docker container로 실행된다. GitHub Actions는 애플리케이션 환경변수를 전달하거나 덮어쓰지 않는다. 인프라 담당자가 각 VM에서 한 번 생성하고 변경 시에만 직접 갱신한다.
STG VM에 접속한 뒤 다음을 실행한다.
sudo install -d -m 700 -o azureuser -g azureuser /opt/procedure-hub/config
sudoedit /opt/procedure-hub/config/stg.env
sudo chown azureuser:azureuser /opt/procedure-hub/config/stg.env
sudo chmod 600 /opt/procedure-hub/config/stg.env
stg.env에서 필수인 값은 DATABASE_URL 하나다. 실제 계정과 password는 문서·shell
history·Git에 남기지 않는다.
DATABASE_URL=postgresql+asyncpg://<USER>:<PASSWORD>@postgresql-csp-centurion-dev.postgres.database.azure.com:5432/procedure_hub_stg?ssl=require
DATABASE_ECHO=false, API_PREFIX=/procedure-hub는 코드 기본값이므로 별도 override가 필요할
때만 파일에 추가한다.
값을 출력하지 않고 파일·key·permission만 확인한다.
sudo test -s /opt/procedure-hub/config/stg.env
sudo grep -Eq '^DATABASE_URL=.+$' /opt/procedure-hub/config/stg.env
sudo stat -c '%U:%G %a' /opt/procedure-hub/config/stg.env
sudo awk -F= '/^[A-Z_][A-Z0-9_]*=/{print $1}' /opt/procedure-hub/config/stg.env
예상 결과는 owner azureuser:azureuser, mode 600, 필수 key DATABASE_URL이다. 배포
script는 이 파일이 비었거나 DATABASE_URL이 없으면 기존 container를 내리기 전에 실패하며,
검증을 통과하면 다음과 동일한 방식으로 사용한다.
docker run -d \
--name procedure-hub \
--restart unless-stopped \
--env-file /opt/procedure-hub/config/stg.env \
-p 10000:8000 \
acrcspmedisolveaishared.azurecr.io/procedure-hub-api:stg
헬스 체크¶
# STG
curl https://api-stg-csp.medisolveai.com/procedure-hub/health
# PROD
curl https://api-csp.medisolveai.com/procedure-hub/health
Canonical baseline 최초 STG 반영¶
Runtime image는 migration과 API만 포함한다. seed/import corpus와 data-init image는 없으며, 초기 데이터는 저장소 밖에서 별도로 준비한 PostgreSQL snapshot으로 배포 전에 수동 복원한다.
주의: 2026-09-04 기준 Gateway의 /procedure-hub/*는 needsAuth:false다. 현재 Backend는
관리 write를 직접 인증하지 않으므로 terminology 수정·삭제와 glossary apply·rollback도 공개
route를 통과한다. STG에서 이를 임시 허용할지, write 보호를 먼저 적용할지는 DB cutover 전에
결정한다. 이 상태로 PROD에 승격하지 않는다.
현재 STG가 과거 Alembic history를 사용한다면 새 baseline을 그 위에 실행하지 않는다. 새 runtime을
먼저 배포하면 시작 시 migration이 알 수 없는 과거 revision에서 실패한다. 따라서 PR의 Backend CI가 성공한
뒤, stg merge 직전에 서비스 중단을 열고 다음 순서로 한 번 교체한다.
- 기존 STG DB를 custom-format으로 백업하고 복원 가능 여부를 확인한다.
- STG DB를 빈 DB로 재구성한다.
uv run alembic upgrade head로b202609030001schema를 적용한다.- 별도로 준비한 데이터 snapshot을
pg_restore --data-only로 수동 복원한다. - revision과 핵심 row count를 확인한 뒤 runtime을 배포한다.
- release를 자동 publish하지 않고 검수 상태를 확인한다.
STG VM에는 PostgreSQL client가 설치되어 있지 않으므로 일회성 postgres:16 container를
사용한다. 아래 명령의 후보 image는 최종 branch와 같은 baseline을 가진 immutable tag여야 한다.
password는 -W prompt에 입력하고 shell history에 쓰지 않는다.
export PHUB_DB_HOST=postgresql-csp-centurion-dev.postgres.database.azure.com
export PHUB_DB_USER=centurion_admin
export PHUB_DB_NAME=procedure_hub_stg
export PHUB_COMMIT_SHA=COMMIT_SHA
export PHUB_CANDIDATE_IMAGE="acrcspmedisolveaishared.azurecr.io/procedure-hub-api:stg-$PHUB_COMMIT_SHA"
docker stop procedure-hub
docker run --rm -it -e PGSSLMODE=require postgres:16 \
psql -h "$PHUB_DB_HOST" -U "$PHUB_DB_USER" -d postgres -W \
-v ON_ERROR_STOP=1 \
-c "DROP DATABASE $PHUB_DB_NAME WITH (FORCE)" \
-c "CREATE DATABASE $PHUB_DB_NAME OWNER $PHUB_DB_USER"
docker run --rm \
--env-file /opt/procedure-hub/config/stg.env \
--entrypoint .venv/bin/alembic \
"$PHUB_CANDIDATE_IMAGE" upgrade head
docker run --rm -it \
-e PGSSLMODE=require \
-v /opt/procedure-hub/cutover:/backup:ro \
postgres:16 pg_restore \
-h "$PHUB_DB_HOST" -U "$PHUB_DB_USER" -d "$PHUB_DB_NAME" -W \
--data-only --no-owner --no-privileges --exit-on-error \
/backup/procedure_hub_data.dump
docker run --rm \
--env-file /opt/procedure-hub/config/stg.env \
--entrypoint .venv/bin/alembic \
"$PHUB_CANDIDATE_IMAGE" current
마지막 명령이 b202609030001 (head)를 반환한 다음에만 release PR을 stg에 merge한다. merge는
Frontend Vercel CD와 Backend GitHub Actions CD를 시작한다. DB를 비운 뒤 CD가 실패하면 old
runtime만 다시 올리지 말고, 아래 rollback image와 전환 전 full DB backup을 함께 복원한다.
docker run --rm \
--env-file /opt/procedure-hub/config/stg.env \
--entrypoint .venv/bin/python \
"$PHUB_CANDIDATE_IMAGE" scripts/manage_canonical_release.py status
docker run --rm \
--env-file /opt/procedure-hub/config/stg.env \
--entrypoint .venv/bin/python \
"$PHUB_CANDIDATE_IMAGE" scripts/manage_canonical_release.py review-queue
임상/근거 검수가 끝나기 전까지 draft projection이 외부로 노출되지 않는 publication gate는 유지한다. STG 최초 반영에서 revision을 일괄 approve하거나 release를 publish하지 않는다.
Smoke test:
curl -sS -X POST \
https://api-stg-csp.medisolveai.com/procedure-hub/api/v1/knowledge/retrieve \
-H 'Content-Type: application/json' \
-d '{"query":"보톡스와 필러를 같이 해도 돼?"}'
최초 release 전까지 status=unavailable, data_revision=null,
published_release_unavailable을 반환해야 한다.
Runtime rollback¶
단일 baseline은 과거 Alembic chain과 revision ID가 호환되지 않는다. 최초 baseline 전환을 되돌릴
때는 직전 runtime image와 전환 직전 DB backup을 함께 복원한다. runtime image만 되돌리면 과거
시작 시 migration이 새 revision b202609030001을 해석하지 못한다.
REGISTRY=acrcspmedisolveaishared.azurecr.io
IMAGE="$REGISTRY/procedure-hub-api"
az acr login --name acrcspmedisolveaishared
docker pull "$IMAGE:stg-rollback"
docker tag "$IMAGE:stg-rollback" "$IMAGE:stg"
docker push "$IMAGE:stg"
# 이 시점 전에 전환 직전 DB backup을 복원한다.
ssh azure-centurion-vm-procedure-hub-stg "./deploy.sh procedure-hub stg"
curl -fsS https://api-stg-csp.medisolveai.com/procedure-hub/health
두 단계 이상 과거 버전은 stg-${commit SHA} 불변 tag를 같은 방법으로 환경 tag에 승격한다.
배포 전 current commit SHA, container image ID, rollback tag digest, DB backup 위치를 기록하고
smoke가 끝날 때까지 삭제하지 않는다. 같은 baseline 안에서 Content release만 되돌리는 작업은
manage_canonical_release.py rollback을 사용한다.
트러블슈팅¶
CD SSH 인증 실패¶
ssh: handshake failed: unable to authenticate
→ GitHub Organization Secrets에서 BASTION_PORT, BASTION_USER의 Repository access에 이 레포가 포함되어 있는지 확인.
az: command not found¶
bash: az: command not found
→ VM에 Azure CLI 미설치. SSH 접속 후:
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
포트 매핑 오류¶
port is already allocated
→ 기존 컨테이너 정리 후 재시작:
docker stop procedure-hub && docker rm procedure-hub
docker run -d \
--name procedure-hub \
--restart unless-stopped \
--env-file /opt/procedure-hub/config/stg.env \
-p 10000:8000 \
acrcspmedisolveaishared.azurecr.io/procedure-hub-api:stg