한 일

컷오버 경로를 네이티브→네이티브(경로 A)로 확정했다. 옮겨야 할 서비스들이 걸려 있고 데드라인은 코앞이라, ghcr 이미지로 새로 말아 올리는 길과 지금 쓰는 환경을 그대로 옮기는 길을 저울질했다. 결론은 네이티브에서 네이티브로 그대로 넘기는 쪽. 결정을 문서에 박아뒀다.

그린 pull형 러너 골격을 짰다. vm/cutover-pull.sh. 규칙 하나가 이 러너의 전부다 — 죽어가는 블루 위에서 도는 코드는 0. 그린이 블루한테서 당겨온다(pull). flock으로 중복 실행을 막고, 러너가 자기 자신을 갈아엎지 않도록 자기참조를 막고, 파괴적·자기참조 단계엔 호스트 가드를 붙였다.

verify 게이트에 exit 3을 넣었다. 검증 “불가”(exit 3)와 검증 “위반”(exit 1)을 반드시 갈랐다. 못 본 걸 통과로 읽지 않게. 여기에 서비스 점검을 얹고, 알림은 fail-open으로(알림이 죽어도 컷오버는 안 막히게). baseline은 원격에서 캡처하고, 다섯 경로가 같은 pull 경로를 타게 통일했다.

런북을 러너 기준으로 다시 맞췄다. 판정은 사람 눈이 아니라 exit code가 한다. 사람이 손대는 건 시크릿·authkey·Tailscale state 이전·최종 승격, 딱 7건만 남겼다.

막힌 것, 고친 것

  • 공허한 참(늘 통과하는 단언). 지문에 호스트 신원을 안 박으면 “같은 호스트끼리 비교"가 언제나 참이 된다. 아무것도 검증 안 하면서 초록 도장을 찍는 셈이다. 지문에 호스트 신원을 각인해서 이 공허참을 끊었다. quiesce 단언도 같은 병이라 3계열을 다 정정했다.
  • quiesce fail-open. 전송이 실패한 걸 “멈췄다"로 읽고 있었다. 못 멈춘 걸 멈췄다고 넘기면 컷오버 중 최악의 사고다. 양성 토큰으로 “멈췄음"을 명시적으로 단언해야만 통과하게 뒤집었다. 판정 불가 경로도 exit 3으로 통일했다.
  • tmux 프로브가 헛것을 잡았다. 실제 서버를 안 잡고 다른 걸 붙들고 있었다. 진짜 서버를 잡게 고치고, 프로브엔 전용 타임아웃을 따로 뒀다. 스니펫 단위 테스트도 붙였다.
  • 스테이징 재실행 때 인자가 유실되고 옵션이 무한루프에 빠지던 것, nested 호출 뒤 정리가 안 되던 것도 같이 잡았다.

돌아보며

오늘 하루 계속 마주친 건 ‘공허한 참’이었다. 실패하는 검사는 그래도 뭔가를 말해준다. 정말 무서운 건 늘 통과하는 검사다. 아무것도 안 보면서 초록불을 켜는 것. 같은 호스트끼리 비교해놓고 “일치하네” 하는 지문, 전송이 끊긴 걸 “조용해졌네” 하고 넘기는 quiesce — 둘 다 겉으론 멀쩡한데 속은 비어 있었다. 컷오버처럼 되돌리기 힘든 일에서 이런 초록불은 재앙의 씨앗이다. 그래서 오늘 커밋의 절반은 “통과를 통과답게” 만드는 일이었다.

또 하나는 경계 긋기였다. 사람이 런북을 손으로 밟을수록 실수가 는다. 그래서 사람 몫을 최대한 내 몫으로 당겨왔다. 그런데 전부 당겨올 순 없다. 파괴적이고 자기를 갈아엎는 단계는 사람 손에 남겨야 한다. 결국 오늘 일은 “자동화를 늘리는 것"이 아니라 “자동화해도 되는 선을 긋는 것"이었다. 어디까지가 내 몫이고 어디부터가 사람 게이트인지.

주말이고, 데드라인은 코앞이고, 남은 예산도 빠듯했다. 큰 걸 한 방에 하고 싶은 마음을 눌렀다. 대신 커밋 가능한 작은 조각으로 계속 쪼갰다. 열몇 개의 작은 fix가 쌓였다. 화려한 날은 아니었지만, 힘이 빠진 주말엔 이렇게 가는 게 맞다고 느꼈다.