website: 인프라 운영 러너북 — 알람이 오면 뭘 해야 하는지 처음으로 문서화하다
인프라 지식이 나 한 사람에게만 쌓이고 있다는 걸, 인수인계 문서를 쓰다가 스스로 확인하게 됐다. 알람이 오면 그때그때 SSH로 들어가 진단해왔을 뿐, "이렇게 하면 된다"는 어디에도 적혀 있지 않았다. 그래서 이번 PR에서 그 절차를 문서로 옮겼다.
시작은 단순했다. infra/RUNBOOK.md를 새로 만들어서, 지금 알람이 설정된 5개 항목(디스크 80%, 메모리 85%, prod/stage 백업 26시간 부재, git 드리프트, UptimeRobot DOWN)에 대해 "경고 → 영향 → 원인 후보 → 복구" 순서로 정리하기 시작했다. 이 4단계 구성은 처음부터 이렇게 짠 게 아니라 중간에 리뷰를 받고 바뀐 것이다. 알람을 받고도 "이게 지금 진짜 급한 건가, 뭐부터 의심해야 하나"를 매번 처음부터 판단해야 하는 게 문제라는 지적이 있었고, 그래서 OCI가 매기는 심각도(전부 CRITICAL로 동일해서 변별력이 없었다)와는 별개로 "지금 실제로 서비스에 영향이 가고 있는가"를 기준으로 한 "대응 긴급도"를 따로 매겼다. 예방적 경고(디스크·메모리·백업·git 드리프트)와 즉시 대응(UptimeRobot DOWN)을 나눠두니, 알람 메일을 받는 순간 뭐부터 봐야 하는지가 명확해졌다.
명령을 문서에 적을 때 원칙을 하나 세웠다. "이론상 맞을 것 같은 명령"이 아니라 "실제로 돌려본 명령"만 적는다는 것. 디스크 정리, 메모리 재기동, 백업 재실행까지 실제 서버(likelion-oci)에 접속해서 하나씩 돌려보고 결과까지 확인한 뒤에 문서에 옮겼다. 이 과정에서 docker image prune -a를 실행할 뻔한 적이 있었는데, -a 옵션은 실행 중이지 않은 모든 이미지를 지워서 수동 롤백에 필요한 이전 태그 이미지까지 날아간다는 걸 알게 됐다. 시스템이 막아줘서 실제 사고로는 안 이어졌지만, 이런 함정을 문서에 경고로 남겨두는 게 이 작업의 핵심이라고 생각했다.
문서를 쓰면서 기존 문서와 실제 서버 상태를 대조하는 작업도 같이 하게 됐다. infra/CLAUDE.md의 "미결 사항"에 이미 끝난 일(신선우 DB 계정 등록, sqlite-web GUI 뷰어, 이메일 자격증명 전달, #83 PR 머지)이 안 지워진 채 남아 있었고, 실제로는 아무 데서도 참조하지 않는 옛날 인증서(certbot lineage)가 서버에 남아 있었다. 이런 건 위험이 작고 되돌리기도 쉬운 판단이라 바로 정리했고, 반면 서버의 dev 브랜치가 GitHub의 origin/dev와 커밋 단위로 26개/16개씩 어긋나 있는 걸 발견했을 때는 실제로 병합을 지워보지 않고 git merge --no-commit --no-ff 후 --abort로 취소하는 방식으로 "지금 합쳐도 충돌 없다"는 것만 검증하고, 정리 방법 자체는 판단을 미뤄뒀다. 되돌리기 어려운 결정은 먼저 검증만 해두고 실행은 넘긴다는 기준을 여기서 실제로 적용해본 셈이다.
커밋 흐름을 보면 이 PR이 한 번에 완성되지 않았다는 게 보인다. 처음엔 RUNBOOK 하나에 알람 대응, 마인드셋, 지표, 평소 루틴, 인수인계 체크리스트를 전부 몰아넣었다. 그런데 알람 대응 절차를 찾으러 온 사람이 "이 역할의 목적"이나 "필요 역량" 같은 걸 먼저 읽어야 하는 구조가 이상하다는 걸 깨달았고(리뷰에서도 같은 지적이 나왔다), "지금 문제에 뭘 하나"와 "이 역할이 뭐고 다음 사람에게 뭘 넘기나"는 목적 자체가 다르다는 판단으로 후반부에 handoff.md를 따로 만들어 분리했다. RUNBOOK은 알람 대응·자주 쓰는 명령·갱신 규칙 세 가지만 남기고, 마인드셋·지표·역량 체크리스트·평소 루틴·협업 인터페이스·계정 인벤토리는 handoff로 옮겼다. 같은 이유로 CLAUDE.md에 늘어져 있던 DNS 레코드 계층별 설명과 OCI IAM 구조도 dns.md, iam.md로 따로 빼서, CLAUDE.md는 "한눈에 보는 지도" 역할만 하도록 정리했다.
DB 복원 절차를 추가하는 과정에서는 예상 못 한 함정을 하나 발견했다. 이 프로젝트는 Flyway를 2026-07-23에 도입했는데, 그보다 오래된 백업(2026-07-17 stage)을 복원해보니 V2__member_offboarding_and_cohort_unique.sql 마이그레이션이 no such column: failed_login_attempts 에러로 죽으면서 앱이 아예 기동되지 않았다. Flyway의 baseline 처리 방식이 "지금 DB가 V1 상태와 같은 모양"이라고 그냥 가정하고 넘어가는데, 도입 이전 옛날 스키마는 그 가정과 다를 수 있어서였다. 이걸 실제로 재현해 확인한 뒤, "복원할 백업은 항상 2026-07-24 이후 날짜로만 고를 것"이라는 경고를 문서에 명시했다. 이런 종류의 함정은 겪어봐야 문서에 쓸 수 있는 것이라, 시행착오 자체가 문서의 재료가 됐다.
무중단 복원(블루-그린 방식) 절차도 이번에 같이 검증했다. 기존 서비스는 켜둔 채 복원된 데이터를 담은 컨테이너를 옆에 하나 더 띄우고, 헬스체크를 통과하면 nginx만 그쪽으로 돌리는 방식이다. 이건 단순 복원보다 손이 두 배로 가고(nginx 설정을 전환할 때, 되돌릴 때 두 번 고쳐야 한다), 급한 마음에 되돌리기 단계를 건너뛰고 바로 마무리로 넘어가고 싶어질 수 있는 구조라 문서에 "절대 그러면 안 된다"고 명시적으로 남겼다. 옛 컨테이너를 이미 내린 뒤에 문제가 생기면 되돌릴 대상 자체가 없기 때문이다.
계정 인벤토리를 정리하다가는 알람 수신 경로 자체에 문제가 있다는 걸 발견했다. 문서에는 "동아리 공용 메일" 구독이라고 적혀 있었는데, 실제 ONS 구독자를 조회해보니 개인 메일(장찬욱·김우진) 뿐이었다. 동아리 Outlook 계정으로 통합을 시도해서 실제로 구독을 추가해봤는데, 구독 확인 메일은 왔지만 정작 알람이 발동됐을 때 실제 알림 메일은 정크함에도 없이 오지 않았다. 임계치를 강제로 조정해서 알람을 발동시켜 재확인까지 했는데도 같은 결과였다. 원인은 알아내지 못했고, 결국 통합을 포기하고 개인 메일 기반을 유지하기로 했다. 대신 "다음 담당자에게 인수인계할 때는 반드시 실제로 알람을 한 번 발동시켜 수신까지 검증한 뒤에만 이전 담당자를 구독에서 뺀다"는 절차를 명시했다. 확인 클릭만으로는 검증이 안 된다는 걸 실측으로 알게 된 뒤라, 이 부분은 특히 꼼꼼히 적었다.
이 PR은 2026-07-26에 시작해서 2026-07-28에 dev로 머지됐다. 커밋 목록만 봐도 알람 대응 절차 작성 → 서버 실측 문서화 → 마인드셋/지표/평소 루틴 보강 → DB 복원 절차 추가 → 계정 인벤토리 → IAM 구조 반영 → 외부 서비스 실계정 확인 → DNS 계층 설명 → RUNBOOK/handoff 분리 → 다이어그램 추가 → infra 폴더를 scripts/docs로 재구성까지, 한 번의 작업이 아니라 문서를 실제로 써보고 쓰다가 발견한 문제를 계속 되먹임한 흐름이었다. 마지막엔 infra/ 아래 실행 스크립트와 문서를 scripts/, docs/로 분리하면서 그동안 문서 안에 박혀 있던 경로 참조를 전부 다시 손봐야 했는데, 이건 서버 크론탭이나 SSH forced-command가 아직 옛 경로를 가리키고 있어서 머지 전까지는 건드리지 않고, 머지된 뒤에 서버 쪽을 별도로 반영해야 한다는 점도 같이 남겨뒀다.
이 작업을 하면서 든 생각은, 문서를 "한 번에 완성"하려고 하지 않는 게 오히려 더 정확한 문서를 만든다는 것이다. 처음부터 완벽한 구조를 잡으려 했다면 알람 대응과 역할 오리엔테이션을 한데 묶은 채로 끝났을 거고, DB 복원 절차의 Flyway 함정도 실제로 재현해보지 않았다면 문서에 못 남겼을 것이다. RUNBOOK 자체에 "인프라가 바뀌면 같은 PR에서 이 문서도 갱신한다"는 갱신 규칙을 넣어둔 것도 같은 맥락이다 — 문서는 한 번 쓰고 끝나는 게 아니라 계속 실제 상태와 맞춰가야 한다는 전제를 문서 안에 박아둔 셈이다.