website: CD 자동 롤백, 무조건 되돌리지 않고 마이그레이션 위험도로 나누기
CD 자동 롤백이 이미지만 되돌려놓고 헬스체크도 없이 "롤백 완료"라고 로그를 찍고 있었다. 거기다 컬럼을 지우거나 이름을 바꾸는 마이그레이션이 배포에 섞여 있으면, 자동 롤백으로 되돌아간 구버전 앱이 이미 사라진 컬럼을 찾다가 새로운 장애를 만들 수 있는 구조였다. 이 두 가지를 #320 이슈로 정리하고 손봤다.
먼저 "롤백 완료" 로그부터 봤다. cd.yml의 롤백 스텝은 이전 태그로 docker compose up -d만 실행하고 바로 완료 메시지를 찍었다. 컨테이너가 떴는지, 앱이 실제로 응답하는지는 아무도 확인하지 않은 채였다. 안전장치가 있다는 착각이 실제로 안전장치가 없는 것보다 나쁘다고 판단해서, 롤백 후에도 배포 스텝과 똑같이 /actuator/health를 3초 간격으로 최대 120초까지 폴링하도록 바꿨다. 이 시간 안에 200이 안 뜨면 로그를 남기고 실패로 종료하게 했다.
두 번째 문제가 더 까다로웠다. 이 프로젝트는 SQLite 제약 때문에 컬럼 삭제·이름변경을 db-migration.md에 정리된 "테이블 재생성 패턴"(새 테이블 만들고 데이터 옮기고 DROP TABLE + RENAME TO)으로 처리한다. 이런 마이그레이션이 성공적으로 커밋된 뒤에 앱 헬스체크가 실패하면, DB는 이미 새 스키마로 넘어간 상태고 롤백으로 되돌아간 구버전 앱만 옛 컬럼을 찾는 애매한 상황이 된다. 그러니까 모든 실패를 똑같이 자동 롤백하면 안 되고, 이번 배포에 새로 추가된 마이그레이션이 "추가형"인지 "삭제/변경형"인지 먼저 판별해야 했다.
판별 방법을 정하는 과정에서 한 번 삽질을 했다. 처음엔 git diff --diff-filter=A로 새로 추가된 마이그레이션 파일만 뽑으려 했는데, 실제 V5→V6 리네임 커밋으로 테스트해보니 git이 내용 유사도로 "리네임"이라고 판단해버려서 새 파일인데도 필터에 안 걸리는 걸 확인했다. 그래서 diff 필터 대신 마이그레이션 폴더의 파일 경로 목록 자체를 이전 커밋과 이후 커밋 사이에서 comm -13으로 비교하는 방식으로 바꿨다. 경로 기준이라 git의 리네임 추정 로직에 흔들리지 않는다. 이렇게 뽑은 새 파일들의 내용에서 주석 줄을 빼고 drop table|drop column|rename to|rename column 패턴을 grep해서, 하나라도 걸리면 이번 배포를 "위험(destructive)"으로 표시한다.
push 이벤트는 GitHub이 주는 github.event.before로 이전 커밋을 정확히 알 수 있지만, workflow_dispatch(수동 배포)는 이전 커밋이 뭔지 알 방법이 없다. 이 경우는 그냥 안전하게 "위험"으로 취급하기로 했다 — 판단 근거가 없는데 낙관적으로 "안전"이라고 가정하는 것보다는, 사람 개입을 한 번 더 거치게 하는 쪽이 낫다고 봤다. 이 diff를 뜨려면 actions/checkout이 전체 히스토리를 갖고 있어야 해서 fetch-depth: 0도 같이 추가했다(기본 얕은 체크아웃으로는 이전 커밋 자체가 없다).
판정 결과에 따라 이후 흐름을 두 갈래로 나눴다. 추가형이면 지금까지처럼 자동 롤백하되, 위에서 얘기한 헬스체크로 실제 복구를 확인한다. 삭제/변경형(또는 판단 불가)이면 롤백 스텝 자체를 if: failure() && steps.migration-check.outputs.destructive == 'false' 조건으로 건너뛰고, 대신 배포 스텝 안에서 스키마를 건드리기 전에 scripts/backup-db.sh로 DB 백업을 한 번 자동으로 뜨도록 했다. 이건 새로운 안전장치를 추가한 게 아니라, 원래 사람이 배포 직전에 수동으로 하던 백업 관행을 CD가 대신 하는 것뿐이다. 이 백업이 실패하면 아직 마이그레이션 전이라 DB가 그대로 남아있는 시점이니, 배포 자체를 중단시켰다. 롤백을 생략한 자리엔 별도 스텝을 하나 추가해서, 실패 원인을 fix-forward로 먼저 고쳐서 재배포하는 걸 우선 검토하고 그래도 안 되면 RUNBOOK의 수동 절차로 넘어가라는 안내를 로그에 남기게 했다.
DB를 자동으로 복원하는 기능은 이번에도 넣지 않았다. 백업까지는 그냥 복사본 하나 더 만드는 거라 위험할 게 없지만, 복원은 "이 실패가 마이그레이션 때문인지 완전히 무관한 문제인지"를 스크립트가 구분할 수 없는 판단이라 여전히 사람이 봐야 한다. 자동화 범위를 백업까지로 딱 그어둔 셈이다.
문서는 CI-CD.md 다이어그램에 마이그레이션 위험도 판단 단계를 새로 넣으면서 이후 번호가 다 한 칸씩 밀렸고, CLAUDE.md에는 실패 시 사람이 판단할 순서(①fix-forward 우선 검토 → ②이미지만 되돌리는 RUNBOOK 절차 → ③필요하면 백업/스냅샷으로 DB까지 복원)를 정리해뒀다. db-migration.md에는 이 판별이 결국 SQL 텍스트를 grep하는 휴리스틱이라 재생성 패턴을 벗어난 방식으로 컬럼을 지우거나 바꾸면 CD가 "안전"으로 잘못 판단해서 위험한 자동 롤백을 그대로 실행할 수 있다는 한계도 적어뒀다. 자동화가 프로젝트 컨벤션(재생성 패턴)에 기대고 있다는 걸 명시해둬야, 나중에 이 패턴을 깜빡 잊고 다른 방식으로 컬럼을 지우는 실수를 줄일 수 있을 것 같았다.