무중단 배포(Zero-Downtime)를 위한 Nginx와 GitHub Actions 연동기
배포할 때마다 10초씩 에러 페이지가 뜨는 치명적인 문제를 방지하기 위해, Nginx와 GitHub Actions를 활용해 무중단 배포(Blue-Green)를 구축한 과정을 담았습니다.
처음 CI/CD 파이프라인을 구축했을 때 남긴 GitHub Actions로 배포 자동화 설정하기 글을 돌아보면, 초창기에는 단순 명료함을 무기로 배포 스크립트를 이렇게 끝냈습니다.
pkill -f 'my-service' || true
nohup java -jar build/libs/my-service.jar > app.log 2>&1 &하지만 유저 트래픽이 있는 시점에 무심코 merge 버튼을 눌렀다가 큰코다친 적이 있습니다. 웹서버가 종료되고 새 .jar 코드가 포트를 붙들고 Spring Context가 로드되기까지의 약 10초~15초 구간 동안, 접근하는 모든 유저가 "502 Bad Gateway"나 무한 로딩 화면을 만나게 되는 치명적인 '배포 중단(Downtime)' 현상이 발생한 것입니다.
이래서는 새벽 3시에 눈 비비고 일어나 수동 배포를 하는 꼴과 다를 바가 없었습니다. 떳떳하게 오후 2시 티타임 중에도 릴리즈할 수 있도록 Nginx의 리버스 프록시(Reverse Proxy)를 이용한 무중단 배포(Blue-Green 방식) 환경으로 환골탈태를 시도했습니다.
핵심 개념: 길라잡이 Nginx의 역방향 프록시
컨셉은 이렇습니다. Nginx가 건물을 지키는 듬직한 문지기 역할을 하고 뒤에 포트 번호가 다른 서버 두 대(8081번, 8082번)를 둡니다.
- 사용자는 Nginx(80 포트)로 구글링해서 들어옵니다.
- Nginx는 현재 구동 중인 8081번 포트(Blue) 서버로 손님을 보냅니다.
- 배포(Green)가 시작됩니다! 이번 스크립트는 기존 서버를 끄는 게 아니라, 비어 있는 8082번 포트(Green)에 새로운 버전을 켭니다.
- 새로 띄운 8082번 포트가 완전히 정상 작동 중인지(Health Check) 확인합니다.
- 이상이 없다면 Nginx 설정 파일에서 트래픽의 방향을 8082번(Green)으로 '스위칭'하고 Nginx를 재장전(Reload)시킵니다.
- 이제 트래픽이 정상적으로 Green으로 흐르는 것을 확인 후, 옛날 버전인 8081번(Blue) 포트를 조용히 꺼버립니다.
이론은 깔끔한데 이 모든 과정을 GitHub Actions의 쉘 스크립트 하나에 녹여내야 했습니다.
스크립트 구현, 생각보다 지난했던 쉘(Shell) 다루기
서버 내부를 판별할 로컬 변수 구조를 짜는 스크립트는 이렇습니다. (자세한 설정은 생략하고 코어만 남겼습니다)
#!/bin/bash
# 1. 현재 돌고 있는 포트 찾기 (health check api 호출)
CURRENT_PORT=$(curl -s http://localhost/actuator/env | grep "server.port" || echo "8081")
if [ $CURRENT_PORT -eq 8081 ]; then
NEW_PORT=8082
else
NEW_PORT=8081
fi
echo ">> 배포할 타겟 포트: $NEW_PORT"
# 2. 어플리케이션 신규 포트로 구동 (Profile 변경)
nohup java -jar -Dserver.port=$NEW_PORT build/libs/my-service.jar > /dev/null 2>&1 &
# 3. 신규 어플리케이션 정상 구동 확인 (최대 30초 대기)
echo ">> Health Check 시작"
for RETRY_COUNT in {1..10}
do
UP_COUNT=$(curl -s http://localhost:$NEW_PORT/actuator/health | grep 'UP' | wc -l)
if [ $UP_COUNT -ge 1 ]; then
echo ">> 상태 확인 성공 (Port: $NEW_PORT)"
break
fi
sleep 3
done
# 4. Nginx 스위칭 (동적 Include 파일 제어)
echo "set \$service_url http://127.0.0.1:$NEW_PORT;" > /etc/nginx/conf.d/service-env.inc
# Nginx 재시작이 아닌, 부드러운 장전(Reload) 처리
sudo nginx -s reload
# 5. 구 버전 프로세스 종료
OLD_PID=$(lsof -ti tcp:$CURRENT_PORT)
if [ -n "$OLD_PID" ]; then
kill -15 $OLD_PID
echo ">> 예전 포트($CURRENT_PORT) 종료 완료."
fi주의할 점
이때 Nginx의 설정 파일인 /etc/nginx/nginx.conf 안에 동적인 위치를 참조하도록 아래의 한 줄을 추가해 두어야 service-env.inc 내용 갱신만으로 훅훅 방향이 바뀝니다.
include /etc/nginx/conf.d/service-env.inc;
location / {
proxy_pass $service_url;
# ... 기본 프록시 설정들
}완성 후, 두려움이 사라진 배포 버튼
스크립트 에러 때문에 kill -9 명령어를 터미널에 수십 번 쳐가며 고생은 했지만, 막상 구현되고 나니 그 쾌감이 엄청났습니다.
무중단 배포를 달성한 뒤로 얻은 가장 큰 이득은 "아무 때나, 작은 버그도 수정 즉시 배포" 할 수 있게 된 심리적인 자유입니다. 사용자는 뒤에서 배포가 10번이 일어나든 전혀 모릅니다. GitHub Actions의 초록 불이 들어옴과 동시에 리플래쉬 없이 새 기능이 짠 하고 나타나게 제어권을 확보한 것, 이것이 진정한 DevOps 자동화의 종착지가 아닐까 싶습니다.