Information Security Study

블루/그린 방식으로 무중단 배포하기 본문

네트워크 캠퍼스/3차 프로젝트

블루/그린 방식으로 무중단 배포하기

gayeon_ 2024. 8. 13. 11:01

블루/그린 방식으로 무중단 배포하기

 

기본 로직은 이전 프로젝트에서 사용했던 블루/그린 방식의 무중단 스크립트를 참고했다.

 

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가 제거된다.