Blue-Green 배포를 사용하면 다운타임 없이 안전하게 서비스를 교체할 수 있습니다.
하지만 **배포 후 사용하지 않는 슬롯(Docker 컨테이너)**이 계속 남아 있으면 자원을 낭비하거나 장애의 원인이 될 수 있습니다.
그래서 저는 **"현재 Nginx가 바라보는 Docker 컨테이너만 남기고, 나머지는 자동 정리"**하는 GitHub Actions 워크플로우를 만들었습니다.
✅ GCP MIG에서 모든 인스턴스 IP 자동 수집
✅ Jump Host를 통한 보안 SSH 접근
✅ 각 인스턴스에서:
CONFIRM_VERSION과 일치하는지 확인이 워크플로우는 다음을 철저히 확인하여 실수로 서비스 중단이 발생하는 것을 완벽히 방지합니다:
docker inspect -f '{{.State.Running}}')CONFIRM_VERSION과 동일한지 확인📦 SLOT_IPS 값: 10.21.10.4,10.21.10.5
🔍 분해된 IP 배열: 10.21.10.4 10.21.10.5
🧹 Cleaning old slot on 10.21.10.4...
✅ 유지: onthetop-backend-blue (version: v1.2.3)
🗑️ 삭제: onthetop-backend-green (version: v1.2.2)
🧹 Cleaning old slot on 10.21.10.5...
✅ 유지: onthetop-backend-green (version: v1.2.3)
🗑️ 삭제: onthetop-backend-blue (version: v1.2.1)
📋 [10.21.10.4] 컨테이너 정리 요약:
- blue 유지
- green 삭제
on:
workflow_dispatch:
inputs:
confirm_version:
description: '확정할 docker 배포 버전 (예: 1.2.3)'
required: true
GitHub Actions에서 수동 실행 시 confirm_version을 입력하면 자동으로 전체 인스턴스를 점검하고 불필요한 슬롯을 제거합니다.
name: Confirm Prod Deployment (Clean Old Docker Slot)
on:
workflow_dispatch:
inputs:
confirm_version:
description: '확정할 docker 배포 버전 (예: 1.2.3)'
required: true
jobs:
confirm:
runs-on: ubuntu-latest
env:
REGION: asia-northeast3
steps:
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: '${{ secrets.GCP_SA_KEY }}'
- name: Set up gcloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Set variables
id: vars
run: |
REGION="${{ env.REGION }}"
MIG_NAME="onthetop-mig-prod"
echo "region=$REGION" >> $GITHUB_OUTPUT
echo "mig_name=$MIG_NAME" >> $GITHUB_OUTPUT
- name: Get list of backend instance IPs
id: get_ips
run: |
MIG="${{ steps.vars.outputs.mig_name }}"
REGION="${{ steps.vars.outputs.region }}"
INSTANCE_NAMES=$(gcloud compute instance-groups managed list-instances "$MIG" \
--region="$REGION" \
--format="get(instance)" | sed -n 's|.*/||p')
if [ -z "$INSTANCE_NAMES" ]; then
echo "❌ MIG 인스턴스를 찾을 수 없습니다. MIG 이름과 REGION을 확인하세요."
exit 1
fi
ZONE_CANDIDATES=("asia-northeast3-a" "asia-northeast3-b" "asia-northeast3-c")
IP_LIST=""
for INSTANCE in $INSTANCE_NAMES; do
for TRY_ZONE in "${ZONE_CANDIDATES[@]}"; do
IP=$(gcloud compute instances describe "$INSTANCE" --zone "$TRY_ZONE" \
--format="get(networkInterfaces[0].networkIP)" 2>/dev/null) || continue
if [ -n "$IP" ]; then
echo "✅ $INSTANCE ($TRY_ZONE) → $IP"
IP_LIST+="$IP,"
break
fi
done
done
IP_LIST="${IP_LIST%,}"
echo " All IPs: $IP_LIST"
echo "slot_ips=$IP_LIST" >> $GITHUB_OUTPUT
- name: Set up SSH keys
run: |
mkdir -p ~/.ssh
echo "${{ secrets.JUMP_SSH_KEY }}" > ~/.ssh/jump_key
chmod 600 ~/.ssh/jump_key
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/dev_key
chmod 600 ~/.ssh/dev_key
- name: Confirm slot and remove old containers
env:
SLOT_IPS: ${{ steps.get_ips.outputs.slot_ips }}
CONFIRM_VERSION: ${{ github.event.inputs.confirm_version }}
JUMP_HOST: ${{ secrets.JUMP_SSH_HOST }}
run: |
mkdir -p ~/.ssh
ssh-keyscan -H "$JUMP_HOST" >> ~/.ssh/known_hosts
echo "📦 SLOT_IPS 값: $SLOT_IPS"
IFS=',' read -ra IPS <<< "$SLOT_IPS"
echo "🔍 분해된 IP 배열: ${IPS[@]}"
echo "🔍 배열 길이: ${#IPS[@]}"
for IP in "${IPS[@]}"; do
echo "🧹 Cleaning old slot on $IP..."
ssh -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ProxyCommand="ssh -i ~/.ssh/jump_key -W %h:%p ubuntu@$JUMP_HOST" \
-i ~/.ssh/dev_key ubuntu@$IP \
CONFIRM_VERSION=$CONFIRM_VERSION bash -s <<'EOF'
set -ex
RUNNING_CONTAINERS=$(sudo docker ps --format '{{.Names}}' | grep '^onthetop-backend-' | wc -l)
INSTANCE_NAME=$(hostname)
SUMMARY+="[$INSTANCE_NAME] 🔎 처음 실행 중 컨테이너 수: $RUNNING_CONTAINERS\n"
if [ "$RUNNING_CONTAINERS" -ne 2 ]; then
echo "❌ Error: '$INSTANCE_NAME' 인스턴스에서 실행 중인 컨테이너 수가 $RUNNING_CONTAINERS 개입니다."
sudo docker ps --format ' → {{.Names}} ({{.Status}})' | grep '^ → onthetop-backend-' || true
exit 1
fi
# 1. 현재 Nginx가 바라보는 포트 확인
ACTIVE_PORT=$(grep "proxy_pass" /etc/nginx/sites-enabled/backend | grep -oE '[0-9]+')
# 2. 각 슬롯의 버전 확인
BLUE_VERSION=$(sudo docker inspect --format='{{index .Config.Image}}' onthetop-backend-blue 2>/dev/null | cut -d: -f2 || echo "")
GREEN_VERSION=$(sudo docker inspect --format='{{index .Config.Image}}' onthetop-backend-green 2>/dev/null | cut -d: -f2 || echo "")
# 3. ACTIVE_PORT 기준으로 실제 Nginx가 사용하는 슬롯 결정
if [ "$ACTIVE_PORT" = "8080" ]; then
ACTIVE_SLOT=blue
elif [ "$ACTIVE_PORT" = "8081" ]; then
ACTIVE_SLOT=green
else
echo "❌ Nginx 설정에서 포트를 찾을 수 없습니다."
exit 1
fi
# 4. Nginx가 바라보는 컨테이너가 CONFIRM_VERSION인지 확인
ACTIVE_CONTAINER="onthetop-backend-$ACTIVE_SLOT"
ACTIVE_VERSION=$(sudo docker inspect --format='{{index .Config.Image}}' "$ACTIVE_CONTAINER" 2>/dev/null | cut -d: -f2 || echo "")
IS_RUNNING=$(sudo docker inspect -f '{{.State.Running}}' "$ACTIVE_CONTAINER" 2>/dev/null || echo "false")
if [ "$IS_RUNNING" != "true" ]; then
echo "❌ Error: Nginx가 바라보는 컨테이너 '$ACTIVE_CONTAINER'가 실행 중이 아닙니다!"
sudo docker ps -a --format '→ {{.Names}} ({{.Status}})' | grep onthetop-backend- || true
exit 1
fi
if [ "$ACTIVE_VERSION" != "$CONFIRM_VERSION" ]; then
echo "❌ Error: Nginx가 바라보는 슬롯 $ACTIVE_SLOT의 버전($ACTIVE_VERSION)이 CONFIRM_VERSION($CONFIRM_VERSION)과 다릅니다."
exit 1
fi
echo "✅ Nginx가 바라보는 슬롯 '$ACTIVE_SLOT'의 버전이 CONFIRM_VERSION=$CONFIRM_VERSION 과 일치합니다. 불필요한 슬롯을 정리합니다..."
# 5. 불필요한 슬롯 제거
for SLOT in blue green; do
CONTAINER="onthetop-backend-$SLOT"
VERSION=$(sudo docker inspect --format='{{index .Config.Image}}' "$CONTAINER" 2>/dev/null | cut -d: -f2 || echo "")
if [ "$SLOT" = "$ACTIVE_SLOT" ]; then
echo "✅ 유지: $CONTAINER (version: $VERSION)"
SUMMARY+="[$INSTANCE_NAME] ✅ 유지: $CONTAINER (version: $VERSION)\n"
elif [ "$VERSION" = "$CONFIRM_VERSION" ]; then
echo "🗑️ 삭제: $CONTAINER (동일 버전이지만 Nginx가 사용하지 않음)"
sudo docker rm -f "$CONTAINER"
SUMMARY+="[$INSTANCE_NAME] 🗑️ 삭제: $CONTAINER (동일 버전이지만 비활성)\n"
elif [ -n "$VERSION" ]; then
echo "🗑️ 삭제: $CONTAINER (version: $VERSION ≠ $CONFIRM_VERSION)"
sudo docker rm -f "$CONTAINER"
SUMMARY+="[$INSTANCE_NAME] 🗑️ 삭제: $CONTAINER (version: $VERSION)\n"
else
echo "❎ 존재하지 않음: $CONTAINER"
SUMMARY+="[$INSTANCE_NAME] ❎ 존재하지 않음: $CONTAINER\n"
fi
done
echo -e "\n📋 [$INSTANCE_NAME] 컨테이너 정리 요약:"
echo -e "$SUMMARY"
EOF
done
8080/8081)