Develop -> Main merge : 2025.10.14 - 11:14 (#180) #57
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
| # 워크플로우 이름 | |
| name: Spring CD (Production) | |
| # main 브랜치 PR에서만 실행 (이미 빌드된 Docker 이미지 사용) | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - 'src/**' | |
| - 'build.gradle*' | |
| - 'settings.gradle*' | |
| - 'gradle/**' | |
| - 'Dockerfile' | |
| - '.github/workflows/**' | |
| jobs: | |
| # ================================== | |
| # CD: Deploy to Production Environment | |
| # ================================== | |
| cd-prod: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Deploy to Test Environment | |
| uses: appleboy/[email protected] | |
| with: | |
| host: ${{ secrets.PROD_SERVER_HOST }} | |
| username: ec2-user | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| script: | | |
| # GHCR 로그인 (EC2) | |
| echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{github.repository_owner}}" --password-stdin | |
| # 최신 이미지 pull | |
| docker pull ghcr.io/${{ github.repository }}/zoopzoop:latest | |
| # NPM API 설정 | |
| NPM_HOST="localhost:81" | |
| NPM_EMAIL="${{secrets.NPM_ADMIN_EMAIL}}" | |
| NPM_PASSWORD="${{secrets.NPM_ADMIN_PASSWORD}}" | |
| PROXY_HOST_ID="${{secrets.NPM_PROXY_HOST_ID}}" | |
| # NPM API 토큰 가져오기 | |
| echo "Getting NPM API token..." | |
| TOKEN=$(curl -s -X POST "http://${NPM_HOST}/api/tokens" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"identity\":\"${NPM_EMAIL}\",\"secret\":\"${NPM_PASSWORD}\"}" | \ | |
| jq -r '.token') | |
| if [ "$TOKEN" == "null" ] || [ -z "$TOKEN" ]; then | |
| echo "❌ Failed to get NPM API token" | |
| exit 1 | |
| fi | |
| # 현재 NPM Proxy Host 설정 확인 | |
| echo "📋 Checking current NPM configuration... 📋" | |
| CURRENT_CONFIG=$(curl -s -H "Authorization: Bearer $TOKEN" \ | |
| "http://${NPM_HOST}/api/nginx/proxy-hosts/${PROXY_HOST_ID}") | |
| CURRENT_TARGET=$(echo $CURRENT_CONFIG | jq -r '.[0].forward_host // .forward_host') | |
| CURRENT_PORT=$(echo $CURRENT_CONFIG | jq -r '.[0].forward_port // .forward_port') | |
| echo "Current NPM target: $CURRENT_TARGET:$CURRENT_PORT" | |
| # Blue-Green 배포 | |
| if [ "$(docker ps -q -f name=zoopzoop-blue)" ]; then | |
| NEW_CONTAINER=zoopzoop-green | |
| OLD_CONTAINER=zoopzoop-blue | |
| NEW_PORT=8082 | |
| else | |
| NEW_CONTAINER=zoopzoop-blue | |
| OLD_CONTAINER=zoopzoop-green | |
| NEW_PORT=8081 | |
| fi | |
| echo "Starting new container: $NEW_CONTAINER on port $NEW_PORT" | |
| docker run -d --restart unless-stopped \ | |
| -p $NEW_PORT:8080 \ | |
| --name $NEW_CONTAINER \ | |
| --network common \ | |
| -e SPRING_PROFILES_ACTIVE=server \ | |
| -e SPRING_DATASOURCE_URL="${{secrets.PROD_DB_URL}}" \ | |
| -e SPRING_DATASOURCE_USERNAME="${{secrets.PROD_DB_USERNAME}}" \ | |
| -e SPRING_DATASOURCE_PASSWORD="${{secrets.PROD_DB_PASSWORD}}" \ | |
| -e AWS_S3_BUCKET_NAME="${{secrets.AWS_S3_BUCKET_NAME}}" \ | |
| -e AWS_S3_PREFIX="${{secrets.PROD_AWS_S3_PREFIX}}" \ | |
| -e SPRING_RABBITMQ_HOST="${{secrets.PROD_RABBITMQ_HOST}}" \ | |
| -e SPRING_RABBITMQ_PORT="${{secrets.PROD_RABBITMQ_PORT}}" \ | |
| -e SPRING_RABBITMQ_USERNAME="${{secrets.PROD_RABBITMQ_USERNAME}}" \ | |
| -e SPRING_RABBITMQ_PASSWORD="${{secrets.PROD_RABBITMQ_PASSWORD}}" \ | |
| -e REDIS_HOST="${{secrets.PROD_REDIS_HOST}}" \ | |
| -e REDIS_PASSWORD="${{secrets.PROD_REDIS_PASSWORD}}" \ | |
| -e KAKAO_CLIENT_ID="${{secrets.OAUTH_KAKAO_CLIENT_ID}}" \ | |
| -e GOOGLE_CLIENT_ID="${{secrets.OAUTH_GOOGLE_CLIENT_ID}}" \ | |
| -e GOOGLE_CLIENT_SECRET="${{secrets.OAUTH_GOOGLE_CLIENT_SECRET}}" \ | |
| -e KAKAO_REDIRECT_URI="${{secrets.PROD_OAUTH_KAKAO_REDIRECT_URI}}" \ | |
| -e GOOGLE_REDIRECT_URI="${{secrets.PROD_OAUTH_GOOGLE_REDIRECT_URI}}" \ | |
| -e SENTRY_DSN="${{secrets.SENTRY_DSN}}" \ | |
| -e OPENAI_API_KEY="${{secrets.PROD_OPENAI_API_KEY}}" \ | |
| -e LIVEBLOCKS_SECRET_KEY="${{secrets.LIVEBLOCKS_SECRET_KEY}}" \ | |
| -e NAVER_CLIENT_ID="${{secrets.NAVER_CLIENT_ID}}" \ | |
| -e NAVER_CLIENT_SECRET="${{secrets.NAVER_CLIENT_SECRET}}" \ | |
| -e JWT_SECRET_KEY="${{secrets.JWT_SECRET_KEY}}" \ | |
| -e JWT_ACCESS_TOKEN_VALIDITY="${{secrets.JWT_ACCESS_TOKEN_VALIDITY}}" \ | |
| -e JWT_REFRESH_TOKEN_VALIDITY="${{secrets.JWT_REFRESH_TOKEN_VALIDITY}}" \ | |
| -e FRONT_REDIRECT_DOMAIN="${{secrets.FRONT_REDIRECT_DOMAIN}}" \ | |
| -e FRONT_MAIN_DOMAIN="${{secrets.MAIN_DOMAIN}}" \ | |
| -e ELASTIC_HOST="${{secrets.PROD_ELASTIC_HOST}}" \ | |
| ghcr.io/${{ github.repository }}/zoopzoop:latest | |
| # 헬스체크 (Spring Boot Actuator) | |
| for i in {1..30}; do | |
| if curl -s http://localhost:$NEW_PORT/actuator/health | grep -q '"status":"UP"'; then | |
| echo "✅New container is healthy!" | |
| break | |
| else | |
| echo "Waiting for new container to be healthy..." | |
| sleep 5 | |
| fi | |
| if [ $i -eq 30 ]; then | |
| echo "❌ Health check failed. Rolling back..." | |
| docker stop $NEW_CONTAINER || true | |
| docker rm $NEW_CONTAINER || true | |
| exit 1 | |
| fi | |
| done | |
| # NPM에서 트래픽 스위칭 | |
| echo "🔄 Switching traffic in Nginx Proxy Manager..." | |
| DOMAIN_NAME=$(echo $CURRENT_CONFIG | jq -r '.domain_names[0]') | |
| CERT_ID=$(echo "$CURRENT_CONFIG" | jq -r '.certificate_id') | |
| SWITCH_RESPONSE=$(curl -s -w "%{http_code}" -X PUT "http://${NPM_HOST}/api/nginx/proxy-hosts/${PROXY_HOST_ID}" \ | |
| -H "Authorization: Bearer $TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"domain_names\": [\"$DOMAIN_NAME\"], | |
| \"forward_scheme\": \"http\", | |
| \"forward_host\": \"$NEW_CONTAINER\", | |
| \"forward_port\": 8080, | |
| \"caching_enabled\": false, | |
| \"block_exploits\": true, | |
| \"advanced_config\": \"\", | |
| \"locations\": [], | |
| \"certificate_id\": $CERT_ID, | |
| \"ssl_forced\": 1, | |
| \"hsts_enabled\": 1, | |
| \"hsts_subdomains\": 1 | |
| }") | |
| HTTP_CODE=${SWITCH_RESPONSE: -3} | |
| if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then | |
| echo "✅ Traffic switching completed successfully!" | |
| echo "🎯 NPM now points to: $NEW_CONTAINER:8080" | |
| # 최종 확인 | |
| sleep 5 | |
| echo "🔍 Final verification..." | |
| VERIFY_CONFIG=$(curl -s -H "Authorization: Bearer $TOKEN" \ | |
| "http://${NPM_HOST}/api/nginx/proxy-hosts/${PROXY_HOST_ID}") | |
| VERIFY_TARGET=$(echo $VERIFY_CONFIG | jq -r '.forward_host') | |
| echo "✅ Verified NPM target: $VERIFY_TARGET" | |
| else | |
| echo "❌ Traffic switching failed! HTTP Code: $HTTP_CODE" | |
| echo "Response: ${SWITCH_RESPONSE%???}" | |
| echo "🔄 Rolling back new container..." | |
| docker stop $NEW_CONTAINER || true | |
| docker rm $NEW_CONTAINER || true | |
| exit 1 | |
| fi | |
| # 이전 컨테이너 종료 및 제거 | |
| echo "Stopping old container: $OLD_CONTAINER" | |
| docker stop $OLD_CONTAINER || true | |
| docker rm $OLD_CONTAINER || true |