[CI/CD] Jenkins와 Docker를 활용한 무중단 배포

General Dong·2024년 11월 11일
0

CI/CD

목록 보기
8/12
post-thumbnail

무중단 배포 방식

Blue/Green 배포

현재 기존 아키텍처에서는 로드 밸런서를 두지 않았다.
그래서 로드 밸런서가 필수가 아닌 Blue/Green 배포 방식으로 무중단 배포를 설계할 것이다.
위와 같이 서비스 중인 환경과 새로 배포되는 환경을 BlueGreen으로 칭하고 교차하여 배포할 것이다.

Blue/Green 배포와 다른 무중단 배포 방식에 대해 자세히 알고 싶다면 여기를 클릭해주세요!

무중단 배포 적용

Pipeline

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 의존성 추가

Gradle에 Actuator를 추가하자. 서버의 상태를 확인하는 용도다.

implementation 'org.springframework.boot:spring-boot-starter-actuator'

deploy.sh

이 분의 코드를 많이 참고했다!!!

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

해당 스크립트에서 발생한 에러

docker-compose 미인식 문제

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번 방식을 사용하였다!

jq 미설치

deploy.sh: line 40: jq: command not found
이 에러가 뜨면 jq 명령어가 설치되어 있지 않았기 때문이다.

# jq 설치
$ sudo yum install -y jq

docker-compose

docker-compose.blue.yaml

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

docker-compose.green.yaml

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

profile
개발에 대한 기록과 복습을 위한 블로그 | Back-end Developer

0개의 댓글