로그인 상태를 확인하는 중입니다…

contextBio 내부 문서 · 전체관리자 · 앱관리자

관리자 로그인이 필요합니다

계정 페이지로 이동합니다. 로그인하면 보던 자리로 그대로 돌아옵니다.

로그인하러 가기

계정이 없으신가요? 같은 화면에서 가입할 수 있습니다.

접근 권한 없음

열 수 없습니다

로그인 계정:

연결 실패

확인하지 못했습니다

contextBio CGV 구현

암 유전체 뷰어의 구현 스펙 — R Shiny 와 shiny-server, 옮기지 않은 3.8TB 데이터의 심볼릭 링크, 인증을 R 밖의 게이트에 맡긴 구조, 콜드 스타트를 사용자가 겪지 않게 하는 warm 타이머.

기준 2026-10-05 · main
79037d1

한눈에

Cancer Genome Viewer. TCGA 등 암 유전체·약물유전체 데이터를 통합해 보여 주는 R Shiny 웹서비스다. 2026-09-05 에 ContextBio 앱으로 편입됐다 — 그 전에는 git.sysmed.kr 의 별도 저장소였고 /data/webservice/CGV/src/bg_aju_cgv 에서 직접 돌았다.

항목 값
저장소 contextBio/CGV (bare ContextBio/CGV.git)
호스트 c1
운영 워크트리 ContextBio/CGV — main. Shiny :3838, 게이트 127.0.0.1:8810 (systemd user cgv-gate)
개발 워크트리 dev/CGV — dev. Shiny :3839 (/cgv-dev), 게이트 127.0.0.1:8811 (systemd user cgv-gate-dev)
운영 주소 진입 https://contextbio.ai/cgv → 본체 https://cgv.sysmed.kr
개발 주소 진입 https://dev-contextbio.web.app/cgv → 본체 https://c1.sysmed.kr/cgv-dev
틀 R Shiny · shiny-server (run_as shiny) + 게이트 Flask/gunicorn (gate/)
데이터 /mnt/S3/data/webservice/CGV/ (3.8TB) — 워크트리에는 심볼릭 링크
로그인 contextBio 통합계정 — 검증은 게이트가, R 은 /gate/whoami 로 묻기만
릴리즈 deploy/release.sh
배포 확인 /gate/version

deploy/shiny-server.conf 와 deploy/apache/ 가 서버 설정의 정본이다. /etc 에만 두면 왜 그 값인지가 코드 옆에 남지 않는다. 지금 /etc/shiny-server/shiny-server.conf 는 저장소 판과 같다.

주소와 요청 경로

contextbio.ai/cgv (Firebase 진입 페이지)
   │ location.href = <backend>/gate/enter#token=<ID토큰>
   ▼
apache cgv.sysmed.kr:443 ─┬─ /gate/whoami     → 막힘 (Require all denied)
                          ├─ /gate/*          → 127.0.0.1:8810 게이트
                          ├─ Upgrade=websocket → ws://127.0.0.1:3838
                          └─ /                → 127.0.0.1:3838 shiny-server
                                                   └ app_dir ContextBio/CGV
  • 진입 페이지만 사이트 저장소에 있다. 원본은 CGV 저장소의 frontpage/index.html 이고, 사이트 저장소의 build_subapp.py 가 cgv 항목(backend: https://cgv.sysmed.kr)으로 굽는다. 본체는 c1 의 Shiny 다. build_subapp.py 의 top_bar.docs 는 꺼져 있다 (공개 문서 쪽이 없으므로 켜면 404 단추가 생긴다).
  • 운영 vhost(deploy/apache/cgv.sysmed.kr_ssl.conf)는 세션 쿠키 (__Secure-cgv_session)가 없는 첫 화면(/) 요청을 https://contextbio.ai/cgv 로 302 한다. 이것은 보안 경계가 아니라 비용 절감이다 — 쿠키도 없는 요청 하나로 R 프로세스를 깨우지 않으려는 것. 실제 차단은 게이트의 서명 검증과, 세션이 없으면 입장 안내만 그리는 Shiny 가 한다.
  • 개발 미러는 전용 도메인 없이 c1 vhost 안의 조각(deploy/apache/c1-cgv-dev.conf)으로 얹힌다. /cgv-dev/gate/ → :8811/gate/(접두사를 떼고 넘긴다), /cgv-dev → :3839/cgv-dev(접두사 유지). 응답 헤더 X-CGV-Node: C1 은 운영 vhost 가 붙이는 진단용이다.

데이터 — 코드만 옮기고 데이터는 그대로

데이터가 3.8TB(CGV.db 2.3T + data/db 1.1T + data/legacy 271G)라 옮기지 않았다. 원본은 /mnt/S3/data/webservice/CGV/ 에 있고 워크트리는 링크로 가리킨다.

<워크트리>/data/
  db      -> /mnt/S3/data/webservice/CGV/data/db       공유 (운영·개발 같은 원본)
  legacy  -> /mnt/S3/data/webservice/CGV/data/legacy   공유
  manager/                                             워크트리별 사본 (80K)
    dbAnalysis.rda  dbListTableDF.rda  dbMetaTableDF.rda  dbTableDF.rda

data/manager 는 데이터셋 목록·분석 목록이다. 워크트리마다 따로 두는 이유는 개발이 운영 목록을 덮어쓰지 않게 하려는 것이다. 반대로 db·legacy 는 운영과 개발이 같은 원본을 보므로, 개발에서 그 아래를 쓰는 코드는 곧 운영 데이터를 건드린다.

R 은 config.yml 의 두 값으로 자리를 찾는다(R/common.R).

default:
  cgv_data_path: './data/manager'
  core_data_path: './data/db'

저장소 밖에 있는 실행 환경

아래는 .gitignore 대상이라 워크트리마다 손으로 만든다. 릴리즈 스크립트는 config.yml· gate/.env·gate/.venv/bin/gunicorn 이 없으면 재기동 전에 멈춘다.

파일 무엇
config.yml cgv_data_path(manager) · core_data_path(db)
.Renviron CGV_GATE_URL · CGV_BASE_PATH · CGV_SITE_ORIGIN — R 이 기동 때 읽는다
gate/.env CGV_GATE_SECRET(필수) · CGV_GATE_PORT · CGV_APP_ORIGIN · CGV_SITE_ORIGIN · (개발만) CGV_BASE_PATH
gate/.venv/ 게이트 파이썬 환경 (gate/requirements.txt — Flask·gunicorn)
data/ 위의 심볼릭 링크와 manager 사본

운영과 개발이 갈리는 값:

변수 운영 개발
CGV_GATE_URL (R) http://127.0.0.1:8810 http://127.0.0.1:8811
CGV_BASE_PATH (비어 있음) /cgv-dev
CGV_SITE_ORIGIN https://contextbio.ai https://dev-contextbio.web.app
CGV_APP_ORIGIN (게이트) https://cgv.sysmed.kr https://c1.sysmed.kr

인증 — R 은 검증하지 않는다

로그인은 contextBio 통합계정이고 검증은 gate/(Flask)가 한다. R 은 쿠키를 게이트에 넘겨 "이 세션은 누구인가"만 묻는다(R/contextbio.R). 검증(서명·만료·폐기·정지·클레임)의 정본인 contextbio_auth.py 가 파이썬이라서다 — R 로 다시 쓰면 네 번째 검증 구현이 생기고, BioWrit·PepDesigner·AURORA 세 곳이 모두 validSince 확인을 빠뜨렸던 전례처럼 그중 하나는 더 관대해진다.

진입 릴레이 (LABIS 와 같은 구조)

  1. contextbio.ai/cgv 진입 페이지가 통합계정 ID 토큰을 얻어 <backend>/gate/enter#token=<ID토큰> 으로 브라우저를 보낸다.
  2. /gate/enter 는 입력 없는 한 장짜리 페이지다. 프래그먼트에서 토큰을 읽고 주소창에서 곧바로 지운 뒤, 같은 오리진으로 POST /gate/session {idToken} 한다.
  3. /gate/session 이 cb.verify(token, check_revoked=True) 로 검증하고, 이메일 미확인이면 403 email_unverified, Identity.can("cgv") 가 거짓이면 403 service_not_allowed. 통과하면 서명 쿠키를 심고 <BASE_PATH>/ 로 보낸다.
  4. Shiny 가 그 쿠키로 /gate/whoami 에 신원을 묻는다.

토큰을 프래그먼트로 건네는 이유는 서버·접근 로그·Referer 에 남지 않아서다. 쿠키를 교차 오리진 XHR 로 심지 않는 이유는 contextbio.ai → cgv.sysmed.kr 가 서드파티 문맥이라 브라우저가 갈수록 막기 때문이다 — 그래서 브라우저를 데려와 같은 오리진에서 심는다.

세션 쿠키

  • 이름 __Secure-cgv_session(평문 오리진이면 cgv_session), HttpOnly · Secure · SameSite=Lax, path <BASE_PATH>/, 수명 12시간(CGV_SESSION_MAX_AGE, LABIS 와 같은 값).
  • 쿠키에는 토큰이 아니라 검증이 끝난 신원(uid·email·name·admin·plan)만 itsdangerous 로 서명해 담는다. 그래서 계정 정지·클레임 변경은 다음 로그인까지 반영되지 않는다. 즉시 끊으려면 Firebase 에서 계정을 정지하고, 전원을 끊으려면 CGV_GATE_SECRET 을 바꿔 게이트를 재기동한다(발급된 쿠키가 모두 무효 — 비상 로그아웃).
  • 게이트는 CGV_GATE_SECRET 없이는 기동을 거부한다(fail closed). 기본값을 지어내면 그 값이 모든 배포의 위조 열쇠가 된다.

게이트 라우트

라우트 무엇
GET /gate/version service·version(기동 시점 커밋)·site·cookie·base — 배포 확인용
GET /gate/enter 프래그먼트 토큰을 같은 오리진 POST 로 넘기는 페이지
POST /gate/session 토큰 검증 → 쿠키 발급
GET /gate/whoami Shiny 전용 — Apache 가 프록시하지 않는다
GET/POST /gate/logout 쿠키 삭제, JSON 응답
GET /gate/signout 쿠키 삭제 후 진입 화면으로 302 — Shiny 의 로그아웃 단추가 이것을 쓴다

게이트는 127.0.0.1 에만 바인드한다(gate/start-gate.sh, gunicorn workers 2 · threads 4). /gate/whoami 의 접근 통제는 이 바인드와 Apache 의 ProxyPass /gate/whoami ! + Require all denied 두 겹이다. 열어 두면 남의 브라우저에서 세션 주인을 조회할 자리가 생긴다. vhost 를 손볼 때 이 차단이 /gate/ 규칙보다 앞에 살아 있는지 확인한다 — 먼저 맞는 규칙이 이긴다.

shiny-server 뒤에서는 쿠키가 R 까지 오지 않는다

shiny-server 는 브라우저의 SockJS 연결을 자기가 끝내고 R 프로세스로 자기 WebSocket 을 새로 열며, 그때 브라우저 헤더를 넘기지 않는다. 그래서 server 함수의 session$request$HTTP_COOKIE 는 항상 비어 있다. 2026-09-05 에 통합로그인이 성립하지 않은 원인이 이것이었다.

페이지 HTTP 요청은 헤더째 프록시되므로 쿠키가 온다. 그래서 신원 확인은 ui 함수 (app.R 의 ui <- function(request))에서 하고, 결과를 일회용 티켓으로 server 함수에 넘긴다.

ui(request)  ── cb.whoami(쿠키) → cb.ticket.issue() → 숨은 입력 cbTicket
server       ── cb.ticket.claim(isolate(input$cbTicket))  (server/00.03.contextbio.R)
  • 티켓은 /dev/urandom 24바이트, 프로세스 메모리에만 있고 한 번 쓰면 지워지며 CGV_TICKET_TTL(기본 120초)이 지나면 무효다. HttpOnly 쿠키를 페이지에 싣지 않으려는 설계다.
  • 앱마다 R 프로세스가 하나라 ui 와 server 가 같은 프로세스에서 돈다. 그사이 프로세스가 죽으면 방문자는 입장 안내 화면을 본다 — 다시 누르면 된다.
  • cb.identity(session) 은 shiny-server 없이 runApp() 으로 직접 띄울 때(개발)만 값을 낸다.

권한

  • admin 클레임 → CGV 의 administrator, 그 밖은 basic (cb.permission()). CGV 안에 별도 관리자 명단을 두지 않는다 — 통합계정에서 관리자를 내리면 여기서도 내려가야 한다. 관리자는 사이드바에 Database·Analysis Controller 가 함께 보인다.
  • 이용 자격은 Identity.can("cgv") 가 정한다 — 관리자 통과, apps 클레임이 없으면 통과 (가입 개방), 있으면 cgv 포함 여부.
  • gate/contextbio_auth.py 는 사본이다. 여기서 고치지 않는다. 정본은 사이트 저장소이고 python tools/sync_auth.py --write 가 dev/CGV/gate/ 로 복사한다.

콜드 스타트와 warm 타이머

R 프로세스 기동에 15초 안팎이 든다. 2026-09-05 dev 워크트리 실측으로 라이브러리 13.4초 + 코어 .rda 3.0초 = 16.8초, 최대 메모리 약 590MB(deploy/shiny-server.conf 주석). 저장소 CLAUDE.md 는 library(tidyverse) 를 stringr+tibble 로 바꾼 뒤 약 14초 (10.9s + 3.1s)로 적고 있다. 대부분이 ComplexHeatmap·survminer·survival 같은 분석 필수 패키지라 더 줄일 여지가 거의 없다. 라이브러리를 더 얹으면 이 값이 늘어난다.

그래서 값을 줄이는 대신 사용자가 그 값을 내지 않게 한다.

장치 값 이유
app_idle_timeout 3600 (shiny-server.conf) 워밍업 주기가 유휴 회수에 확실히 이기도록. 0(무제한)은 아무도 안 쓰는 앱이 600~760MB 를 영구히 쥐므로 피했다
cgv-warm.timer OnCalendar=*:0/10, Persistent=true, RandomizedDelaySec=30s 10분마다 정각 기준. OnBootSec 계열은 오래 켜진 기계에 나중에 설치하면 첫 기준점이 없어 스케줄되지 않았다(NEXT n/a)
cgv-warm.sh http://127.0.0.1:3838/ 에 직접 요청, 연결 거부도 재시도(기본 20회·2초) 데워야 할 것은 R 프로세스라 Apache·TLS 를 거치지 않는다. 재기동 직후 포트가 아직 안 열렸을 때가 가장 필요한 순간이다

cgv-warm.sh 는 5초 이상 걸리면 warm: 콜드 스타트를 겪음 (N초) 을 남긴다(데워져 있으면 0.1초대, 식은 채로 뜨면 15~19초). 이 줄이 자주 보이면 타이머 주기나 app_idle_timeout 을 의심한다. 워밍업은 운영 포트(3838)만 두드린다(CGV_WARM_PORT 기본값) — 개발 미러는 데우지 않는다.

기동과 배포

게이트와 워밍업은 systemd user 유닛(sudo 불필요), shiny-server 는 시스템 서비스 (재기동에 sudo 필요)다.

./deploy/install-gate.sh   # 게이트 유닛 설치 — main 워크트리면 cgv-gate, 아니면 cgv-gate-dev
./deploy/install-warm.sh   # cgv-warm.service/.timer 설치 — 설치한 워크트리의 cgv-warm.sh 를 가리킨다
./deploy/release.sh        # 운영 워크트리에서: main 에 dev 머지 → 태그 → 푸시 → 재기동 → 검증
./deploy/release.sh --dry-run   # 무엇이 나가는지만
  • 유닛 파일에는 경로가 박혀 있지 않다(__GATE_SH__·__WARM_SH__). 설치 스크립트가 설치한 워크트리 기준으로 채운다. 유닛 이름을 브랜치로 가르는 이유는 같은 이름이면 나중에 설치한 쪽이 앞의 것을 조용히 밀어내기 때문이다.
  • release.sh 는 main 체크아웃이 아니거나 추적 파일 변경이 있거나 main..dev 가 비어 있으면 멈춘다. 이어서 systemctl --user restart cgv-gate → sudo systemctl restart shiny-server → cgv-warm.sh → 검증 순서로 돈다.
  • 커밋만으로는 반영되지 않는다. 게이트는 프로세스 기동 때, Shiny 는 R 프로세스가 새로 뜰 때 코드를 읽는다.
  • shiny-server 하나가 3838(운영)과 3839(개발)를 함께 낸다 — sudo systemctl restart shiny-server 는 양쪽 R 프로세스를 모두 내린다. 개발 게이트는 systemctl --user restart cgv-gate-dev.
  • 첫 전환(자체 계정 → 통합계정)의 한 번짜리 절차와 되돌리기는 deploy/CUTOVER.md 에 있다. 이후 배포에는 쓰지 않는다.

운영 점검

# 게이트 — version 은 기동 시점의 커밋이다. HEAD 와 다르면 옛 코드가 돌고 있다
curl -s http://127.0.0.1:8810/gate/version     # 운영
curl -s http://127.0.0.1:8811/gate/version     # 개발 (base 가 /cgv-dev)

# whoami 가 바깥에 닫혀 있는지 — 403/404 여야 한다
curl -s -o /dev/null -w '%{http_code}\n' https://cgv.sysmed.kr/gate/whoami

# 쿠키 없는 첫 화면 — 302 → contextbio.ai/cgv
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://cgv.sysmed.kr/

# 유닛·타이머
systemctl --user status cgv-gate cgv-gate-dev
systemctl --user list-timers cgv-warm.timer
journalctl --user -u cgv-warm -n 20            # 콜드 스타트를 겪었는지
journalctl --user -u cgv-gate -f               # 게이트 접근·오류 로그

# 포트 — 게이트는 127.0.0.1, Shiny 는 3838/3839
ss -ltn | grep -E ':(3838|3839|8810|8811)\b'

Shiny 로그는 /var/log/shiny-server, Apache 로그는 error-cgv_ssl.log·access-cgv_ssl.log 다. R 쪽은 [SC]00.03.contextbio.R 입장: <이메일> (<권한>) 또는 세션 없음 — 입장 안내 화면 을 남긴다. 브라우저 확인은 https://contextbio.ai/cgv → 로그인 → 입장 → 분석 화면이 로그인 화면 없이 열리는지까지다.

알아 둘 함정

  • 게이트가 죽으면 아무도 들어오지 못한다. 설계상 맞는 동작이다(fail closed) — R 은 게이트 호출이 실패하면 로그인하지 않은 것으로 본다. 입장이 막히면 게이트부터 본다.
  • hash 패키지가 get·assign 등을 S4 제네릭으로 덮는다. 환경을 다루는 코드는 base::get 처럼 네임스페이스를 붙여 부른다. 2026-09-05 에 unused arguments (envir=…) 로 실제로 죽었다.
  • USER 는 reactiveValues 다. 반응 문맥 밖에서 되읽으면 세션이 죽는다 — 값을 지역 변수에 먼저 담는다(server/00.03.contextbio.R).
  • 옛 자체 계정 코드를 되살리지 않는다. 아이디·비밀번호 .rda 방식은 삭제됐고, 그 전에도 로그인 화면은 display:none 에 스크립트가 게스트 로그인을 자동 클릭하던 — 사실상 무인증 공개였다. 옛 자리 /data/webservice/CGV/src/bg_aju_cgv 는 되돌릴 자리로 남아 있고, 그쪽 data/manager/dbCredentials.rda 에는 옛 가입자의 비밀번호 해시가 있다 (안정되면 지울 대상 — deploy/CUTOVER.md).
  • db·legacy 는 운영과 개발이 공유한다. 개발에서 그 아래에 쓰면 운영 데이터가 바뀐다.
  • c1 vhost 맨 아래의 catch-all rewrite 가 모든 요청을 RStudio 로 넘긴다. 개발 조각의 RewriteRule ^/cgv-dev - [L] 이 없거나 조각을 catch-all 아래에 Include 하면 /cgv-dev 가 RStudio 로 간다(rewrite 가 proxy 보다 앞 단계).
  • 사이트 배포 검사와 이름. tools/check_release.py 는 비활성 앱 이름(cgv 포함)이 보이는 쪽이 있으면 사이트 배포 전체를 실패시킨다. 위키(docs/wiki/)를 예외로 두는 규칙이 있어야 이 쪽이 배포될 수 있다 — 위키 밖(플레이북·연구·회사 화면)에 CGV 이름이나 /cgv 링크를 싣지 않는다.