우아한테크코스 레벨 4 마지막 데모 요구 사항으로 무중단 배포가 주어졌습니다. 팀원들과 무중단 배포를 한 내용을 공유해보겠습니다.
서비스의 새로운 버전이 배포되는 순간 동안 사용자가 서비스를 사용할 수 없는 시간(다운 타임)이 발생하는데, 다운 타임 없이 사용자가에게 새로운 버전을 배포하는 개념입니다.
이미지 출처 : https://loosie.tistory.com/m/781
로드밸런싱 환경의 인스턴스들을 하나씩 배포시키는 방식입니다.
이미지 출처 : https://loosie.tistory.com/m/781
Canary 배포 전략은 과거에 Canary라는 새로 위험을 미리 감지하는데에서 유래한 개념입니다.
새로운 버전을 소수의 인스턴스에만 배포시켜두고 모니터링을 하고 문제를 발견하는 방식입니다.
이미지 출처 : https://loosie.tistory.com/m/781
새로운 버전이 배포된 새로운 환경을 현재 버전과 독립적으로 구축해두고, 한번에 새로운 버전이 배포된 환경으로 트래픽을 전환하는 방식입니다.
Canary와 Rolling은 동시간에 두 가지 버전이 제공되는 시점이 있습니다. 이를 세부적으로 컨트롤해주지 않으면 사용자에게 오류를 안겨줄 수 있다고 판단했기에, 두 방식은 채택하지 않았습니다. 물론 세부적으로 컨트롤하려면 할 수 있으나, Blue-Green 방식을 채택하면 해결된 문제라고 판단했습니다.
현재 저희 팀은 아래와 같이 두 개의 서버로 로드밸런싱을 해둔 상태입니다. 로드밸런싱을 할 정도의 트래픽이 발생하지 않지만, 학습의 목적으로 구축해두었습니다.
따라서, Blue-Green 방식을 적용하려면 2개의 추가적인 서버가 필요한데요.
저희 팀에선 이를 Port로 해결했습니다.
아래와 같이 동일한 ec2의 2개의 port에 SpringBoot를 2개 실행하는 것입니다.
사실 위의 방법은, 실무에선 쓰일 수 없을 것이라고 판단됩니다. 서버가 트래픽을 처리하고 있는 중에 다른 port에 SpringBoot를 하나 더 실행한다면, 두 개의 SpringBoot는 하나의 ec2 리소스를 공유하기 때문에 서버가 다운될 위험이 크다고 생각합니다.
하지만, 저희 서비스의 경우에는 동시 접속자 수가 많지 않기 때문에 위와 같은 port를 통한 Blue-Green 방식을 채택했습니다.
여러 가지 방식이 있겠지만, 저희는 레퍼런스들을 따라가는 것보다 저희의 힘으로 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})")
}
}
}
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번 대 응답코드가 반환될 것 입니다.
if [ $RESPONSE_CODE -ne 200 ]
위에서 할당한 상태코드를 -ne명령어를 확인합니다. 8080가 죽어있다면 8080에 새로운 버전을 배포하고 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 포트에 배포합니다.
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합니다.
Blue-Green 배포의 장점 중 하나는 배포된 신버전을 모니터링을 하고 신버전에 문제가 있다면, 빠르게 Roll-Back을 할 수 있다는 점입니다. 하지만, 저희는 구버전의 port를 바로 kill 하고 있습니다.
따라서, 모니터링 시간을 지정하고 cron 등의 방식으로 구버전을 kill 해야할 것이라고 생각합니다.
모니터링 기간을 하루로 지정한다면, 하루 동안은 하나의 ec2에 2개의 SpringBoot가 동작하고 있을 겁니다. 물론 하나의 port가 트래픽을 처리할 것이지만, 리소스를 공유하기 때문에 위험할 수 있습니다. 위의 경우에는, ec2의 성능과 트래픽의 정도를 통해 테스트를 진행해보고 문제가 된다면, port를 통한 방식이 아닌 ec2 2대를 사용해서 해결해야할 것 같습니다.