Information Security Study

240719-23 vpc 변경(기본->새vpc), 롤백, 무중단 배포(롤링, 블루/그린) 이어서 본문

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

240719-23 vpc 변경(기본->새vpc), 롤백, 무중단 배포(롤링, 블루/그린) 이어서

gayeon_ 2024. 7. 23. 13:33

오늘 해야 할 것

  • db용 private 서브넷 하나 더 만들고 -> 했음
  • 로드밸런서 없애고 -> 롤백 성공하고 할 예정
  • 두 가용영역을 부하 분산하는 nginx 생성하기(public, nginx 끼리 연결) -> 롤백 성공하고 할 예정
  • 이 nginx랑 인터넷 게이트웨이랑 연결하기 -> 롤백 성공하고 할 예정

 

 

궁금한 점

2) 그럼 동일 가용영역 내의 프라이빗 서브넷은 어떻게 통신하는가 -> 라우팅테이블만 하면 됨 0.0.0.0으로 하면 nat가 해줌

3) 가용영역이 a, b밖에 없는데 ingress nginx는 어떻게 생성하는가..(인그레스용 서브넷, 가용영역은 a)

 

 

 

인스턴스들을 기본 vpc에서 새 vpc로 변경하기(기본->새 vpc)

그동안 기본 vpc 사용했었는데 이제는 새 vpc 생성해서 인스턴스들을 옮길 것이다.

 

vpc 생성

 

 

 

인터넷 게이트웨이를 생성했다.

 

 

 

가용영역 a의 퍼블릭 서브넷 1개, 프라이빗 서브넷 2개 생성했다.

 

 

 

우선 라우팅 테이블을 생성만 했다.

public용 라우팅 테이블에는 인터넷 게이트웨이를 연결했고

private에는 nat 게이트웨이를 붙여야 하는데 이는 이후에 설정할 것이다.

 

 

 

생성한 서브넷에 라우팅 테이블을 연결한다.

private 서브넷에는 private 라우팅 테이블을

public 서브넷에는 public 라우팅 테이블을 연결했다.

 

 

 

서브넷에 라우팅 테이블을 연결했기 때문에 이제 nat 게이트웨이를 생성할 수 있다.

nat는 퍼블릭에서 프라이빗으로 접근하기 위한 게이트웨이임으로 public 서브넷에 생성한다.

 

 

다시 라우팅 테이블로 돌아와

private 라우팅 테이블에 nat gateway를 붙여주면 목적지를 잘 찾아갈 것이다!

 

 

 

그리고 가용영역에 있는 인스턴스들을 각각 클릭해

네트워크 탭에 들어가서

기존에 연결되어 있던 vpc가 아닌

새로 만든 vpc와 subnet으로 변경해 준다.

 

 

 

vpc와 subnet확인 후 보안그룹도 맞게 설정한다.

이렇게 가용영역 a에 있는 모든 인스턴스의 vpc, subnet을 변경했다.

 

 

이제 ip가 변경되었기 때문에 nginx 설정 파일을 변경해야 한다.

 

 

 

위 설정파일로 들어가 연결할 인스턴스의 내부 아이피를 변경하고

적용했다.

 

 

 

push를 감지할 웹훅의 url도 새로 부여받은 젠킨스의 플로팅 ip로 변경했다.

 

 

이제 젠킨스 쪽에 ssh 정보를 다시 입력해야 한다.

 

시스템 탭으로 들어가서

 

 

 

app01, app01-1의 hostname을 변경했다.

 

 

 

파이프라인 스크립트에서 사용하는 app01, app01-1의 private ip 주소도 변경했다.

 

 

 

그리고 애플리케이션의 db properties에서도 변경된 db url을 수정했다.

 

 

 

배포 및 접속 성공!

vpc를 완벽하게 변경했다.

 

 

+) 젠킨스의 플로팅 ip 변경 후 브라우저 속도가 굉장히 느려졌었는데 

 

시스템 창에서 jenkins URL을 현재 플로팅 ip로 수정하면 다시 빨라진다.

느려졌던 이유는 다른 주소를 거쳐서 현재 주소로 오기 때문에 느렸던 것이다.

 


롤백 이어서

 

/users/signup

페이지를 주석처리하고 commit change를 하면 젠킨스에서 자동으로 빌드된다.

 

 

 

백업파일이 덮어씌워졌다고 출력되어서 app01에 접속해 확인하니

 

 

 

잘 덮어씌워졌다.

 

 

 

app01은 롤백에 성공했는데

 

 

 

이상하게 app01-1이 바로 스킵된다.

 

 

 

우선 롤백이 된 app01이 문제가 없는지 확인하기 위해

파워셸에서 java -jar 명령어로 덮어씌워진 .jar 파일을 실행했다.

 

 

 

이미 젠킨스에서 app01이 배포되어 있어 8080 포트를 쓰고 있기 때문에

jar 파일을 실행하기 전 java 관련 프로세스가 돌고 있나 확인하고 kill로 종료시킨 뒤에 해야 한다.

 

 

 

/users/signup

을 주석처리했는데도 백업파일로 잘 실행되었으니 app01은 롤백에 성공한 것이 분명하다.

하지만 이상하게 app01-1은 스킵된다..

 

pipeline {
    tools {
        gradle "GRADLE" // Jenkins에서 설정한 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://서버 아이피:포트/데이터베이스명?useSSL=false -Pdb.username=유저명 -Pdb.password=비밀번호 -Pdb.driver=com.mysql.cj.jdbc.Driver'
                // 빌드 후 JAR 파일을 원격 서버로 복사
                script {
                    copyJarToRemote('app01 ip')
                    copyJarToRemote('app01-1 ip')
                }
            }
        }

        stage('Deploy1') {
            steps {
                script {
                    catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
                        deployAndTest('app01 ip')
                    }
                }
            }
        }
        
        stage('Deploy2') {
            steps {
                script {
                    catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
                        deployAndTest('app01-1 ip')
                    }
                }
            }
        }
    }
    post {
        success {
            echo "This will run when the run finishes successfully"
        }
        failure {
            echo "This will run if failed"
        }
    }
}

def copyJarToRemote(targetServerIp) {
    def jarFile = '.jar 경로'
    def deployPath = '/home/ubuntu'

    sshagent(['credential key 이름']) {
        // 새로운 JAR 파일 전송
        def scpCmd = "scp -o StrictHostKeyChecking=no $jarFile ubuntu@$targetServerIp:$deployPath/"
        def scpResult = sh(script: scpCmd, returnStatus: true)
        if (scpResult != 0) {
            echo "Failed to copy jar file to $targetServerIp"
            error "Failed to copy jar file"
        } else {
            echo "Successfully copied jar file to $targetServerIp"
        }
    }
}

def deployAndTest(targetServerIp) {
    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 maxAttempts = 40
    def sleepInterval = 3 // 3 seconds
    def backupJarFile = '파일명-backup.jar'

    sshagent(['credentail key 이름']) {
        sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'pgrep -f 파일명.jar && pkill -f 파일명.jar || echo \"No process found\"'", returnStatus: true

        // 백업 파일 생성
        def checkBackupCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '[ ! -f $deployPath/$backupJarFile ]'"
        def checkBackupResult = sh(script: checkBackupCmd, returnStatus: true)
        if (checkBackupResult == 0) {
            def backupCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'cp $deployPath/파일명.jar $deployPath/$backupJarFile'"
            def backupResult = sh(script: backupCmd, returnStatus: true)
            if (backupResult != 0) {
                echo "Failed to create backup file on $targetServerIp"
                error "Failed to create backup file"
            }
        } else {
            echo "Backup file already exists on $targetServerIp"
        }

        // 애플리케이션 실행
        def runCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '$runAppCommand'"
        def runResult = sh(script: runCmd, returnStatus: true)
        if (runResult != 0) {
            echo "Failed to start application on $targetServerIp"
            error "Failed to start application"
        }

        def attempts = 0
        def deploymentSuccess = false
        while (attempts < maxAttempts) {
            int result = sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '$checkLogCommand'", returnStatus: true
            if (result == 0) {
                echo "Deployment to $targetServerIp was successful."
                deploymentSuccess = true
                break
            }
            attempts++
            sleep sleepInterval
        }

        if (!deploymentSuccess) {
            echo "Deployment to $targetServerIp failed. Rolling back to previous version."
            rollbackToPreviousVersion(targetServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand)
            error "Deployment to $targetServerIp failed."
        }

        // CD 테스트
        def cdTestResult = sh script: "curl -s -o /dev/null -w '%{http_code}' http://$targetServerIp:8080", returnStdout: true
        echo "cdTestResult: '${cdTestResult.trim()}'"
        if (cdTestResult.trim() != "200") {
            echo "CD test failed for $targetServerIp. Rolling back to previous version."
            rollbackToPreviousVersion(targetServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand)
        } else {
            echo "CD test passed for $targetServerIp."
        }
    }
}

def rollbackToPreviousVersion(targetServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand) {
    def maxAttempts = 40
    def sleepInterval = 3 // 3 seconds

    sshagent(['JENKINS-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 && echo "Backup file moved successfully."
            else
                echo "Backup file does not exist."
            fi'
        """, returnStatus: true

        // 원본 파일에 실행 권한 설정
        sh script: """
            ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'chmod +x $deployPath/파일명.jar && echo "Execution permission set on original file."'
        """, returnStatus: true

        // 애플리케이션 재실행
        sh "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '$runAppCommand'"

        // 롤백 완료 여부 확인
        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."
        }
    }
}

 

찾아보니 error는 파이프라인 이후의 단계가 모두 스킵될 수 있다고 해서

rollbackToPreviousVersion 함수에 있는 error를 catchError로 변경했다.

 

 

 

주석처리한 /users/signup 페이지에 잘 접속도 되고

각 app의 log.log를 봐도 정상 실행 중이다.

 

 

 

이 명령어로 nginx가 app01과 app01-1 모두 보여주고 있는지 확인할 수 있다.

 

 

 

backend_server를 보면 app01, app01-1의 주소가 모두 나오고 있다.

 

 

 

콘솔 output에도 롤백이 성공적으로 되었다고 하지만

 

 

 

배포에 실패했다고 뜬다..

분명 배포가 되었기 때문에 회원가입 페이지도 접속되는 걸 텐데.. 뭐지

 

 

 

상태도 unstable로 뜬다.

 

 

스크립트를 다시 자세히 살펴보니

괜히 배포 단계에 try catch문을 써서 그런 거였다..

 

 

app01 백업 파일 덮어씌우기

 

 

 

app 01 롤백 성공

 

 

 

app01 CD 테스트 통과

 

 

 

app01-1 백업 파일 덮어씌우기

 

 

 

app01-1 롤백 성공

 

 

 

app01-1 CD 테스트 통과

 

 

 

롤백 성공~~!!

 

 

롤백 성공한 코드

pipeline {
    tools {
        gradle "GRADLE" // Jenkins에서 설정한 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://주소:포트번호/데이터베이스명?useSSL=false -Pdb.username=유저명 -Pdb.password=비밀번호 -Pdb.driver=com.mysql.cj.jdbc.Driver'
                script {
                    copyJarToRemote('app01 ip')
                    copyJarToRemote('app01-1 ip')
                }
            }
        }

        
        stage('Deploy1') {
            steps {
                script {
                    deployAndTest('app01 ip')
                }
            }
        }
        
        stage('Deploy2') {
            steps {
                script {
                    deployAndTest('app01-1 ip')
                }
            }
        }
    }
    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) {
            echo "Failed to copy jar file to $targetServerIp"
            error "Failed to copy jar file"
        } else {
            echo "Successfully copied jar file to $targetServerIp"
        }
    }
}

def deployAndTest(targetServerIp) {
    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 maxAttempts = 10
    def sleepInterval = 3 // 3 seconds
    def backupJarFile = '파일명.jar'

    sshagent(['credentail key 이름']) {
        sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'pgrep -f 파일명.jar && pkill -f 파일명.jar || echo \"No process found\"'", returnStatus: true

        def checkBackupCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '[ ! -f $deployPath/$backupJarFile ]'"
        def checkBackupResult = sh(script: checkBackupCmd, returnStatus: true)
        if (checkBackupResult == 0) {
            def backupCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'cp $deployPath/파일명.jar $deployPath/$backupJarFile'"
            def backupResult = sh(script: backupCmd, returnStatus: true)
            if (backupResult != 0) {
                echo "Failed to create backup file on $targetServerIp"
                error "Failed to create backup file"
            }
        } else {
            echo "Backup file already exists on $targetServerIp"
        }

        def runCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '$runAppCommand'"
        def runResult = sh(script: runCmd, returnStatus: true)
        if (runResult != 0) {
            echo "Failed to start application on $targetServerIp"
            error "Failed to start application"
        }

        def attempts = 0
        def deploymentSuccess = false
        while (attempts < maxAttempts) {
            int result = sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '$checkLogCommand'", returnStatus: true
            if (result == 0) {
                echo "Deployment to $targetServerIp was successful."
                deploymentSuccess = true
                break
            }
            attempts++
            sleep sleepInterval
        }

        if (!deploymentSuccess) {
            echo "Deployment to $targetServerIp failed. Rolling back to previous version."
            rollbackToPreviousVersion(targetServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand)
            error "Deployment to $targetServerIp failed."
        }

        def cdTestResult = sh script: "curl -s -o /dev/null -w '%{http_code}' http://$targetServerIp:8080", returnStdout: true
        echo "CD test result: '${cdTestResult.trim()}'"
        if (cdTestResult.trim() != "200") {
            echo "CD test failed for $targetServerIp. Rolling back to previous version."
            rollbackToPreviousVersion(targetServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand)
        } else {
            echo "CD test passed for $targetServerIp."
        }
    }
}

def rollbackToPreviousVersion(targetServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand) {
    def maxAttempts = 10
    def sleepInterval = 3 // 3 seconds

    sshagent(['JENKINS-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 && echo "Backup file moved successfully."
            else
                echo "Backup file does not exist."
            fi'
        """, returnStatus: true

        sh script: """
            ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'chmod +x $deployPath/파일명.jar && echo "Execution permission set on original file."'
        """, returnStatus: true

        sh "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '$runAppCommand'"

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

 

위에서 app01과 app01-1 둘 다 롤백에 성공한 코드와 다른 코드이다.

이 코드는 app01이 롤백을 성공했다면 app01-1은 새 배포를 스킵하는 코드이다.

왜 이렇게 수정했냐면

app01이 새로 배포할 애플리케이션에 오류가 있어서 롤백을 하게 된 건데 굳이 잘 돌아가던 app01-1까지 롤백시킬 이유가 없기 때문이다.

오류가 있는 새 버전이라면 두번째로 다시 빌드될 app01-1에서는 건너뛰도록 바꿨다.

 


무중단 배포로 변경하기

 

깃허브에 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를 추가한다.

대기

두 번째 서버에 배포를 시작하기 전에 약간의 대기로 첫 번째 서버의 안정성을 확보한다.



 

blue/green 방식

 

블루: 구버전

그린: 신버전

 

장점

  • 구버전의 인스턴스가 그대로 남아있어 롤백이 비교적 쉽다.

 

원래의 signup 페이지에 있는 회원가입 글자를

-무중단배포(블루/그린)-를 추가해서 무중단 배포가 되고 있는지 확인할 것이다.

 

 

 

서비스가 끊기지 않도록 새 버전으로 배포되는 중에는 Nginx 로드 밸런서 설정에서

업데이트 중인 서버 아이피를 삭제했다가 업데이트가 완료되면 다시 넣도록 할 것이다.

젠킨스에서 nginx 설정 파일을 수정하기 위해 젠킨스의 공개키를 nginx에 저장했다.

 

 

 

저장한 뒤

SSH 인증을 위해 공개 키 기반 인증을 사용하려면

위 파일의 맨 아래에 PubkeyAuthentication yes를 입력하고

 

 

 

서비스를 재시작한다.

 

 

 

app01-1에만 새로운 버전이 배포되었다.

 

 

 

배포되는 과정에서 설정파일을 cat으로 출력해보면

 

 

 

업데이트 전에는 지웠다가

 

 

 

업데이트가 되면 다시 추가되는 모습을 볼 수 있었다.

 

 

 

 

blue/green 코드

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 {
                    copyJarToRemote('app01 ip')
                    copyJarToRemote('app01-1 ip')
                }
            }
        }

        stage('Deploy') {
            steps {
                script {
                    def appServerIp = 'app01 ip'
                    def standbyServerIp = 'app01-1 ip'
                    def nginxIp = 'nginx ip' // Nginx 인스턴스 IP 주소 설정
                    deployAndTest(appServerIp, standbyServerIp, 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) {
            echo "Failed to copy jar file to $targetServerIp"
            error "Failed to copy jar file"
        } else {
            echo "Successfully copied jar file to $targetServerIp"
        }
    }
}

def deployAndTest(appServerIp, standbyServerIp, 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 maxAttempts = 10
    def sleepInterval = 3
    def backupJarFile = '파일명-backup.jar'

    sshagent(['credential key 이름']) {
        // Stopping existing process on standby server
        sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$standbyServerIp 'pgrep -f 파일명.jar && pkill -f 파일명.jar || echo \"No process found\"'", returnStatus: true

        // Creating backup if not exists
        def checkBackupCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$standbyServerIp '[ ! -f $deployPath/$backupJarFile ]'"
        def checkBackupResult = sh(script: checkBackupCmd, returnStatus: true)
        if (checkBackupResult == 0) {
            def backupCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$standbyServerIp 'cp $deployPath/파일명.jar $deployPath/$backupJarFile'"
            def backupResult = sh(script: backupCmd, returnStatus: true)
            if (backupResult != 0) {
                echo "Failed to create backup file on $standbyServerIp"
                error "Failed to create backup file"
            }
        } else {
            echo "Backup file already exists on $standbyServerIp"
        }

        // Remove standby server from upstream
        def nginxConfigPath = '/etc/nginx/nginx.conf'
        def removeStandbyServerCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$nginxIp 'sudo sed -i \"/server $standbyServerIp:8080/d\" $nginxConfigPath && sudo systemctl reload nginx'"
        def removeStandbyResult = sh(script: removeStandbyServerCmd, returnStatus: true)
        if (removeStandbyResult != 0) {
            echo "Failed to remove $standbyServerIp from load balancer"
            error "Failed to remove $standbyServerIp from load balancer"
        } else {
            echo "Successfully removed $standbyServerIp from load balancer"
        }

        // Deploying new version on standby server
        def runCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$standbyServerIp '$runAppCommand'"
        def runResult = sh(script: runCmd, returnStatus: true)
        if (runResult != 0) {
            echo "Failed to start application on $standbyServerIp"
            error "Failed to start application"
        }

        // Checking if the new version started successfully
        def attempts = 0
        def deploymentSuccess = false
        while (attempts < maxAttempts) {
            int result = sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$standbyServerIp '$checkLogCommand'", returnStatus: true
            if (result == 0) {
                echo "Deployment to $standbyServerIp was successful."
                deploymentSuccess = true
                break
            }
            attempts++
            sleep sleepInterval
        }

        if (!deploymentSuccess) {
            echo "Deployment to $standbyServerIp failed. Rolling back to previous version."
            rollbackToPreviousVersion(standbyServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand)
            error "Deployment to $standbyServerIp failed."
        }

        // Re-adding standby server to upstream
        def addStandbyServerCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$nginxIp 'sudo sed -i \"/upstream cpu-bound-app/a\\    server $standbyServerIp:8080 weight=100 max_fails=3 fail_timeout=3s;\" $nginxConfigPath && sudo systemctl reload nginx'"
        def addStandbyResult = sh(script: addStandbyServerCmd, returnStatus: true)
        if (addStandbyResult != 0) {
            echo "Failed to re-add $standbyServerIp to load balancer"
            error "Failed to re-add $standbyServerIp to load balancer"
        } else {
            echo "Successfully re-added $standbyServerIp to load balancer"
        }

        // Running CD test
        def cdTestResult = sh script: "curl -s -o /dev/null -w '%{http_code}' http://$standbyServerIp:8080", returnStdout: true
        echo "CD test result: '${cdTestResult.trim()}'"
        if (cdTestResult.trim() != "200") {
            echo "CD test failed for $standbyServerIp. Rolling back to previous version."
            rollbackToPreviousVersion(standbyServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand)
        } else {
            echo "CD test passed for $standbyServerIp."
        }
    }
}

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) {
            echo "Rollback to previous version on $targetServerIp failed."
            error "Rollback failed."
        }
    }
}

 

 

 

+) 문제점

현재 app01-1과 app01만 있기 때문에

구버전, 신버전이 모두 로드밸런싱되어

새로고침할 때마다 구버전, 신버전이 같이 나오게 된다.

 

 

코드 수정 및 구현 완료

 

그린 = app01(72)

블루 = app01-1(26)

 

 

회원가입 --블루/그린 -- 에서

update 문자열을 추가할 것이다.

 

블루/그린 로직은 다음과 같다.

1) 그린서버 업데이트 전 nginx 설정파일에서 그린 서버의 아이피 제거

2) 그린서버 업데이트 동안에는 블루 서버만 응답 가능

3) 그린서버 업데이트 완료 후 nginx 설정파일에 다시 서버 아이피 추가 및 블루 서버 아이피 제거

 

이렇게 하면 업데이트 중에도 서비스는 중단되지 않는다.

 

 

 

그린 서버 업데이트 전에는 두 서버 모두 응답하다가

 

 

nginx 설정파일에 두 서버 모두 존재

 

 

 

그린 서버 업데이트 들어가기 전에는 블루 서버만 남겨놓는다.

 

 

 

그린 서버 업데이트 후에는 블루서버 제거, 그린서버만 남겨놓는다.

 

 

 

 

최종 blue/green 코드

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 {
                script {
                    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
                    """
                    def greenServerIp = 'app01 ip'
                    copyJarToRemote(greenServerIp) // Copy JAR to Green server
                }
            }
        }

        stage('Deploy') {
            steps {
                script {
                    def greenServerIp = 'app01 ip'
                    def blueServerIp = 'app01-1 ip'
                    def nginxIp = 'nginx ip'

                    sshagent(['credential key 이름']) {
                        // 1. Remove Green Server from Load Balancer
                        updateLoadBalancer(greenServerIp, nginxIp, 'remove')

                        // 2. Deploy to Green Server
                        deployOnServer(greenServerIp, nginxIp)

                        // 3. Access Blue Server (no change, just ensure it's running)
                        def blueStatus = checkApplicationStatus(blueServerIp)
                        echo "Blue Server status: ${blueStatus}"

                        // 4. Update Nginx configuration for Green Server
                        updateLoadBalancer(greenServerIp, nginxIp, 'add')

                        // 5. Remove Blue Server from Load Balancer after Green is confirmed
                        if (blueStatus == "200") {
                            updateLoadBalancer(blueServerIp, nginxIp, 'remove')
                        } else {
                            echo "Blue server is not responding as expected. Not removing it from the load balancer."
                        }
                    }
                }
            }
        }
    }
    post {
        success {
            echo "Build and deployment succeeded."
        }
        failure {
            echo "Build or deployment failed."
        }
    }
}

def copyJarToRemote(targetServerIp) {
    def jarFile = '.jar 파일 경로'
    def deployPath = '/home/ubuntu'

    sshagent(['JENKINS-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."
        }
    }
}

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) {
    sshagent(['credential key 이름']) {
        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
        "'
        """

    sshagent(['credential key 이름']) {
        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"
        }
    }
}