0. 들어가기 전
팀바팀 서비스 요구사항 중에 다음과 같은 요구사항이 존재했습니다.
서비스를 배포하는 중간에도 사용자는 서비스를 계속해서 사용할 수 있어야 한다.
이러한 요구사항을 구현하기 위해서는 서비스 배포 시에 무중단 배포를 구현해야 했습니다.
그래서 기존 팀바팀 서비스의 배포 시 문제 상황과 무중단 배포 방식들을 살펴보고
팀바팀에는 어떤 무중단 배포 방식을 사용했는지 기록하고자 합니다.
1. 팀바팀 서비스의 배포 시 문제 상황
기존 팀바팀 서비스의 배포 시에는 '다운 타임'이라는 시간이 존재했습니다.
다운 타임 : 서비스가 중단되는 시간
이처럼 다운 타임 시 사용자들은 정상적인 서비스 이용이 불가능하게 됩니다.
팀바팀 서비스의 다운 타임을 측정해본 결과, 약 15초 정도의 다운 타임이 발생했습니다.
15초가 짧은 시간처럼 보이지만, 사용자 입장에서 15초 동안 서비스 이용이 불가능하면 당연히 서비스를 사용하지 않을 것입니다.
그렇다면, 이러한 다운 타임이 왜 기존 팀바팀 서비스에서 발생했을까요?
기존 배포 방식의 문제점
#!/bin/bash
echo "deploy dev server"
# 8080 포트가 실행중인지 확인하고 있다면 종료
if lsof -t -i:8080
then
echo "8080 포트에 실행 중인 프로세스를 종료합니다."
sudo lsof -t -i:8080 | xargs kill -15
echo "프로세스가 종료될 때까지 기다립니다."
while lsof -t -i:8080
do
sleep 1
done
else
echo "8080 포트에 실행 중인 프로세스가 없습니다."
fi
# .jar 파일 실행
echo "run java"
cd /home/ubuntu/
nohup java -jar /home/ubuntu/team-by-team/backend/current/team-by-team.jar --spring.config.location=/home/ubuntu/team-by-team/backend/current/application-dev.yml > /home/ubuntu/team-by-team/backend/current/app.log 2>&1 &
위는 기존 팀바팀 서비스의 배포 스크립트입니다.
간단히 분석해보면 다음과 같습니다.
- 신버전을 배포하기 위해 기존 포트에 실행중인 프로세스를 종료
- 프로세스 종료 후 신버전의 jar 파일을 실행
여기서 1번과 2번 사이, 즉 프로세스 종료 후 신버전의 jar 파일이 실행되기 전까지의 시간이 '다운 타임'이 됐던 것입니다.
이러한 다운 타임을 없애기 위해 무중단 배포의 방식을 알아보게 되었습니다.
2. 3가지 무중단 배포 방식
무중단 배포의 방법들을 찾아본 결과, 다음과 같은 3가지가 존재했습니다.
- Rolling 방식
- Canary 방식
- Blue/Green 방식
위의 일반적인 3가지 방식을 간단하게 소개해보겠습니다.
공통적으로 인프라 환경은 다음과 같이 트래픽 제어를 위한 로드 밸런서 1대가 있다고 가정합니다.
2-1. Rolling 방식
Rolling 방식은 구버전으로 실행중인 서버들을 점진적으로 신버전으로 교체하는 방식입니다.
구버전으로 실행중인 서버들을 신버전으로 배포 후 트래픽을 향하게 하는 방식입니다.
롤링 방식의 장단점은 다음과 같습니다.
장점
- 서버별로 배포를 진행하기 때문에 문제 발생 시 롤백이 간편하다.
- 별도의 추가 자원이 필요하지 않다.
단점
- 서비스 도중 하나의 서버로 향하는 트래픽을 멈추고, 다른 서버에게 향하도록 하기 때문에 다른 서버의 트래픽이 증가한다.
- 따라서, 전체 트래픽의 양과 각 서버가 부담할 수 있는 트래픽을 잘 판단해서 진행해야한다.
- 신버전으로 교체하는 과정에서 서비스에 신버전, 구버전이 공존하기 때문에 균일한 서비스를 제공하지 못한다.
2-2. Canary 방식
Canary 방식은 신버전 서버와 구버전 서버를 정해두고,
신버전 배포 시 구버전으로 향하던 트래픽을 점차적으로 신버전 서버로 향하도록 하는 방법입니다.
신버전을 운영 서비스에 배포해보고 조금씩 테스트를 해보면서 점차 트래픽을 늘려가는 방법입니다.
카나리 방식의 장단점은 다음과 같습니다.
장점
- 서비스에 신버전과 구버전이 공존하기 때문에 서로 다른 버전에서 보이는 사용자의 반응을 테스트하는 A/B 테스트에 용이하다.
- 신버전에 대한 트래픽을 점진적으로 늘리기 때문에 신버전 운영의 문제에 빠르게 대처할 수 있다.
단점
- 롤링 방식과 마찬가지로 신버전과 구버전이 공존하기 때문에 균일한 서비스를 제공하지 못한다.
2-3. Blue/Green 방식
Blue/Green 방식은 점진적인 배포였던 Rolling, Canary 배포와 달리
모든 트래픽을 한번에 신버전으로 향하게 하는 무중단 배포 방식입니다.
기존 구버전 서버 운영 중에 신버전 서버를 배포하고, 배포가 완료되면 해당 신버전 서버로 모든 트래픽을 향하게 하고
구버전 서버는 종료하는 방식입니다.
Blue/Green 방식의 장단점은 다음과 같습니다.
장점
- 트래픽을 한번에 신버전으로 변경하기 때문에 운영하는 서비스에 구버전과 신버전이 공존하지 않는다.
- 구버전의 서버가 그대로 남아있기 때문에 롤백이 간편하다.
단점
- 구버전, 신버전별로 서버가 다르게 나뉘기 때문에 기존 자원의 2배가 필요하다.
3. 팀바팀 인프라 환경에 적절했던 무중단 배포 방식
앞서 무중단 배포 방식 3가지를 알아봤습니다!
그렇다면 위의 3가지 방식 중에 팀바팀의 인프라 환경을 바탕으로 어떤 무중단 배포 방식을 적용했을까요?
결론부터 말씀드리면, 3가지 방식 중에 Blue/Green 방식으로 무중단 배포를 진행하게 되었습니다.
3가지 무중단 배포 방식 중 Blue/Green 방식을 적용했던 이유는 다음과 같습니다.
- 운영되는 서비스에서 구버전과 신버전이 공존하는 경우에 장점보다 단점이 많다고 생각했습니다.
- 현재 규모가 작은 서비스이기 때문에 A/B 테스트가 불필요합니다.
- 구버전과 신버전이 공존할 때 호환성 문제의 트러블 슈팅이 더 힘들다고 판단했습니다.
따라서, 위와 같은 이유로 Blue/Green 방식을 적용하기로 했습니다.
이때 그 당시 인프라 환경에서 해결해야 할 Blue/Green 방식의 문제는 다음과 같았습니다.
- Blue용 EC2, Green용 EC2가 분리되므로 EC2가 현재의 2배로 필요하다.
그 당시 상황으로, 추가로 EC2를 생성할 수 없는 상황이었습니다.
따라서 Blue/Green의 기준을 EC2(서버)가 아닌, Port를 기준으로 분리하여 구현하게 되었습니다.
4. 팀바팀 무중단 배포 구조
팀바팀의 무중단 배포는 어떻게 구현되었는지 살펴보기 전에 팀바팀 CI/CD 인프라 구조를 살펴보도록 하겠습니다.
간략하게 요약하면, Github actions의 Self-hosted Runner를 통해 무중단 배포 스크립트를 실행하는 구조입니다.
- 배포 Workflow 실행
- 배포 Workflow 내에서 Self-hosted Runner를 통해 각 EC2에서 무중단 배포 스크립트 실행
운영 배포 시 Workflow는 다음과 같습니다.
deploy:
needs: [build-frontend, build-backend]
runs-on: self-hosted
env:
BACK_PROD_PROPERTY: ${{ secrets.PROD_BACK_PROPERTY }}
BACK_DEPLOY_SCRIPT: ${{ secrets.PROD_BACK_DEPLOY_SCRIPT }}
CLOUD_FRONT_DISTRIBUTION_ID: ${{ secrets.CLOUD_FRONT_PROD_DISTRIBUTION_ID}}
steps:
...
- name: Download BACK jar file from artifact
uses: actions/download-artifact@v3
with:
name: BackendApplicationJar
path: backend/prod/
- name: Make BACK prod application.yml
working-directory: backend/prod/
run: echo "$BACK_PROD_PROPERTY" > application-prod.yml
- name: Deploy BACK to production server
working-directory: backend/prod/
run: |
echo "$BACK_DEPLOY_SCRIPT" > prod-deploy.sh
chmod 700 prod-deploy.sh
./prod-deploy.sh
...
step별로 간략하게 요약하면, 다음과 같습니다.
- Artifact로 저장된 빌드한 jar 파일 다운로드
- Github Secret으로 관리하는 중요 환경 설정 application.yml 생성
- Github Secret으로 관리하는 배포 스크립트 실행
3번의 배포 스크립트 실행 후에, 서버에 내장된 무중단 배포 스크립트까지 실행시키는 로직으로 구현했습니다.
무중단 배포 스크립트를 살펴보기 전에,
포트로 Blue/Green 무중단 배포를 구현하는 방식을 생각해봤을 때 2가지가 존재했습니다.
기본적으로 포트는 8080, 8081 두 개의 포트를 사용한다고 가정했습니다.
1. 배포 시 실행되는 포트의 반대 포트에서 신버전 배포 후 Nginx 포트 포워딩
- 8080 포트로 배포 중 -> 8081 포트로 배포 후 8081로 Ngnix 포트 포워딩
- 8081 포트로 배포 중 -> 8080 포트로 배포 후 8080으로 Nginx 포트 포워딩
2. 8080 포트를 기본 포트로 사용하고, Down Time 동안만 8081 포트 사용
- 배포 시 8081 포트로 배포 후 8081로 Nginx 포트 포워딩
- 그 후 바로 8080으로 다시 배포 후에 8080으로 Nginx 포트 포워딩
위의 2가지 방법 중에서 결론적으로는 2번 방법을 사용했습니다.
2번 방법을 결정한 이유는 다음과 같습니다.
- 1번 방법을 사용하게 되면, 배포되는 서버의 포트를 바로 추적할 수 없다.
- 8080 포트로 배포 중인지, 8081 포트로 배포 중인지 헬스 체크 작업이 한번 필요하다.
- 여러 서버의 작업에서 디버깅을 할 때 디버깅 요소에서 '포트'를 줄이기 위해 사용
- 다운 타임이 일어나는 동안만 8081 포트고, 나머지 거의 모든 시간을 8080 포트로 사용하기 때문에 디버깅 시에 포트는 8080으로 고정해놓고 나머지 에러를 추적할 수 있다.
5. 팀바팀 무중단 배포 스크립트
5-1. Workflow 실행 스크립트
#!/bin/bash
echo "Send To WAS"
mv team-by-team-*.jar team-by-team.jar
server_ip='xxx.xxx.x.xxx'
back_deploy_path=/home/ubuntu/back
scp -i /home/ubuntu/ssh/team-by-team.pem team-by-team.jar ubuntu@${server_ip}:${back_deploy_path}
scp -i /home/ubuntu/ssh/team-by-team.pem application-prod.yml ubuntu@${server_ip}:${back_deploy_path}
echo "finish send to was"
# SSH 접속
echo "connect to ssh"
ssh -i /home/ubuntu/ssh/team-by-team.pem ubuntu@${server_ip} << 'ENDSSH'
temp_port=8081
default_port=8080
# 배포 스크립트 디렉터리로 이동
cd /home/ubuntu/back/deploy_scripts
./deploy_temp_port.sh ${temp_port} ${default_port}
ENDSSH
위는 Workflow 내의 Github Self-hosted Runner에서 실행되는 스크립트로, 요약하면 다음과 같습니다.
- scp 명령어로 다운받은 jar 파일, 환경 설정 파일을 운영 서버 배포 경로에 저장합니다.
- ssh 명령어로 운영 서버에 접속하여 내장 temp port 배포 스크립트를 실행합니다.
5-2. 운영 서버 temp port 배포 스크립트 (8081 포트 배포 스크립트)
Workflow의 배포 스크립트에서 서버에 내장된 temp port 배포 스크립트를 실행하도록 구현했습니다.
그래서 배포가 시작되고 Workflow가 끝나면, temp port 배포 스크립트가 실행됩니다.
... (변수 설정)
# temp port로 .jar 파일 실행
echo "run temp_port - ${temp_port} java"
cd ${back_deploy_path}
sudo nohup java -jar -Duser.timezone=Asia/Seoul team-by-team.jar --spring.config.location=./application-prod.yml --server.port=${temp_port} /> app.log 2>&1 &
# temp port 실행중인지 판단
for retry_count in $(seq 10)
do
if sudo curl -s "http://${server_ip}:${temp_port}" > /dev/null
then
echo "Health Check Success!"
echo "set \$green_url http://${server_ip}:${temp_port};" | sudo tee ${nginx_green_url_file} > /dev/null
sudo nginx -s reload
# default port 실행중인지 확인하고 있다면 종료
if sudo lsof -t -i:${default_port}
then
echo "default port - ${default_port}에 실행 중인 모든 프로세스를 종료합니다."
sudo lsof -t -i:${default_port} | xargs -I {} sudo kill -15 {}
echo "프로세스가 종료될 때까지 기다립니다."
while lsof -t -i:${default_port}
do
sleep 1
done
else
echo "default port - ${default_port}에 실행 중인 프로세스가 없습니다."
fi
cd ${back_deploy_path}/deploy_scripts
./deploy_default_port.sh ${default_port} ${temp_port} ${back_deploy_path}
break
fi
if [ $retry_count -eq 10 ]
then
echo "Health Check Failed!"
exit 1
fi
sleep 10
echo "temp port - ${temp_port} Server is not alive yet. Retry health check in 10 seconds"
done
위의 스크립트를 요약하면 다음과 같습니다.
- 8081 temp port로 신버전 배포
- temp port 헬스 체크
- default port 프로세스 종료
- default 포트 배포 스크립트(deploy_default_port.sh) 실행
※ Nginx 포트 포워딩
echo "set \$green_url http://${server_ip}:${temp_port};" | sudo tee /etc/nginx/conf.d/green-url.inc > /dev/null
위의 스크립트로 nginx의 green-url.inc 파일에 green_url 변수를 port별로 바꿔주어 포트포워딩 되도록 진행했습니다.
5-3. 운영 서버 default 포트 배포 스크립트 (8080 포트 배포 스크립트)
... (변수 설정)
# default port로 .jar 파일 실행
echo "run default_port java"
cd ${back_deploy_path}
sudo nohup java -jar -Duser.timezone=Asia/Seoul team-by-team.jar --spring.config.location=./application-prod.yml --server.port=${default_port} /> app.log 2>&1 &
# default port 실행중인지 판단
for retry_count in $(seq 10)
do
if sudo curl -s "http://${server_ip}:${default_port}" > /dev/null
then
echo "Health Check Success!"
echo "set \$green_url http://${server_ip}:${default_port};" | sudo tee ${nginx_green_url_file} > /dev/null
sudo nginx -s reload
# temp port 실행중인지 확인하고 있다면 종료
if sudo lsof -t -i:${temp_port}
then
echo "temp port - ${temp_port}에 실행 중인 모든 프로세스를 종료합니다."
sudo lsof -t -i:${temp_port} | xargs -I {} sudo kill -15 {}
echo "프로세스가 종료될 때까지 기다립니다."
while lsof -t -i:${temp_port}
do
sleep 1
done
else
echo "temp port - ${temp_port}에 실행 중인 프로세스가 없습니다."
fi
break
fi
if [ $retry_count -eq 10 ]
then
echo "Health Check Failed!"
exit 1
fi
sleep 10
echo "Default port - ${default_port} Server is not alive yet. Retry health check in 10 seconds"
done
위의 스크립트를 요약하면 다음과 같습니다.
- 8080 default port로 신버전 배포
- default port 헬스 체크
- 8081 temp port 프로세스 종료
※ Nginx 포트 포워딩
echo "set \$green_url http://${server_ip}:${default_port};" | sudo tee /etc/nginx/conf.d/green-url.inc > /dev/null
위의 스크립트로 nginx의 green-url.inc 파일에 green_url 변수를 port별로 바꿔주어 포트포워딩 되도록 진행했습니다.
결론적으로 무중단 배포 실행 과정을 보면,
Workflow -> temp port 배포 스크립트 실행 -> temp port 내부에서 default port 배포 스크립트 실행
이러한 순서로 무중단 배포 스크립트 실행을 거쳐 무중단 배포가 진행되게 됩니다.
Reference
https://hudi.blog/zero-downtime-deployment/
'우아한테크코스 5기 팀바팀 Project > 설계' 카테고리의 다른 글
[설계] 팀바팀 이미지 업로드 설계 (1) - 이미지 Storage, 업로드 주체 결정 (4) | 2023.09.09 |
---|---|
[설계] Git Branch 전략이란? & Git Branch 전략 알아보기 (Git Flow, GitHub Flow) (0) | 2023.07.16 |
[기획 & 설계] 이벤트 스토밍(Event Storming) 도입기 (3) | 2023.07.01 |