본문 바로가기
Spring Framework/스프링

(3) Jenkins 스프링 무중단 CI/CD 배포 구현 - Docker Blue&Green, Jenkins 빌드설정

by 스코리아 2024. 7. 15.
반응형

안녕하세요, 스코리아입니다.

오늘은 Jenkins를 이용한 스프링 무중단 CI/CD 배포, 세 번째 시간: Docker Blue&Green, Jenkins 빌드 파일 설정에 대한 포스팅입니다.

 

Dockerfile

목차

(1) Jenkins 소개 및 Docker에 Jenkins 설치 : 바로가기

(2) Jenkins에 Github 연동 + credentials 파일(application-prod.yml) 추가 + gradle 설치 : 바로가기

(3) <현재> Jenkins 빌드 Item 추가 + Docker, Jenkins 설정 파일 작성 + Nginx 로드밸런싱 설정

(4) Jenkins와 Slack 연동 : 바로가기

 

 

Jenkins에 빌드 Item 추가하기

Dashboard(홈) -> 왼쪽 탭의 '새로운 Item' 클릭

 

item name에는 Organization(조직)의 이름이나 Github UserId 등 원하는 이름을 입력해 주세요.

Github App과의 연동을 위해서 Organization Folder를 선택해 주세요.

OK 버튼을 누르고, 'Projects - Repository Sources' 섹션으로 이동해 주세요.

처음에 'Add' 버튼을 누르고 'Github Organization'을 선택합니다.

  • API endpoint : 비워두면 자동으로 채워집니다.
  • Credentials : 저번 시간에 만든 Github App Credential을 선택합니다.
  • Owner: 자신의 Github ID 혹은 Orignization 이름을 입력합니다.
  • Behaviors: (선택) Repositories 쪽에 'Filter by name(with wildcards)'을 'Add'하여 Include에 원하는 Repository 이름만 가져오게끔 설정할 수 있습니다. 처음에 Github App을 Install 할 때 이미 특정 Repository만 허용하였다면, 해당 내용은 따로 설정하지 않아도 됩니다.

'Save' 버튼을 누르고 저장합니다. 그러면 아래와 같이 'Sacn Organization Log'를 확인할 수 있는데, 처음에는 JenkinsFile을 찾을 수 없다고 Fail 할 것입니다. 아직 Github Repository에 JenkinsFile을 추가하지 않았기 때문입니다.

 

 

 

빌드를 위한 스크립트 파일 준비

파일 경로

스크립트 파일을 총 5개 만들 예정이며, 경로는 아래와 같습니다.

  • 프로젝트경로/docker/docker-compose.blue.yml
  • 프로젝트경로/docker/docker-compose.green.yml
  • 프로젝트경로/docker/Dockerfile
  • 프로젝트경로/scripts/deploy.sh
  • 프로젝트경로/Jenkinsfile

1. Docker Compose 파일 (Blue&Green)

우리는 스프링 무중단 서비스 배포를 위해 Blue Docker 영역과 Green Docker 영역이 필요합니다. 여기서 두 도커의 포트는 달라야 합니다.

 

먼저 docker-compose.blue.yml 파일입니다.

  • build: 현재 디렉터리의 Dockerfile을 사용하여 Docker 이미지를 빌드하기 위해서 '.'으로 설정하였습니다.
  • ports: 호스트의 8111 포트와 컨테이너의 80 포트를 매핑하였습니다. 즉, 스프링 application-prod.yml 에는 'server.port=80'으로 설정하고, Docker는 8111 포트로 통신할 수 있도록 매핑한 것입니다. 여러분이 원하는 Port로 설정해 주세요.
  • container_name: 컨테이너 이름은 알아보기 쉽도록 'spring-프로젝트이름-blue'와 같이 설정해 주세요.
  • networks: 도커로 띄워진 스프링과 도커로 띄워진 Redis/MariaDB 서버가 소통하기 위해 같은 도커 네트워크에 위치시켰습니다. 아래와 같은 명령어를 통해서 새로운 도커 Network을 생성할 수 있습니다.
docker network create servernetwork
version: '3'
services:
  backend:
    # 현재 디렉토리에서의 Dockerfile을 사용하여 Docker 이미지를 빌드
    build: .
    # 호스트의 8111 포트와 컨테이너의 80 포트를 매핑
    ports:
      - "8111:80"
    # 컨테이너의 이름
    container_name: spring-snapcampus-blue
    environment:
      - TZ=Asia/Seoul
    networks:
      - servernetwork

networks:
  servernetwork:
    external: true
    driver: bridge

 

다음으로 docker-compose.green.yml 파일입니다.

  • ports: 호스트의 8112 포트와 컨테이너의 80 포트를 매핑하였습니다. Blue(8111)와는 다른 포트로 설정해 주세요.
version: '3'
services:
  backend:
    build: .
    ports:
      - "8112:80"
    container_name: spring-snapcampus-green
    environment:
      - TZ=Asia/Seoul
    networks:
      - servernetwork

networks:
  servernetwork:
    external: true
    driver: bridge

 

2. Dockerfile 파일

Dockerfile은 Docker 이미지를 생성하기 위한 스크립트나 명령어들의 집합입니다.

Java JDK 17을 사용하여 Docker가 실행될 때 java 명령어를 이용하여 스프링부트 서버가 실행될 수 있도록 설정해야 합니다.

 

  • openjdk:17-jdk-slim 이미지를 사용합니다.
  • 현재 디렉터리에서 뒤가 '. jar'로 끝나는 파일을 도커 이미지 내부에 spring-prod.jar 이름으로 복사합니다.
  • Docker 컨테이너가 시작될 때 spring-prod.jar 파일이 실행될 수 있도록 명령어를 지정합니다.
# Java 17 베이스 이미지 사용
FROM openjdk:17-jdk-slim

# Dockerfile 내에서 사용할 변수 JAR_FILE을 정의
ARG JAR_FILE=*.jar

# JAR_FILE 경로에 해당하는 파일을 Docker 이미지 내부로 복사
COPY ${JAR_FILE} spring-prod.jar

# Docker 컨테이너가 시작될 때 실행할 명령을 지정
ENTRYPOINT ["java", "-jar", "/spring-prod.jar"]

 

3. deploy.sh 파일

deploy.sh는 무중단 서비스를 위해 Blue&Green 도커를 제어합니다.

  • Blue가 켜져 있을 경우, Green을 실행시킨 뒤 30초 후에 Blue를 종료시킵니다. 
  • Green이 켜져 있을 경우, Blue을 실행시킨 뒤 30초 후에 Green을 종료시킵니다.

여기서 30초는 스프링부트가 완전히 실행되는 평균적인 시간입니다. 실제 서비스에서는 'Health Check'을 통해서 진행하는 것이 좋습니다.

 

  • 첫 시간에 Jenkins docker를 설치할 때, 'docker-compose.yml' 파일에서 설정한 '/var/jenkins_home' 매핑 경로가 있을 겁니다. 저는 '/volume1/docker/jenkins'으로 설정하였는데요. 해당 경로로 이동하여, custom 디렉터리를 만들고 그 안에 프로젝트이름(ex: snapcampus) 디렉터리를 만들어주세요.
    • deploy.sh 파일의 cd 뒷부분을 해당 디렉터리 경로로 수정해 주세요.
    • (Docker 내부에서 Docker 외부 경로(호스트)를 접근하지 못하기 때문에, Docker 내부에 디렉터리를 만들었습니다)
  • DOCKER_APP_NAME : docker-compose.blue.yml, docker-compose.green-yml 파일에서 설정한 컨테이너 이름의 앞부분입니다. blue의 컨테이너 이름이 spring-snapcampus-blue라면, 앞부분인 spring-snapcampus만 입력해 주세요.
  • 나머지 부분은 파일에 주석을 달아두었습니다.
#!/bin/bash
# 작업 디렉토리를 /var/jenkins_home/custom/snapcampus으로 변경
cd /var/jenkins_home/custom/snapcampus

# 환경변수 DOCKER_APP_NAME : 컨테이너 메인 이름
DOCKER_APP_NAME=spring-snapcampus
LOG_FILE=./deploy.log

# 실행중인 blue가 있는지 확인
# 프로젝트의 실행 중인 컨테이너를 확인하고, 해당 컨테이너가 실행 중인지 여부를 EXIST_BLUE 변수에 저장
EXIST_BLUE=$(docker-compose -p "${DOCKER_APP_NAME}-blue" -f docker-compose.blue.yml ps | grep -E "Up|running")

# 배포 시작한 날짜와 시간을 기록
echo "배포 시작일자 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> $LOG_FILE


# green이 실행중이면 blue up
# EXIST_BLUE 변수가 비어있는지 확인
if [ -z "$EXIST_BLUE" ]; then

  # 로그 파일에 "blue up - blue 배포 : port:8081"이라는 내용을 추가
  echo "blue 배포 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> $LOG_FILE

  # docker-compose.blue.yml 파일을 사용하여 blue 컨테이너를 빌드하고 실행
  docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d --build

  # 30초 동안 대기
  sleep 30

  # 로그 파일에 "green 중단 시작"이라는 내용을 추가
  echo "green 중단 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> $LOG_FILE

  # docker-compose.green.yml 파일을 사용하여 green 컨테이너를 중지
  docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml down

   # 사용하지 않는 이미지 삭제
  docker image prune -af

  echo "green 중단 완료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> $LOG_FILE

# blue가 실행중이면 green up
else
  echo "green 배포 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> $LOG_FILE
  docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml up -d --build

  sleep 30

  echo "blue 중단 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> $LOG_FILE
  docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml down
  docker image prune -af

  echo "blue 중단 완료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> $LOG_FILE

fi
  echo "배포 종료  : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> $LOG_FILE

  echo "===================== 배포 완료 =====================" >> $LOG_FILE
  echo >> $LOG_FILE
  • EXISTS_BLUE 변수: 처음에 grep 'Up'으로만 설정하였는데, Blue가 켜져 있는데도 불구하고 Blue가 켜져있지 않다고 판단하였습니다.
    • 로그를 찍어보았는데, docker의 status 상태가 'Up'이 아닌 'Running'으로 표시되는 경우도 있다는 것을 파악하였습니다.
    • 그래서 해당 부분을 grep -E "Up|running"으로 수정하여 Up 혹은 Running이라고 표시되어 있을 때 Blue가 켜져 있다고 판단시켰습니다.

 

4. Jenkinsfile 파일

Jenkinsfile은 Jenkins에서 Github push 감지 후, Github Repository를 내려받은 후에 가장 먼저 실행하는 파일입니다. 'Pipeline 스크립트'라고 합니다.

 

동작 과정은 다음과 같습니다.

  1. gradle clean을 통해서 이전 빌드 파일이 현재 빌드에 의존하지 않도록 이전 빌드 파일들을 모두 삭제합니다.
  2. Jenkins credential에 등록했던 application-prod.yml 파일을 gradle 빌드 전에 끼워 넣습니다.
  3. 빌드를 테스트하여 새로운 코드에 문제가 없는지 확인합니다.
  4. 위에서 만들었던 docker-compose.blue.yml, docker-compose.green.yml, Dockerfile, deploy.sh 파일 그리고, 빌드가 끝난 jar파일을 'SCRIPT_PATH' 경로로 복사합니다. 이때 deploy.sh를 실행하기 위해 +x (실행권한)을 주고, 실행시킵니다.

이 과정에서 하나라도 오류가 발생하면, 빌드는 즉시 중단됩니다.


Jenkinsfile에서 여러분이 수정하셔야 할 부분은 다음과 같습니다.

  • SCRIPT_PATH : deploy.sh 생성 시 만들었던 디렉터리 경로입니다. (deploy.sh 파일의 cd 뒷부분 경로)
  • tools->gradle: 2번째 시간에 'Jenkins에 Gradle 설치' 섹션에서 만들었던 이름입니다. 저는 그때 'gradle 8.6' 이름으로 지정하였습니다.
  • stage('Replace Prod Properties') : 'snapCampusProd'라고 적힌 3곳을 2번째 시간에 ''application-prod.yml' 파일을 Jenkins Credentials에 올리기' 섹션에서 만들었던 credential ID로 바꿔주세요.
pipeline{
    agent any
    environment {
        SCRIPT_PATH = '/var/jenkins_home/custom/snapcampus'
    }
    tools {
        gradle 'gradle 8.6'
    }
    stages{
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        stage('Prepare'){
            steps {
                sh 'gradle clean'
            }
        }
        stage('Replace Prod Properties') {
            steps {
                withCredentials([file(credentialsId: 'snapCampusProd', variable: 'snapCampusProd')]) {
                    script {
                        sh 'cp $snapCampusProd ./src/main/resources/application-prod.yml'
                    }
                }
            }
        }
        stage('Build') {
            steps {
                sh 'gradle build -x test'
            }
        }
        stage('Test') {
            steps {
                sh 'gradle test'
            }
        }
        stage('Deploy') {
            steps {
                sh '''
                    cp ./docker/docker-compose.blue.yml ${SCRIPT_PATH}
                    cp ./docker/docker-compose.green.yml ${SCRIPT_PATH}
                    cp ./docker/Dockerfile ${SCRIPT_PATH}
                    cp ./scripts/deploy.sh ${SCRIPT_PATH}
                    cp ./build/libs/*.jar ${SCRIPT_PATH}
                    chmod +x ${SCRIPT_PATH}/deploy.sh
                    ${SCRIPT_PATH}/deploy.sh
                '''
            }
        }
    }
}

 

 

Nginx Blue&Green 로드밸런싱 설정

Blue와 Green 스프링 도커 중 유저가 Healthy 한 상태의 도커서버에 연결할 수 있도록 Nginx 로드밸런싱 설정이 필요합니다.

저 같은 경우, 집에 있는 Synology DSM 7 서버를 사용하고 있어서 'Nginx Proxy Manager'를 쓰고 있습니다. (시놀로지를 사용할 경우 DSM 7 및 NPM Docker 최신버전이 필요합니다) 기본적인 Nginx와 설정은 유사합니다.


Nginx의 경우, /etc/nginx/nginx.conf 파일을 열고 http { } 부분 안에 아래 내용을 추가해 주시고, Nginx Proxy Manager의 경우 NPM설치경로/nginx/custom/http_top.conf (없으면 만들기) 파일을 열고 아래 내용을 추가해 주세요.

  • 'spring-프로젝트이름-server'와 같이 이름을 직접 설정해 주세요.
  • '172.30.1.10'과 같이 내부에서 접속할 수 있는 주소로 바꿔주세요.
  • Blue&Green 포트로 바꿔주세요
upstream spring-snapcampus-server {
    server 172.30.1.10:8111;
    server 172.30.1.10:8112;
}

 

Nginx의 경우 /etc/nginx/nginx.conf 파일, Nginx Proxy Manager의 경우 NPM설치경로/nginx/proxy_host/[숫자].conf (숫자는 프록시 서버의 순서 번호) 파일을 열고, 'location /' 부분을 찾아, 아래 내용으로 대체해 주세요.

  • proxy_pass : 위에서 설정한 'http://Upstream이름'으로 수정해 주세요.
location / {
    add_header       X-Served-By $host;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Scheme $scheme;
    proxy_set_header X-Forwarded-Proto  $scheme;
    proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP          $remote_addr;
    proxy_pass       http://spring-snapcampus-server;
    proxy_redirect   off;
}

 

이렇게 설정하면 Upstream 서버가 Blue&Green 중 살아있는 서버로 자동 접속하게끔 해줍니다.

이제 아래 명령어를 통해 Nginx 설정을 다시 불러오겠습니다.

nginx -s reload

 

 

Github push 테스트! (드디어)

모든 설정을 마쳤습니다. 이제 Github push를 통해 Jenkins에서 CI/CD가 일어나 배포까지 잘되는지 테스트해 보겠습니다.

 

Github push를 하고나서, Jenkins에서 'Item 이름' 클릭 -> 'Repository 이름' 클릭 -> 'Branch 이름' 클릭 -> Build History 부분을 보시면, 빌드 기록을 확인할 수 있습니다. 저는 18번째 빌드를 시도했기 때문에 #18로 뜨는데요, #18을 눌러보겠습니다.

 

눌러보시면, 실시간으로 Console Output (콘솔 출력 내역)을 확인할 수 있습니다.

어떠한 부분에서 오류가 발생하였는지 바로 확인할 수 있어서 편합니다.

 

빌드를 성공적으로 마쳤습니다!!

 


지금까지 Jenkins 빌드 Item 추가 및 Docker Blue&Green, Jenkins 스크립트 파일을 작성하고 Nginx 로드밸런싱 작업까지 해보았습니다. Blue&Green 무중단 서비스까지 설명을 하려다 보니 포스팅 내용이 길어진 것 같습니다.

 

다음 시간에는 Jenkins 빌드 후 빌드 결과를 Slack으로 받아보는 방법에 대해서 알아보겠습니다.

긴 글 읽어주셔서, 감사합니다.

 

다음글 바로가기 >

반응형