혼자공부한거

혼자 만든 앱에 무중단 배포 적용해보기 – Jenkins, Docker, Nginx

컴공코딩러 2025. 4. 24. 14:51

 

 

 

 

 

운영 중인 앱: 축잘알 테스트 (구글 플레이)

 


🧭 왜 무중단 배포를 하게 됐을까

혼자서 기획하고 개발한 축잘알 테스트 앱은
EC2에서 Spring Boot + Docker + Nginx 조합으로 서비스되고 있다.

처음에는 그냥 수동으로 배포했다.
방법은 아주 단순했다:

1. GitHub에 코드 push
2. EC2에 SSH 접속해서 git pull
3. Docker 이미지 다시 만들고 기존 컨테이너 정리
4. 새 컨테이너 띄우고 Nginx 설정 수정

 

근데 이 과정이 너무 귀찮았다.
캐시도 직접 지워야 하고, SSH도 매번 접속해야 했다.

그래서 자동화 스크립트를 먼저 만들었고,
이후에는 Jenkins + GitHub Webhook으로 자동 배포까지 붙였다.


💥 무중단 배포를 결심하게 된 계기

새벽에 배포하면서 서버를 잠깐 내린 적이 있었는데,
그 타이밍에 이런 리뷰가 달렸다.

“앱 실행이 안 돼요. 서버 오류인가요?”

그때 느꼈다.
배포 중 잠깐이라도 끊기면 사용자 입장에선 그냥 ‘망가진 앱’으로 느껴질 수 있다는 걸.
그 이후로는 서비스 끊기지 않고 배포하는 걸 목표로 삼았다.


🔧 전체 구조


GitHub → Webhook → Jenkins → go2.sh(자체 스크립트) 실행
                             ↳ 새 포트 컨테이너 실행
                             ↳ Health Check
                             ↳ Nginx Proxy 전환
                             ↳ 이전 컨테이너 제거​
 

 


구성 요소 역할
GitHub 코드 변경 트리거
Jenkins Webhook 받아서 스크립트 실행
go2.sh 전체 무중단 배포 흐름 담당
Nginx 프록시 + 포트 스위칭
Spring Boot Docker로 실행되는 서비스

⚙️ Jenkins 설정

Jenkins에서는 그냥 스크립트 실행용으로만 썼다.
CI/CD를 본격적으로 구성한 건 아니고, webhook만 받아서 shell 실행만 하게 했다.

- Freestyle 프로젝트 생성
- 트리거 설정: `GitHub hook trigger for GITScm polling`
- 빌드 단계: `Execute Shell`에서 `go2.sh` 실행

✅ 무중단 배포 스크립트 (go2.sh) 주요 흐름

1. 현재 포트 확인 → IDLE 포트 계산

2. Docker/Gradle 캐시 정리

3. 최신 코드 pull + 빌드

4. IDLE 포트 컨테이너 제거

5. 새 컨테이너 실행

6. Health Check (최대 30초 대기)

7. Nginx 프록시 포트 전환

8. Nginx reload

9. 기존 컨테이너 종료

📌 완전한 Blue-Green 배포는 아니지만,
끊김 없이 포트 스위칭으로 유사 구조 구현


📦 배포 결과 확인

 

실제 배포된 로그와 Jenkins 기록을 통해 무중단 배포가 잘 동작하는지 확인했다.

아래는 Jenkins의 실제 빌드 기록이다.
여러 차례 배포를 반복했지만, 서비스는 한 번도 중단되지 않았다.
(무중단 배포 스크립트가 정상 작동하고 있다는 것을 확인할 수 있다.)

 

✅ 초록 체크 → 성공
❌ 빨간 X → 실패한 초기 테스트

여러 번의 배포가 있었지만 사용자는 끊김 없이 앱을 사용할 수 있었다.

초기 실패(2번) 원인은 Jenkins가 sudo 명령어를 실행하지 못했던 권한 문제였다.
Jenkins 사용자에게 sudo 권한을 부여하고 나서는 모든 빌드가 정상적으로 작동했다.

 

 


🧠 회고 & 다음 계획

이번 프로젝트를 통해
Bash + Jenkins만으로도 실무 수준의 무중단 배포가 가능하다는 걸 실제로 경험했다.

앞으로는 Docker Compose나 GitHub Actions도 추가로 활용해서
더 안정적이고 효율적인 CI/CD 구조를 만들어 갈 계획이다.

 


 

 
if sudo docker ps | grep -q 9070; then
  CURRENT_PORT=9070
  IDLE_PORT=9071
else
  CURRENT_PORT=9071
  IDLE_PORT=9070
fi
 
echo "> 현재 실행 중인 포트: $CURRENT_PORT"
echo "> 새 컨테이너는 $IDLE_PORT 포트로 실행됩니다."
 
# 🔹 도커 시스템 캐시 정리
echo "> Docker 캐시 정리 (dangling images, stopped containers)"
sudo docker system prune -af --volumes
 
# 🔹 Gradle 캐시 중 오래된 캐시 제거 (원한다면 완전 삭제도 가능)
echo "> Gradle 캐시 일부 정리"
find ~/.gradle/caches/ -type d -name "buildOutputCleanup" -exec rm -rf {} +
 
# 🔹 이전 빌드 아티팩트 정리
echo "> 이전 빌드 파일 정리"
rm -rf $BASE_DIR/build
 
# 1. git pull
echo "> Git Pull"
cd $BASE_DIR
sudo git config --global --add safe.directory $BASE_DIR
sudo git pull $REPO_URL
 
# 2. 기존 JAR 정리 및 빌드
echo "> Gradle Build"
sudo ./gradlew clean build
 
# 3. 기존 IDLE_PORT 컨테이너 삭제 (혹시 남아 있을 경우)
echo "> IDLE 포트($IDLE_PORT) 컨테이너 정리"
sudo docker stop quiz-$IDLE_PORT 2>/dev/null || true
sudo docker rm quiz-$IDLE_PORT 2>/dev/null || true
sudo docker rmi $DOCKER_IMAGE 2>/dev/null || true
 
# 4. Docker Build
echo "> Docker Build"
sudo docker build --build-arg JAR_FILE=build/libs/*.jar -t $DOCKER_IMAGE .
 
# 5. 새 컨테이너 실행
echo "> Docker Run (port: $IDLE_PORT)"
sudo docker run -d --name quiz-$IDLE_PORT -e SERVER_PORT=$IDLE_PORT -p $IDLE_PORT:$IDLE_PORT $DOCKER_IMAGE
 
# 6. 새 컨테이너 Health Check (최대 30초 기다림)
for i in {1..30}
do
  echo "> 헬스체크 시도... ($i)"
  RESPONSE=$(curl -s http://localhost:$IDLE_PORT/actuator/health)
  if echo "$RESPONSE" | grep -q '"status":"UP"'; then
    echo "> 헬스체크 성공!"
    break
  fi
  sleep 1
done
 
# 7. Nginx 설정 수정
echo "> Nginx 설정 수정 ($IDLE_PORT)"
NGINX_CONF=/etc/nginx/conf.d/quiz.conf
sudo sed -i "s/proxy_pass http:\/\/127.0.0.1:$CURRENT_PORT;/proxy_pass http:\/\/127.0.0.1:$IDLE_PORT;/" $NGINX_CONF
 
# 8. Nginx reload
echo "> Nginx Reload"
sudo nginx -s reload
 
# 9. 이전 컨테이너 종료
echo "> 이전 컨테이너 종료: quiz-$CURRENT_PORT"
sudo docker stop quiz-$CURRENT_PORT
sudo docker rm quiz-$CURRENT_PORT