← Start here · Infra note
HTML 문서는 그저 글자인데, 버튼 하나로 진짜 앱이 페이지 안에서 실행됩니다. 비밀은 페이지가 아니라 런처(launcher) — 요청이 오면 컨테이너를 깨우고, 준비되면 URL을 돌려주고, 놀고 있으면 꺼버리는 작은 API예요.
docs/run.js(버튼) · infra/launcher/(스포너 API) ·
k3s의 scale-to-zero Deployment. 배포 절차는 infra/README.md.
"HTML 안에서 실행"은 착시예요 — 실행은 서버에서, 페이지는 깨우고·기다리고·담아 보여줄 뿐. 그런데 그 세 동작이면 정적 문서가 살아있는 데모가 됩니다.
보통 문서 사이트와 데모 서버는 별개죠. 여기선 문서의 버튼이 곧 데모의 전원 스위치입니다. 항상 켜두면 돈과 메모리가 새고, 항상 꺼두면 데모가 죽어있는데 — 필요할 때만 깨우는 게 정답이에요. 서버리스(scale-to-zero)의 아이디어를 교실 크기로 줄인 겁니다.
초록 공이 가장 흔한 길(클릭→기동→폴링→준비)을 돕니다. 앱이 이미 깨어 있으면 노란 공이 지름길(즉시 ready)로 빠져요.
점선이 마지막 조각이에요: 아무도 안 쓰면 리퍼(reaper)가 replicas=0으로 되돌립니다 —
"쓸 때만 존재하는 앱". 다음 클릭이 다시 깨우니 데모는 영원히 안 죽어요.
위의 흐름도는 어떤 길인지 보여줘요. 이건 언제를 보여줍니다 — 앱이 잠들어 있으면 첫 클릭은 컨테이너가 깨어나는 ~15초를 기다립니다. 그동안 앱 URL은 503을 뱉고, 페이지는 /status를 폴링하며 버팁니다.
그래서 "이미 켜져 있을 때"(노란 지름길)는 즉시 뜨지만, 식은 뒤 첫 클릭은 기다림이 정상이에요. 폴링 예산이 2분이라 15초 콜드 스타트는 넉넉히 흡수합니다 — 중간에 포기하지만 않으면요.
docs/run.js (브라우저) — 버튼을 그리고, /launch를 쏘고, /status를 폴링하고, ready가 오면 iframe으로 그 URL을 페이지 안에 담아 보여줘요. 런처가 없으면 "run it locally"로 안내하는 안전한 기본값.infra/launcher/app.py, ~120줄 Flask) — 허용된 앱 이름만 받아 kubectl scale을 대신 눌러주는 손. 권한은 RBAC으로 이 네임스페이스의 deployment scale만.replicas: 0) — 앱의 본체. 0이면 존재하지 않고(비용 0), 1이면 몇 초 만에 살아나요. Ingress가 agent.bit-habit.com으로 연결.런처가 깨우는 컨테이너 안에는 진짜 앱이 있어요. 4B의 우승 RAG 앱은 두 국면이에요 — 한 번 색인을 만들어 디스크에 굳히고(offline), 질문마다 그 색인으로 근거를 찾아 Claude가 인용과 함께 답합니다(online). 초록 공이 온라인 질의 경로를 돕니다.
디스크의 index.pkl이 두 국면을 잇는 이음매예요 — 컨테이너가 잠들어도(replicas=0)
색인은 이미지 안에 그대로라, 다시 깨면 다시 색인하지 않고 바로 답합니다.
rag-starter/와 harness/가 형제여야 하나앱 코드(rag-starter/)와 공유 라이브러리(harness/)는 서로를 가리킵니다. 그래서 이미지는 레포 레이아웃을 글자 그대로 미러링해야 해요 — 이름 하나만 바꿔도 import가 깨집니다.
예전 이미지는 빌드 컨텍스트를 rag-starter/로 잡고 COPY . /srv만 했어요 —
harness/는 컨텍스트 바깥 한 칸 위라 이미지에 아예 안 담겼죠.
컨텍스트를 Module_04_rag/로 올려 둘을 형제로 복사하면 양방향 import가 모두 풀립니다.
런처는 파드가 readyReplicas ≥ 1이면 "ready"라고 합니다. 그런데 떠 있는 것과 멀쩡한 것은 다릅니다 — Streamlit은 앱이 import에서 죽어도 그 트레이스백을 HTTP 200으로 렌더해요. 그래서 런처는 "준비됨"이라 외치는데 화면엔 파이썬 에러만 뜹니다.
from harness.recorder import …가 컨테이너에서 ModuleNotFoundError. import 단계 크래시라 앱이 통째로 죽음.readyReplicas 기반 헬스체크가 "정상"으로 오판.이 페이지의 다이어그램들이 곧 그 수리의 지도예요 — 타임라인은 콜드 스타트가 503을 내는 정상 구간을, 레이아웃은 import가 깨진 지점을 보여줍니다.
5D의 agent는 웹앱이 아니라 터미널 REPL이에요. 래퍼를 새로 짜는 대신 ttyd로 진짜 터미널을 웹소켓으로 서빙합니다 — 학생이 로컬에서 보는 그 세션 그대로.
컨테이너도 학생이 받는 그 zip에서 빌드해요(infra/apps/agent/Dockerfile) —
라이브 데모와 수업 자료가 절대 어긋날 수 없는 구조입니다.
정적 페이지 + 작은 스포너 API + scale-to-zero = 문서가 곧 실행 환경. 페이지는 실행하지 않아요 — 깨우고, 기다리고, 담아서 보여줄 뿐입니다.
"HTML 안에서 앱이 실행된다"에서 실제로 코드가 도는 곳은?
페이지가 하는 일은 ① /launch 호출 ② /status 폴링 ③ iframe 표시, 셋뿐이에요.
실행은 k3s가 깨운 컨테이너에서 일어나고, ttyd가 그 터미널을 웹소켓으로 페이지까지 흘려보냅니다.
"실행처럼 보이는 것"과 "실행되는 곳"을 가르는 게 이 패턴 이해의 전부예요.