Skip to content

refactor: prod/stage 환경 nginx 블루/그린 배포 방식 도입 (#753) #205

refactor: prod/stage 환경 nginx 블루/그린 배포 방식 도입 (#753)

refactor: prod/stage 환경 nginx 블루/그린 배포 방식 도입 (#753) #205

Workflow file for this run

name: "[DEV] Build Gradle and Deploy"
on:
push:
branches: [ "develop" ]
workflow_dispatch:
jobs:
# --- Job 1: 빌드 및 이미지 푸시 (쓰기 권한 필요) ---
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image_tag: ${{ steps.image_meta.outputs.image_tag }}
steps:
- name: Checkout the code
uses: actions/checkout@v4
# --- Java, Gradle 설정 ---
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Grant execute permission for Gradle wrapper
run: chmod +x ./gradlew
- name: Build with Gradle
run: ./gradlew bootJar
# --- Docker 설정 ---
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/arm64
- name: Log in to GitHub Container Registry (GHCR)
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# --- 이미지 메타데이터 정의 ---
- name: Define image name and tag
id: image_meta
run: |
OWNER_LOWERCASE=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
IMAGE_TAG=$(date +'%Y%m%d-%H%M%S')
echo "image_name=ghcr.io/${OWNER_LOWERCASE}/solid-connection-dev" >> $GITHUB_OUTPUT
echo "image_tag=${IMAGE_TAG}" >> $GITHUB_OUTPUT
# --- Docker 빌드 및 푸시 ---
- name: Build, push, and cache Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/arm64
push: true
tags: ${{ format('{0}:{1}', steps.image_meta.outputs.image_name, steps.image_meta.outputs.image_tag) }}
cache-from: type=registry,ref=${{ steps.image_meta.outputs.image_name }}:buildcache
cache-to: type=registry,ref=${{ steps.image_meta.outputs.image_name }}:buildcache,mode=max
# --- 이미지 정리 ---
- name: Clean up old image versions from GHCR
uses: snok/container-retention-policy@v2
with:
token: ${{ secrets.PACKAGE_DELETE_TOKEN }}
image-names: solid-connection-dev
delete-untagged: true
keep-n-tags: 2
account-type: org
org-name: ${{ github.repository_owner }}
cut-off: '7 days ago UTC'
# --- Job 2: 배포 (읽기 권한만 필요) ---
deploy:
needs: build-and-push
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
steps:
- name: Checkout config files
uses: actions/checkout@v4
with:
sparse-checkout: |
docker-compose.dev.yml
docs/infra-config
sparse-checkout-cone-mode: false
- name: Copy config files to remote
run: |
echo "${{ secrets.DEV_PRIVATE_KEY }}" > deploy_key.pem
chmod 600 deploy_key.pem
scp -i deploy_key.pem \
-o StrictHostKeyChecking=no \
./docker-compose.dev.yml \
${{ secrets.DEV_USERNAME }}@${{ secrets.DEV_HOST }}:/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/
- name: Blue/Green deploy
run: |
ssh -i deploy_key.pem \
-o StrictHostKeyChecking=no \
${{ secrets.DEV_USERNAME }}@${{ secrets.DEV_HOST }} \
'
set -e
OWNER_LOWERCASE=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]")
IMAGE_TAG_ONLY="${{ needs.build-and-push.outputs.image_tag }}"
FULL_IMAGE_NAME="ghcr.io/${OWNER_LOWERCASE}/solid-connection-dev:${IMAGE_TAG_ONLY}"
IMAGE_NAME_BASE="ghcr.io/${OWNER_LOWERCASE}/solid-connection-dev"
WORK_DIR="/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev"
CONTAINER_BASE="solid-connection-dev"
# 1. Active 슬롯 확인 (upstream.conf 기준)
UPSTREAM_PORT=$(grep -oE "server 127\.0\.0\.1:[0-9]+" /etc/nginx/conf.d/upstream.conf 2>/dev/null | grep -oE "[0-9]+$" || echo "8081")
if [ "$UPSTREAM_PORT" = "8080" ]; then
ACTIVE_SLOT="blue"; ACTIVE_PORT=8080; NEW_SLOT="green"; NEW_PORT=8081; MANAGEMENT_PORT=9081
else
ACTIVE_SLOT="green"; ACTIVE_PORT=8081; NEW_SLOT="blue"; NEW_PORT=8080; MANAGEMENT_PORT=9080
fi
echo "Active: ${ACTIVE_SLOT}(${ACTIVE_PORT}) → Deploy: ${NEW_SLOT}(${NEW_PORT}), management: ${MANAGEMENT_PORT}"
# 2. 작업 디렉토리 이동 (이후 모든 compose 명령 기준)
cd "${WORK_DIR}"
# 3. MySQL 기동 확인 (블루/그린 전환 대상 아님)
docker compose -f docker-compose.dev.yml up -d mysql 2>/dev/null || true
# 4. Pull 전 디스크 정리 (태그 이미지 최근 2개 유지)
docker images "${IMAGE_NAME_BASE}" --format "{{.Tag}}" | \
grep -v buildcache | sort -r | tail -n +3 | \
xargs -I {} docker rmi "${IMAGE_NAME_BASE}:{}" 2>/dev/null || true
docker image prune -f
# 5. GHCR 로그인 & Pull
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
docker pull "${FULL_IMAGE_NAME}"
# 6. 새 슬롯 잔여 컨테이너 정리
docker stop "${CONTAINER_BASE}-${NEW_SLOT}" 2>/dev/null || true
docker rm "${CONTAINER_BASE}-${NEW_SLOT}" 2>/dev/null || true
# 7. 새 컨테이너 시작
SLOT="${NEW_SLOT}" APP_PORT="${NEW_PORT}" MANAGEMENT_PORT="${MANAGEMENT_PORT}" OWNER_LOWERCASE="${OWNER_LOWERCASE}" IMAGE_TAG="${IMAGE_TAG_ONLY}" \
docker compose -p "${CONTAINER_BASE}-${NEW_SLOT}" -f docker-compose.dev.yml up -d solid-connection-dev
# 8. 헬스 체크 (앱 기동 대기, 최대 150초)
echo "Waiting for app on management port ${MANAGEMENT_PORT}..."
for i in $(seq 1 30); do
STATUS=$(curl -s --connect-timeout 2 "http://localhost:${MANAGEMENT_PORT}/actuator/health" | grep -o '"status":"UP"' || true)
[ "$STATUS" = '"status":"UP"' ] && { echo "App healthy (attempt ${i})"; break; }
[ "$i" = "30" ] && {
echo "Health check timed out after 150s" >&2
docker stop "${CONTAINER_BASE}-${NEW_SLOT}" 2>/dev/null || true
docker rm "${CONTAINER_BASE}-${NEW_SLOT}" 2>/dev/null || true
exit 1
}
sleep 5
done
# 9. Nginx upstream 전환 (무중단)
sudo sed -i "s|server 127.0.0.1:[0-9]*;|server 127.0.0.1:${NEW_PORT};|" /etc/nginx/conf.d/upstream.conf
sudo nginx -s reload
echo "Traffic switched → ${NEW_SLOT}(${NEW_PORT})"
# 10. 구 컨테이너 종료
docker compose -p "${CONTAINER_BASE}-${ACTIVE_SLOT}" -f docker-compose.dev.yml down 2>/dev/null || true
echo "Deployment complete. Active: ${NEW_SLOT}(${NEW_PORT})"
'