website: DB 조회용 sqlite-web GUI 추가 — dbclient 조작 경로와 분리
백엔드 개발자가 DB를 볼 때 쓰던 `dbclient` CLI가 텍스트 결과만 뱉어서, 스키마나 데이터를 훑어보기엔 불편했다. 그래서 read-only 웹 GUI를 조회용으로 따로 붙이고, 조작은 기존 `dbclient` 경로 그대로 남겨두는 작업을 했다.
처음 고민한 지점은 "GUI 하나로 조회랑 조작을 다 처리할 수 있지 않나"였다. sqlite-web은 브라우저에서 테이블을 보고 쿼리도 날릴 수 있는 도구라, 굳이 CLI를 따로 둘 필요가 있나 싶었다. 그런데 sqlite-web은 read-only냐 read-write냐 두 모드밖에 없고, "INSERT/UPDATE는 되는데 ALTER/DROP은 안 된다" 같은 문장 단위 구분을 못 한다. 지금 dbclient 쪽에는 dbclient-sqlite-guard.sh라는 wrapper 스크립트가 그 DDL 차단 로직을 이미 갖고 있는데, GUI에도 같은 걸 넣으려면 sqlite-web을 포크해서 Python 코드에 차단 로직을 심어야 한다. 그러면 같은 종류의 검증 로직이 bash wrapper와 포크한 Python 두 군데에 중복되고, 나중에 규칙을 하나 고칠 때 한쪽만 고치고 한쪽을 놓치는 사고가 나기 쉽다고 판단했다. 그래서 방향을 바꿔서, GUI는 아예 전체 read-only로만 열고 조작은 계속 dbclient CLI로만 하도록 역할을 나눴다.
read-only를 강제하는 것도 한 가지 방법만 믿기엔 불안해서 이중으로 걸었다. sqlite-web 실행 커맨드에 -r 플래그를 주고, docker-compose 볼륨 마운트도 :ro로 걸었다. 앱 레벨에서 막히더라도 컨테이너/OS 레벨에서 한 번 더 막히게 한 셈이다.
sqlite-web-stage:
image: ghcr.io/coleifer/sqlite-web:latest
command: ["/data/stage.db", "-H", "0.0.0.0", "-p", "8080", "-r"]
ports:
- "127.0.0.1:8090:8080"
volumes:
- ./data:/data:ro
restart: unless-stopped
포트도 127.0.0.1에만 바인딩해서 공인 포트로 열리지 않게 했다. OCI 시큐리티 리스트를 건드릴 필요가 없어서 배포 리스크도 줄었다.
계정 쪽도 dbclient와 완전히 분리했다. 새로 만든 dbtunnel 계정은 셸이 없고(nologin), permitopen으로 8090/8091 포트포워딩만 허용한다. dbclient가 "포워딩은 안 되지만 forced command로 SQL은 실행되는" 계정이라면, dbtunnel은 정반대로 "포워딩만 되고 그 외엔 아무것도 안 되는" 계정이다. 권한 모델이 정반대인 두 계정을 나란히 두니, 한쪽 설정 실수가 다른 쪽 권한까지 새게 만들 여지가 없어졌다.
화면 쪽 편의는 따로 챙겼다. 조회(터널)와 조작(dbclient CLI)을 매번 따로 열기 귀찮으니, 로컬에서 실행하는 infra/db-dev-ui.sh stage|prod 스크립트를 만들어서 tmux 한 창에 두 pane으로 띄우게 했다.
if command -v tmux >/dev/null 2>&1; then
tmux new-session -d -s "TUNNEL_CMD"
tmux split-window -h -t "CLI_CMD"
...
elif command -v wt.exe >/dev/null 2>&1; then
wt.exe -w 0 new-tab bash -lc "CLI_CMD"
else
# 안내 메시지만 출력
fi
여기서도 같은 원칙을 지켰다. 화면만 합치고 권한 로직은 절대 안 섞는다. 두 pane은 서로 다른 SSH 키로 붙는 완전히 독립된 세션이라, 왼쪽 터널이 오른쪽 CLI를 게이트하거나 하는 일이 없다. tmux가 없는 환경(Windows)을 위해 Windows Terminal 분할로 폴백하고, 그것도 없으면 두 명령어를 그냥 안내만 하고 끝나게 했다.
PR 본문에 남긴 주의사항 하나는 실제로 겪은 사고에서 나온 거다. 이날 기록된 dbclient forced command 사고에서, CD의 git checkout -f가 stage/prod가 공유하는 서버 워킹 디렉터리를 브랜치 전체로 덮어쓴다는 걸 실측했다. 이 PR도 dev에만 머지하고 main으로 안 올리면, 다음 prod 배포 때 docker-compose.yml이 이 서비스 정의가 없던 예전 버전으로 조용히 되돌아갈 수 있다. 그래서 머지 후 할 일 목록에 "main으로도 되도록 빨리 승격"을 명시해뒀다.
서버에 실제로 계정을 만들고 컨테이너를 띄우는 작업은 자동화하지 않고 수동으로 남겼다. 공유 서버에 새 SSH 접근 수단을 여는 일이라, 정확한 명령만 db-access.md에 적어두고 실행은 인프라 오너가 직접 하기로 했다. .claude/skills/db-access/SKILL.md에도 "GUI로 보고 싶다"는 질문 유형과 아키텍처 다이어그램을 추가해서, 다음에 팀원이 물어보면 이 스킬이 바로 답할 수 있게 해뒀다.
PR은 생성되자마자 바로 병합됐다. 이제 남은 건 서버에서 dbtunnel 계정을 실제로 만들고 컨테이너를 올리는 것, 그리고 main으로의 승격이다.