홈 네트워크에서 젠킨스를 통한 무중단 서비스 배포 시스템 구축기

홈 네트워크에서 젠킨스를 통한 무중단 서비스 배포 시스템 구축기

2025-07-13
수정

홈 네트워크에서 효율적이고 안정적인 서비스 운영을 위해 젠킨스(Jenkins)를 활용한 무중단 배포 환경을 구축했습니다. 이 글에서는 인프라 구축의 전체 과정과 주요 구성 요소에 대해 상세히 설명하고, 실제 구현 과정에서 마주친 문제점과 해결 방법에 대해서도 공유하겠습니다. 홈 네트워크에서 무중단 배포 시스템을 직접 구축하고자 하는 분들에게 실질적인 도움이 되길 바랍니다.

배경 및 목적

CleanShot 2025-07-12 at 23.20.36@2x.pngCleanShot 2025-07-12 at 23.20.36@2x.png

GCP를 사용하다 보니 비용이 예상보다 많이 발생하는 문제가 있었습니다. 위 이미지에서 볼 수 있듯이 생각보다 많은 비용이 발생하고 있었고, 장기적인 관점에서 비용 효율성을 고려할 때 자체 인프라 구축이 더 합리적이라고 판단했습니다. 특히 소규모 프로젝트나 개인 서비스의 경우, 클라우드 서비스의 유연성보다 비용 절감이 더 중요한 요소로 작용하였습니다.

IMG_0883.jpgIMG_0883.jpg

다행히 알리익스프레스 할인 기간에 좋은 가격으로 하드웨어를 구매할 수 있었고, 이를 활용해 자체 서버 환경을 구축하기로 결정했습니다. 초기 투자 비용은 발생했지만, 장기적으로 보았을 때 클라우드 서비스보다 훨씬 경제적이며 인프라에 대한 완전한 제어권을 가질 수 있다는 이점이 있었습니다. 또한 이 과정에서 인프라 구축과 관련된 다양한 기술을 습득할 수 있는 기회가 되었습니다.

시스템 아키텍처

무중단 배포를 위한 시스템은 크게 다음과 같은 구성 요소로 설계하였습니다. 미니PC를 구매하면서 시놀로지 나스도 함께 구매하게 되어 방화벽, 리버스 프록시, SSL 인증서는 시놀로지에 맡기도록 하였습니다. 시놀로지가 없었다면, 미니PC의 Nginx에서 이러한 설정을 추가로 해야 했을 것입니다.

아래와 같이 각 구성 요소는 독립적으로 작동하면서도 유기적으로 연결되어 완전한 CI/CD 파이프라인을 형성합니다:

  • 젠킨스(CI/CD 파이프라인): 코드 통합, 테스트, 빌드, 배포를 자동화하는 핵심 엔진으로, 전체 개발 워크플로우를 관리합니다.
  • Nginx(로드 밸런서 및 리버스 프록시): 들어오는 트래픽을 적절히 분산시키고, 블루-그린 배포 전략을 실현하기 위한 중요한 구성 요소입니다.
  • 시놀로지 NAS(SSL 및 리버스 프록시 관리): 보안 연결을 위한 SSL 인증서 관리와 외부에서의 안전한 접근을 위한 추가 리버스 프록시 레이어를 제공합니다.
  • n8n Webhook(자동 배포 트리거): 코드 변경을 감지하고 자동으로 배포 프로세스를 시작하는 트리거 메커니즘으로, 개발자의 수동 개입 없이 지속적 배포를 가능하게 합니다.

목표 주요 단계

배포 파이프라인의 주요 단계는 다음과 같으며, 각 단계는 실패 시 즉시 중단되도록 설정했습니다:

  1. n8n에서 GitHub 릴리즈 감지 및 배포 트리거: 개발자가 새로운 버전을 릴리즈하면 n8n에서 내부 Webhook을 통해 젠킨스에 이벤트를 전송합니다.
  2. 젠킨스에서 소스 코드 가져오기: 젠킨스는 GitHub에서 최신 코드를 클론하거나 풀(pull)하여 작업 공간에 가져옵니다.
  3. 애플리케이션 빌드 및 테스트: 소스 코드를 컴파일하고, 단위 테스트와 통합 테스트를 실행하여 코드 품질을 검증합니다. 이 단계에서 코드 분석 도구를 활용하여 잠재적인 버그나 보안 취약점을 미리 발견할 수 있습니다.
  4. Blue/Green Docker 이미지 생성: 빌드된 애플리케이션을 매 배포시마다 교차적으로 Blue/Green Docker 이미지로 패키징합니다.
  5. 새 버전 배포 및 Nginx 설정 업데이트: 새로운 Docker 컨테이너를 실행하고, 정상 작동을 확인한 후 Nginx 설정 파일을 업데이트합니다. 정상 작동하지 않는 경우 즉시 배포가 중단됩니다.
  6. 이전 버전에서 새 버전으로 트래픽 전환(무중단 배포): Nginx를 리로드하여 트래픽을 새 버전으로 전환합니다. 이 과정에서 기존 연결은 유지되고, 새로운 요청만 새 버전으로 라우팅되어 서비스 중단이 발생하지 않습니다.

1. 젠킨스 설정

젠킨스는 Docker 컨테이너로 구동되며, 다음과 같은 docker-compose 설정을 사용했습니다. 컨테이너화된 젠킨스를 사용함으로써 설치 및 관리가 용이하며, 필요시 쉽게 백업 및 복원이 가능합니다. 또한 다른 환경으로의 이전도 컨테이너 이미지와 볼륨 데이터만 옮기면 되어 매우 편리합니다.

docker-compose.yaml

services:
  jenkins:
    build: .
    container_name: jenkins_cicd
    restart: unless-stopped
    user: root
    privileged: true
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
      - ./jenkins/config:/var/jenkins_home/config
      - ./scripts:/var/jenkins_home/scripts
    environment:
      - DOCKER_HOST=unix:///var/run/docker.sock
      - JAVA_OPTS=-Djenkins.install.runSetupWizard=false
      - TZ=Asia/Seoul
    command: >
      sh -c "
        # 필수 도구 설치
        apt-get update &&
        apt-get -y install sshpass &&
        curl -sSL \"https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose &&
        # 호스트의 docker 그룹 GID 확인 및 매칭
        DOCKER_GID=$$(stat -c '%g' /var/run/docker.sock) &&
        groupadd -f -g $$DOCKER_GID docker &&
        usermod -aG docker jenkins &&
        # Jenkins 시작
        /usr/local/bin/jenkins.sh
      "

volumes:
  jenkins_home:

위 설정은 젠킨스가 Docker 컨테이너 내에서 Docker 명령어를 사용할 수 있도록 호스트의 Docker 소켓을 마운트하고, 필요한 권한을 부여합니다. 이는 Docker-in-Docker 패턴을 사용하지 않고도 컨테이너 내에서 Docker 명령을 실행할 수 있게 해주는 중요한 설정입니다. 또한 젠킨스 홈 디렉토리를 볼륨으로 마운트하여 컨테이너가 재시작되더라도 설정과 작업 히스토리가 유지되도록 했습니다.

Dockerfile

FROM jenkins/jenkins:jdk17

USER root

# Docker CLI 설치
RUN apt-get update && \
    apt-get -y install apt-transport-https ca-certificates curl gnupg2 software-properties-common lsb-release && \
    curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
    apt-get update && \
    apt-get -y install docker-ce-cli && \
    rm -rf /var/lib/apt/lists/*

# Node.js 설치
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
    apt-get install -y nodejs && \
    rm -rf /var/lib/apt/lists/*

USER jenkins

젠킨스 이미지에 Docker CLI와 Node.js를 추가로 설치하여 다양한 애플리케이션을 빌드하고 배포할 수 있도록 했습니다. 기본 젠킨스 이미지만으로는 특정 언어나 프레임워크를 위한 빌드 도구가 부족하기 때문에, 필요한 도구들을 Dockerfile을 통해 추가했습니다. 특히 Node.js는 프론트엔드 애플리케이션 빌드에 필수적이며, Docker CLI는 빌드된 애플리케이션을 컨테이너화하는 데 필요합니다. 이렇게 사용자 정의 이미지를 만들어 필요한 모든 도구가 사전 설치된 환경을 구성했습니다.

2. Nginx 설정

docker-compose.yaml

networks:
  gateway-network:
    # 이 네트워크는 외부에서 생성되어야 함을 의미합니다.
    name: gateway-network
    external: true

services:
  nginx:
    image: nginx:alpine
    container_name: nginx-loadbalancer
    restart: unless-stopped
    ports:
      # 도메인 기반 리버스 프록시 (외부 80 -> 내부 80, 외부 443 -> 내부 443)
      - "80:80"
      - "443:443"
      # 백엔드 API 서비스 (외부 9090 -> 내부 9090)
      - "9090:9090"
      # 프론트엔드 서비스 (외부 5050 -> 내부 81)
      - "5050:81"
      ...
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/ssl:/etc/nginx/ssl
    networks:
      - gateway-network

Nginx는 로드 밸런서 역할을 하며, 다음과 같은 설정을 사용했습니다. Nginx는 들어오는 요청을 여러 백엔드 서버로 분산시키는 로드 밸런서 역할뿐만 아니라, 정적 콘텐츠 제공, SSL 종료, 요청/응답 변환 등 다양한 기능을 수행합니다. 특히 무중단 배포를 위해서는 트래픽을 새 버전과 이전 버전 사이에서 원활하게 전환할 수 있어야 하는데, Nginx가 이러한 역할을 완벽하게 수행합니다.

이 설정은 백엔드 API와 프론트엔드 서비스에 대한 프록시 설정을 포함하고 있습니다. 각 서비스마다 별도의 포트를 할당하여 서비스 간 분리를 명확히 했으며, gateway-network라는 Docker 네트워크를 통해 다른 컨테이너들과 통신할 수 있도록 구성했습니다. 이렇게 분리된 구성은 각 서비스의 독립적인 배포와 스케일링을 가능하게 하며, 한 서비스의 문제가 다른 서비스에 영향을 미치지 않도록 합니다.

nginx.conf

nginx.conf

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    keepalive_timeout  65;

    # 이 부분이 conf.d 디렉터리의 모든 .conf 파일을 읽어옵니다.
    include /etc/nginx/conf.d/*.conf;
}%

기본 Nginx 설정 파일로 로그 포맷과 성능 관련 설정을 포함합니다. 이 파일에서는 워커 프로세스 수, 연결 처리 방식, 로깅 형식 등 Nginx의 핵심 동작을 정의합니다. 특히 로그 형식은 추후 모니터링과 트러블슈팅에 중요한 정보를 제공하므로, 필요한 모든 정보가 포함되도록 구성했습니다. 또한 성능 최적화를 위한 sendfile 및 keepalive 설정도 포함되어 있어, 높은 트래픽 상황에서도 안정적인 성능을 발휘할 수 있습니다.

API 서비스 설정

/nginx/conf.d/api.conf

server {
    listen 9090;

    # --- Real IP 모듈 설정 ---
    # Docker 네트워크 대역을 신뢰할 수 있는 프록시로 설정합니다.
    # 이 설정을 통해 X-Forwarded-For 헤더의 IP를 실제 클라이언트 IP로 인식합니다.
    ...
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;

    # --- CORS Origin Whitelist ---
    # 허용된 Origin 목록
    ...

    # 동적 URL 설정을 포함합니다.
    # 이 파일은 deploy.sh 스크립트에 의해 생성/수정됩니다.
    include /etc/nginx/conf.d/api_url.inc;

    location / {
        # --- CORS 설정 ---
        # Preflight(OPTIONS) 요청을 처리합니다.
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $allowed_origin;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            return 204;
        }

        # 실제 요청에 CORS 헤더를 추가합니다.
        add_header 'Access-Control-Allow-Origin' $allowed_origin always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;

        resolver 127.0.0.11 valid=10s;
        proxy_pass $service_url;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}%

/nginx/conf.d/api_url.inc

set $service_url http://localhost:8082;

백엔드 API 서비스를 위한 Nginx 설정으로, CORS 및 실제 IP 주소 관리를 처리합니다. CORS(Cross-Origin Resource Sharing) 설정은 프론트엔드 애플리케이션이 API를 안전하게 호출할 수 있도록 해주는 중요한 부분입니다. 특히 허용된 Origin 목록을 명시적으로 관리하여 보안을 강화했습니다. 또한 real_ip 모듈을 사용하여 프록시 환경에서도 클라이언트의 실제 IP 주소를 애플리케이션에 전달할 수 있도록 했습니다. 이는 로깅과 보안 모니터링에 있어 매우 중요한 정보입니다.

동적 URL 설정을 위해 별도의 include 파일을 사용하여 배포 스크립트에서 쉽게 업데이트할 수 있도록 했습니다. 이 방식은 무중단 배포 과정에서 트래픽을 새 버전의 서비스로 전환할 때 매우 유용합니다. 배포 스크립트는 새 버전의 서비스가 준비되면 이 include 파일만 업데이트하고 Nginx를 리로드하여, 기존 연결에 영향을 주지 않으면서도 새로운 요청은 새 버전으로 라우팅되도록 합니다. 이러한 동적 설정 방식은 블루-그린 배포 전략의 핵심 요소입니다.

3. 서버 배포 파이프라인 설정

이제 각 배포 파이프라인 구성 요소들을 하나씩 살펴보겠습니다. 블루-그린 배포 전략을 구현하기 위해 Spring Boot 애플리케이션, Jenkins 파이프라인, Docker 컨테이너를 조합하여 완전 자동화된 무중단 배포 시스템을 구축했습니다.

애플리케이션 프로필 설정

application-blue.yml

spring:
  config:
    activate:
      on-profile: blue
server:
  port: 8082 # Blue-Green 배포를 위한 Blue 서버 포트

application-green.yml

spring:
  config:
    activate:
      on-profile: green
server:
  port: 8083 # Blue-Green 배포를 위한 Green 서버 포트

먼저 블루-그린 배포를 위해 Spring Boot 애플리케이션에 두 개의 프로필을 정의했습니다. 각 프로필은 서로 다른 포트에서 실행되어 두 버전의 애플리케이션이 동시에 실행될 수 있도록 합니다.

위 코드에서 볼 수 있듯이, Blue 프로필은 8082 포트에서, Green 프로필은 8083 포트에서 실행됩니다. 이렇게 별도의 포트로 구성함으로써 한 버전에서 다른 버전으로 트래픽을 전환할 때 사용자 경험을 방해하지 않고 원활하게 전환할 수 있습니다.

Jenkins 파이프라인 구성

Jenkinsfile

// Jenkinsfile
pipeline {
    agent any

    tools {
        jdk 'JDK17'     // 젠킨스에 설정한 JDK 도구 이름
        gradle 'Gradle7' // 젠킨스에 설정한 Gradle 도구 이름
    }

    environment {
        // .env 파일의 내용을 Jenkins Credentials를 통해 주입합니다.
        // 예: DB_USERNAME = credentials('mysql-username-credential-id')
        //     DB_PASSWORD = credentials('mysql-password-credential-id')
    }

    stages {
        stage('Checkout') {
            steps {
                // GitHub에서 소스 코드 가져오기
                checkout scm
            }
        }

        stage('Build') {
            steps {
                // Gradle 빌드 (테스트 제외)
                sh './gradlew clean build -x test'
            }
        }

        stage('Deploy') {
            steps {
                // 배포 스크립트에 실행 권한 부여 및 실행
                sh 'chmod +x ./scripts/deploy.sh'
                sh './scripts/deploy.sh'
            }
        }
    }

    post {
        always {
            echo 'Pipeline finished.'
            // 빌드 후 생성된 파일 정리 등
            cleanWs()
        }
    }
}

Jenkins 파이프라인은 지속적 통합과 배포를 자동화하는 핵심 구성 요소입니다. Jenkinsfile은 파이프라인의 모든 단계를 선언적으로 정의하여 코드로서의 인프라(Infrastructure as Code) 원칙을 따릅니다.

파이프라인은 크게 세 단계로 나뉩니다:

  • Checkout 단계: 소스 코드를 가져오는 단계로, 보안을 위해 GitHub 자격 증명을 Jenkins Credential에서 안전하게 관리합니다. 서브모듈까지 함께 클론하여 완전한 코드베이스를 확보합니다.
  • Build 단계: 애플리케이션을 빌드하는 단계로, Gradle 빌드 스크립트를 실행합니다. 빌드 결과물은 JAR 파일 형태로 생성됩니다.
  • Deploy 단계: 빌드된 애플리케이션을 배포하는 단계로, 애플리케이션 설정 파일과 같은 민감한 정보는 Jenkins Credential을 통해 안전하게 주입됩니다. 이후 배포 스크립트가 실행되어 블루-그린 배포 전략에 따라 애플리케이션을 배포합니다.

또한 파이프라인이 종료된 후에는 항상 작업 공간을 정리하여 디스크 공간을 효율적으로 관리하고, 배포 결과에 따른 알림을 제공합니다.

블루-그린 배포 스크립트

deploy.sh

#!/bin/bash

# Blue-Green 배포 스크립트

# 현재 실행 중인 컨테이너를 확인하여 현재 Profile 결정
# `docker-compose ps -q spring-blue`의 결과가 있으면 blue가 실행 중인 것
if [ -n "$(docker-compose ps -q spring-blue)" ]; then
    CURRENT_SERVICE_URL="http://spring-blue:8080"
else
    # blue가 없으면 green이 실행 중이거나 아무것도 없는 초기 상태
    CURRENT_SERVICE_URL="http://spring-green:8080"
fi

if [[ $CURRENT_SERVICE_URL == *"blue"* ]]; then
    CURRENT_PROFILE="blue"
    IDLE_PROFILE="green"
    IDLE_PORT=8083
    IDLE_ACTUATOR_PORT=9093
else
    # 기본값 또는 green일 경우
    CURRENT_PROFILE="green"
    IDLE_PROFILE="blue"
    IDLE_PORT=8082
    IDLE_ACTUATOR_PORT=9092
fi

echo "현재 실행 Profile: $CURRENT_PROFILE"
echo "배포 Profile: $IDLE_PROFILE"

# Idle Profile 컨테이너 실행
docker-compose up -d --build spring-$IDLE_PROFILE

# 헬스체크 (컨테이너 내부에서 실행)
for i in {1..10}; do
    echo "헬스체크 시도... ($i/10)"
    # docker-compose exec를 사용하여 컨테이너 내부에서 헬스 체크 실행
    # Alpine에는 curl이 없을 수 있으므로 wget 사용
    docker-compose exec spring-$IDLE_PROFILE wget --quiet --spider http://localhost:9090/actuator/health
    
    # wget의 종료 코드로 성공 여부 확인 (0이면 성공)
    if [ $? -eq 0 ]; then
        echo "✅ 헬스체크 성공"

        # 임시 Nginx 설정 파일 생성
        TEMP_NGINX_CONFIG="/tmp/api_url.inc"
        # SERVER_HOST는 Jenkins에서 전달받은 서버의 IP 또는 호스트명입니다.
        echo "set \$service_url http://$SERVER_HOST:$IDLE_PORT;" > $TEMP_NGINX_CONFIG
        echo "임시 Nginx 설정 파일 생성 완료: $IDLE_PROFILE"

        # scp를 사용하여 원격 서버의 임시 경로로 설정 파일 복사
        REMOTE_TEMP_PATH="/tmp/api_url.inc"
        echo "원격 서버의 임시 경로로 설정 파일 복사 중..."
        sshpass -p $NGINX_PASSWORD scp -o StrictHostKeyChecking=no $TEMP_NGINX_CONFIG ${NGINX_USER}@${SERVER_HOST}:${REMOTE_TEMP_PATH}

        # ssh를 사용하여 파일을 최종 위치로 이동하고 Nginx 리로드
        echo "원격 Nginx 설정 적용 및 리로드 중..."
        sshpass -p $NGINX_PASSWORD ssh -o StrictHostKeyChecking=no ${NGINX_USER}@${SERVER_HOST} \
            "cd /home/okdohyuk/nginx-loadbalancer && mkdir -p ./nginx/conf.d && mv ${REMOTE_TEMP_PATH} ./nginx/conf.d/api_url.inc && docker-compose exec nginx nginx -s reload"
        echo "✅ 원격 Nginx 리로드 완료. 트래픽 전환 성공"

        # 이전 버전 컨테이너 종료
        docker-compose stop spring-$CURRENT_PROFILE
        echo "이전 버전 ($CURRENT_PROFILE) 컨테이너 종료"
        exit 0
    fi
    sleep 10
done

echo "❌ 헬스체크 실패"

echo "상세 정보를 확인하기 위해 마지막 헬스체크를 다시 시도합니다..."
# wget의 상세 옵션을 사용하여 응답 확인
docker-compose exec spring-$IDLE_PROFILE wget -S --spider http://localhost:9090/actuator/health

# 헬스체크 실패 시, 원인 파악을 위해 컨테이너의 로그를 출력합니다.
echo "배포 실패. $IDLE_PROFILE 컨테이너의 로그를 확인합니다."
docker-compose logs spring-$IDLE_PROFILE

echo "배포 실패. $IDLE_PROFILE 컨테이너를 종료합니다."
docker-compose stop spring-$IDLE_PROFILE
exit 1

deploy.sh 스크립트는 블루-그린 배포 전략의 핵심 로직을 구현합니다. 이 스크립트는 현재 실행 중인 서비스를 확인하고, 비활성 상태인 환경(Blue 또는 Green)에 새 버전을 배포한 다음, 새 버전이 정상적으로 작동하는지 확인한 후 트래픽을 전환합니다.

주요 단계는 다음과 같습니다:

  • 현재 프로필 확인: docker-compose 명령어를 통해 현재 실행 중인 컨테이너를 확인하고, 그에 따라 배포할 대상 프로필(Idle Profile)을 결정합니다.
  • 새 버전 배포: 비활성 프로필에 새 버전의 애플리케이션을 빌드하고 실행합니다.
  • 헬스 체크: 새 버전이 정상적으로 실행되는지 Spring Boot Actuator의 health 엔드포인트를 통해 확인합니다.
  • 트래픽 전환: 헬스 체크가 성공하면, Nginx 설정 파일을 업데이트하여 트래픽을 새 버전으로 전환합니다. 이 과정에서 sshpass와 scp를 사용하여 원격 서버에 설정 파일을 전송합니다.
  • 이전 버전 종료: 트래픽 전환이 완료되면 이전 버전의 컨테이너를 종료합니다.

이 스크립트는 배포 과정에서 발생할 수 있는 다양한 오류 상황을 처리하고, 실패 시 로그를 출력하여 문제 해결에 도움을 줍니다.

Docker Compose 구성

docker-compose.yaml

version: "3.8"

services:
  spring-blue:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: spring-blue
    restart: unless-stopped
    ports:
      - "8082:8080"
      - "9092:9090"
    environment:
      - SPRING_PROFILES_ACTIVE=blue
    networks:
      - gateway-network

  spring-green:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: spring-green
    restart: unless-stopped
    ports:
      - "8083:8080"
      - "9093:9090"
    environment:
      - SPRING_PROFILES_ACTIVE=green
    networks:
      - gateway-network
      
networks:
  gateway-network:
    name: gateway-network
    driver: bridge

docker-compose.yml 파일은 애플리케이션의 컨테이너 환경을 정의합니다. Blue와 Green 두 개의 서비스를 정의하여 블루-그린 배포를 위한 기반을 제공합니다.

각 서비스는 다음과 같은 특징을 가집니다:

  • 독립적인 포트 매핑: 각 서비스는 호스트의 다른 포트에 매핑되어 독립적으로 접근 가능합니다.
  • 프로필 환경변수: SPRING_PROFILES_ACTIVE 환경변수를 통해 각 컨테이너가 어떤 프로필로 실행될지 결정합니다.
  • 네트워크 구성: gateway-network라는 이름의 브릿지 네트워크를 통해 컨테이너 간 통신이 가능하도록 구성했습니다.

주석 처리된 부분은 MySQL 데이터베이스 설정으로, 필요에 따라 활성화하여 사용할 수 있습니다. 이는 애플리케이션의 확장성을 고려한 설계입니다.

Dockerfile

Dockerfile

FROM openjdk:17-jdk-alpine

RUN mkdir -p /software

ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} /software/app.jar

#  application.yml: Spring Boot가 외부 설정을 참조할 수 있도록 /config 디렉터리에 복사합니다.
#    Spring Boot는 기본적으로 jar파일 외부의 /config 디렉토리를 탐색합니다.
COPY config/application.yml /config/application.yml

CMD java -Dserver.port=${PORT} $JAVA_OPTS -jar /software/app.jar

Dockerfile은 애플리케이션 컨테이너 이미지를 정의합니다. OpenJDK 17을 기반으로 하는 알파인 리눅스 이미지를 사용하여 경량화된 컨테이너를 구성했습니다.

주요 특징은 다음과 같습니다:

  • 애플리케이션 JAR 파일 복사: 빌드된 JAR 파일을 컨테이너 내부의 /software 디렉토리로 복사합니다.
  • 설정 파일 복사: application.yml 설정 파일을 Spring Boot가 기본적으로 탐색하는 /config 디렉토리에 복사합니다.
  • 유연한 포트 설정: 환경변수를 통해 서버 포트를 동적으로 설정할 수 있도록 구성했습니다.

이러한 구성을 통해 동일한 Dockerfile로 Blue와 Green 두 환경 모두를 구축할 수 있으며, 환경변수와 설정 파일만 달리하여 다양한 환경에 배포할 수 있는 유연성을 확보했습니다.

무중단 배포의 작동 원리

위에서 설명한 모든 구성 요소들이 함께 작동하여 다음과 같은 무중단 배포 흐름을 구현합니다:

  • 1. 코드 변경 감지: Jenkins가 GitHub 저장소의 변경을 감지하면 파이프라인이 자동으로 시작됩니다.
  • 2. 새 버전 빌드: 소스 코드를 가져와 새 버전의 애플리케이션을 빌드합니다.
  • 3. 비활성 환경 배포: 현재 활성화되지 않은 환경(Blue 또는 Green)에 새 버전을 배포합니다.
  • 4. 새 버전 검증: 새 버전이 정상적으로 작동하는지 확인합니다.
  • 5. 트래픽 전환: Nginx 설정을 업데이트하여 트래픽을 새 버전으로 전환합니다.
  • 6. 이전 버전 종료: 트래픽 전환이 완료되면 이전 버전을 종료합니다.

이 전체 과정은 사용자 경험에 영향을 주지 않고 자동으로 이루어지며, 배포 과정에서 오류가 발생하면 자동으로 롤백되어 안정성을 보장합니다.

4. 젠킨스 웹 파이프라인 설정

필수 플러그인 목록

  1. PipelineIDworkflow-aggregator설명: Jenkins 파이프라인을 생성하고 실행하기 위한 핵심 플러그인 모음입니다.
  2. GitIDgit설명: Git 저장소에서 소스 코드를 체크아웃하고 scm 변수를 사용하는 데 필요합니다.
  3. CredentialsIDcredentials설명: Jenkins에서 각종 인증 정보(ID/비밀번호, Secret 파일 등)를 안전하게 관리합니다.
  4. Credentials BindingIDcredentials-binding설명withCredentials 단계를 제공하여 파이프라인 스크립트 내에서 인증 정보를 환경 변수나 파일로 안전하게 사용할 수 있도록 합니다.
  5. Workspace CleanupIDws-cleanup설명cleanWs() 단계를 제공하여 파이프라인 실행이 끝난 후 작업 공간(workspace)을 깨끗하게 정리합니다.
  6. JDK ToolIDjdk-tool설명tools 섹션에서 JDK 버전을 명시하고 관리하는 데 사용됩니다.
  7. GradleIDgradle설명tools 섹션에서 Gradle 버전을 명시하고 관리하는 데 사용됩니다.

시스템 도구 설정 (Global Tool Configuration)

젠킨스가 프로젝트를 빌드하는 데 필요한 도구들을 설정합니다.

  • 위치Jenkins 관리 >Tools
  • JDK:
    • Add JDK를 클릭하고, 을 선택합니다.
      Install from java.sun.com
    • 이름을 지정하고 (예: ), 프로젝트와 동일한 버전의 JDK(Java 17)를 선택하여 설치합니다.
      JDK17
  • Gradle:
    • Add Gradle을 클릭하고, 를 선택합니다.
      Install from Gradle.org
    • 이름을 지정하고 (예: ), 프로젝트의  파일에 명시된 버전과 일치하는 Gradle 버전을 선택하여 설치합니다.
      Gradle7
      gradle/wrapper/gradle-wrapper.properties

인증 정보 관리 (Credentials)

젠킨스가 GitHub 레포지토리에 접근할 수 있도록 인증 정보를 등록합니다.

  • 위치: Jenkins 관리 > Credentials > System > Global credentials
  • Add Credentials:
    • Kind: Username with password
    • Username: GitHub 사용자명을 입력합니다.
    • Password: GitHub에서 발급받은 Personal Access Token (PAT)를 입력합니다. (스코프 권한 필요)
    • ID: 젠킨스 파이프라인에서 사용할 ID를 지정합니다. (예: github-credentials)

젠킨스 파이프라인(Pipeline) 생성

CleanShot 2025-07-06 at 15.34.04@2x.pngCleanShot 2025-07-06 at 15.34.04@2x.png

CleanShot 2025-07-06 at 15.35.07@2x.pngCleanShot 2025-07-06 at 15.35.07@2x.png

실제 CI/CD 프로세스를 정의하는 파이프라인을 생성합니다.

  1. 새로운 Item 생성: ◦ 젠킨스 메인 화면에서 New Item을 클릭합니다. ◦ 아이템 이름을 입력하고 (예: okdohyuk-api-pipeline), Pipeline을 선택한 후 OK를 클릭합니다.
  2. 파이프라인 설정: ◦ GeneralGitHub project를 체크하고, 프로젝트의 GitHub 레포지토리 URL을 입력합니다. ◦ Build TriggersGitHub hook trigger for GITScm polling을 체크합니다. (GitHub에서 Webhook 설정 시 자동으로 빌드를 시작하게 해줍니다.) (저는 젠킨스가 외부망에 노출되고 싶지 않아서 n8n을 경유해주었습니다.) ◦ Pipeline: ▪ DefinitionPipeline script from SCM을 선택합니다. ▪ SCMGit을 선택합니다. ▪ Repository URL: 프로젝트의 .git URL을 입력합니다. (예:  https://github.com/okdohyuk/okdohyuk-api.git) ▪ Credentials: 위에서 등록한 GitHub 인증 정보(github-credentials)를 선택합니다. ▪ Branch Specifier: 빌드할 브랜치를 지정합니다. (예: */main) ▪ Script PathJenkinsfile (기본값)

5. 시놀로지 NAS 활용

CleanShot 2025-07-13 at 23.14.08@2x.pngCleanShot 2025-07-13 at 23.14.08@2x.png

CleanShot 2025-07-13 at 21.37.26@2x.pngCleanShot 2025-07-13 at 21.37.26@2x.png

CleanShot 2025-07-06 at 23.02.05@2x.pngCleanShot 2025-07-06 at 23.02.05@2x.png

CleanShot 2025-07-06 at 23.02.46@2x.pngCleanShot 2025-07-06 at 23.02.46@2x.png

시놀로지 NAS를 사용하여 SSL 인증서 관리와 외부 접근을 위한 리버스 프록시를 설정했습니다. 시놀로지 NAS는 단순한 저장 장치 이상의 기능을 제공하며, 내장된 웹 서버와 리버스 프록시 기능을 활용하면 별도의 서버 없이도 SSL 인증서 관리와 외부 접근 관리가 가능합니다. 특히 Let's Encrypt와의 통합으로 SSL 인증서의 자동 갱신이 가능하여 인증서 만료로 인한 서비스 중단 위험을 최소화할 수 있습니다.

시놀로지의 제어판에서 다음과 같은 설정을 진행했으며, 각 설정은 보안과 안정성을 최우선으로 고려했습니다:

  • SSL 인증서 관리 및 갱신: Let's Encrypt 인증서를 자동으로 발급받고 주기적으로 갱신하도록 설정하여 항상 유효한 SSL 연결을 보장합니다.
  • 도메인별 리버스 프록시 설정: 각 서비스마다 서브도메인을 할당하고, 해당 도메인으로 들어오는 요청을 내부 서비스로 적절히 라우팅합니다.
  • 보안 설정 및 접근 제어: IP 기반 접근 제한, 요청 필터링, 속도 제한 등을 설정하여 보안을 강화하고 잠재적인 공격으로부터 서비스를 보호합니다.

6. 자동화된 배포 트리거

CleanShot 2025-07-13 at 14.56.26@2x.pngCleanShot 2025-07-13 at 14.56.26@2x.png

보통 배포 트리거를 하기위해서 GitHub Webhook을 설정하여 코드 변경 시 젠킨스 파이프라인이 자동으로 트리거되도록 구성합니다. 하지만 jenkins를 내부 네트워크에서만 사용하고싶었고, 이를 해결하기위해 n8n의 github trigger 노드를 사용하였습니다. 덕분에 개발자는 코드를 푸시하기만 하면 나머지 과정(빌드, 테스트, 배포)이 자동으로 진행되어 개발 생산성이 크게 향상되었습니다.

결론

젠킨스와 Nginx를 활용한 무중단 배포 환경을 구축함으로써 다음과 같은 구체적인 이점을 얻을 수 있었으며, 이는 서비스 품질과 개발 생산성 모두에 긍정적인 영향을 미쳤습니다:

  • 서비스 중단 없는 안정적인 배포: 사용자 경험을 해치지 않으면서 새로운 기능과 버그 수정을 신속하게 적용할 수 있게 되었습니다. 특히 트래픽이 많은 시간대에도 안전하게 배포할 수 있어 운영의 유연성이 크게 향상되었습니다.
  • 클라우드 비용 절감: 초기 하드웨어 투자 이후 월간 운영 비용이 GCP 대비 100% 감소(전기세 별도)했으며, 장기적으로 더 큰 비용 효율을 얻을 수 있을 것으로 예상됩니다.
  • 자동화된 CI/CD 파이프라인: 수동 배포 과정에서 발생할 수 있는 인적 오류를 최소화하고, 개발자가 코드 작성에 더 집중할 수 있는 환경을 제공합니다. 또한 배포 주기가 단축되어 새로운 기능이나 버그 수정을 빠르게 제공할 수 있게 되었습니다.
  • 확장 가능한 인프라 구조: 현재 시스템은 필요에 따라 쉽게 확장할 수 있도록 설계되었으며, 추가 서비스나 마이크로서비스 아키텍처로의 전환도 기존 인프라를 크게 변경하지 않고 수용할 수 있습니다.

앞으로는 모니터링 및 알림 시스템을 추가하고, 롤백 프로세스를 개선하여 더욱 안정적인 시스템을 구축할 계획입니다. 특히 Prometheus와 Grafana를 활용한 실시간 모니터링 시스템을 도입하여 서비스 상태를 시각적으로 파악하고, 잠재적인 문제를 조기에 발견할 수 있도록 할 예정입니다. 또한 자동화된 롤백 메커니즘을 구현하여 배포 후 문제가 발생했을 때 신속하게 이전 버전으로 복원할 수 있는 안전망을 강화할 것입니다. 이러한 개선을 통해 더욱 견고하고 신뢰할 수 있는 서비스 인프라를 구축해 나갈 계획입니다.