Merge pull request #131 from Industry-Academic-SW-Capstone/feat/#49 #295
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: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: | |
| - develop | |
| - main | |
| - "feat/**" | |
| pull_request: | |
| branches: | |
| - develop | |
| - main | |
| workflow_dispatch: # 수동 실행 가능 | |
| env: | |
| DOCKER_IMAGE: choij17/stockit-backend | |
| HELM_RELEASE_NAME: stockit-release | |
| HELM_NAMESPACE: default | |
| GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} | |
| GCP_GKE_CLUSTER: ${{ secrets.GCP_GKE_CLUSTER }} | |
| GCP_GKE_ZONE: ${{ secrets.GCP_GKE_ZONE }} | |
| jobs: | |
| build-and-test: | |
| name: Build and Test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up JDK 21 | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: "21" | |
| distribution: "temurin" | |
| cache: "gradle" | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x gradlew | |
| - name: Build with Gradle | |
| run: ./gradlew build -x test | |
| build-and-push-image: | |
| name: Build and Push Docker Image | |
| runs-on: ubuntu-latest | |
| needs: build-and-test | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to Docker Hub | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKER_USERNAME }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Generate image tag | |
| id: image-tag | |
| env: | |
| TZ: Asia/Seoul | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| # 수동 실행 시 커밋 SHA 사용 | |
| TAG="${{ github.sha }}" | |
| elif [ "${{ github.ref }}" == "refs/heads/main" ]; then | |
| # main 브랜치: 버전 번호 생성 (날짜 기반, Asia/Seoul 타임존) | |
| TAG="$(TZ=Asia/Seoul date +%Y%m%d)-${{ github.sha }}" | |
| elif [ "${{ github.ref }}" == "refs/heads/develop" ]; then | |
| # develop 브랜치: develop-커밋SHA | |
| TAG="develop-${{ github.sha }}" | |
| else | |
| # feature 브랜치: 브랜치명-커밋SHA (특수문자 제거) | |
| BRANCH_NAME=$(echo "${{ github.ref }}" | sed 's/refs\/heads\///' | sed 's/[^a-zA-Z0-9._-]/-/g') | |
| TAG="${BRANCH_NAME}-${{ github.sha }}" | |
| fi | |
| echo "tag=${TAG:0:50}" >> $GITHUB_OUTPUT | |
| echo "Generated tag: ${TAG:0:50}" | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| platforms: linux/amd64 | |
| push: ${{ github.event_name != 'pull_request' }} | |
| tags: | | |
| ${{ env.DOCKER_IMAGE }}:${{ steps.image-tag.outputs.tag }} | |
| ${{ env.DOCKER_IMAGE }}:latest | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Output image tag | |
| run: | | |
| echo "Image pushed with tag ${{ steps.image-tag.outputs.tag }}" | |
| deploy-to-gke: | |
| name: Deploy to GKE | |
| runs-on: ubuntu-latest | |
| needs: build-and-push-image | |
| if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main') | |
| environment: | |
| name: production | |
| url: https://www.stockit.live | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Authenticate to Google Cloud | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| - name: Set up Cloud SDK | |
| uses: google-github-actions/setup-gcloud@v2 | |
| with: | |
| install_components: "gke-gcloud-auth-plugin" | |
| - name: Configure kubectl | |
| env: | |
| USE_GKE_GCLOUD_AUTH_PLUGIN: True | |
| run: | | |
| gcloud container clusters get-credentials ${{ env.GCP_GKE_CLUSTER }} \ | |
| --zone ${{ env.GCP_GKE_ZONE }} \ | |
| --project ${{ env.GCP_PROJECT_ID }} | |
| - name: Install Helm | |
| uses: azure/setup-helm@v3 | |
| with: | |
| version: "3.13.0" | |
| - name: Generate image tag | |
| id: image-tag | |
| env: | |
| TZ: Asia/Seoul | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| TAG="${{ github.sha }}" | |
| elif [ "${{ github.ref }}" == "refs/heads/main" ]; then | |
| # main 브랜치: 버전 번호 생성 (날짜 기반, Asia/Seoul 타임존) | |
| TAG="$(TZ=Asia/Seoul date +%Y%m%d)-${{ github.sha }}" | |
| else | |
| TAG="develop-${{ github.sha }}" | |
| fi | |
| echo "tag=${TAG:0:50}" >> $GITHUB_OUTPUT | |
| echo "Generated tag: ${TAG:0:50}" | |
| - name: Clean up old pods before deployment | |
| run: | | |
| echo "🧹 배포 전 이전 Pod 정리 중..." && \ | |
| # 모든 Pod (Running, Terminating 포함) 가져오기 | |
| ALL_PODS=$(kubectl get pods -n ${{ env.HELM_NAMESPACE }} -l app.kubernetes.io/component=spring-backend --sort-by=.metadata.creationTimestamp -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.deletionTimestamp}{"\n"}{end}' 2>/dev/null || echo "") && \ | |
| if [ -n "$ALL_PODS" ]; then | |
| POD_COUNT=$(echo "$ALL_PODS" | wc -l) && \ | |
| if [ "$POD_COUNT" -gt 0 ]; then | |
| echo "발견된 Pod: $POD_COUNT개" && \ | |
| echo "$ALL_PODS" | while IFS=$'\t' read -r pod_name deletion_time; do | |
| if [ -n "$pod_name" ]; then | |
| echo "Pod 삭제 시도: $pod_name (deletionTimestamp: ${deletion_time:-none})" && \ | |
| # finalizers 제거 후 강제 삭제 | |
| kubectl patch pod "$pod_name" -n ${{ env.HELM_NAMESPACE }} -p '{"metadata":{"finalizers":[]}}' --type=merge 2>/dev/null || true && \ | |
| kubectl delete pod "$pod_name" -n ${{ env.HELM_NAMESPACE }} --grace-period=0 --force 2>/dev/null || true | |
| fi | |
| done && \ | |
| echo "⏳ 이전 Pod 삭제 후 10초 대기..." && \ | |
| sleep 10 && \ | |
| # 삭제 확인 | |
| REMAINING=$(kubectl get pods -n ${{ env.HELM_NAMESPACE }} -l app.kubernetes.io/component=spring-backend --no-headers 2>/dev/null | wc -l || echo "0") && \ | |
| echo "남은 Pod 수: $REMAINING" | |
| else | |
| echo "정리할 이전 Pod 없음" | |
| fi | |
| else | |
| echo "Pod 없음 (첫 배포)" | |
| fi | |
| - name: Deploy with Helm | |
| run: | | |
| helm upgrade --install ${{ env.HELM_RELEASE_NAME }} ./stockit-backend-chart \ | |
| --namespace ${{ env.HELM_NAMESPACE }} \ | |
| --create-namespace \ | |
| --set backend.image.tag="${{ steps.image-tag.outputs.tag }}" | |
| - name: Verify deployment | |
| run: | | |
| set -e # 오류 발생 시 즉시 종료 | |
| # 변수 설정 | |
| DEPLOYMENT_NAME="${{ env.HELM_RELEASE_NAME }}-stockit-backend-chart-spring-backend" | |
| NAMESPACE="${{ env.HELM_NAMESPACE }}" | |
| EXPECTED_IMAGE_TAG="${{ steps.image-tag.outputs.tag }}" | |
| echo "⏳ Pod가 Ready 상태가 될 때까지 대기 중..." | |
| echo "예상 이미지 태그: $EXPECTED_IMAGE_TAG" | |
| echo "Deployment 이름: $DEPLOYMENT_NAME" | |
| echo "Namespace: $NAMESPACE" | |
| # 2분마다 stuck된 Pod 확인 및 삭제 (백그라운드) | |
| ( | |
| for i in {1..5}; do | |
| sleep 120 | |
| echo "🔍 배포 진행 상황 확인 (${i}/5)..." | |
| # Terminating 상태인 Pod 찾기 | |
| TERMINATING_PODS=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/component=spring-backend -o jsonpath='{range .items[?(@.metadata.deletionTimestamp)]}{.metadata.name}{"\n"}{end}' 2>/dev/null || echo "") | |
| if [ -n "$TERMINATING_PODS" ]; then | |
| echo "⚠️ Terminating 상태 Pod 발견, 강제 삭제 시도..." | |
| for pod in $TERMINATING_PODS; do | |
| echo "강제 삭제: $pod" | |
| kubectl patch pod "$pod" -n "$NAMESPACE" -p '{"metadata":{"finalizers":[]}}' --type=merge 2>/dev/null || true | |
| kubectl delete pod "$pod" -n "$NAMESPACE" --grace-period=0 --force 2>/dev/null || true | |
| done | |
| fi | |
| done | |
| ) & | |
| MONITOR_PID=$! | |
| # rollout status 대기 | |
| if kubectl rollout status deployment/"$DEPLOYMENT_NAME" -n "$NAMESPACE" --timeout=10m; then | |
| echo "✅ Rollout 완료!" | |
| # 모니터링 프로세스 종료 | |
| kill $MONITOR_PID 2>/dev/null || true | |
| # Rollout 완료 후 Pod 상태 안정화를 위한 대기 | |
| echo "⏳ Pod 상태 안정화 대기 (5초)..." | |
| sleep 5 | |
| # 실제 Pod Ready 상태 확인 | |
| echo "" | |
| echo "=== Pod Ready 상태 확인 ===" | |
| # 간단한 방법으로 변경: kubectl get pods로 직접 확인 | |
| kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/component=spring-backend | |
| READY_COUNT=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/component=spring-backend --field-selector=status.phase=Running --no-headers 2>/dev/null | grep -E '([0-9]+)/\1' | wc -l || echo "0") | |
| TOTAL_COUNT=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/component=spring-backend --field-selector=status.phase=Running --no-headers 2>/dev/null | wc -l || echo "0") | |
| echo "Running Pod 중 Ready 상태: $READY_COUNT / $TOTAL_COUNT" | |
| if [ "$READY_COUNT" -ge 1 ] && [ "$READY_COUNT" -eq "$TOTAL_COUNT" ]; then | |
| echo "✅ 모든 Pod가 Ready 상태입니다!" | |
| echo "" | |
| echo "=== 배포된 이미지 태그 확인 ===" | |
| DEPLOYED_IMAGE=$(kubectl get deployment "$DEPLOYMENT_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || echo "") | |
| if [ -n "$DEPLOYED_IMAGE" ]; then | |
| echo "배포된 이미지: $DEPLOYED_IMAGE" | |
| if [[ "$DEPLOYED_IMAGE" == *"$EXPECTED_IMAGE_TAG"* ]]; then | |
| echo "✅ 최신 코드로 배포되었습니다!" | |
| else | |
| echo "⚠️ 경고: 예상한 이미지 태그와 다릅니다!" | |
| echo " 예상: *$EXPECTED_IMAGE_TAG*" | |
| echo " 실제: $DEPLOYED_IMAGE" | |
| fi | |
| else | |
| echo "⚠️ 배포된 이미지 정보를 가져올 수 없습니다." | |
| fi | |
| else | |
| set +e # 오류 출력 중에는 즉시 종료하지 않음 | |
| echo "❌ Pod가 Ready 상태가 아닙니다!" | |
| echo "" | |
| echo "=== Pod 상세 정보 ===" | |
| kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/component=spring-backend -o wide | |
| echo "" | |
| echo "=== Pod 이벤트 ===" | |
| kubectl get events -n "$NAMESPACE" --sort-by='.lastTimestamp' | grep spring-backend | tail -20 || true | |
| echo "" | |
| echo "=== Pod 로그 (최신 Pod) ===" | |
| LATEST_POD=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/component=spring-backend --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}' 2>/dev/null) | |
| if [ -n "$LATEST_POD" ]; then | |
| echo "Pod: $LATEST_POD" | |
| kubectl logs "$LATEST_POD" -n "$NAMESPACE" --tail=100 2>&1 || echo "로그를 가져올 수 없습니다." | |
| echo "" | |
| echo "=== Pod Describe ===" | |
| kubectl describe pod "$LATEST_POD" -n "$NAMESPACE" | tail -50 || true | |
| fi | |
| exit 1 | |
| fi | |
| else | |
| set +e # 오류 출력 중에는 즉시 종료하지 않음 | |
| # 모니터링 프로세스 종료 | |
| kill $MONITOR_PID 2>/dev/null || true | |
| echo "❌ Rollout 실패!" | |
| echo "" | |
| echo "=== Pod 상세 정보 ===" | |
| kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/component=spring-backend -o wide | |
| echo "" | |
| echo "=== Pod 이벤트 ===" | |
| kubectl get events -n "$NAMESPACE" --sort-by='.lastTimestamp' | grep spring-backend | tail -20 || true | |
| echo "" | |
| echo "=== Pod 로그 (최신 Pod) ===" | |
| LATEST_POD=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/component=spring-backend --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}' 2>/dev/null) | |
| if [ -n "$LATEST_POD" ]; then | |
| echo "Pod: $LATEST_POD" | |
| kubectl logs "$LATEST_POD" -n "$NAMESPACE" --tail=100 2>&1 || echo "로그를 가져올 수 없습니다." | |
| echo "" | |
| echo "=== Pod Describe ===" | |
| kubectl describe pod "$LATEST_POD" -n "$NAMESPACE" | tail -50 || true | |
| fi | |
| exit 1 | |
| fi | |
| - name: Check pod status | |
| run: | | |
| kubectl get pods -n ${{ env.HELM_NAMESPACE }} -l app.kubernetes.io/instance=${{ env.HELM_RELEASE_NAME }} |