Information Security Study
블루/그린 방식으로 무중단 배포하기 본문
블루/그린 방식으로 무중단 배포하기
기본 로직은 이전 프로젝트에서 사용했던 블루/그린 방식의 무중단 스크립트를 참고했다.
https://gayeon-l.tistory.com/431
240719-23 vpc 변경(기본->새vpc), 롤백, 무중단 배포(롤링, 블루/그린) 이어서
오늘 해야 할 것db용 private 서브넷 하나 더 만들고 -> 했음로드밸런서 없애고 -> 롤백 성공하고 할 예정두 가용영역을 부하 분산하는 nginx 생성하기(public, nginx 끼리 연결) -> 롤백 성공하고 할 예정
gayeon-l.tistory.com
현 프로젝트에 맞게 수정한 블루/그린 방식 스크립트
pipeline {
tools {
gradle "GRADLE"
}
agent any
environment {
GREEN_ENV_IP = 'ip' // 그린 환경 서버 IP
BLUE_ENV_IP = 'ip' // 블루 환경 서버 IP
NGINX_IP = 'ip' // 로드 밸런서 IP
SSH_KEY_ID = 'credential key 이름' // Jenkins에서 설정한 SSH 키 ID
}
stages {
stage('Clone') {
steps {
git branch: 'main', url: 'git 레포지토리 url'
}
}
stage('Set Permissions') {
steps {
sh 'chmod +x ./gradlew'
}
}
stage('Test') {
steps {
script {
sh './gradlew test'
}
}
}
stage('Build') {
steps {
script {
sh './gradlew clean build'
def jarFilePattern = 'jar 경로'
def jarFile = sh(script: "ls $jarFilePattern | grep -v plain", returnStdout: true).trim()
echo "Built JAR file: $jarFile"
// Save the jarFile path to environment variable
env.JAR_FILE = jarFile
}
}
}
stage('Deploy to Green') {
steps {
script {
// 그린 환경에 JAR 파일을 복사하고 배포
sshagent([env.SSH_KEY_ID]) {
copyJarToRemote(env.GREEN_ENV_IP, env.JAR_FILE)
deployOnServer(env.GREEN_ENV_IP, env.NGINX_IP)
}
// 그린 환경 배포 확인
def greenStatus = checkApplicationStatus(env.GREEN_ENV_IP)
if (greenStatus == "200") {
echo "Green environment deployment succeeded."
} else {
error "Green environment deployment failed. Rolling back."
}
}
}
}
stage('Switch Traffic') {
steps {
script {
sshagent([env.SSH_KEY_ID]) {
// 블루에서 그린으로 트래픽 전환
updateLoadBalancer(env.GREEN_ENV_IP, env.NGINX_IP, 'add')
updateLoadBalancer(env.BLUE_ENV_IP, env.NGINX_IP, 'remove')
}
}
}
}
stage('Clean Up Blue') {
steps {
script {
sshagent([env.SSH_KEY_ID]) {
// 블루 환경에서 오래된 JAR 파일 삭제
def deleteOldJarsCmd = """
ssh -o StrictHostKeyChecking=no ubuntu@${env.BLUE_ENV_IP} '
# Delete all JAR files matching the pattern
rm -f /home/ubuntu/logging-sample-prj-*-no_db.jar
'
"""
def deleteResult = sh(script: deleteOldJarsCmd, returnStatus: true)
if (deleteResult != 0) {
error "Failed to delete old JAR files on Blue environment."
} else {
echo "Old JAR files deleted on Blue environment."
}
}
}
}
}
}
post {
success {
echo "Build and deployment succeeded."
}
failure {
echo "Build or deployment failed."
}
}
}
def copyJarToRemote(targetServerIp, jarFile) {
def deployPath = '/home/ubuntu'
sshagent([env.SSH_KEY_ID]) {
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 deployPath = '/home/ubuntu'
def jarFilePattern = "파일명.jar"
def runAppCommand = "nohup java -jar $deployPath/$jarFilePattern > $deployPath/log.log 2>&1 &"
def checkLogCommand = "grep -q 'Started' $deployPath/log.log"
def backupJarFile = '파일명-backup.jar'
def maxAttempts = 10
def sleepInterval = 5
sshagent([env.SSH_KEY_ID]) {
echo "Starting deployment on $ip"
// 기존 프로세스 종료
stopExistingProcess(ip)
// JAR 파일 복사
copyJarToRemote(ip, env.JAR_FILE)
// 백업 파일 생성
def backupCmd = """
ssh -o StrictHostKeyChecking=no ubuntu@$ip '
jarFile=\$(find $deployPath -name "$jarFilePattern" | head -n 1)
if [ -n "\$jarFile" ]; then
if [ ! -f $deployPath/$backupJarFile ]; then
cp "\$jarFile" "$deployPath/$backupJarFile"
echo "Backup created: $deployPath/$backupJarFile"
else
echo "Backup file already exists on $ip"
fi
else
echo "No JAR file found to back up"
fi
'
"""
def backupResult = sh(script: backupCmd, returnStatus: true)
if (backupResult != 0) {
error "Failed to create backup file on $ip"
}
// 새 버전 배포
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)
}
// 배포 성공 여부 확인
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)
}
// 애플리케이션 상태 확인
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."
}
}
}
def stopExistingProcess(ip) {
sshagent([env.SSH_KEY_ID]) {
sh script: """
ssh -o StrictHostKeyChecking=no ubuntu@$ip '
ps aux | grep 파일명.jar | grep -v grep | awk "{print \$2}" | xargs sudo kill -9 || true
sudo lsof -ti:8080 | xargs sudo kill -9 || true
'
""", returnStatus: true
}
}
def checkApplicationStatus(ip) {
def checkStatusCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$ip 'curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8080/products'"
def statusCode
sshagent([env.SSH_KEY_ID]) {
statusCode = sh(script: checkStatusCmd, returnStdout: true).trim()
}
return statusCode
}
def rollbackToPreviousVersion(targetServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand) {
def maxAttempts = 10
def sleepInterval = 3
sshagent([env.SSH_KEY_ID]) {
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
cp $deployPath/$backupJarFile $deployPath/파일명-rollback.jar
nohup java -jar $deployPath/logging-sample-prj-rollback.jar > $deployPath/log.log 2>&1 &
else
echo "No backup found for rollback"
fi
'
""", returnStatus: true
// Rollback 상태 확인
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(targetIp, nginxIp, action) {
def cmd = """
ssh -o StrictHostKeyChecking=no ubuntu@$nginxIp '
if [ "$action" == "add" ]; then
# Add the target IP to the load balancer
echo "$targetIp" >> /etc/nginx/conf.d/targets.conf
elif [ "$action" == "remove" ]; then
# Remove the target IP from the load balancer
sed -i "/$targetIp/d" /etc/nginx/conf.d/targets.conf
fi
sudo systemctl reload nginx
'
"""
def result = sh(script: cmd, returnStatus: true)
if (result != 0) {
error "Failed to $action IP $targetIp on load balancer."
} else {
echo "Successfully $action IP $targetIp on load balancer."
}
}
Switch Traffic 단계에서 nginx 설정 파일이 수정되지 않는 문제가 발생했다.
스크립트를 자세히 보니 nginx 설정 파일을 업데이트하는 메서드에 문제가 있어 수정했다.
기존 updateLoadBalancer 메서드의 문제점
1) 설정 파일의 위치가 잘못되었다.
현재 nginx 인스턴스에서는 /etc/nginx/nginx.conf에서 서버 ip를 관리하는데 위 스크립트에서는 /etc/nginx/conf.d/targets.conf 파일을 사용했다.
2) nginx 재로드 명령어를 사용하지 않았다.
nginx 설정 파일을 수정한 후 sudo systemctl reload nginx 명령어를 사용하지 않아 설정이 제대로 적용되지 않을 가능성이 있다.
수정된 updateLoadBalancer
1) 설정 파일의 경로를 수정했다.
nginx가 설정 파일로 사용하고 있는 /etc/nginx/nginx.conf로 수정했다.
또한 해당 파일 내부에 upstream 블록에 서버를 추가 또는 제거하는 방식으로 관리하도록 수정했다.
sed 명령어로 ip를 추가하고 제거할 때는 해당 ip를 찾는 방식이다.
2) nginx 구성을 테스트하고 재로드하는 로직을 추가했다.
설정 파일 수정 후 nginx -t 명령어로 구성에 문제가 있는지 테스트하도록 했다.
테스트 결과에 문제가 없다면 sudo systemctl reload nginx 명령어룰 수행해 nginx를 재로드한다.
수정한 updateLoadBalancer 메서드
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 nginx {/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"
}
}
이제 Switch Traffic 단계에서 action이 add일 때 그린 서버의 ip가 추가되고
곧바로 블루 서버의 ip가 제거된다.
'네트워크 캠퍼스 > 3차 프로젝트' 카테고리의 다른 글
프로메테우스 페더레이트 설정하기 + 알람 등록하기 (0) | 2024.08.13 |
---|---|
nginx 설정파일 내의 아이피가 중복되는 문제와 무중단 배포가 안 되는 문제 해결하기 (0) | 2024.08.13 |
Infile Load 방식으로 병합된 로그 MySQL에 저장하기 (0) | 2024.08.12 |
반복문으로 파일 내에 있는 모든 로그를 DB에 저장하기(시도중) (0) | 2024.08.12 |
젠킨스 파이프라인으로 배포 시 로그 파일이 다른 위치에 생기는 문제 해결 (0) | 2024.08.09 |