ChooseMe의 무중단 배포(2)

김동헌·2024년 11월 10일
1

ChooseMe

목록 보기
3/3
post-thumbnail

Nginx를 이용하여 무중단 배포를 진행하였으며, 배포 전략은 Blue-GreenCI/CDJenkins를 사용했습니다.

두 개의 포트번호(8082, 8083)을 사용하며 현재 배포 중인 포트번호는 8082이 배포되었다는 가정으로 배포 시나리오를 작성했습니다.

배포 시나리오

  1. 빌드 트리거
    GitHub의 webhook으로 Jenkins가 자동으로 빌드를 시작합니다.

  2. 빌드 & 새로운 포트 결정
    Gradle 빌드 후, 새로운 포트를 할당하기 위해 toggle_port.sh 스크립트를 사용하여 새로운 포트8083을 저장합니다.

  3. 컨테이너 준비 및 배포
    새 서비스 이름과 포트를 새로운 포트(8083)에 맞게 설정하여, Nginx를 통해 새로운 포트로 트래픽을 전달받을 수 있도록 설정을 동적으로 업데이트합니다.

  4. Health Check
    새로운 서비스(8083)가 정상적으로 실행되는지 확인하기 위해 최대 3회의 헬스 체크를 진행합니다.
    만약, 3번의 헬스 체크가 실패할 경우Nginx 설정을 이전 포트(8082)로 되돌려 트래픽을 처리할 수 있도록 합니다.

  5. Nginx 업데이트 및 Reload
    헬스 체크에 성공하면 새로운 포트(8083)로 Nginx 설정을 업데이트하고 nginx -s reload 명령어로 구성을 새로 고칩니다.
    이 단계로 Nginx가 새로운 서비스로 트래픽을 전다랗게 됩니다.

  6. 이전 컨테이너 정리
    새로운 배포가 안정적으로 완료되면, 이전에 사용하던 포트(8082)의 Docker 컨테이너를 종료하고 삭제하여 자원을 해제합니다.


새로운 포트 결정

toggle.sh

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"

Pipeline

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

컨테이너 준비 및 배포

Pipeline

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
			""" 
		}
	}
} 

Health Check

Pipeline

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}"
			}
		}
	}
}
        

Nginx 업데이트 및 Reload

Pipeline

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"
			}
		}
	}
}

이전 컨테이너 정리

Pipeline

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}"
		}
	}
}
profile
백엔드 기록 공간😁

0개의 댓글