배포는 성공했는데 사이트는 그대로였다
애드센스 심사에서 또 떨어졌다. 사유는 두 가지였는데, 그중 하나가 "복제된 콘텐츠가 있는 화면에 광고를 게재했다"는 것이었다. 원인을 찾아보니 nginx가 존재하지 않는 주소로 들어온 요청에도 홈 화면을 200으로 돌려줬다. SPA를 쓰다 보면 흔히 넣게 되는 폴백인데, 크롤러 입장에서는 같은 내용을 가진 페이지가 무한히 존재하는 사이트로 보인다. 설정 한 줄을 고쳐서 없는 경로는 진짜 404를 내도록 바꾸고 커밋했다.
우리 집 서버는 커밋하면 젠킨스가 알아서 빌드하고 배포한다. 그래서 커밋 후에 사이트를 열어봤는데 아무것도 바뀌지 않았다. robots.txt는 예전 그대로였고 없는 주소는 여전히 200을 뱉었다.
컨테이너는 만들어졌는데 시작이 안 됐다
먼저 컨테이너 목록을 봤다. 프론트엔드와 백엔드가 created 상태였다. 만들어지긴 했는데 시작에 실패했다. 그런데 사이트는 멀쩡히 떠 있었다. 이 조합이 이상했다. 새 컨테이너가 안 떴는데 서비스가 살아 있다면, 옛날 프로세스가 아직 포트를 붙잡고 있다는 뜻이다.
포트를 누가 쥐고 있는지 확인해봤다. 컨테이너는 이미 지워졌는데 docker-proxy 프로세스 네 개가 80과 443을 계속 물고 있었다. 스냅으로 설치한 도커에서 종종 생기는 문제라고 한다. 새 컨테이너는 포트를 못 잡아서 created에서 멈췄고 죽은 컨테이너의 프록시가 계속 옛날 내용을 서빙하고 있었던 것이다.
프로세스를 죽이고 컨테이너를 다시 올렸다. 이번엔 정상적으로 떴다.
그런데도 여전히 옛날 화면이었다
컨테이너 안에 들어가서 파일을 직접 열어봤다. robots.txt는 새 버전이 맞았다. 설정 파일도 새로 반영돼 있었다. 그런데 브라우저로 접속하면 여전히 옛날 내용이 나왔다.
서버에서 직접 요청을 보내보니 더 이상했다. 127.0.0.1로 요청하면 새 내용이 오는데, 같은 서버의 랜 아이피로 요청하면 옛날 내용이 왔다. 같은 컨테이너, 같은 포트인데 어디로 접속하느냐에 따라 응답이 달랐다.
한참 들여다보다가 도커가 포트를 두 갈래로 처리한다는 걸 떠올렸다. 루프백으로 들어오는 요청은 docker-proxy라는 별도 프로세스가 받고 바깥에서 들어오는 요청은 iptables의 NAT 규칙이 처리한다. 두 경로가 다른 곳을 가리키고 있다면 이런 증상이 나올 수 있다.
iptables를 열어보니 정말 그랬다. 삭제된 옛 네트워크의 규칙이 체인 위쪽에 그대로 남아 있었고 외부에서 들어온 요청을 유령 주소로 먼저 채가고 있었다. 도커 명령으로도, 포트 확인으로도 절대 안 잡히는 종류의 문제였다.
규칙을 지웠더니 사이트가 죽었다
낡은 규칙 두 줄을 지웠다. 랜 아이피로 요청하니 이제 새 내용이 왔다. 됐다 싶었는데 브라우저로 접속하니 522가 떴다. 클라우드플레어가 오리진에 닿지 못한다는 뜻이다.
원인은 이랬다. iptables에는 주소를 바꿔주는 nat 테이블과 통과를 허용하는 filter 테이블이 따로 있다. nat 쪽 규칙은 내가 고쳤는데, filter 쪽에는 새 컨테이너를 허용하는 규칙이 애초에 없었다. 주소는 제대로 바뀌는데 패킷이 조용히 버려진다. 호스트에서 보낸 요청은 이 검사를 거치지 않아서 성공했고 바깥에서 온 요청만 죽었다. 그래서 더 헷갈렸다.
손으로 고치는 걸 포기하고 도커 데몬을 재시작했다. 데몬이 올라오면서 규칙을 처음부터 다시 만든다. 그제야 안팎이 모두 정상이 됐다.
그리고 다른 것들이 전부 내려갔다
데몬을 재시작했더니 서버에 있던 컨테이너가 전부 exited로 남았다. restart always를 걸어뒀는데도 자동으로 돌아오지 않았다. 스택마다 폴더를 찾아가서 하나씩 올려야 했다.
여기서 또 한참 헤맸다. 블로그 터널은 환경변수 파일을 지정하지 않고 올려서 토큰이 빈 값으로 들어갔고 일정관리 서비스의 터널은 프로파일 안에 숨어 있어서 일반 명령으로는 아예 뜨지 않았다. 프록시를 정리하니 이번엔 젠킨스와 포테이너가 포트를 못 잡았다.
그리고 마지막이 가장 놀라웠다. 젠킨스에 들어가니 파이프라인 설정이 전부 사라져 있었다. 계정도 예전에 쓰고 버린 아이디만 남았다. SVN은 더 심했다. 저장소 하나는 통째로 없어졌고 다른 하나는 초기 상태로 돌아가 있었다.
사라진 게 아니라 다른 데를 보고 있었다
다행히 데이터는 그대로 있었다. 컨테이너가 엉뚱한 볼륨을 물고 있었을 뿐이다. 예전에 쓰다가 버린 볼륨과 지금 쓰는 폴더가 같은 이름으로 둘 다 남아 있었고 재생성 과정에서 컨테이너가 옛날 쪽을 잡았다.
어느 쪽이 진짜인지 판단하는 데서 두 번이나 틀렸다. 처음에는 SVN 리비전 번호를 비교했다. 폐기된 쪽이 32였고 진짜 쪽이 29였다. 숫자만 보면 폐기된 쪽이 최신 같지만 사실은 두 달 전에 저장소를 새로 만들면서 번호가 1부터 다시 시작한 것이었다. 두 번째로는 파일 수정 시각을 봤다. 이것도 틀렸다. 컨테이너가 기동하기만 해도 파일이 갱신돼서 방금 잘못 붙인 쪽이 가장 최신으로 잡혔다.
결국 답은 훨씬 단순한 데 있었다. 계정 폴더에 내가 쓰는 아이디가 있는가. 빌드 기록이 실제로 쌓여 있는가. 있어야 할 저장소가 목록에 보이는가. 숫자나 타임스탬프가 아니라 내용물을 봤어야 했다.
남은 것
반나절이 걸렸다. 처음 고치려던 nginx 설정은 십 분이면 끝나는 일이었는데, 그게 반영되지 않는 이유를 찾느라 하루를 썼다.
배운 걸 정리해두자면 이렇다. 배포했는데 반영이 안 되면 컨테이너 상태부터 본다. created로 멈춰 있는데 서비스가 살아 있다면 옛 프로세스가 포트를 쥐고 있다. 컨테이너 안의 파일이 새것인데 바깥에서 옛것이 보이면 트래픽이 그 컨테이너에 닿지 않는 것이다. 그리고 루프백과 랜 아이피의 응답이 다르면 iptables를 의심해야 한다. 규칙을 손으로 고치는 것보다 데몬을 재시작하는 편이 안전하다.
데이터가 사라진 것처럼 보일 때는 일단 멈추고 백업부터 뜨는 게 맞다. 오늘도 그렇게 해놨기 때문에 두 번 틀리고도 되돌릴 수 있었다.