운영 중인 앱: 축잘알 테스트 (구글 플레이)
🧭 왜 무중단 배포를 하게 됐을까
혼자서 기획하고 개발한 축잘알 테스트 앱은
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
'혼자공부한거' 카테고리의 다른 글
Redis로 유저 랭킹을 불러오면 정말 더 빠를까? (0) | 2025.04.17 |
---|---|
랜덤 퀴즈 데이터 추출, 어떤 방식이 가장 빠를까? (MySQL vs Redis vs Java) (2) | 2025.04.16 |
키클락 취약점 살펴보기 (CVE-2024-8698) (1) | 2024.12.01 |
SSO 란 무엇일까 (0) | 2024.11.22 |
JWT 토큰 (0) | 2024.05.24 |