Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 8
refactor: prod/stage 환경 nginx 블루/그린 배포 방식 도입#753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -66,7 +66,7 @@ jobs: | ||
| 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 | ||
| # --- 이미지 정리 (이전 Job에 있던 것) --- | ||
| # --- 이미지 정리 --- | ||
| - name: Clean up old image versions from GHCR | ||
| uses: snok/container-retention-policy@v2 | ||
| with: | ||
| @@ -87,7 +87,6 @@ jobs: | ||
| packages: read | ||
| steps: | ||
| # 설정 파일 전송을 위해 코드 체크아웃 (서브모듈 불필요) | ||
| - name: Checkout config files | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| @@ -96,52 +95,84 @@ jobs: | ||
| 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/ | ||
| # --- 서버에서 Docker Pull 및 재시작 --- | ||
| - name: Run deployment on server | ||
| - name: Blue/Green deploy | ||
| run: | | ||
| ssh -i deploy_key.pem \ | ||
| -o StrictHostKeyChecking=no \ | ||
| ${{ secrets.DEV_USERNAME }}@${{ secrets.DEV_HOST }} \ | ||
| ' | ||
| set -e | ||
| # 1. 환경 변수 설정 (이전 Job의 Output 사용) | ||
| export OWNER_LOWERCASE=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") | ||
| export IMAGE_TAG_ONLY="${{ needs.build-and-push.outputs.image_tag }}" | ||
| export FULL_IMAGE_NAME="ghcr.io/${OWNER_LOWERCASE}/solid-connection-dev:${IMAGE_TAG_ONLY}" | ||
| export IMAGE_NAME_BASE="ghcr.io/${OWNER_LOWERCASE}/solid-connection-dev" | ||
| # 2. Pull 전 정리 (디스크 공간 확보) | ||
| echo "Cleaning up old tagged images (keeping last 2)..." | ||
| docker images "${IMAGE_NAME_BASE}" --format "{{.Tag}}" | \ | ||
| sort -r | \ | ||
| tail -n +3 | \ | ||
| xargs -I {} docker rmi "${IMAGE_NAME_BASE}:{}" || true | ||
| echo "Pruning dangling images..." | ||
| docker image prune -f | ||
| # 3. GHCR 로그인 & Pull | ||
| echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin | ||
| echo "Pulling new image: $FULL_IMAGE_NAME" | ||
| docker pull $FULL_IMAGE_NAME | ||
| # 4. Spring Boot 앱 재시작 | ||
| echo "Restarting Docker Compose with tag: $IMAGE_TAG_ONLY" | ||
| cd /home/${{ secrets.DEV_USERNAME }}/solid-connection-dev | ||
| docker compose -f docker-compose.dev.yml down || true | ||
| OWNER_LOWERCASE=$OWNER_LOWERCASE IMAGE_TAG=$IMAGE_TAG_ONLY docker compose -f docker-compose.dev.yml up -d | ||
| echo "Deployment finished successfully." | ||
| ' | ||
| -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}" | ||
Hexeong marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| # 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})" | ||
| ' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -57,17 +57,17 @@ jobs: | ||
| id: image_meta | ||
| run: | | ||
| OWNER_LOWERCASE=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | ||
| # Trigger가 Release인 경우: Release의 Tag Name (예: v1.0.0) 사용 | ||
| if [ "${{ github.event_name }}" == "release" ]; then | ||
| IMAGE_TAG="${{ github.ref_name }}" | ||
| # Trigger가 수동(workflow_dispatch)인 경우: 입력받은 tag_name 사용 | ||
| else | ||
| IMAGE_TAG="${{ inputs.tag_name }}" | ||
| fi | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| echo "Docker Image Tag: $IMAGE_TAG" | ||
| echo "image_name=ghcr.io/${OWNER_LOWERCASE}/solid-connection-server" >> $GITHUB_OUTPUT | ||
| echo "image_tag=${IMAGE_TAG}" >> $GITHUB_OUTPUT | ||
| @@ -103,7 +103,6 @@ jobs: | ||
| packages: read | ||
| steps: | ||
| # 설정 파일 전송을 위해 코드 체크아웃 (서브모듈 불필요) | ||
| - name: Checkout config files | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| @@ -112,44 +111,79 @@ jobs: | ||
| docs/infra-config | ||
| sparse-checkout-cone-mode: false | ||
| # --- 설정 파일 전송 --- | ||
| - name: Copy config files to remote | ||
| run: | | ||
| echo "${{ secrets.PRIVATE_KEY }}" > deploy_key.pem | ||
| chmod 600 deploy_key.pem | ||
| scp -i deploy_key.pem \ | ||
| -o StrictHostKeyChecking=no \ | ||
| ./docker-compose.prod.yml \ | ||
| ${{ secrets.USERNAME }}@${{ secrets.HOST }}:/home/${{ secrets.USERNAME }}/solid-connection-prod/ | ||
| # --- 서버에서 Docker Pull 및 재시작 --- | ||
| - name: Run docker compose and apply nginx config | ||
| - name: Blue/Green deploy | ||
| run: | | ||
| ssh -i deploy_key.pem \ | ||
| -o StrictHostKeyChecking=no \ | ||
| ${{ secrets.USERNAME }}@${{ secrets.HOST }} \ | ||
| ' | ||
| set -e | ||
| # 1. 변수 설정 (이전 Job의 Output 사용) | ||
| export OWNER_LOWERCASE=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") | ||
| export IMAGE_TAG_ONLY="${{ needs.build-and-push.outputs.image_tag }}" | ||
| export FULL_IMAGE_NAME="ghcr.io/${OWNER_LOWERCASE}/solid-connection-server:${IMAGE_TAG_ONLY}" | ||
| # 2. GHCR 로그인 & Pull | ||
| # App Token 대신 현재 워크플로우의 임시 토큰을 사용합니다. | ||
| echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin | ||
| echo "Pulling new image: $FULL_IMAGE_NAME" | ||
| docker pull $FULL_IMAGE_NAME | ||
| # 3. Spring Boot 앱 재시작 | ||
| echo "Restarting Docker Compose with tag: $IMAGE_TAG_ONLY" | ||
| cd /home/${{ secrets.USERNAME }}/solid-connection-prod | ||
| docker compose -f docker-compose.prod.yml down || true | ||
| OWNER_LOWERCASE=$OWNER_LOWERCASE IMAGE_TAG=$IMAGE_TAG_ONLY docker compose -f docker-compose.prod.yml up -d | ||
| # 6. 정리 | ||
| docker image prune -f | ||
| echo "Deployment finished successfully." | ||
| ' | ||
| -o StrictHostKeyChecking=no \ | ||
| ${{ secrets.USERNAME }}@${{ secrets.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-server:${IMAGE_TAG_ONLY}" | ||
| IMAGE_NAME_BASE="ghcr.io/${OWNER_LOWERCASE}/solid-connection-server" | ||
| WORK_DIR="/home/${{ secrets.USERNAME }}/solid-connection-prod" | ||
| CONTAINER_BASE="solid-connection-server" | ||
| # 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. 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 | ||
| # 3. GHCR 로그인 & Pull | ||
| echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin | ||
| docker pull "${FULL_IMAGE_NAME}" | ||
| # 4. 새 슬롯 잔여 컨테이너 정리 | ||
| docker stop "${CONTAINER_BASE}-${NEW_SLOT}" 2>/dev/null || true | ||
| docker rm "${CONTAINER_BASE}-${NEW_SLOT}" 2>/dev/null || true | ||
| # 5. 새 컨테이너 시작 | ||
| cd "${WORK_DIR}" | ||
| 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.prod.yml up -d solid-connection-server | ||
| # 6. 헬스 체크 (앱 기동 대기, 최대 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 | ||
| # 7. 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})" | ||
| # 8. 구 컨테이너 종료 | ||
| docker compose -p "${CONTAINER_BASE}-${ACTIVE_SLOT}" -f docker-compose.prod.yml down 2>/dev/null || true | ||
| echo "Deployment complete. Active: ${NEW_SLOT}(${NEW_PORT})" | ||
| ' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -23,7 +23,7 @@ management: | ||
| endpoints: | ||
| web: | ||
| exposure: | ||
| include: prometheus | ||
| include: prometheus, health | ||
| --- | ||
| spring: | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.