현재 기존 아키텍처에서는 로드 밸런서를 두지 않았다.
그래서 로드 밸런서가 필수가 아닌 Blue/Green 배포
방식으로 무중단 배포를 설계할 것이다.
위와 같이 서비스 중인 환경과 새로 배포되는 환경을 Blue
와 Green
으로 칭하고 교차하여 배포할 것이다.
Blue/Green 배포
와 다른 무중단 배포 방식에 대해 자세히 알고 싶다면 여기를 클릭해주세요!
pipeline {
agent any
environment {
VERSION = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
DOCKER_IMAGE = "${DOCKER_REGISTRY}/test:${VERSION}"
}
stages {
stage ('Git clone') {
steps {
checkout scmGit(
branches: [[name: 'main']],
extensions: [submodule(parentCredentials: true, recursiveSubmodules: true, reference: '', trackingSubmodules: true)],
userRemoteConfigs: [[credentialsId: 'github-id', url: 'https://github.com/DongminL/ci-cd-practice.git']]
)
}
}
stage ('Gradle Build') {
steps {
sh '''
chmod +x gradlew
./gradlew clean bootJar
'''
}
}
stage ('Docker Image Build') {
steps {
sh '''
docker build -t test .
docker tag test ${DOCKER_IMAGE}
export CR_PAT=${GITHUB_TOKEN}
echo \$CR_PAT | docker login ghcr.io -u ${GITHUB_ID} --password-stdin
docker push ${DOCKER_IMAGE}
docker rmi -f ${DOCKER_IMAGE}
'''
}
}
stage ('Deploy') {
steps {
sshagent (credentials: ['ssh']) {
sh '''
ssh -o StrictHostKeyChecking=no ${API_SERVER_USER}@${API_SERVER_IP} "docker login ghcr.io -u ${GITHUB_ID} --password ${GITHUB_TOKEN}"
ssh -t ${API_SERVER_USER}@${API_SERVER_IP} "docker pull ${DOCKER_IMAGE}"
ssh -t ${API_SERVER_USER}@${API_SERVER_IP} "echo 'DOCKER_IMAGE=${DOCKER_IMAGE}' > ${SUBMODULE_REPOSITORY}/docker/.env && sudo sh ${SUBMODULE_REPOSITORY}/script/deploy.sh"
ssh -t ${API_SERVER_USER}@${API_SERVER_IP} "docker system prune -a -f || true"
'''
}
}
}
}
}
이번에 무중단 배포를 하면서 배포 코드를 Git Submodule
로 관리하였다.
Deploy
단계는 deploy.sh
에 배포 세부 단계를 구성하여 코드의 양이 줄었다!
또한 docker-compose
에서 도커 이미지 이름
을 환경 변수
로 받아야 하기에 .env
을 만들어 참조
하도록 하였다!
Gradle에 Actuator를 추가하자. 서버의 상태를 확인하는 용도다.
implementation 'org.springframework.boot:spring-boot-starter-actuator'
이 분의 코드를 많이 참고했다!!!
APP_NAME="test"
MAX_RETRIES=10 # health check를 재시도할 최대 횟수 설정
DOCKER_PATH="docker-compose.yaml_파일의_경로"
DOCKER_COMPOSE_CMD="/usr/local/bin/docker-compose"
# 컨테이너 스위칭 (활성 컨테이너를 blue 또는 green으로 전환)
switch_container() {
# 현재 blue 컨테이너가 활성 상태인지 확인
IS_BLUE=$(${DOCKER_COMPOSE_CMD} -p "${APP_NAME}-blue" -f ${DOCKER_PATH}docker-compose.blue.yaml ps | grep Up)
# blue 컨테이너가 비활성 상태이면 green에서 blue로 전환
if [ -z "$IS_BLUE" ]; then
echo "### GREEN => BLUE ###"
${DOCKER_COMPOSE_CMD} -p "${APP_NAME}-blue" -f ${DOCKER_PATH}docker-compose.blue.yaml up -d
BEFORE_COMPOSE_COLOR="green"
AFTER_COMPOSE_COLOR="blue"
# 컨테이너가 완전히 준비될 시간을 확보
sleep 20
# health check 수행 (blue 컨테이너)
health_check "http://127.0.0.1:9001/actuator/health"
else
echo "### BLUE => GREEN ###"
${DOCKER_COMPOSE_CMD} -p "${APP_NAME}-green" -f ${DOCKER_PATH}docker-compose.green.yaml up -d
BEFORE_COMPOSE_COLOR="blue"
AFTER_COMPOSE_COLOR="green"
# 컨테이너가 완전히 준비될 시간을 확보
sleep 30
# health check 수행 (green 컨테이너)
health_check "http://127.0.0.1:9091/actuator/health" # health check를 할 수 있는 URI를 인자로 명시
fi
}
# 컨테이너 상태 체크
health_check() {
local RETRIES=0 # 현재 시도 횟수
local URL=$1 # health check를 할 URL
# 지정된 시도 횟수만큼 반복
while [ $RETRIES -lt $MAX_RETRIES ]; do
echo "Checking service at $URL... (attempt: $((RETRIES+1)))"
sleep 3 # 응답 대기 시간
# JSON 응답 파싱
RESPONSE=$(curl -s "$URL")
if [ -n "$RESPONSE" ]; then
# JSON 응답에서 status 값 추출
STATUS=$(echo "$RESPONSE" | jq -r '.status')
# health check 성공
if [ "$STATUS" = "UP" ]; then
echo "health check success"
return 0
fi
fi
RETRIES=$((RETRIES+1))
done;
# 시도 횟수 초과 시 실패 메시지 출력하고 새 컨테이너 종료
echo "Failed to check service after $MAX_RETRIES attempts."
${DOCKER_COMPOSE_CMD} -p "${APP_NAME}-${AFTER_COMPOSE_COLOR}" -f ${DOCKER_PATH}docker-compose.${AFTER_COMPOSE_COLOR}.yaml down
echo "### DEPLOY FAILED ###"
exit 1
}
# 이전 컨테이너 종료
down_container() {
${DOCKER_COMPOSE_CMD} -p "${APP_NAME}-${BEFORE_COMPOSE_COLOR}" -f ${DOCKER_PATH}docker-compose.${BEFORE_COMPOSE_COLOR}.yaml down
echo "### $BEFORE_COMPOSE_COLOR DOWN ###"
}
switch_container
down_container
deploy.sh: line 8: docker-compose: command not found
이 에러가 뜨면 docker-compose
가 설치되어 있지 않거나, 실행 경로가 제대로 설정되어 있지 않기 때문이다.
# 1. PATH 설정
export PATH=$PATH:/usr/local/bin
# 2. 절대 경로 명시
/usr/local/bin/docker-compose -p "${APP_NAME}-blue" -f ${DOCKER_PATH}docker-compose.blue.yaml up -d
1, 2번 중 하나의 명령어를 선택하면 된다.
나의 경우 ssh로 접속할 때 export 명령어가 적용되지 않아 2번 방식을 사용하였다!
deploy.sh: line 40: jq: command not found
이 에러가 뜨면 jq 명령어가 설치되어 있지 않았기 때문이다.
# jq 설치
$ sudo yum install -y jq
version: '3.9'
services:
test-api:
image: '${DOCKER_IMAGE}'
container_name: test-blue
ports:
- '9001:8080'
networks:
- docker-inner-network
networks:
docker-inner-network:
external: true
version: '3.9'
services:
test-api:
image: '${DOCKER_IMAGE}'
container_name: test-green
ports:
- '9091:8080'
networks:
- docker-inner-network
networks:
docker-inner-network:
external: true
이미 생성되어 있는 도커 네트워크
중 docker-inner-network라는 이름의 네트워크에 연결하여 사용한다.
서버 로컬에서 도커로 실행 중인 컨테이너와 통신하기 위함이다.
배포될 때마다 위처럼 blue
, green
그룹의 서버가 교대로 동작되는 것을 확인할 수 있었다!
처음으로 무중단 배포를 적용시켜보았다.
아직 가벼운 연습용 프로젝트로 진행 중이지만, 서비스를 위한 프로젝트에서도 충분히 적용할 수 있을 것 같다.
도커를 활용하니 명령어도 비교적 쉬워서 금방 이해하고 적용할 수 있었다!
그러나 linux 명령어를 기초적인 것밖에 모르고, 쉘 스크립트는 작성해본 적이 없어서 막막했다.
구글링이 없었더라면 하기 너무 어려웠을 것이라 생각이들고, 참고에 적어놓은 링크에서 많은 도움을 받았다...!
그리고 아직 웹 서비스를 배포하기까지는 부족한 점이 많다.
다음에는 간단한 React 파일을 만들어서 Nginx로 배포해보고, 서버 개수도 2개 이상으로 늘려서 로드밸런싱도 적용해 볼 것이다!
Jenkins/Nginx로 무중단 배포 하기 2편 | HYK
Jenkins와 Docker, nginx를 이용한 자동 무중단 배포 🔥 | 초록
스프링부트 + 젠킨스 + Docker + Nginx CICD 무중단 배포 구축 | Elmo
Blue Green 무중단 배포 적용하기 | Chan Young Jeong
.sh file not recognizing docker-compose | reddit