Skip to content

Merge pull request #131 from Industry-Academic-SW-Capstone/feat/#49 #295

Merge pull request #131 from Industry-Academic-SW-Capstone/feat/#49

Merge pull request #131 from Industry-Academic-SW-Capstone/feat/#49 #295

Workflow file for this run

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 }}