← 개발 로그 목록

website: CD 헬스체크 로깅 개선으로 배포 디버깅 편의성 향상

/ 6분 분량 / 개발 로그

CD 파이프라인의 헬스체크 로직이 대기 중엔 아무 로그도 남기지 않던 문제를 고쳤습니다. curl -sf의 침묵 옵션을 걷어내고 매 시도마다 HTTP 상태를 출력하도록 바꿔서, SSH 없이도 CD 로그만으로 배포 상황을 파악할 수 있게 되었습니다.

요약

2026년 7월 12일, fix/cd-healthcheck-logging 브랜치를 dev에 머지했습니다. 커밋 메시지 그대로 "헬스체크 대기 중에도 로그가 보이게 개선"하는 작업이었고, 변경된 파일은 .github/workflows/cd.yml 하나뿐입니다. 추가 8줄, 삭제 3줄로 규모는 작지만, CD 파이프라인을 다뤄본 사람이라면 공감할 만한 "로그가 안 보여서 답답했던" 문제를 해결한 커밋입니다.

배경 및 목적

배포 자동화 워크플로우에서 헬스체크는 필수 단계입니다. 컨테이너를 새로 띄운 뒤 애플리케이션이 정상적으로 뜰 때까지 기다렸다가, 문제없으면 트래픽을 넘기고 문제 있으면 배포를 중단시키는 역할을 하죠. 그런데 기존 코드는 curl -sf를 사용하고 있었습니다. -s는 진행 상황 표시를 끄는 silent 옵션이고, -f는 실패 시 에러 본문 대신 조용히 종료 코드만 반환하는 옵션입니다. 두 옵션이 합쳐지면 헬스체크가 실패하는 매 순간마다 로그에는 아무것도 남지 않습니다.

문제는 배포가 잘 안 될 때 드러납니다. 헬스체크가 2분 동안 계속 실패하고 있어도 CD 로그 화면에는 그냥 빈 화면만 흘러가고, 최종적으로 "❌ 헬스체크 실패"라는 한 줄만 뜨는 식이었습니다. 이 상태에서 원인을 파악하려면 결국 서버에 SSH로 들어가서 컨테이너 로그를 직접 까봐야 했는데, 이건 CD를 만든 취지—사람이 개입하지 않아도 배포 상태를 알 수 있게 하자는 목적—에 어긋나는 상황이었습니다.

구현 내용

변경된 파일은 .github/workflows/cd.yml 하나이며, 헬스체크를 수행하는 until 루프 부분만 수정했습니다.

먼저 curl 호출 방식을 바꿨습니다.

until curl -sf http://localhost:${{ steps.config.outputs.port }}/actuator/health; do

이 코드를

until STATUS={{ steps.config.outputs.port }}/actuator/health) && [ "$STATUS" == "200" ]; do

로 바꿨습니다. -o /dev/null로 응답 본문은 버리고, -w '%{http_code}'로 HTTP 상태 코드만 뽑아서 STATUS 변수에 담습니다. 그리고 이 값이 정확히 "200"인지를 조건으로 확인하도록 했습니다. 기존에는 curl의 종료 코드(성공/실패)만으로 판단했다면, 이제는 실제 HTTP 상태 코드를 손에 쥐고 있으니 로그에 찍을 수 있게 된 겁니다.

루프 안에는 이 상태를 매 시도마다 출력하는 줄을 추가했습니다.

echo "[{STATUS:-연결 실패}"

${STATUS:-연결 실패}는 bash의 파라미터 확장 문법으로, STATUS 변수가 비어있으면(즉 curl 자체가 연결에 실패해서 상태 코드를 못 받아온 경우) "연결 실패"라는 문자열을 대신 보여줍니다. 컨테이너가 아직 완전히 안 떠서 포트 자체가 안 열려 있는 상황과, 포트는 열렸지만 헬스체크 엔드포인트가 200이 아닌 다른 코드를 반환하는 상황을 로그만 보고도 구분할 수 있게 됩니다.

마지막으로 타임아웃(120초)에 도달해서 최종 실패로 처리되는 부분도 손봤습니다.

echo "❌ 헬스체크 실패 — 마지막 응답: HTTP ${STATUS:-연결 실패}"
echo "--- ${{ steps.config.outputs.service }} 최근 로그 ---"
docker compose -f docker-compose.yml logs --tail=50 ${{ steps.config.outputs.service }}

기존엔 "❌ 헬스체크 실패" 한 줄만 찍고 끝났는데, 이제는 마지막으로 받은 응답 상태를 함께 보여주고, 해당 서비스 컨테이너의 최근 50줄 로그까지 CD 로그에 바로 출력하도록 했습니다. 이 부분이 개인적으로는 이번 커밋에서 가장 실용적인 변화라고 생각합니다. 헬스체크가 왜 실패했는지—애플리케이션이 아예 안 떴는지, 떴는데 예외가 터졌는지—를 SSH 접속 없이 GitHub Actions 화면에서 바로 확인할 수 있게 됐으니까요.

기술적 의사결정

이번 변경에서 핵심적인 선택은 curl -sf를 버리고 curl -s -o /dev/null -w '%{http_code}' 조합으로 바꾼 것입니다.

-f 옵션은 사용하기 간편하지만 "성공/실패"라는 이진 정보만 주기 때문에 디버깅에는 불리합니다. 반면 -w로 상태 코드를 직접 추출하는 방식은 코드가 한 줄 길어지는 대신, 실패의 종류(연결 자체가 안 됨 vs 4xx/5xx 응답)를 구분할 수 있는 정보를 얻습니다. CD 스크립트처럼 사람이 실시간으로 지켜보지 않고 나중에 로그만 보고 원인을 파악해야 하는 환경에서는 이 정보량의 차이가 꽤 큽니다.

대안으로는 헬스체크 실패 시마다 docker compose logs를 매번 찍는 방법도 있었겠지만, 이렇게 하면 최대 40번(120초/3초)까지 로그가 반복 출력되어 오히려 노이즈가 커집니다. 그래서 매 시도마다는 상태 코드만 한 줄로 남기고, 최종 실패가 확정된 시점에만 컨테이너 로그 50줄을 붙이는 절충안을 택한 것으로 보입니다. 이 방식이 로그의 가독성과 디버깅 정보량 사이에서 합리적인 균형점이라고 생각합니다.

배운 점 및 개선점

이번 커밋을 보면서 새삼 느낀 건, curl의 -s와 -f 같은 조용한 기본 옵션들이 로컬에서 테스트할 땐 편리하지만 CI/CD 환경에서는 오히려 독이 될 수 있다는 점입니다. 사람이 눈앞에서 지켜보는 게 아니라 나중에 로그만 보고 판단해야 하는 상황에서는, 조금 verbose하더라도 매 단계의 상태를 남기는 쪽이 훨씬 유리합니다.

다음 단계로 생각해볼 만한 개선점은, 지금은 실패 확정 시에만 컨테이너 로그를 출력하는데 여기에 타임스탬프 필터링(--since)을 추가하면 더 깔끔한 로그를 얻을 수 있을 것 같습니다. 또한 헬스체크 URL이나 타임아웃 값이 하드코딩되어 있는 부분도, 서비스가 늘어나면 환경변수나 설정 파일로 분리하는 방향을 고민해볼 수 있겠습니다.