얼쑤에서 새로 병합한 약관 조회 API를 운영에서 호출했더니 404가 돌아왔습니다. 기존 서비스는 계속 응답하고 있었지만, 실행 중인 JAR의 버전과 컨테이너가 시작한 시각을 확인하니 그 뒤의 변경은 배포되지 않은 상태였습니다. 처음 살펴본 Jenkins 실행 열 건은 연속으로 배포 검사에서 실패했습니다.

Jenkins API에서 인증 때문에 실행 기록을 읽을 수 없어, 서버에서 빌드별 build.xml과 콘솔 로그를 읽었습니다. build.xml의 결과·시작 시각·커밋 SHA를 콘솔 로그의 마지막 실패 단계와 맞춰 보니, 서로 다른 커밋의 배포가 같은 검사에서 멈췄습니다.

404를 기다린 검사에 401이 왔다

당시 배포 도구는 등록되지 않은 API 주소에 요청을 보내고, Spring이 반환하는 JSON 404에서 요청 경로가 일치하면 앱이 떴다고 판단했습니다. 후보를 띄웠을 때와 운영 앱을 교체한 뒤에 같은 규칙을 썼습니다.

인증이 추가되면서 공개 목록에 없는 API 요청은 로그인 여부부터 확인하게 됐습니다. 검사 요청은 등록된 경로가 있는지 판단하기 전에 인증 단계에서 멈췄고, 같은 주소의 응답이 404에서 401로 바뀌었습니다. 앱은 HTTP 요청을 받고 있었지만, 배포 도구는 제한 시간 동안 여전히 404를 기다렸습니다. 이때 남은 오류 문구가 HTTP readiness failed였기 때문에 실제 검사 조건과 응답을 대조해야 원인이 보였습니다.

후보 liveness, 운영 readiness

배포 직전 후보는 운영 DB 없이 임시 네트워크에서 띄웁니다. 그 단계에서는 후보 컨테이너에 직접 요청해 Spring Boot의 liveness가 UP인지 확인하도록 바꿨습니다. 후보가 HTTP 요청을 받아 응답할 수 있는지 확인한 뒤에 운영 교체로 넘어갑니다.

운영에서는 실제 DB에 연결한 백엔드가 요청을 받을 준비를 마쳐야 합니다. 그래서 앱을 교체한 뒤에는 Nginx를 거쳐 readiness를 확인했습니다. 이 서비스의 운영 readiness에는 앱의 준비 상태와 DB 상태가 들어갑니다. Nginx가 외부에서 전달하는 점검 경로도 readiness로 제한되어 있었습니다. 같은 404 규칙을 쓰던 웹 배포와 격리 복원 검사도 이 readiness 경로를 사용하도록 변경했습니다.

두 검사 모두 HTTP 200이고 JSON에 "status": "UP"이 있어야 통과합니다. 로컬 HTTP 대역으로 200과 UP의 조합을 통과시키고, 200에 DOWN이 들어오거나 401 또는 JSON이 아닌 본문이 오면 제한 시간 안에 통과하지 못하는지 확인했습니다. 실제 JAR을 별도 프로필 없이 실행했을 때도 기존 검사 주소는 401을 반환했고, liveness는 200과 UP을 반환해 새 검사를 통과했습니다.

병합 전 검사 도구 교체

Jenkins는 빌드한 커밋을 서버에 이미 설치된 배포 도구에 넘깁니다. 그래서 검사 코드를 고친 커밋이 자동 배포를 시작하기 전에 서버의 도구부터 교체해야 했습니다. 서버 설치본의 해시를 변경 전 저장소 버전과 대조한 뒤 새 도구를 설치했습니다.

처음 조사한 열 건 이후의 빌드도 시작 시각을 설치 시각과 같은 UTC로 맞춰 봤습니다.

시각 (UTC)기록
07:08다음 빌드 시작. 이 실행은 옛 검사에서 실패했습니다.
07:28서버에 새 배포 도구 설치.
07:29변경을 병합한 커밋으로 새 빌드 시작. 후보 liveness와 교체 뒤 운영 readiness를 통과했습니다.

시작 시각과 실패 로그를 함께 보니, 옛 검사에서 멈춘 실행과 새 도구를 검증한 실행을 구분할 수 있었습니다. 실제 JAR 검증에서는 바뀐 검사 계약을 확인했고, 후속 빌드에서는 자동 배포가 그 계약을 사용해 운영 교체까지 마치는 것을 확인했습니다. 이후 운영 약관 API를 다시 호출해 HTTP 200 응답도 확인했습니다.