안녕!
우아한테크코스 5기 [스탬프크러쉬]팀 깃짱이라고 합니다.
사장모드: stampcrush.site/admin
고객모드: stampcrush.site
💋 무중단 배포 도입 전
✔️ 스탬프크러쉬의 인프라 상황
한 대의 클라우드 서버에서 운영 서버를 가지고 있으며, 외부의 한 대의 또다른 클라우드 서버를 배포 관련 서버로 사용중이다. 해당 서버에서는 젠킨스를 통해서 배포를 자동화하고 있다.
✔️ 배포 방식
현재 스탬프크러쉬는 jenkins 서버에서 아래의 4가지 단계로 배포를 자동화하고 있다.
- 우리가 트래킹하는 브랜치(main, develop)에 push가 되면, 젠킨스는 깃허브로부터 최신 코드를 가져온다.
- 최신 코드를 빌드해
.jar
파일을 만든다. .jar
파일을 운영서버로 전달한다.- 운영서버의
run.sh
스크립트 파일 실행한다. 해당 스크립트 파일에는 spring boot application 을 실행하는 내용이 있다.
✔️ 배포 스크립트
아래는 무중단 배포 도입 전 스탬프크러쉬의 배포 관련 스크립트이다.
[젠킨스 파이프라인]
pipeline {
agent any
stages {
stage('Github') { // 깃허브로부터 최신 코드를 pull해오는 단계
steps {
checkout scmGit(
branches: [[name: '*/develop']], // develop 브랜치의 코드를 pull 한다는 뜻
extensions: [submodule(parentCredentials: true, trackingSubmodules: true, recursiveSubmodules: false, disableSubmodules: false)], // 서브모듈의 내용을 함께 가져옴.
userRemoteConfigs: [[credentialsId: 'leo-git', url: 'https://github.com/woowacourse-teams/2023-stamp-crush']] // credential과 소스 코드의 저장 위치
)
}
}
stage('Build') { // 받아온 코드를 빌드하는 단계
steps {
dir('backend') {
sh 'pwd'
sh "./gradlew bootJar" // jar 파일을 빌드한다.
}
}
}
stage('Deploy') { // jar 파일을 배포 서버에서 실행하는 단계
steps {
dir('backend/build/libs') {
sshagent(credentials: ['key-stamp-crush']) {
sh 'scp -o StrictHostKeyChecking=no backend-0.0.1-SNAPSHOT.jar ubuntu@192.XXX.X.XX:/home/ubuntu'
sh 'ssh ubuntu@192.168.1.173 "sudo sh run.sh" &' // 운영 서버에 들어있는 스크립트인 run.sh를 실행한다.
sh 'ssh ubuntu@192.168.1.173 "sh sleep.sh"' // 배포가 끝나기 전에 이 단계를 종료해버리지 않도록 10초 간 멈추도록 한다.
}
}
}
}
}
위의 코드에서 StrictHostKeyChecking=no 는 배포 서버에 보내는 key의 유효성을 검증하지 않도록 하는 명령어인데, 뺄 수 있다면 빼는 것이 좋다.
[운영 서버의 배포 스크립트 run.sh
]
#! /bin/bash
PROJECT_NAME=backend
// 8080 포트에 띄워져 있는 프로세스가 있다면 종료한다.
CURRENT_PID=`sudo lsof -i :8080 -t`
echo $CURRENT_PID
if [ -z "$CURRENT_PID" ]; then
echo " 실행중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo " 실행중인 애플리케이션을 종료했습니다. (pid : $CURRENT_PID)"
sudo kill -9 $CURRENT_PID
sleep 5
fi
// 최신 코드로 빌드된 jar 파일을 실행한다.
echo "\n SpringBoot 애플리케이션을 실행합니다.\n"
JAR_NAME=$(ls | grep .jar | head -n 1)
echo $JAR_NAME
sudo nohup java -jar -Dspring.profiles.active=prod -Duser.timezone=Asia/Seoul /home/ubuntu/$JAR_NAME &
✔️ 다운타임
무중단 배포 도입 전에 총 20초의 다운 타임이 존재했다.
기존에 실행되던 프로그램을 종료하는 데 10초, 새로운 프로그램을 띄우고 기다리는 데 10초가 소요되었다.
✔️ 무중단 배포의 필요성
스탬프크러쉬 서비스를 실제 카페에서 사용하게 되었고, 우리 카페는 매일매일 아침 11시부터 저녁 11시까지 운영 한다. 따라서 우리가 주로 일하는 시간인 낮에 코드에 변경사항이 있더라도 배포하는 것이 어려워졌다.
따라서 우리는 무중단 배포를 도입하기로 결정했다.
💋 무중단 배포 계획
우리는 운영 서버로 사용할 EC2 서버가 1개이기 때문에 서로 다른 서버를 이용해서 무중단 배포를 하는 일반적인 방식을 사용할 수는 없었다. 따라서 하나의 서버 내에서 2개의 포트를 사용해 각 포트에 도커 컨테이너를 띄우고 각각의 컨테이너에서 애플리케이션을 돌아가면서 배포하기로 결정했다.
우리는 이전과 달리, 배포를 할 때에 .jar
파일을 운영 서버로 직접 보내는 방식 대신 Docker Hub를 사용해서 젠킨스에서 도커 허브로 도커 이미지를 push하는 방식을 채택하기로 했다.
그렇게 되면 자연스럽게 운영 서버에서는 젠킨스 서버로부터 파일을 받는 대신에 도커 허브로부터 도커 이미지를 pull 받아 오면 된다.
💋 무중단 배포 자동화 방법
✔️ 도커 이미지를 빌드하기 위한 Dockerfile을 프로젝트 코드 내에 생성한다.
Dockerfile은 도커 컨테이너를 구성하는 데 사용되는 텍스트 파일로, 도커 컨테이너 내부의 파일 시스템을 정의하는 명령문들로 구성되어 있다.
FROM openjdk:17-jdk
// 우리 프로젝트에서 빌드가 되었을때의 .jar 파일의 위치를 환경변수로 설정해준다
ARG JAR_FILE=build/libs/backend-0.0.1-SNAPSHOT.jar
// 젠킨스에서 build한 .jar 파일을 도커 이미지 내부로 복사
// 이 과정을 해야만 docker image를 다른 환경에서 실행하더라도 같은 .jar 파일을 실행하게 된다.
COPY ${JAR_FILE} stampcrush.jar
// 실제 도커 이미지가 실행될 때, 도커 컨테이너 내에서 실행될 명령어
CMD ["java", "-jar", "-Dspring.profiles.active=dev", "-Duser.timezone=Asia/Seoul", "stampcrush.jar"]
✔️ Jenkins에서 최신 코드로 .jar 파일을 도커 이미지로 빌드한다.
우리는 젠킨스를 도커 환경에서 실행중이다.
따라서 도커가 이미지를 사용해 안정적으로 배포 환경을 관리하는 장점을 최대한 살리기 위해서, 배포할 때 코드 및 실행 환경을 도커 이미지로 관리하기로 했다.
따라서 아래 명령어를 통해 최신 코드를 pull 받아 빌드 한 뒤 생성된 .jar
파일을 도커 이미지로 빌드한다.
sh 'docker build --platform linux/arm64/v8 -t stampcrush/stampcrush-dev -f Dockerfile-dev .'
docker build를 해주는 명령어를 통해 도커 이미지를 생성한다.
-t
이후에 있는 내용은 도커 허브의 어느 위치에 도커 이미지를 저장할 지 결정하는 내용으로, {docker hub 계정}/{repository명}
이다.
-f
이후는 사용할 도커 파일의 경로이다. 우리는 Root 디렉토리에 파일을 생성해두었기 때문에, 앞에 별다른 경로 없이 도커 파일의 이름만을 적어주었다. dev
브랜치에 대한 도커 파일은 Dockerfile-dev
에 정의해 두었다.
✔️ Jenkins에서 도커 이미지를 Docker Hub에 push한다.
sh 'docker login -u [도커 허브 id] -p [도커 허브 pw]'
sh 'docker push stampcrush/stampcrush-dev'
도커에 로그인할 수 있도록 로그인과 관련된 정보를 준다.
이후에 docker push를 통해서 생성된 이미지를 도커 허브에 푸시한다.
✔️ Jenkins에서 운영 서버의 배포 스크립트를 실행한다.
stage('Deploy') {
steps {
sshagent(credentials: ['key-stamp-crush']) {
sh 'ssh -o "StrictHostKeyChecking=no" ubuntu@192.168.1.173 "sudo sh deploy.sh"'
}
}
}
deploy.sh
는 우리 운영 서버에 들어 있는 배포 스크립트다. 젠킨스를 통해 해당 배포 스크립트를 실행한다.
배포 스크립트의 내용은 아래와 같다.
우리가 사용할 두 개의 포트에 올라갈 각 컨테이너의 이름을 우리 팀원인 깃짱(GITCHAN), 레오(LEO)를 따서 정했다.
#1
EXIST_GITCHAN=$(sudo docker-compose -p test-gitchan -f docker-compose.gitchan.yml ps | grep Up)
if [ -z "$EXIST_GITCHAN" ]; then
echo "GITCHAN 컨테이너 실행"
sudo docker-compose -p test-gitchan -f /home/ubuntu/docker-compose.gitchan.yml up -d
BEFORE_COLOR="leo"
AFTER_COLOR="gitchan"
BEFORE_PORT=8081
AFTER_PORT=8080
else
echo "LEO 컨테이너 실행"
sudo docker-compose -p test-leo -f /home/ubuntu/docker-compose.leo.yml up -d
BEFORE_COLOR="gitchan"
AFTER_COLOR="leo"
BEFORE_PORT=8080
AFTER_PORT=8081
fi
echo "${AFTER_COLOR} server up(port:${AFTER_PORT})"
# 2
for cnt in {1..10}
do
echo "서버 응답 확인중(${cnt}/10)";
UP=$(curl -s http://localhost:${AFTER_PORT}/health-check)
if [ "${UP}" != "up" ]
then
sleep 10
continue
else
break
fi
done
if [ $cnt -eq 10 ]
then
echo "서버가 정상적으로 구동되지 않았습니다."
exit 1
fi
# 3
sudo sed -i "s/${BEFORE_PORT}/${AFTER_PORT}/" /etc/nginx/conf.d/service-url.inc
sudo nginx -s reload
echo "Deploy Completed!!"
# 4
echo "$BEFORE_COLOR server down(port:${BEFORE_PORT})"
sudo docker-compose -p test-${BEFORE_COLOR} -f docker-compose.${BEFORE_COLOR}.yml down
#1
프로세스가 실행중이지 않은 도커 컨테이너를 찾는다.
- 깃짱 컨테이너가 실행중이면, 레오 컨테이너를 실행한다.
- 깃짱 컨테이너가 실행중이지 않으면, 깃짱 컨테이너를 실행한다.
#2
새롭게 실행된 컨테이너에 /health-check
라는 end point로 요청을 보내서, 컨테이너가 잘 실행되었는지 확인한다.
- 이 과정을 총 10초 씩 기다려가면서 최대 10번까지 반복하므로, 최대 100초까지 기다려준다.
- end point는 미리 “up” 문자열을 반환하도록 코드를 작성해 놓아야 한다.
- 서버 응답이 확인되지 않으면, 위의 1단계에서 컨테이너가 제대로 실행되지 않았음을 의미하므로, 배포 과정을 중단한다. 이 경우에는, 기존에 사용하던 서버는 그대로 유지되므로 그냥 없던 일로 할 수 있다.
#3
새롭게 실행된 컨테이너 쪽으로 Nginx의 포트 포워딩 설정을 변경하고, Nginx를 reload한다.
sudo sed -i "s/${BEFORE_PORT}/${AFTER_PORT}/" /etc/nginx/conf.d/service-url.inc
는 Nginx 내service-url
이라는 변수를 변경하는 명령어다.- 예를 들어서, 8080 포트에서 실행중이던 깃짱 컨테이너가 이전 코드를 실행중이고, 8081 포트에 새롭게 띄운 레오 컨테이너는 최신 코드를 실행한다면 8080으로 향하던 Nginx의 포트 포워딩을 8081로 변경한다.
- reload를 통해서 서버를 중단하지 않고 새로운 설정 파일을 적용할 수 있다. 참고로, restart 명령어는 새로운 설정을 적용하고 모든 연결을 끊어서 서비스 중단이 발생한다.
#4
이전 코드를 실행중인 도커 컨테이너를 종료시킨다.
💋 배포 스크립트 전체
✔️ Jenkins Pipeline 스크립트
아래는 우리 서비스의 Jenkins pipeline 전체다.
무중단 배포와 관련된 단계는 Docker Build and Push
와 Deploy
다.
pipeline {
agent any
stages {
stage('Github') {
steps {
checkout scmGit(
branches: [[name: '*/develop']],
extensions: [submodule(parentCredentials: true, trackingSubmodules: true, recursiveSubmodules: false, disableSubmodules: false)],
userRemoteConfigs: [[credentialsId: 'leo-git', url: 'https://github.com/woowacourse-teams/2023-stamp-crush']]
)
}
}
stage('Build') {
steps {
dir('backend') {
sh 'pwd'
sh "./gradlew bootJar"
}
}
}
stage('Docker Build and Push') {
steps {
dir('backend') {
sh 'pwd'
sh 'docker build --platform linux/arm64/v8 -t stampcrush/stampcrush-dev -f Dockerfile-dev .'
sh 'docker login -u [도커 허브 id] -p [도커 허브 pw]'
sh 'docker push stampcrush/stampcrush-dev'
}
}
}
stage('Deploy') {
steps {
sshagent(credentials: ['key-stamp-crush']) {
sh 'ssh -o "StrictHostKeyChecking no" ubuntu@192.168.1.173 "sudo sh deploy.sh"'
}
}
}
}
}
✔️ 운영 서버의 배포 스크립트(deploy.sh)
#1
EXIST_GITCHAN=$(sudo docker-compose -p test-gitchan -f docker-compose.gitchan.yml ps | grep Up)
if [ -z "$EXIST_GITCHAN" ]; then
echo "GITCHAN 컨테이너 실행"
sudo docker-compose -p test-gitchan -f /home/ubuntu/docker-compose.gitchan.yml up -d
BEFORE_COLOR="leo"
AFTER_COLOR="gitchan"
BEFORE_PORT=8081
AFTER_PORT=8080
else
echo "LEO 컨테이너 실행"
sudo docker-compose -p test-leo -f /home/ubuntu/docker-compose.leo.yml up -d
BEFORE_COLOR="gitchan"
AFTER_COLOR="leo"
BEFORE_PORT=8080
AFTER_PORT=8081
fi
echo "${AFTER_COLOR} server up(port:${AFTER_PORT})"
# 2
for cnt in {1..10}
do
echo "서버 응답 확인중(${cnt}/10)";
UP=$(curl -s http://127.0.0.1:${AFTER_PORT}/health-check)
if [ "${UP}" != "up" ]
then
sleep 10
continue
else
break
fi
done
if [ $cnt -eq 10 ]
then
echo "서버가 정상적으로 구동되지 않았습니다."
exit 1
fi
# 3
sudo sed -i "s/${BEFORE_PORT}/${AFTER_PORT}/" /etc/nginx/conf.d/service-url.inc
sudo nginx -s reload
echo "Deploy Completed!!"
# 4
echo "$BEFORE_COLOR server down(port:${BEFORE_PORT})"
sudo docker-compose -p test-${BEFORE_COLOR} -f docker-compose.${BEFORE_COLOR}.yml down
✔️ docker-compose.yml 파일
docker-compose.gitchan.yml
version: '3.1'
services:
api:
image: stampcrush/stampcrush-dev:latest
container_name: test-gitchan
environment:
- LANG=ko_KR.UTF-8
- HTTP_PORT=8080
ports:
- '8080:8080'
docker-compose.leo.yml
version: '3.1'
services:
api:
image: stampcrush/stampcrush-dev:latest
container_name: test-leo
environment:
- LANG=ko_KR.UTF-8
- HTTP_PORT=8081
ports:
- '8081:8080'
✔️ Nginx 설정파일
nginx 로 reverse proxy를 변경해, 새로 띄우고자 하는 컨테이너의 포트에 요청이 가도록 설정한다.
[service-url.inc]
해당 파일은 새로 생성을 해줘야 한다
set $service_url http://127.0.0.1:8080;
이 부분에서 localhost 말고, 127.0.0.1로 설정해 줘야 한다.
DNS 서버에서 주소를 찾기 때문에 localhost로 설정하는 경우에 IP를 찾을 수 없어서 동작하지 않는다.
[Nginx 설정]
server {
include /etc/nginx/conf.d/service-url.inc;
location /api {
proxy_pass $service_url;
}
}
💋 참고자료
- https://velog.io/@imsooyeon/Jenkins-pipeline을-구축하여-Docker-build-및-이미지-push-하기
- https://velog.io/@chang626/docker-build-push-jenkins-pipeline
- https://hudi.blog/zero-downtime-deployment/
- https://velog.io/@eeheaven/SpringBootNginx-무중단-배포
- https://kjw1313.tistory.com/86
- https://velog.io/@devmin/Docker-deployment
도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
'PROJECT > Stamp Crush' 카테고리의 다른 글
[모집공고] 스탬프크러쉬 기획/영업/마케팅 팀원 모집 (0) | 2023.10.16 |
---|---|
[우테코] 스탬프크러쉬의 실제 사용자(카페 사장)와 함께한 일주일 (5) | 2023.10.12 |
[우테코] 인덱스를 활용한 스탬프크러쉬의 쿼리 성능 개선 (1) | 2023.10.07 |
[우테코] 스탬프크러쉬 서비스 사용 카페 출장 영업 일기 (25) | 2023.10.05 |
[우테코] JWT 방식에서 로그아웃, Refresh Token 만들기(1): JWT의 Stateless한 특징을 최대한 살리려면? (6) | 2023.09.27 |