롤링방식 무중단배포 구현하기
롤링방식 무중단배포 구현하기
깃허브에 commit, push 등 변경 사항이 생겨 webhooks이 감지하면 젠킨스가 다시 빌드를 시작하게 되어
프로세스를 종료했다가 시작하는 준비 시간이 필요한데 이때 서비스가 중단된다.
이와 같은 배포 방식을 중단배포라고 한다.
지금까지는 중단 배포 방식으로 했고 젠킨스가 다시 빌드를 해도 서비스가 중단하지 않도록
무중단 배포로 변경해 볼 것이다.
블루-그린 배포 방식
: 현재 서비스와 새로운 서비스를 각각의 환경에서 실행하고 배포가 완료되면 트래픽을 새로운 환경으로 전환하는 방식이다.
롤링 배포
: 여러 서버에 걸쳐 애플리케이션을 순차적으로 업데이트하는 방식이고 각 서버에서 새로운 버전을 배포하며 점진적으로 전체 서비스를 업데이트한다.
rolling 방식
회원가입 --롤링-- 글자를 회원가입 --롤링 update-- 으로 바꿔볼 것이다.
초반에 app01이 업데이트 될 때는 구버전인 app01-1에만 접속되고
app01이 업데이트된 후에는 app01-1에만 접속되다가
두 서버 모두 업데이트가 되었을 때 로드밸런싱되는 모습을 볼 수 있다.
롤링 코드
pipeline {
tools {
gradle "GRADLE"
}
agent any
stages {
stage('Clone') {
steps {
git branch: 'master', url: 'git 레포지토리 주소'
}
}
stage('Set Permissions') {
steps {
sh 'chmod +x ./gradlew'
}
}
stage('Test') {
steps {
script {
sh './gradlew test'
}
}
}
stage('Build') {
steps {
sh './gradlew clean build'
sh """
./gradlew clean build \
-Pdb.url=jdbc:mysql://ip:포트번호/데이터베이스명?useSSL=false \
-Pdb.username=유저명 \
-Pdb.password=비밀번호 \
-Pdb.driver=com.mysql.cj.jdbc.Driver
"""
script {
def appServerIps = ['app01 ip', 'app01-1 ip']
copyJarToRemote(appServerIps[0]) // Copy JAR to first server
}
}
}
stage('Deploy') {
steps {
script {
def appServerIps = ['app01 ip', 'app01-1 ip']
def nginxIp = 'nginx ip'
// Deploy on first server
deployOnServer(appServerIps[0], nginxIp)
// Copy JAR to the second server
copyJarToRemote(appServerIps[1])
// Deploy on second server
deployOnServer(appServerIps[1], nginxIp)
}
}
}
}
post {
success {
echo "Build and deployment succeeded."
}
failure {
echo "Build or deployment failed."
}
}
}
def copyJarToRemote(targetServerIp) {
def jarFile = '.jar 경로'
def deployPath = '/home/ubuntu'
sshagent(['credential key 이름']) {
def scpCmd = "scp -o StrictHostKeyChecking=no $jarFile ubuntu@$targetServerIp:$deployPath/"
def scpResult = sh(script: scpCmd, returnStatus: true)
if (scpResult != 0) {
error "Failed to copy jar file to $targetServerIp"
} else {
echo "Successfully copied jar file to $targetServerIp"
}
}
}
def deployOnServer(ip, nginxIp) {
def jarFile = '/home/ubuntu/파일명.jar'
def deployPath = '/home/ubuntu'
def runAppCommand = "nohup java -jar $jarFile > $deployPath/log.log 2>&1 &"
def checkLogCommand = "grep -q 'Started' $deployPath/log.log"
def backupJarFile = '파일명-backup.jar'
def maxAttempts = 10
def sleepInterval = 5
sshagent(['credential key 이름']) {
echo "Starting deployment on $ip"
// 1. Stop existing process and free port
stopExistingProcess(ip)
// 2. Create backup if not exists
def checkBackupCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$ip '[ ! -f $deployPath/$backupJarFile ]'"
def checkBackupResult = sh(script: checkBackupCmd, returnStatus: true)
if (checkBackupResult == 0) {
def backupCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$ip 'cp $deployPath/파일명.jar $deployPath/$backupJarFile'"
def backupResult = sh(script: backupCmd, returnStatus: true)
if (backupResult != 0) {
error "Failed to create backup file on $ip"
}
} else {
echo "Backup file already exists on $ip"
}
// 3. Deploy new version
def runCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$ip '$runAppCommand'"
def runResult = sh(script: runCmd, returnStatus: true)
if (runResult != 0) {
error "Failed to start application on $ip"
rollbackToPreviousVersion(ip, backupJarFile, deployPath, runAppCommand, checkLogCommand)
}
// 4. Check if the new version started successfully
def attempts = 0
def deploymentSuccess = false
while (attempts < maxAttempts) {
int result = sh(script: "ssh -o StrictHostKeyChecking=no ubuntu@$ip '$checkLogCommand'", returnStatus: true)
if (result == 0) {
echo "Deployment to $ip was successful."
deploymentSuccess = true
break
}
attempts++
sleep sleepInterval
}
if (!deploymentSuccess) {
error "Deployment to $ip failed. Rolling back to previous version."
rollbackToPreviousVersion(ip, backupJarFile, deployPath, runAppCommand, checkLogCommand)
}
// 5. Run CD test
def cdTestResult = checkApplicationStatus(ip)
echo "CD test result for $ip: '${cdTestResult}'"
if (cdTestResult != "200") {
error "CD test failed for $ip. Rolling back to previous version."
rollbackToPreviousVersion(ip, backupJarFile, deployPath, runAppCommand, checkLogCommand)
} else {
echo "CD test passed for $ip."
}
// 6. Update load balancer with new server IP
updateLoadBalancer(ip, nginxIp, 'add')
// Wait before moving to the next server to ensure stability
sleep 10
}
}
def checkApplicationStatus(ip) {
def checkStatusCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$ip 'curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8080'"
def statusCode = sh(script: checkStatusCmd, returnStdout: true).trim()
return statusCode
}
def stopExistingProcess(ip) {
sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$ip 'pgrep -f 파일명.jar && pkill -f 파일명.jar || echo \"No process found\"'", returnStatus: true
sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$ip 'sudo lsof -ti:8080 | xargs sudo kill -9'", returnStatus: true
}
def rollbackToPreviousVersion(targetServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand) {
def maxAttempts = 10
def sleepInterval = 3
sshagent(['credential key 이름']) {
sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'pgrep -f 파일명.jar && pkill -f 파일명.jar || echo \"No process found\"'", returnStatus: true
sh script: """
ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'if [ -f $deployPath/$backupJarFile ]; then
mv -f $deployPath/$backupJarFile $deployPath/파일명.jar
$runAppCommand
else
echo "Backup file not found on $targetServerIp"
exit 1
fi'
""", returnStatus: true
def attempts = 0
def rollbackSuccess = false
while (attempts < maxAttempts) {
int result = sh(script: "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '$checkLogCommand'", returnStatus: true)
if (result == 0) {
echo "Rollback to previous version on $targetServerIp was successful."
rollbackSuccess = true
break
}
attempts++
sleep sleepInterval
}
if (!rollbackSuccess) {
error "Rollback to previous version on $targetServerIp failed."
}
}
}
def updateLoadBalancer(serverIp, nginxIp, action) {
def nginxConfigPath = '/etc/nginx/nginx.conf'
def serverCommand = action == 'remove' ?
"""
ssh -o StrictHostKeyChecking=no ubuntu@$nginxIp 'bash -c "
# 서버 IP 제거
sudo sed -i \\"/server $serverIp:8080/d\\" $nginxConfigPath && \\
# Nginx 구성을 테스트
sudo nginx -t && \\
# Nginx를 재로드
sudo systemctl reload nginx
"'
""" :
"""
ssh -o StrictHostKeyChecking=no ubuntu@$nginxIp 'bash -c "
# 서버 IP 추가
sudo sed -i \\"/upstream cpu-bound-app {/a \\
\\ server $serverIp:8080 weight=100 max_fails=3 fail_timeout=3s;\\" $nginxConfigPath && \\
# Nginx 구성을 테스트
sudo nginx -t && \\
# Nginx를 재로드
sudo systemctl reload nginx
"'
"""
def result = sh(script: serverCommand, returnStatus: true)
if (result != 0) {
error "Failed to ${action == 'remove' ? 'remove' : 'add'} $serverIp ${action == 'remove' ? 'from' : 'to'} load balancer"
} else {
echo "Successfully ${action == 'remove' ? 'removed' : 'added'} $serverIp ${action == 'remove' ? 'from' : 'to'} load balancer"
}
}
함수 설명
copyJarToRemote
: JAR 파일을 원격 서버로 복사한다.
deployOnServer
: 서버에 새로운 버전을 배포하고 상태를 확인한다. 배포에 실패할 경우 롤백을 수행한다.
checkApplicationStatus
: 애플리케이션의 HTTP 상태 코드를 확인한다.
stopExistingProcess
: 실행 중인 애플리케이션 프로세스를 중지한다.
rollbackToPreviousVersion
: 새로운 버전 배포에 실패할 경우 이전 버전으로 롤백한다.
updateLoadBalancer
: Nginx 로드 밸런서의 설정을 업데이트하여 서버를 추가하거나 제거한다.
클론 단계
파이프라인의 첫 단계에서는 지정된 Git 저장소에서 `master` 브랜치를 클론한다.
권한 설정
클론한 코드의 `gradlew` 스크립트에 실행 권한을 부여하여 Gradle 빌드를 수행할 수 있도록 했다.
테스트 단계
Gradle을 사용하여 프로젝트의 테스트를 실행한다.
테스트가 성공적으로 완료되면 다음 단계로 진행된다.
빌드 단계
Gradle을 사용하여 프로젝트를 클린하고 빌드한다.
이 과정에서 `JAR` 파일이 생성된다.
배포
롤링 방식 무중단 배포를 위해 두 원격 서버가 순서대로 각 절차를 수행한다.
첫 번째 서버에 `JAR` 파일을 복사하고 서버에서 애플리케이션을 중지한 뒤에 백업을 생성한다.
그 후 새 버전을 실행해 애플리케이션이 제대로 시작되는지 확인한다.
배포가 성공적으로 완료되면 Nginx 로드 밸런서에 서버를 추가한다.
기존 프로세스 중지
배포를 시작하기 전에 서버에서 실행 중인 기존 애플리케이션 프로세스를 중지한다.
백업 생성
새 버전의 배포 전에 현재 버전의 백업을 생성한다.
백업이 성공적으로 생성되면 새 `JAR` 파일로 애플리케이션을 실행한다.
새 버전 검증
새 버전이 제대로 시작되었는지 확인하기 위해 로그 파일을 검사한다.
만약 새 버전이 시작되지 않았다면 백업 파일로 롤백해 이전 파일로 실행한다.
CD 테스트
애플리케이션이 정상적으로 동작하는지 확인하기 위해 HTTP 상태 코드를 검사한다.
상태 코드가 `200`이 아니면 롤백한다.
로드 밸런서 업데이트
배포가 성공하면 Nginx 로드 밸런서의 설정을 업데이트해 새 서버의 IP를 추가한다.
대기
두 번째 서버에 배포를 시작하기 전에 약간의 대기로 첫 번째 서버의 안정성을 확보한다.