프로그래밍/개발 일지

[CD/CI] blue-green 배포 공부해보기 2

jtw7977 2026. 1. 14. 03:03

1. 글의 목적

이전글에 이어 blue-green 배포에 대해서 공부하고 실습하고 정리하고 있었다. 글이 길어져서 2편으로 나누게 되었는데 이번 편에서는 blue-green 버전 나누기, 롤백, 실행 자동화를 실습하고 정리해보려고 한다.

 

2. 전체 아키텍처 한 장 요약

[ GitHub Actions ]
  ├─ 1. 코드 변경 감지
  ├─ 2. Docker 이미지 빌드
  ├─ 3. Docker Hub에 :deploy 푸시
  └─ 4. 서버에 "배포 시작" 신호 (ssh)
                ↓
[ Server ]
  ├─ 현재 Active 색상 판별
  ├─ 반대편(Target) 결정
  ├─ Target 컨테이너만 업데이트
  ├─ 헬스체크
  └─ 성공 시 Nginx 전환 (실패 시 롤백)

(made by chatGPT)

 

3. 서버에서 준비하기

이전에 켜둔 docker 컨테이너들을 docker compose down으로 모두 종료하자.

다음 파일들을 /usr/local/bin/ 경로에 만들자. 파일을 만들때 sudo nano /usr/local/bin/<file_name> 을 이용해 만들어 주자. sudo가 필수다!

sudo chmod +x <file_name>를 실행해서 실행 권한을 주는 것도 잊지 말자!

 

먼저 다음과 같이 nginx 설정 파일을 준비하자.

server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://localhost:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

/etc/nginx/sites-available/blue.conf 에 저장하자.

 

server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://localhost:3002;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

/etc/nginx/sites-available/green.conf 에 저장하자.

 

sudo rm -f /etc/nginx/sites-enabled/*
sudo ln -s /etc/nginx/sites-available/blue.conf \
  /etc/nginx/sites-enabled/active.conf

sudo nginx -t && sudo systemctl reload nginx

 

이것을 통해 직접 설정파일을 수정핮 않고 심볼릭링크만 수정하여 blue-green간 스위치가 가능하다.

 

① detect-active.sh

역할

현재 사용자가 보고 있는 색상(Active)을 판별

하는 일

  • Nginx 설정 파일을 읽음
  • proxy_pass가 가리키는 포트 확인
  • 결과: blue 또는 green

왜 필요한가?

  • 사람 기억 ❌
  • 자동화 판단의 출발점
#!/bin/bash
set -e

ACTIVE_LINK=$(readlink /etc/nginx/sites-enabled/active.conf)

case "$ACTIVE_LINK" in
  *blue.conf)
    echo "blue"
    ;;
  *green.conf)
    echo "green"
    ;;
  *)
    echo "unknown"
    exit 1
    ;;
esac

 

② get-target.sh

역할

이번 배포 대상(Target)을 결정

하는 일

  • detect-active.sh 결과를 뒤집음
  • Active가 blue → Target은 green
  • Active가 green → Target은 blue

왜 필요한가?

  • 운영 중인 컨테이너 보호
  • “반대편에만 배포” 보장
#!/bin/bash

ACTIVE=$(/usr/local/bin/detect-active.sh)

if [ "$ACTIVE" = "blue" ]; then
  echo "green"
elif [ "$ACTIVE" = "green" ]; then
  echo "blue"
else
  echo "error"
  exit 1
fi

 

③ deploy-target.sh

역할

반대편(Target) 컨테이너만 새 버전으로 교체

하는 일

  1. Docker Hub에서 :deploy 이미지 pull
  2. 이를 :blue 또는 :green으로 tag
  3. docker-compose로 해당 서비스만 재시작

왜 필요한가?

  • GitHub Actions는 색상 모름
  • 서버에서 색상에 맞게 “옮겨 심기”
#!/bin/bash
set -e

TARGET=$(/usr/local/bin/get-target.sh)

echo "Deploying to $TARGET"

docker pull <your_id>/blue-green-test:deploy # <your_id> 를 자신의 docker id로 바꾸세요.
docker tag <your_id>/blue-green-test:deploy <your_id>/blue-green-test:$TARGET

docker compose up -d $TARGET

④ health-check.sh

역할

배포된 Target이 정상인지 확인

하는 일

  • Target 포트로 HTTP 요청
  • 200 OK 아니면 실패 처리

왜 필요한가?

  • “배포 성공” ≠ “서비스 정상”
  • 전환 전 마지막 안전장치
#!/bin/bash

TARGET=$(/usr/local/bin/get-target.sh)

if [ "$TARGET" = "blue" ]; then
  PORT=3001
else
  PORT=3002
fi

echo "Health check on port $PORT..."

for i in {1..10}; do
  if curl -f http://localhost:$PORT/health > /dev/null; then
    echo "Health check passed"
    exit 0
  fi

  echo "Waiting for app... ($i)"
  sleep 1
done

echo "Health check failed"
exit 1

⑤ switch.sh

역할

Nginx 트래픽을 Target으로 전환

하는 일

  • proxy_pass 포트를 Target 쪽으로 변경
  • nginx -t
  • reload

왜 필요한가?

  • 무중단의 핵심
  • 컨테이너 재시작 ❌
#!/bin/bash
set -e

TARGET=$(/usr/local/bin/get-target.sh)

if [ "$TARGET" != "blue" ] && [ "$TARGET" != "green" ]; then
  echo "Usage: switch.sh blue|green"
  exit 1
fi

echo "Switching traffic to $TARGET..."

sudo ln -sf /etc/nginx/sites-available/$TARGET.conf \
  /etc/nginx/sites-enabled/active.conf

sudo nginx -t
sudo systemctl reload nginx

echo "Traffic switched to $TARGET"

⑥ blue-green-deploy.sh (총괄 스크립트)

역할

전체 Blue-Green 배포 흐름을 한 번에 실행

실행 순서

 
deploy-target → health-check → switch

특징

  • 하나라도 실패하면 즉시 중단
  • Active 쪽은 절대 건드리지 않음
#!/bin/bash
set -e

ACTIVE=$(/usr/local/bin/detect-active.sh)
TARGET=$(/usr/local/bin/get-target.sh)

/usr/local/bin/deploy-target.sh

docker compose up -d $TARGET

if /usr/local/bin/health-check.sh $TARGET; then
  /usr/local/bin/switch.sh $TARGET
  echo "Deployment succeeded"
else
  echo "Health check failed. Rolling back."
  docker compose stop $TARGET
  exit 1
fi

 

 

 

4. GitHub Actions가 하는 일 & 코드

  1. 코드 변경 감지 (push)
  2. Docker 이미지 빌드
  3. Docker Hub에 항상 동일한 태그 :deploy로 푸시
  4. 서버에 SSH로 접속해 blue-green-deploy.sh 실행

 

name: Blue-Green Deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push deploy image
        run: |
          docker build -t your-id/blue-green-test:deploy .
          docker push your-id/blue-green-test:deploy

      - name: Trigger deploy on server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /srv/blue-green
            /usr/local/bin/blue-green-deploy.sh

 

 

5. docker-compose.yml 수정

services:
  blue:
    image: <your_id>/blue-green-test:blue # 본인의 Docker ID로 수정하세요
    ports:
      - "3001:3000"
    environment:
      - COLOR=blue

  green:
    image: <your_id>/blue-green-test:green # 본인의 Docker ID로 수정하세요
    ports:      - "3002:3000"
    environment:
      - COLOR=green

6. Docker Hub 태그의 역할 정리

태그 의미 업데이트 시점
:deploy 이번 배포 후보 (항상 최신) GitHub Actions 빌드 직후
:blue 이전 안정 버전 서버 배포 시점 (deploy-target.sh)
:green 현재 운영 or 대기 버전 서버 배포 시점 (deploy-target.sh)

 

 

7. 직접 실행

// app.js
const express = require("express");
const app = express();

const COLOR = process.env.COLOR || "unknown";

app.get("/", (req, res) => {
  res.send(`Hello from ${COLOR} v3!`);
});

app.get("/health", (req, res) => {
  res.send("OK");
});

app.listen(3000, () => {
  console.log(`Server running on ${COLOR}`);
});

앱의 코드를 다음과 같이 바꾸고 깃허브에 push 하자. 이전코드에서는 /health 라우터가 없었는데 꼭 넣어야 한다!!

깃허브 액션이 실행되고 

다음과 같이 Blue-Green deployment completed. 가 뜬다면 성공이다. 이전과 같이 브라우저에 http://<server_ip>에 접속해본다면 Hello from green v3! 가 뜨면 잘 된 것이다. (이전에 3002포트가 nginx에 되어 있었다면 blue가 나올 것이다.)

 

8. 의도적인 롤백 발생시키기

의도하지는 않았지만 blue-green-deploy.sh를 수정중 실수로 인해 에러가 발생하였다.

위 로그를 보면 health-check 실패 시 switch.sh로 넘어가지 않고 즉시 docker compose stop을 실행하여 잘못된 컨테이너를 정리하는 것을 볼 수 있다. 이 덕분에 트래픽은 여전히 안전한 'Green' 환경에 머물러 있게 된다.

9. 신버전에 버그 등으로 인해 이전 버전으로 돌려야 한다면?

sudo /usr/local/bin/switch.sh

 

이 코드로 트래픽을 이전버전으로 돌린후 코드를 수정하고 다시 올리면 끝!

10. 마무리

막연하게만 느껴졌던 무중단 배포를 내 손으로 직접 구현해 보면서, CI/CD가 단순히 코드를 옮기는 것이 아니라 안전하게 서비스를 지속하는 기술이라는 것을 깨달았다.

물론 이번 실습은 단일 서버 안에서 이루어졌지만 실제 운영 환경에서는 여러 대의 서버나 쿠버네티스(K8s) 환경에서 더 복잡하게 돌아갈 것 같다고 생각했다. 하지만 그 핵심 원리는 오늘 배운 Blue-Green과 크게 다르지 않다는 자신감을 얻을 수 있었습니다.

에러 메시지를 보며 당황했던 시간만큼 제 실력도 한 뼘 자란 것 같아 뿌듯하네요. 다음에는 이 배포 과정을 더욱 고도화하거나 오류 발생시 외부로 report 그리고 다른 배포 전략(Canary 등)에 대해서도 공부해 보고 싶네요. 긴 글 읽어주셔서 감사합니다!

 

전체 코드 레포는 여기서 볼 수 있습니다: https://github.com/hafskjfha/Blue-Green-study (nginx/ 부분의 코드는 사용을 하지 않습니다.)