← 개발 로그 목록

website: dev→main 승격 — dbclient 사고 기록과 sqlite-web GUI 뷰어

/ 5분 분량 / 개발 로그

dev 브랜치에 쌓여있던 두 변경(dbclient 사고 후속 기록, sqlite-web GUI 뷰어)을 main으로 승격한 PR이다. 정기 승격이긴 한데, docker-compose.yml을 건드리는 변경이 포함돼 있어서 dev에만 묵혀두면 다음 prod 배포 때 조용히 되돌아갈 수 있는 상황이라 바로 올렸다.

dev에는 두 커밋이 올라와 있었다. 하나는 docs(pm): dbclient 권한 사고 후속으로, 이건 코드 변경 없이 pm/docs/learnings.md에 학습 기록만 남긴 것이다. 다른 하나는 #167에서 작업한 feat(infra): sqlite-web GUI 뷰어 추가인데, infra/** 안에서만 변경이 일어났고 backend/frontend/shared는 건드리지 않았다.

이 PR 자체는 새로 뭔가를 구현한 게 아니라 승격이 전부지만, 왜 "지금" 승격해야 하는지가 이 PR의 본론이다. CD가 브랜치를 배포할 때 git checkout -f $TARGET_BRANCH로 stage/prod가 공유하는 워킹 디렉터리 전체를 덮어쓰는 구조로 되어 있다. 이게 2026-07-24 dbclient 사고를 조사하면서 실측한 부분인데, docker-compose.yml처럼 서비스 정의가 들어있는 파일이 dev에만 있으면 다음 main 배포(prod 백엔드 머지) 시점에 checkout -f가 새 서비스 정의 없는 예전 버전으로 그대로 되돌려버린다. #167에서 sqlite-web-stage/prod 서비스를 새로 추가했으니, 이걸 dev에만 두고 방치하면 다음 prod 배포 때 사라질 수 있다는 뜻이다. 그래서 별도 기능 추가 없이 곧바로 승격 PR을 올렸다.

#167에서 sqlite-web을 넣게 된 배경도 같이 정리해두면, 원래 DB 조회는 dbclient CLI로만 했는데 결과가 터미널 텍스트로만 나와서 스키마나 데이터를 훑어보기가 불편했다. read-only 웹 GUI를 붙이려다가 계정을 어떻게 나눌지가 고민이었다. sqlite-web 같은 범용 GUI 도구는 "DML은 허용, DDL은 차단" 같은 문장 단위 구분을 못 하고 전체 read-only냐 read-write냐 둘 중 하나만 고를 수 있다. dbclient가 이미 갖추고 있던 DDL 차단 로직(dbclient-sqlite-guard.sh)을 GUI에도 얹으려면 sqlite-web을 포크해서 같은 차단 로직을 또 심어야 하는데, 그러면 로직이 bash wrapper와 포크한 Python 두 군데로 나뉘어서 한쪽만 고치고 한쪽을 놓치는 사고가 나기 쉽다고 판단했다. 그래서 GUI는 아예 전체 read-only로만 열기로 했다. -r 플래그(앱 레벨)와 :ro 볼륨 마운트(OS 레벨)로 이중 방어를 걸고, 조작은 계속 기존 dbclient CLI로만 하도록 분리했다.

계정도 dbclient와 완전히 분리해서 dbtunnel이라는 신규 SSH 계정을 만들었다. 셸이 없고(nologin), permitopen으로 127.0.0.1:8090/8091 포트포워딩만 허용한다. dbclient가 no-port-forwarding으로 포워딩을 막고 forced command로만 동작하는 것과 정반대로, dbtunnel은 포워딩만 되고 그 외엔 아무것도 안 되는 계정이다. 두 계정은 서로 다른 SSH 인증으로 완전히 독립적으로 붙기 때문에 한쪽이 다른 쪽을 게이트하는 구조가 아니다.

화면을 합치는 부분은 infra/db-dev-ui.sh라는 tmux 스크립트로 처리했다. 왼쪽 pane은 dbtunnel 계정으로 연 SSH 터널(브라우저용 sqlite-web), 오른쪽 pane은 dbclient CLI 세션이다. 서버 쪽 권한 모델은 그대로 두고 로컬에서 화면만 나눠 붙인 거라서, 조회와 조작을 한 창에서 볼 수 있으면서도 권한 로직 자체는 중복되지 않는다. tmux가 없는 환경(Windows)을 위해 Windows Terminal 분할로 폴백하는 것도 넣었고, 그마저 없으면 두 명령을 안내만 하고 끝나게 했다.

이번 승격 PR 자체는 생성부터 머지까지 1분이 채 안 걸렸다. 새로 판단할 게 없는, dev에서 이미 검증된 변경을 그대로 main에 옮기는 작업이었기 때문이다. 다만 머지된다고 sqlite-web-prod 컨테이너가 바로 뜨는 건 아니고, 인프라 오너가 서버에서 dbtunnel 계정을 직접 생성하고 authorized_keys를 등록한 뒤 docker compose up -d를 수동으로 돌려야 실제로 동작한다. 공유 서버에 새 SSH 접근 수단을 추가하는 일이라 자동화하지 않고 사람이 직접 하도록 남겨뒀다.

이번 건에서 다시 확인한 건, infra 변경이 CD의 paths 필터 밖에 있어서 자동 배포는 안 걸리지만 그렇다고 dev/main 동기화를 미뤄도 되는 건 아니라는 점이다. checkout -f가 브랜치 전체를 덮어쓰는 구조라는 걸 실측으로 알게 된 이상, docker-compose.yml처럼 서비스 정의를 담은 파일은 dev에 머지되는 즉시 main 승격까지 같이 챙겨야 한다는 걸 명시적으로 남겨뒀다.