Nginx
를 이용하여 무중단 배포를 진행하였으며, 배포 전략은Blue-Green
을CI/CD
는Jenkins
를 사용했습니다.
두 개의 포트번호(8082
, 8083
)을 사용하며 현재 배포 중인 포트번호는 8082
이 배포되었다는 가정으로 배포 시나리오를 작성했습니다.
빌드 트리거
GitHub
의 webhook으로 Jenkins
가 자동으로 빌드를 시작합니다.
빌드 & 새로운 포트 결정
Gradle
빌드 후, 새로운 포트를 할당하기 위해 toggle_port.sh
스크립트를 사용하여 새로운 포트8083
을 저장합니다.
컨테이너 준비 및 배포
새 서비스 이름과 포트를 새로운 포트(8083
)에 맞게 설정하여, Nginx
를 통해 새로운 포트로 트래픽을 전달받을 수 있도록 설정을 동적으로 업데이트합니다.
Health Check
새로운 서비스(8083
)가 정상적으로 실행되는지 확인하기 위해 최대 3회의 헬스 체크를 진행합니다.
만약, 3번의 헬스 체크가 실패할 경우Nginx
설정을 이전 포트(8082
)로 되돌려 트래픽을 처리할 수 있도록 합니다.
Nginx 업데이트 및 Reload
헬스 체크에 성공하면 새로운 포트(8083
)로 Nginx
설정을 업데이트하고 nginx -s reload
명령어로 구성을 새로 고칩니다.
이 단계로 Nginx
가 새로운 서비스로 트래픽을 전다랗게 됩니다.
이전 컨테이너 정리
새로운 배포가 안정적으로 완료되면, 이전에 사용하던 포트(8082
)의 Docker
컨테이너를 종료하고 삭제하여 자원을 해제합니다.
toggle.sh
을 사용해 현재 배포된 포트 번호를 확인하고, 새로 배포될 포트번호를 젠킨스
# 현재 사용 중인 포트를 저장하는 파일 경로
PORT_FILE="/home/scripts/active_port"
# 파일이 존재하지 않을 경우 default 8082 포트 사용
if [ ! -f "$PORT_FILE" ]; then
echo "8082" > "$PORT_FILE"
fi
# 현재 포트 번호 읽기
CURRENT_PORT=$(cat "$PORT_FILE")
# 포트 toggle 배포
if [ "$CURRENT_PORT" -eq "8082" ]; then
NEW_PORT=8083
else
NEW_PORT=8082
fi
# 새로운 포트 번호 파일 저장
echo "$NEW_PORT" > "$PORT_FILE"
# 새로운 포트 번호 출력
echo "$NEW_PORT"
stage('Get new port') {
steps {
script {
NEW_PORT = sh(script: "/home/scripts/toggle_port.sh", returnStdout: true).trim()
env.PORT = NEW_PORT
echo "New port selected: ${PORT}"
}
}
}
젠킨스
Docker
를 스프링 프로젝트의docker-compose
로 실행 중이지 않고, 서버에서 직접Jenkins
를 올린 환경으로 아래 작업을 수행했습니다.
기존의 젠킨스 Docker 백업
기존의 젠킨스 데이터를 백업하여 이후 새 컨테이너에서 마운트할 수 있도록 준비합니다.
sudo docker cp {CONTAINER_ID}:/var/jenkins_home /path/to/existing/jenkins_home_backup
젠킨스 Docker를 실행 시 마운트할 경로 지정
백업된 데이터와 Nginx 설정 파일, 스크립트를 젠킨스에서 사용할 수 있도록 마운트합니다.
sudo docker run -d -p 8081:8080 -p 50000:50000 \
-v /path/to/existing/jenkins_home_backup:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /etc/nginx/conf.d:/home/nginx \
-v /home/scripts:/home/scripts \
--name jenkins_with_nginx \
-u root jenkins/jenkins:lts
stage('Prepare docker-compose') {
steps {
script {
def serviceName = "${serviceName}${PORT}"
sh """
sed -i 's/backend/${serviceName}/g' docker-compose.yml
sed -i 's/8080:8080/${PORT}:8080/g' docker-compose.yml
sed -i 's/main/dev/g' docker-compose.dev.yml
"""
}
}
}
stage('Dockerize and Deploy') {
steps {
script {
sh """
/usr/local/bin/docker-compose -f docker-compose.yml up --build -d
"""
}
}
}
stage('Health Check') {
steps {
script {
sleep 30
int maxRetries = 3
int retryCount = 0
int response = 1
while (retryCount < maxRetries && response != 0) {
response = sh(script: "curl -f http://${domain}/health-${PORT}", returnStatus: true)
if (response != 0) {
echo "Health check failed, retrying in 10 seconds... (${retryCount + 1}/${maxRetries})"
sleep 10
}
retryCount++
}
if (response != 0) {
error "Health check failed after ${maxRetries} retries for port ${PORT}"
// Nginx 롤백 (이전 포트로 재설정)
def oldPort = (PORT == '8082') ? '8083' : '8082'
sh """
ssh -i /var/jenkins_home/.ssh/${serverKey}.pem ec2-user@ 'sudo sed -i \"s/proxy_pass http:\\/\\/127.0.0.1:${PORT}/proxy_pass http:\\/\\/127.0.0.1:${oldPort}/g\"
/etc/nginx/conf.d/${domain}.conf && sudo nginx -s reload' /home/scripts/toggle_port.sh
"""
error "Health check failed and Nginx rolled back to port ${oldPort}"
}
}
}
}
stage('Update Nginx and Reload') {
steps {
// 새로운 포트로 Nginx 설정을 업데이트하고 reload
script {
try {
sh """
ssh -i /var/jenkins_home/.ssh/${serverKey}.pem ec2-user@ 'sudo nginx -t && sudo sed -i \"s/proxy_pass http:\\/\\/127.0.0.1:[0-9]*/proxy_pass http:\\/\\/127.0.0.1:${PORT}/g\" /etc/nginx/conf.d/${domain}.conf && sudo nginx -s reload'
"""
} catch (err) {
echo "Error during Nginx reload: ${err}"
error "Nginx reload failed"
}
}
}
}
stage('Cleanup Old Container') {
steps {
script {
def oldPort = (PORT == '8082') ? '8083' : '8082'
def oldContainerName = "${ContainerName}_${oldPort}_1"
echo "Cleaning up old container: ${oldContainerName}"
sh "docker stop ${oldContainerName} && docker rm ${oldContainerName}"
}
}
}