안녕하세요, 스코리아입니다.
오늘은 Jenkins를 이용한 스프링 무중단 CI/CD 배포, 세 번째 시간: Docker Blue&Green, Jenkins 빌드 파일 설정에 대한 포스팅입니다.
목차
(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 스크립트'라고 합니다.
동작 과정은 다음과 같습니다.
- gradle clean을 통해서 이전 빌드 파일이 현재 빌드에 의존하지 않도록 이전 빌드 파일들을 모두 삭제합니다.
- Jenkins credential에 등록했던 application-prod.yml 파일을 gradle 빌드 전에 끼워 넣습니다.
- 빌드를 테스트하여 새로운 코드에 문제가 없는지 확인합니다.
- 위에서 만들었던 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으로 받아보는 방법에 대해서 알아보겠습니다.
긴 글 읽어주셔서, 감사합니다.
다음글 바로가기 >
'Spring Framework > 스프링' 카테고리의 다른 글
스프링 AWS S3 이미지/파일 업로드 방법 (30) | 2024.07.30 |
---|---|
(4) Jenkins 스프링 무중단 CI/CD 배포 구현 - Slack 연동 (0) | 2024.07.20 |
(2) Jenkins 스프링 무중단 CI/CD 배포 구현 - Github 연동/Credentials (0) | 2024.07.14 |
(1) Jenkins 스프링 무중단 CI/CD 배포 구현 - Jenkins Docker 설치 (0) | 2024.07.13 |
Cloudfront CORS preflight 에러 해결방법 (스프링 API 서버) (5) | 2024.07.09 |