[CICD] 블루그린 배포로 변경하기 (spring + nginx + github action)

2025. 3. 13. 13:58·Server/Docker

현재 상황 - 무중단 배포를 적용한 이유


[CICD 과정]

코드 머지 → github action 실행 → 재배포

현재 구조에서는 재배포할 때 기존의 docker container를 제거하고 새로운 container를 실행하는 과정에서 잠깐의 서버 다운타임이 발생한다. 개발할 당시엔 길어야 5초 정도 서버가 다운되는 것이라 큰 상관이 없지 않을까 생각했지만, 실제 서비스 환경에서는 잠깐이라도 서버가 다운되면 프론트엔드 요청을 처리할 수 없는 문제가 발생한다.

이를 방지하기 위해 무중단 배포(Zero Downtime Deployment)를 적용하여, 배포 중에도 트래픽이 끊기지 않고 지속적으로 서비스를 제공할 수 있도록 개선하였다.

 

어떻게 해결할 수 있나?


1. 롤링 배포(Roling Deployment)

롤링 배포는 서버를 한 대씩 순차적으로 업데이트하는 방법이다.
롤링 배포를 적용하려면 여러 개의 인스턴스를 운영하거나 추가적인 로드밸런서 설정을 해야한다. 현재는 단일 인스턴스에서 서비스를 운영하고 있기 때문에 롤링 배포는 적용이 어렵다 .

2. 카나리 배포(Canary Deployment)

카나리 배포는 새로운 버전(신버전)을 일부 사용자(소수의 트래픽)에게 먼저 배포한 후, 점진적으로 트래픽을 늘려가면서 전체 배포를 진행하는 방식이다.

기존 서버에 영향을 주지 않고 새로운 버전을 테스트할 수 있기 때문에 A/B 테스트나 특정 지표를 통계적으로 확인할 때 유용하다. 하지만 현재 환경에서는 단일 인스턴스에서 트래픽을 조절하기 어렵고, A/B 테스트와 같은 검증 과정이 필요하지 않기 때문에 카나리 배포는 적합하지 않다.

3. 블루/그린 배포(Blue/Green Deployment)

미리 새로운 버전(그린) 서버를 준비한 후, 기존 버전(블루)에서 트래픽을 신버전으로 전환한 뒤 구버전을 폐기하는 방식이다.

일반적으로 이 방식을 구현하려면 기존 서버와 동일한 사양의 추가적인 서버가 필요하지만, 단일 인스턴스 내에서 자원을 분할하고 Nginx를 활용하여 트래픽을 전환하는 방식으로도 블루그린 배포를 구현할 수 있다. 이러한 점을 고려했을 때 현재 운영 방식에서 가장 적합한 배포 방법이다.

 

블루그린 배포


1. workflow 수정

  • 기존
name: Java CI with Gradle

on:
  push:
    branches: [ "dev" ]

jobs:
  build:
    runs-on: ubuntu-22.04

    steps:
      - uses: actions/checkout@v4

      - name: create application.yml
        run: |
          mkdir ./src/main/resources
          cd ./src/main/resources

          touch ./application.yml
          echo "${{ secrets.APPLICATION_YML }}" >> ./application.yml

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle Wrapper
        run: ./gradlew -x test build

      - name: docker image build
        run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/{도커이미지이름} .

      - name: docker login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}

      - name: Docker Hub push
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/{도커이미지이름}

      - name: Deploy to EC2 via SSH
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
          script: |
            docker stop cocos-container || true
            docker rm cocos-container || true
            docker pull ${{ secrets.DOCKERHUB_USERNAME }}/{도커이미지이름}
            docker run -d -p 8080:8080 --name cocos-container \
              ${{ secrets.DOCKERHUB_USERNAME }}/{도커이미지이름}
  • 변경
    • docker build 시에 —no-cache 옵션을 추가하였다. 이전 버전이 빌드될 때의 캐시로 인해 오류가 나는 상황을 방지하기 위함이다. (배포 방식 변경과는 상관없이 오류를 방지하기 위해 추가하였다.)
    • script 부분의 Docker container 실행 부분을 shell 파일로 분리하였다. 해당 부분이 길어지고, 인스턴스 내부에서 상호작용하는 부분이 있기 때문에 파일을 분리하였다.
name: Java CI with Gradle

on:
  push:
    branches: [ "dev" ]

jobs:
  build:
    runs-on: ubuntu-22.04

    steps:
      - uses: actions/checkout@v4

      - name: create application.yml
        run: |
          mkdir ./src/main/resources
          cd ./src/main/resources

          touch ./application.yml
          echo "${{ secrets.APPLICATION_YML }}" >> ./application.yml

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle Wrapper
        run: ./gradlew -x test build

      - name: docker image build
        run: docker build --no-cache -t ${{ secrets.DOCKERHUB_USERNAME }}/{도커이미지이름} . # --no-cache 옵션 추가 (배포 방식과는 연관 X)

      - name: docker login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}

      - name: Docker Hub push
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/{도커이미지이름}

      - name: Deploy to EC2 via SSH
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
          script: | # script에서 docker container 실행하던 부분을 deploy.sh로 옮기고 deploy.sh을 실행
            echo "Starting deployment..."
            chmod +x /home/ubuntu/deploy.sh 
            /home/ubuntu/deploy.sh

2. shell 파일을 통한 docker container 실행

#!/bin/bash

BLUE_PORT=8081
GREEN_PORT=8082
DEPLOY_URL="http://localhost" #내부에 떴는지 테스트
HEALTH_CHECK_PATH="/api/dev/test/health-check"

CURRENT_CONTAINER=$(docker ps --filter "name=cocos-" --format "{{.Names}}")

if [[ -z "$CURRENT_CONTAINER" ]]; then
  echo "No active container found. Deploying BLUE instance."
  NEXT_PORT=$BLUE_PORT
  NEXT_NAME="cocos-blue"
else
  CURRENT_PORT=$(docker ps --filter "name=cocos-" --format "{{.Ports}}" | awk -F'->' '{print $1}' | awk -F':' '{print $2}')

  if [[ "$CURRENT_PORT" -eq "$BLUE_PORT" ]]; then
    NEXT_PORT=$GREEN_PORT
    NEXT_NAME="cocos-green"
  else
    NEXT_PORT=$BLUE_PORT
    NEXT_NAME="cocos-blue"
  fi
fi

HEALTH_CHECK_URL="${DEPLOY_URL}:${NEXT_PORT}${HEALTH_CHECK_PATH}"

echo "Pulling latest Docker image..."
docker pull {도커계정명}/{도커이미지명}

echo "Running new container on port $NEXT_PORT..."
docker run -d --restart unless-stopped -p $NEXT_PORT:8080 --name $NEXT_NAME {도커계정명}/{도커이미지명}

echo "Waiting for the new container to be healthy..."
for i in {1..45}; do
  sleep 2
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X GET "$HEALTH_CHECK_URL")
  if [[ "$STATUS" == "200" ]]; then
    echo "New container is healthy!"
    break
  fi
  if [[ "$i" == "45" ]]; then
    echo "Health check failed after multiple attempts. Rolling back..."
    docker stop $NEXT_NAME
    docker rm $NEXT_NAME
    exit 1
  fi
done

echo "Updating Nginx..."
if [[ "$NEXT_PORT" -eq "$BLUE_PORT" ]]; then
    sudo sed -i 's|proxy_pass http://localhost:[0-9]*;|proxy_pass http://localhost:8081;|' /etc/nginx/sites-enabled/default # 설정하고 있는 파일 경로를 넣어야 함
else
    sudo sed -i 's|proxy_pass http://localhost:[0-9]*;|proxy_pass http://localhost:8082;|' /etc/nginx/sites-enabled/default
fi

sudo systemctl reload nginx

echo "Stopping old container..."
docker stop "$CURRENT_CONTAINER" --time=5
docker rm "$CURRENT_CONTAINER"

3. nginx 설정 변경


  • nginx 파일에 proxy_pass 설정을 하고 있는 파일을 찾아야 한다.
    • 보통은 /etc/nginx/nginx.conf 파일에 server 블록을 추가하면 된다.
    • /etc/nginx/sites-enabled/default 파일에서 관리하고 있는 경우도 있기 때문에 해당 파일을 수정해야 할 수도 있다.
      • cat /etc/nginx/sites-enabled/default | grep "proxy_pass”를 통해 확인 필요
  • 위 파일에 proxy_pass 블록이 이미 있는 경우 추가할 필요 없다.
    • (deploy.sh 파일에서 찾아서 바꾸는 코드만 추가하면 된다)
...

http {
      ... 
    upstream backend {
            server localhost:8081 # 포트 번호가 shell 파일에서 변경한 포트로 업데이트되어야 함
    }

    server {
        listen 80;  
        server_name {도메인 이름};  # 도메인 이름 (실제 서비스 도메인으로 변경)

        location / {
            proxy_pass http://backend;  # proxy 부분 있는지 확인
        }
    }
}
  • nginx 설정 변경 후 반드시 다음 명령어를 통해 문법에 어긋나지 않는지 확인해야 한다.
    • sudo nginx -t
  • nginx 설정 변경 시 반드시 다음 명령어를 통해 변경 사항을 적용해야 한다.
    • sudo systemctl reload nginx

4. test


  1. docker ps -a 로 포트 설정값으로 돌고 있는지 확인
  2. 내부 url로 접속해서 접속되는지 확인
  3. curl -I http://localhost:8081/api/dev/test/health-check
  4. nginx에서 올바른 port로 proxy하고 있는지 확인
  5. curl -I https://배포url/api/dev/test/health-check

만약 nginx 설정(cors, 접속 오류 등)에서 오류 난다면, proxy 설정이 잘 안 되고 있을 가능성이 높다.

→ sudo tail -n 50 /var/log/nginx/error.log 에러 로그를 확인해서 어느부분에서 에러가 났는지 확인하자.
→ nginx 설정이 nginx.conf 파일과 sites-enabled/default 모두에서 되고 있을 경우 포트 설정잉 제대로 안 되고 있을 가능성이 있다. 이 경우 설정을 한 파일에서 하도록 변경해주면 된다.

 

 

 

 

++) 2탄도 있어요 (Docker image 경량화 도전기)

https://mylife-codinglife.tistory.com/236

저작자표시 비영리 변경금지 (새창열림)

'Server > Docker' 카테고리의 다른 글

[CICD] 도커 이미지 경량화하기 (CICD 구축기 2탄)  (0) 2025.10.03
DOCKER SERVICE FAILED TO START UNRAID,DOCKER LOG SHOWS "FATAL ERROR: FAULT,[SIGNAL SIGSEGV: SEGMENTATION VIOLATION CODE=0X20 ADDR= PC=]"  (0) 2023.01.10
'Server/Docker' 카테고리의 다른 글
  • [CICD] 도커 이미지 경량화하기 (CICD 구축기 2탄)
  • DOCKER SERVICE FAILED TO START UNRAID,DOCKER LOG SHOWS "FATAL ERROR: FAULT,[SIGNAL SIGSEGV: SEGMENTATION VIOLATION CODE=0X20 ADDR= PC=]"
YONJAAN
YONJAAN
코딩일기
  • YONJAAN
    마이라이프해피라이프
    YONJAAN
  • 전체
    오늘
    어제
    • 분류 전체보기 (40)
      • Server (4)
        • Docker (3)
        • Node (0)
        • Spring (0)
        • Django (1)
      • Algorithm (22)
        • Python (9)
        • C++ (13)
      • Front (0)
      • 컴퓨터 (0)
        • Go (0)
        • C++ (3)
      • Diary (9)
        • 휴학일기 (0)
        • 진로 탐색 (0)
        • 책 (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    GIT
    생각
    내가쓴글
    가십
    횡설수설
    도커 이미지 경량화
    아이즈원
    백준
    아름다운색
    오블완
    졸려
    티스토리챌린지
    소프트웨어마에스트로
    ㅇ
    작아지지말자
    소마
    docker
    SW마에스트로
    일기
    Soma
    합격
    CICD
    C++
    golang
    빛나는사람
    리액트
    공부기록
    노래추천
    공부나하러가이꼬맹아
    도커
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
YONJAAN
[CICD] 블루그린 배포로 변경하기 (spring + nginx + github action)
상단으로

티스토리툴바