블로그 이미지

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 무중단 배포 적용기
    Diary/우아한테크코스 2022. 10. 17. 22:38

    우아한테크코스 최종 데모데이의 요구사항으로 무중단 배포가 있다. 이를 학습하고 적용한 내용을 정리한다.

    무중단 배포는 왜 필요할까?

    스프린트4 때쯤, 기능을 개발하고 팀원들끼리 QA를 한 후에 운영서버에 배포를 하고 있었다. 그런데, 그 찰나의 순간에 우리 속닥속닥 서비스를 이용하고 있던 다른팀 크루가 속닥속닥 왜 안되냐는 말을 했다. 우리 서비스의 경우 사용자가 많지 않고, 잠깐 서비스가 중단된다 해서 금전적인 손해가 발생하지는 않기 때문에 큰 문제는 없다. 그런데, 서비스가 잠깐이라도 중단되면 안되는 서비스라면 어떨까? 특히, 금전적인 문제가 달려있다면? 새로운 기능을 배포하거나 버그 픽스는 위험을 감수해가며 배포 할 것이지만, 리팩터링에 대한 배포는 쉽지 않을 것이다.

    무중단 배포 3가지 방법

    Rolling Deployment

    Rolling Deployment는 새 버전을 점진적으로 배포한다. 위 그림처럼 세 서버를 로드밸런싱 하고 있는 상황이라면, 인스턴스 하나씩 차례로 로드밸런서가 라우팅하지 않도록 하고 배포를 진행후 다시 라우팅하도록 한다.

    장점

    • 한번에 바꾸지 않고 차례로 바꾸기 때문에 버그를 발견했을 시에 손쉬운 롤백이 가능하다.

    단점

    • 배포를 진행할때는 로드밸런서가 해당 인스턴스에 라우팅을 하지 않는데, 그동안 트래픽이 나머지 인스턴스에 몰려 이를 감당해야 한다.
    • 배포하는 동안 새로운 버전과 구 버전이 공존하는데, 신버전과 구버전의 호환이 맞지 않는다면 문제가 발생한다.

    Canary Deployment

    신버전의 배포 범위를 점진적으로 늘려가는 배포 방식이다. 로드밸런서를 통해 새로운 버전의 유저 트래픽을 조절해 신버전에 대한 모니터링과 피드백을 할 수 있다.

    장점

    • 실제 환경에서 신 버전에 대한 테스트를 할 수 있다.
      • 이를 통해 A/B 테스트가 가능하다.
    • 손쉬운 롤백이 가능하다.

    단점

    • 롤링배포와 마찬가지로 신버전과 구버전의 호환성 문제가 발생할 수 있다.

    Blue/Green Deployment

    로드밸런서가 블루 그룹을 라우팅하고 있을때, 그린 그룹에 신버전을 배포한 뒤에 배포가 완료되면 그린 그룹으로 라우팅 방향을 바꿔주는 방식이다.

    장점

    • 구버전과 신버전에 대한 호환성 문제가 발생하지 않는다.
    • 구버전으로 라우팅이 되지 않을뿐 어플리케이션은 작동하고 있기 때문에 문제 발생시 롤백이 쉽다.

    단점

    • 운영환경의 인스턴스 자원이 두배로 필요하기 때문에 비용이 비교적 많이 든다.

    무엇을 선택해야 할까?

    개인적으로 구버전과 신버전의 호환성 문제 때문에 롤링 배포와 카나리는 선호하지 않는다. 예를 들면, 신버전에서는 엔티티의 필드가 추가되어 DB의 컬럼이 추가되었는데 이 컬럼이 NULL을 허용하지 않는다고 해보자. 이미 DB 컬럼은 바뀌었는데 구버전에서는 해당 필드에 대한 처리를 해주지 않고있다. 이렇게 되면 호환이 되지 않아 오류가 발생하는 문제가 생긴다.

    하지만, 롤링배포와 카나리가 좋지 않는 방법이라는 것은 아니다. 특히 카나리 배포의 경우 신버전과 구버전에 대한 동시 테스트가 가능하기 때문에 많은 이익을 가져다 줄 것으로 생각된다. 블루그린 배포를 기본 배포방식으로 두고, 신 버전에 대한 모니터링을 해야하고 구버전과 신버전에 대한 호환성 문제가 없다는 것이 확실할 때 카나리 배포를 진행하면 베스트 이지 않을까 생각된다.

    속닥속닥은 무중단 배포 방법을 두개나 챙겨갈 정도로 여유로운 상황은 아니기 때문에 하나를 선택했고, 블루그린을 적용하게 되었다.

    그런데, 블루그린을 적용하고자 하니 문제가 있었다. 현재 속닥속닥은 nginx로 2대의 인스턴스에 대해 로드밸런싱을 하고 있는데, 인스턴스 자원이 두배가 들면 4대의 인스턴스를 사용하게 된다는 것이다. 사용자가 그리 많은 것도 아닌데, 인스턴스가 4대나 사용하면 그야말로 오버 엔지니어링이 아닐까? 하는 생각이 들었다. 그러면 인스턴스 2대로 블루그린 배포를 적용하면 로드밸런싱을 포기해야되는 것인가? 하는 생각을 하다가, 인스턴스 2개 각각에 포트 2개를 이용해 블루그린 배포를 진행하면 두마리 토끼를 다 잡을수 있을것 같았다.

    적용해보자

    무중단 배포를 키워드로 검색하면 대부분 비슷한 자료들이 나온다. 한 인스턴스에 두 포트를 이용해 블루그린 배포를 이용하는 방법인데, 속닥속닥의 경우는 조금 달랐다. nginx 서버가 분리되어 있고, 두개의 인스턴스의 포트를 컨트롤 해주어야 하는 상황이다. 그래서 차근차근 우리의 뇌피셜로 작업을 해보자! 하고 블루그린 배포를 구현했다.

    속닥속닥의 무중단 배포 구조

    우리의 무중단 배포 구조이다. 각 인스턴스에 8080과 8081 포트에 대한 배포 스크립트 파일을 준비하고, 리버스 프록시를 담당하는 인스턴스에 8080과 8081의 conf 파일을 둘 다 준비해놓았다.

            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
                                RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://192.168.1.243:8080/actuator/health)
                                cd backend/sokdak/build/libs
                                if [ $RESPONSE_CODE -ne 200 ]
                                then
                                    echo '############## 8081 LOAD START ##############'
                                    scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ubuntu@{WAS1-privateIP}:/home/ubuntu/sokdak
                                    scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ubuntu@{WAS2-privateIP}:/home/ubuntu/sokdak
                                    ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@{WAS1-privateIP} 'cd sokdak && ./deploy-8080.sh\'
                                    ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@{WAS2-privateIP} 'cd sokdak && ./deploy-8080.sh\'
    
                                    for var in {1..100}
                                    do
                                        sleep 1
                                        HEALTH_CHECK_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://192.168.1.209:8080/actuator/health)
                                        if [ $HEALTH_CHECK_CODE -eq 200 ]
                                        then
                                            ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@{nginx-privateIP} 'cd /etc/nginx/sites-enabled && sudo rm * && cd ~/zero-down-confs && sudo cp sokdak-8080.conf /etc/nginx/sites-enabled/ && sudo service nginx restart && sudo kill -9 $(lsof -ti tcp:8081)\'
                                            ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@{WAS1-privateIP} 'sudo kill -9 $(lsof -ti tcp:8081)'
                                            ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@{WAS2-privateIP} 'sudo kill -9 $(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@{WAS1-privateIP}:/home/ubuntu/sokdak
                                    scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ubuntu@{WAS2-privateIP}:/home/ubuntu/sokdak
                                    ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@{WAS1-privateIP} 'cd sokdak && ./deploy-8081.sh\'
                                    ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@{WAS2-privateIP} '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://192.168.1.209:8081/actuator/health)
                                        if [ $HEALTH_CHECK_CODE -eq 200 ]
                                        then
                                            ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@{nginx-privateIP} 'cd /etc/nginx/sites-enabled && sudo rm * && cd ~/zero-down-confs && sudo cp sokdak-8081.conf /etc/nginx/sites-enabled/ && sudo service nginx restart\'
                                            ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@{WAS1-privateIP} 'sudo kill -9 $(lsof -ti tcp:8080)'
                                            ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@{WAS2-privateIP} 'sudo kill -9 $(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 ###########'"
                        }
                    }
                }
            }

    사실 여러 자료에 나와있는 것처럼 스크립트가 깔끔하진 않다. 추후에 정리하는 것으로 하고… 로직에 대해 얘기해보자면 8080포트에 대한 health check 를 진행하고, 해당 포트가 살아있다면 8081포트에 대해 WAS1 과 WAS2 에 대한 배포를 진행한다. 그리고 nginx 서버의 설정파일을 8081을 바라보도록 바꾸어 준다. 8080이 죽어있는 상태라면 반대로 진행한다. 또한, 배포가 완료되면 원래 돌아가고 있던 포트번호에 대한 PID를 kill 해주는 작업을 한다.

    현재 상태의 문제점

    블루그린 배포를 진행했지만, 블루그린 배포의 장점인 손쉬운 롤백을 누리지 못하고 있다. 구버전의 포트번호에 대한 프로세스를 죽이기 때문이다. 그렇다고 두 어플리케이션을 모두 실행하고 있자니 인스턴스의 메모리를 차지하고 있어 서버 자체의 성능이 좋지 않아진다.

    두번째 문제점은, 새로운 어플리케이션을 띄울 때 상당한 리소스를 잡아 먹는다는 것이다. 배포를 진행하는 와중에 트래픽이 몰리는 상황이라면, 로드밸런싱을 진행중이라고 하더라도 어플리케이션을 띄우면서의 리소스 때문에 서버가 죽는 상황이 발생할 수도 있을 것이라 생각한다.

    사실 두 문제점 모두 인스턴스를 2배로 띄우면 해결될 문제점이다. 그럼에도 불구하고 우리 서비스는 과도한 트래픽이 몰리는 상황이 아니기 때문에 이 문제점을 충분히 안고갈 수 있다고 생각돼 현재의 방법으로 구현하였다.

    댓글

Designed by Tistory.