Spring Blue/Green 무중단 배포하기(Jenkins with Jenkins Agent)
시작하기 앞서
나는 2개의 서버를 활용하여 무중단 배포를 구현하였다.
2개의 오라클 서버를 갖고 있어서 이왕이면 사용가능한 자원을 최대한 활용하고자 하였다.
아래 시스템은 전부 컨테이너 환경에서 실행하고 있다.
컨테이너 내부에서 호스트의 도커소켓의 권한을 얻어 다른 컨테이너들을 실행하고 종료한다.
하나의 서버에서 구현한다면 젠킨스 마스터만 있으면 되고 복잡함이 줄 것 같다.
1.BLUE / GREEN 이해하기
사용자에게 서비스를 제공할때 서버가 하나라면 해당 서버가 업데이트가 될때마다 새로운 빌드파일을 실행하기 위해 서버를 잠시 내렸다가 올려야한다.
잠깐이지만 사용자가 서비스를 이용할 수 없는 텀이 생긴다.
이러한 사용자 경험을 향상하기 위하여 새로운 업데이트가 서버에 배포될 때마다
가동 중이지 않은 그린 서버를 작동하고 안정화될 때까지 블루 서버가 유지되며
그린 서버가 안정화 되면 블루 서버는 종료되고
다음 업데이트 때 블루 서버가 그린 서버의 역할을 대신하며 이 과정이 배포에서 반복된다.

나의 서버는 컨테이너에서 실행되기 때문에 위 그림과 같이 6개의 흐름으로 나누었다.
1. IDE에서 작업한 프로젝트 결과물을 저장소에 올린다.
2. 저장소에 결과물이 반영되면 저장소가 젠킨스 컨트롤러에 웹훅 요청을 날린다.
3. 젠킨스 컨트롤러는 해당 프로젝트와 관련된 에이전트(노드)를 찾아 빌드 명령을 내린다.
4. 젠킨스 에이전트에서 빌드가 완료되면 리눅스 쉘 스크립트를 실행한다.
5. 쉘 스크립트를 이용하여 현재 가동중이지 않은 블루 또는 그린 서버를 가동한다.
6. 블루 또는 그린 서버가 정상적으로 가동되면 NGINX의 PROXY PASS가 새롭게 구성된 서버를 바라보게 되고 기존 서버는 종료한다.
블루와 그린을 번갈아가면서 사용해 배포중에도 서비스를 중단하지 않고 제공할 수 있다.
2. 깃허브 설정
웹훅
프로젝트 > Settings > Webhooks

Payload URL에 본인의 도메인과 이용중인 젠킨스 포트번호를 등록한다.
http 또는 https ://도메인:포트번호/github-webhook/
맨 아래에 Active 체크박스를 체크한다.
3. 젠킨스 설정
서버1에 젠킨스 서버2에 젠킨스 에이전트를 설치해준다.
젠킨스 마스터
도커 이미지
docker pull jenkins/jenkins:lts-jdk17
도커 실행
docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --restart=on-failure jenkins/jenkins:lts-jdk17
젠킨스 설정
에이전트 노드 생성
Dashboard > Jenkins 관리 > Nodes > New node

- name : 젠킨스 노드 관리에서 보여지는 이름
- Remote root directory : .env 에 설정된 작업 디렉토리를 따른다.
- Labels : 젠킨스 컨트롤러에서 스크립트 작성시 사용될 노드 이름

만들고 나면 위 사진과 같이 x표시가 되어있다 눌러서 들어가면

다음과 같이 secret키를 얻을 수 있다.
해당 secret키는 .env파일에 JENKINS_SECRET에 작성하면 된다.
젠킨스 에이전트
젠킨스 에이전트는 조금 복잡하다.
docker-compose.yml
version: '3.8'
services:
jenkins-agent:
container_name: jenkins-agent
init: true
build:
context: .
dockerfile: Dockerfile
env_file: .env
volumes:
# 호스트의 도커 소켓을 이용하기 위해 지정
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/bin/docker
# 호스트의 도커 그룹에 컨테이너 사용자를 추가하기 위해 사용
group_add:
- '999'
command: >
-url $JENKINS_URL
-workDir $JENKINS_AGENT_WORKDIR
-secret $JENKINS_SECRET
-name $JENKINS_AGENT_NAMEDockerfile
FROM jenkins/inbound-agent:latest
USER root
# 패키지 업데이트 및 필요한 도구 설치
RUN apt-get update && \
apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release
# 도커 공식 GPG 키 추가
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
# 도커 저장소 추가
RUN echo "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list
# 저장소 업데이트 및 도커 클라이언트 설치
RUN apt-get update && \
apt-get install -y docker-ce-cli
USER jenkins
.env
JENKINS_AGENT_WORKDIR=/home/jenkins
JENKINS_SECRET=젠킨스 컨트롤러에서 노드 생성시 발급된 시크릿 키
JENKINS_AGENT_NAME=노드네임
JENKINS_URL=http://도메인:포트번호젠킨스 연결 확인

밑줄 옆에 연결이 안되었다면 X표시로 나온다.
만약 x표시가 난다면 젠킨스 에이전트를 실행하는 쪽에서 로그를 확인해보자
docker logs <컨테이너 이름>
젠킨스 Credentials 추가
Jenkins관리 > Security > Credentials > (global) > Add Credentials

- ID는 파이프라인 잡에서 사용하기 때문에 적절하게 설정해서 입력한다.
- Secret에 DB 계정의 아이디 또는 비밀번호 입력
젠킨스 파이프라인 구축하기
대시보드에서 새로운 Item을 item name 입력후 Pipline으로 생성한다.

- Github project 체크 > 저장소 입력
- Github hook trigger for GITscm polling 체크
파이프라인 작성
배포 자동화의 가장 핵심적인 내용이다.
스프링부트 프로젝트 기준 DB 설정파일은 빌드할때 생성하여 만들고
빌드가 끝나면 마지막에 항상 삭제해준다.
pipeline {
agent {
node{
label 'mini-pc-node'
}
}
environment {
PROPERTIES_FILE = 'src/main/resources/application-db.properties'
}
stages {
stage('Checkout') {
steps {
git credentialsId: 'cannon397', url: 'https://github.com/cannon397/OracleServer.git'
}
}
stage('Prepare Properties') {
steps {
withCredentials([
string(credentialsId: 'db-username-id', variable: 'DB_USERNAME'),
string(credentialsId: 'db-password-id', variable: 'DB_PASSWORD')
]) {
script {
// db.properties 파일에 환경변수 적용
writeFile file: "${PROPERTIES_FILE}", text: """
spring.datasource.url=jdbc:mariadb://mariadb-container:3306/mydb
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
"""
}
}
}
}
stage('Build') {
steps {
sh 'chmod +x gradlew && ./gradlew clean build'
}
}
stage('Deploy') {
steps {
sh '''
chmod +x deploy.sh && ./deploy.sh
'''
}
}
}
post {
always {
sh "rm -f ${PROPERTIES_FILE}"
}
}
}deploy.sh 구성
배포에 실패했을때에 대한 예외처리는 없어서 스크립트 실행중 오류가나면 이전으로 돌아갈 수 없지만 빌드가 성공했다고 가정한다면 항상 성공하는 스크립트다.
sed로 특정패턴을 찾아서 nginx설정파일의 blue서버와 green서버를 변경 후 저장하는데 쉘스크립트로 변경하려하면 자꾸 수정권한이 없다 나온다.
그래서 임시파일을 만들고 교체 후 다시 삭제했다.
#!/bin/bash
# docker-compose.yml 서비스 이름 정의
service_name="blue"
# Docker Compose 시작 및 Nginx 설정 변경
deploy_and_reload() {
new_version=$1
old_version=$2
old_server=$3
new_server=$4
# Docker Compose 실행
if docker compose up -d --build $new_version; then
echo "$new_version version started successfully."
sleep 60 # 성공 시 1분 대기
# Nginx 설정 파일을 수정하고 리로드
if docker exec nginx-container sh -c \
"sed 's|$old_server|$new_server|g' /etc/nginx/conf.d/backend.conf > /etc/nginx/conf.d/backend.conf.temp \
&& cp /etc/nginx/conf.d/backend.conf.temp /etc/nginx/conf.d/backend.conf \
&& rm /etc/nginx/conf.d/backend.conf.temp \
&& nginx -s reload"; then
docker compose down $old_version
echo ""
else
echo "Failed to update Nginx configuration on $new_version."
exit 1
fi
else
echo "Failed to start $new_version version."
exit 1
fi
}
# Docker Compose를 사용하여 지정된 컨테이너가 실행 중인지 확인
is_active=$(docker compose ps -q $service_name)
# 실행 중인 컨테이너가 있는지 확인
if [ -n "$is_active" ]; then
deploy_and_reload "green" "blue" "spring-boot-container-blue" "spring-boot-container-green"
else
deploy_and_reload "blue" "green" "spring-boot-container-green" "spring-boot-container-blue"
fi
빌드 테스트

파이프라인이 정상적으로 실행되었다.
기존에 오라클 인스턴스에 배포하던 스크립트를 그대로 라벨만 변경하여 테스트용으로 홈서버에 배포해봤고 성공적으로 배포가 되었다.
4. 마무리
nginx와 db까지 작성하면 너무 길어질 것 같아 작성하지 않았지만 모두 컨테이너 환경으로 실행되고 있다.
도커 실행 장점
- 서로 같은 네트워크에 속해있는 도커 컨테이너는 컨테이너 이름을 도메인처럼 사용 가능하다.(지원되는 프로그램에 한하여)
- 도커 소켓을 활용하여 컨테이너 내부에서 호스트 도커 권한을 이용하여 다른 컨테이너를 조작할 수 있다.
도커 실행 단점
- 해당 컨테이너의 보안이 뚫리면 공격자가 호스트 도커 권한을 얻어 다른 컨테이너를 조작할 수 있어 위험하다.(도커API 사용 추천)
의문점
실제 서비스에서 사용자가 blue서버와 연결을 맺고 있는중에 green서버를 배포하고 nginx를 reload하면 blue서버에 연결되어 있는 사용자는 요청을 잃게 되지 않나 라는 생각을 했다.
알아본 결과 nginx에 worker process가 기존 요청은 마무리 짓고 새로운 설정에 대한 worker process를 하나 더 만들어서 중간에 proxy pass를 변경하여도 사용자에게 중단되지 않는 서비스를 제공할 수 있다고 한다. (이것도 블루 그린 배포인거 같음)
블루 그린 배포에 관심이 생기고 나서 적용하느라 애먹었는데 처음 적용하시는분께 조금이나마 도움이 되었으면 합니다.