무중단배포

공병주(Chris)·2022년 10월 18일
1
post-thumbnail

무중단배포

우아한테크코스 레벨 4 마지막 데모 요구 사항으로 무중단 배포가 주어졌습니다. 팀원들과 무중단 배포를 한 내용을 공유해보겠습니다.

무중단배포란?

서비스의 새로운 버전이 배포되는 순간 동안 사용자가 서비스를 사용할 수 없는 시간(다운 타임)이 발생하는데, 다운 타임 없이 사용자가에게 새로운 버전을 배포하는 개념입니다.

무중단 배포 전략

1. Rolling

이미지 출처 : https://loosie.tistory.com/m/781

로드밸런싱 환경의 인스턴스들을 하나씩 배포시키는 방식입니다.

장점

  • 뒤에 설명할 Canary, Blue-Green 방식에 비해 적용하기가 비교적 수월합니다.
  • 추가적인 인스턴스 필요 없이, 무중단 배포 환경을 구축할 수 있습니다.

단점

  • 여러 사용자가 동일한 시간에 다른 버전의 서비스를 사용할 수 있습니다.
    A, B, C 인스턴스 중, A에만 새 버전이 배포되어있는 시점에는 A와 B, C는 다른 서비스를 제공합니다.
    만약에, API가 달라지는 등의 경우에는 버그를 야기할 수 있기 때문에, 신경 써서 배포를 해야할 것으로 생각됩니다.
  • 하나의 인스턴스가 배포중인 시점에 다른 인스턴스들이 추가적인 트래픽을 감당해야 합니다.
    A가 배포중일 때, A, B, C가 함께 담당하던 트래픽은 B, C에게 몰리게 됩니다.

2. Canary

이미지 출처 : https://loosie.tistory.com/m/781

Canary 배포 전략은 과거에 Canary라는 새로 위험을 미리 감지하는데에서 유래한 개념입니다.

새로운 버전을 소수의 인스턴스에만 배포시켜두고 모니터링을 하고 문제를 발견하는 방식입니다.

장점

  • 사용자에게 발생할 수 있는 문제를 미리 확인하고 대처할 수 있습니다.

단점

  • 미리 배포가 된 버전을 사용하는 사용자와 일반 사용자들은 다른 버전의 서비스를 사용하게 됩니다.

3. Blue-Green

이미지 출처 : https://loosie.tistory.com/m/781

새로운 버전이 배포된 새로운 환경을 현재 버전과 독립적으로 구축해두고, 한번에 새로운 버전이 배포된 환경으로 트래픽을 전환하는 방식입니다.

장점

  • 새로운 버전에서 문제가 발생하였을 경우, 빠르게 트래픽을 이전 버전 환경으로 보내면서 롤백시킬 수 있습니다.
  • 모든 사용자들이 동일한 버전의 서비스를 사용할 수 있습니다.
  • 완전히 새로운 환경에 신버전을 배포하기 때문에, 배포 과정에서 기존 서버에 부담이 가지 않습니다.

단점

  • 완전히 새로운 환경에 신버전을 배포하기 때문에, 자원 소모가 큽니다.

속닥속닥팀이 채택한 방식

버전 호환의 문제로 Canary와 Rolling은 out!

Canary와 Rolling은 동시간에 두 가지 버전이 제공되는 시점이 있습니다. 이를 세부적으로 컨트롤해주지 않으면 사용자에게 오류를 안겨줄 수 있다고 판단했기에, 두 방식은 채택하지 않았습니다. 물론 세부적으로 컨트롤하려면 할 수 있으나, Blue-Green 방식을 채택하면 해결된 문제라고 판단했습니다.

Blue-Green은 똑같은 환경의 자원이 필요한데?

현재 저희 팀은 아래와 같이 두 개의 서버로 로드밸런싱을 해둔 상태입니다. 로드밸런싱을 할 정도의 트래픽이 발생하지 않지만, 학습의 목적으로 구축해두었습니다.

따라서, Blue-Green 방식을 적용하려면 2개의 추가적인 서버가 필요한데요.

저희 팀에선 이를 Port로 해결했습니다.

아래와 같이 동일한 ec2의 2개의 port에 SpringBoot를 2개 실행하는 것입니다.

사실 위의 방법은, 실무에선 쓰일 수 없을 것이라고 판단됩니다. 서버가 트래픽을 처리하고 있는 중에 다른 port에 SpringBoot를 하나 더 실행한다면, 두 개의 SpringBoot는 하나의 ec2 리소스를 공유하기 때문에 서버가 다운될 위험이 크다고 생각합니다.

하지만, 저희 서비스의 경우에는 동시 접속자 수가 많지 않기 때문에 위와 같은 port를 통한 Blue-Green 방식을 채택했습니다.

무중단배포 Script

여러 가지 방식이 있겠지만, 저희는 레퍼런스들을 따라가는 것보다 저희의 힘으로 script를 작성하려고 했습니다. 아래의 Jenkins에서 진행되는 CD의 script 입니다.

pipeline {
    agent any

    tools {
        gradle 'gradle'
    }

    stages {
        
        # BUILD까지 완료

        stage('PROD-DEPLOY') {
            when {
                expression { env.GIT_BRANCH == 'main' }
            }
            steps {
                script {
                    withCredentials([sshUserPrivateKey(credentialsId: "back-key", keyFileVariable: 'my_private_key_file')]) {
                        sh "echo '########### BACK-END DEV-DEPLOY START ###########'"

                        sh '''#!/bin/bash
                            #1 - health check
                            RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://${WAS_1_PRIVATE_IP}:8080/actuator/health)
                            cd backend/sokdak/build/libs
                            #2 - 8080가 살아있다면 
                            if [ $RESPONSE_CODE -ne 200 ]
                            then
                                echo '############## 8081 LOAD START ##############'
                                #3 - 8081에 배포
                                scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ubuntu@${WAS_2_PRIVATE_IP}:/home/ubuntu/sokdak
                                scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ububtu@${WAS_1_PRIVATE_IP}:/home/ubuntu/sokdak
                                ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_2_PRIVATE_IP} 'cd sokdak && ./deploy-8080.sh\'
                                ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_1_PRIVATE_IP} 'cd sokdak && ./deploy-8080.sh\'
                                
                                #4 - 8081 포트 kill
                                for var in {1..100}
                                do
                                    sleep 1
                                    HEALTH_CHECK_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://${WAS_2_PRIVATE_IP}:8080/actuator/health)
                                    if [ $HEALTH_CHECK_CODE -eq 200 ]
                                    then
                                        ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${NGINX_PRIVATE_IP} 'cd /etc/nginx/sites-enabled && sudo rm * && cd ~/zero-down-confs && sudo cp sokdak-8080.conf /etc/nginx/sites-enabled/ && sudo nginx -s reload\'
                                        ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_2_PRIVATE_IP} 'sudo kill -15 $(lsof -ti tcp:8081)'
                                        ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_1_PRIVATE_IP} 'sudo kill -15 $(lsof -ti tcp:8081)'
                                        echo 'SWITCHING FROM 8081 TO 8080 SUCCESS'
                                        break
                                    fi
                                done
                                
                                echo '############## 8080 LOAD COMPLETE ##############'
                                
                            else
                                echo '############## 8081 LOAD START ##############'
                                scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ubuntu@${WAS_2_PRIVATE_IP}/home/ubuntu/sokdak
                                scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ubuntu@${WAS_1_PRIVATE_IP}:/home/ubuntu/sokdak
                                ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_2_PRIVATE_IP} 'cd sokdak && ./deploy-8081.sh\'
                                ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_1_PRIVATE_IP} 'cd sokdak && ./deploy-8081.sh\'
                                
                                for var in {1..100}
                                do
                                    sleep 1
                                    HEALTH_CHECK_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://${WAS_2_PRIVATE_IP}:8081/actuator/health)
                                    if [ $HEALTH_CHECK_CODE -eq 200 ]
                                    then
                                        ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${NGINX_PRIVATE_IP} 'cd /etc/nginx/sites-enabled && sudo rm * && cd ~/zero-down-confs && sudo cp sokdak-8081.conf /etc/nginx/sites-enabled/ && sudo nginx -s reload\'
                                        ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_2_PRIVATE_IP} 'sudo kill -15 $(lsof -ti tcp:8080)'
                                        ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_1_PRIVATE_IP} 'sudo kill -15 $(lsof -ti tcp:8080)'
                                        echo 'SWITCHING FROM 8080 TO 8081 SUCCESS'
                                        break
                                    fi
                                done
                                
                                echo '############## 8081 LOAD COMPLETE ##############'
                            fi
                        '''
                        sh "echo '########### BACK-END DEV-DEPLOY SUCCESS ###########'"
                        sh "echo '########### BACK-END DEV COMPLETE ###########'"
                    }
                }
            }
        }
    }

    post {
        success {
            slackSend(channel: 'jenkins', color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
        failure {
            slackSend(channel: 'jenkins', color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
    }
}

1. health check

RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://${WAS_1_PRIVATE_IP}:8080/actuator/health)

curl 명령어를 통해, 8080 포트에 대한 health check를 해서 상태코드를 변수에 할당합니다.

8080 포트가 살아있다면 200이 반환될 것이고, 죽어있다면 500번 대 응답코드가 반환될 것 입니다.

2. 8080가 살아있는지 확인

if [ $RESPONSE_CODE -ne 200 ]

위에서 할당한 상태코드를 -ne명령어를 확인합니다. 8080가 죽어있다면 8080에 새로운 버전을 배포하고 8081 포트를 죽이게 됩니다.

3. 8081에 배포

scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ubuntu@${WAS_2_PRIVATE_IP}:/home/ubuntu/sokdak
scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ububtu@${WAS_1_PRIVATE_IP}:/home/ubuntu/sokdak
ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_2_PRIVATE_IP} 'cd sokdak && ./deploy-8080.sh\'
ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_1_PRIVATE_IP} 'cd sokdak && ./deploy-8080.sh\'

8080이 죽어있기 때문에, 8080 포트에 새로운 버전을 배포합니다. scp로 jar파일을 전송하고, ssh 명령어를 ec2에 접속해 8080 포트에 배포합니다.

4. 8081 포트 kill

for var in {1..100}
do
    sleep 1
    HEALTH_CHECK_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://${WAS_2_PRIVATE_IP}:8080/actuator/health)
    if [ $HEALTH_CHECK_CODE -eq 200 ]
    then
        ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${NGINX_PRIVATE_IP} 'cd /etc/nginx/sites-enabled && sudo rm * && cd ~/zero-down-confs && sudo cp sokdak-8080.conf /etc/nginx/sites-enabled/ && sudo nginx -s reload\'
        ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_2_PRIVATE_IP} 'sudo kill -15 $(lsof -ti tcp:8081)'
        ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${WAS_1_PRIVATE_IP} 'sudo kill -15 $(lsof -ti tcp:8081)'
        echo 'SWITCHING FROM 8081 TO 8080 SUCCESS'
        break
    fi
done

반복문을 돌면서, 1초 마다 8080 포트에 health check를 하면서 8080 포트에 배포가 잘 되었는지 확인합니다.

그 후, 8080의 health check code가 200이라면(8080포트에 배포가 잘 됐다면) ssh 명령어로 nginx에 접속합니다. 그 후, nginx의 proxy_pass가 8080으로 지정된 conf 파일로 갈아끼우고 nginx를 reload합니다.

그리고 ssh 명령어로 ec2들에 접속하여 8081 포트에 실행된 SpringBoot를 kill합니다.

문제점

구버전의 port를 바로 kill 한다.

Blue-Green 배포의 장점 중 하나는 배포된 신버전을 모니터링을 하고 신버전에 문제가 있다면, 빠르게 Roll-Back을 할 수 있다는 점입니다. 하지만, 저희는 구버전의 port를 바로 kill 하고 있습니다.

따라서, 모니터링 시간을 지정하고 cron 등의 방식으로 구버전을 kill 해야할 것이라고 생각합니다.

모니터링 기간에 리소스가 부족하지 않을까?

모니터링 기간을 하루로 지정한다면, 하루 동안은 하나의 ec2에 2개의 SpringBoot가 동작하고 있을 겁니다. 물론 하나의 port가 트래픽을 처리할 것이지만, 리소스를 공유하기 때문에 위험할 수 있습니다. 위의 경우에는, ec2의 성능과 트래픽의 정도를 통해 테스트를 진행해보고 문제가 된다면, port를 통한 방식이 아닌 ec2 2대를 사용해서 해결해야할 것 같습니다.

0개의 댓글