CI/CD Pipeline for Microservices (Ultra-Fast) #125
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 for Microservices (Ultra-Fast) | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| security-events: write | |
| env: | |
| DOCKER_REGISTRY: kwa06001 | |
| IMAGE_TAG: ${{ github.sha }} | |
| jobs: | |
| # 변경된 서비스 감지 + 빌드를 한 번에 | |
| build-jars: | |
| name: Build All JARs | |
| runs-on: ubuntu-latest | |
| outputs: | |
| services: ${{ steps.set-matrix.outputs.services }} | |
| has-changes: ${{ steps.set-matrix.outputs.has-changes }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| cache: 'gradle' | |
| - name: Detect changed files | |
| id: changes | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| echo "서비스 전체 배포 (수동 실행)" | |
| echo "server=true" >> $GITHUB_OUTPUT | |
| echo "gateway=true" >> $GITHUB_OUTPUT | |
| echo "fe=true" >> $GITHUB_OUTPUT | |
| echo "deploy=true" >> $GITHUB_OUTPUT | |
| echo "user=true" >> $GITHUB_OUTPUT | |
| elif [ "${{ github.event_name }}" == "pull_request" ]; then | |
| CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }}) | |
| echo "Changed files:" | |
| echo "$CHANGED_FILES" | |
| if echo "$CHANGED_FILES" | grep -qE "^server/|^build.gradle$|^settings.gradle$"; then | |
| echo "server=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "server=false" >> $GITHUB_OUTPUT | |
| fi | |
| if echo "$CHANGED_FILES" | grep -qE "^gateway/|^build.gradle$|^settings.gradle$"; then | |
| echo "gateway=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "gateway=false" >> $GITHUB_OUTPUT | |
| fi | |
| if echo "$CHANGED_FILES" | grep -qE "^fe/|^build.gradle$|^settings.gradle$"; then | |
| echo "fe=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "fe=false" >> $GITHUB_OUTPUT | |
| fi | |
| if echo "$CHANGED_FILES" | grep -qE "^deploy/|^build.gradle$|^settings.gradle$"; then | |
| echo "deploy=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "deploy=false" >> $GITHUB_OUTPUT | |
| fi | |
| if echo "$CHANGED_FILES" | grep -qE "^user/|^build.gradle$|^settings.gradle$"; then | |
| echo "user=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "user=false" >> $GITHUB_OUTPUT | |
| fi | |
| else | |
| echo "메인 브랜치 푸시 - 전체 배포" | |
| echo "server=true" >> $GITHUB_OUTPUT | |
| echo "gateway=true" >> $GITHUB_OUTPUT | |
| echo "fe=true" >> $GITHUB_OUTPUT | |
| echo "deploy=true" >> $GITHUB_OUTPUT | |
| echo "user=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Set matrix services | |
| id: set-matrix | |
| run: | | |
| CHANGED_SERVICES="" | |
| if [ "${{ steps.changes.outputs.server }}" == "true" ]; then | |
| CHANGED_SERVICES="${CHANGED_SERVICES}\"server\"," | |
| fi | |
| if [ "${{ steps.changes.outputs.gateway }}" == "true" ]; then | |
| CHANGED_SERVICES="${CHANGED_SERVICES}\"gateway\"," | |
| fi | |
| if [ "${{ steps.changes.outputs.fe }}" == "true" ]; then | |
| CHANGED_SERVICES="${CHANGED_SERVICES}\"fe\"," | |
| fi | |
| if [ "${{ steps.changes.outputs.deploy }}" == "true" ]; then | |
| CHANGED_SERVICES="${CHANGED_SERVICES}\"deploy\"," | |
| fi | |
| if [ "${{ steps.changes.outputs.user }}" == "true" ]; then | |
| CHANGED_SERVICES="${CHANGED_SERVICES}\"user\"," | |
| fi | |
| CHANGED_SERVICES=$(echo $CHANGED_SERVICES | sed 's/,$//') | |
| if [ -z "$CHANGED_SERVICES" ]; then | |
| echo "services=[]" >> $GITHUB_OUTPUT | |
| echo "has-changes=false" >> $GITHUB_OUTPUT | |
| echo "⚠️ 변경된 서비스 없음" | |
| else | |
| echo "services=[$CHANGED_SERVICES]" >> $GITHUB_OUTPUT | |
| echo "has-changes=true" >> $GITHUB_OUTPUT | |
| echo "✅ 변경된 서비스: [$CHANGED_SERVICES]" | |
| fi | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x ./gradlew | |
| - name: Build all services (parallel) | |
| run: | | |
| echo "🔨 Building all services in parallel..." | |
| ./gradlew build -x test --parallel --max-workers=5 --build-cache | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-artifacts | |
| path: | | |
| server/build/libs/*.jar | |
| gateway/build/libs/*.jar | |
| fe/build/libs/*.jar | |
| deploy/build/libs/*.jar | |
| user/build/libs/*.jar | |
| retention-days: 1 | |
| # 5개 서비스 완전 병렬 빌드 | |
| docker-build-scan-push: | |
| name: Docker Build & Push | |
| needs: build-jars | |
| runs-on: ubuntu-latest | |
| if: needs.build-jars.outputs.has-changes == 'true' | |
| strategy: | |
| matrix: | |
| service: ${{ fromJson(needs.build-jars.outputs.services) }} | |
| fail-fast: false | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-artifacts | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_PASSWORD }} | |
| # 최적화된 Dockerfile 사용 (JAR 파일 직접 복사) | |
| - name: Create optimized Dockerfile | |
| run: | | |
| cat > ${{ matrix.service }}/Dockerfile.fast << 'EOF' | |
| FROM eclipse-temurin:17-jre-focal | |
| WORKDIR /app | |
| COPY ${{ matrix.service }}/build/libs/*.jar app.jar | |
| EXPOSE 8080 | |
| ENTRYPOINT ["java", "-jar", "app.jar"] | |
| EOF | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: ./${{ matrix.service }}/Dockerfile.fast | |
| push: true | |
| tags: | | |
| ${{ env.DOCKER_REGISTRY }}/${{ matrix.service }}:${{ env.IMAGE_TAG }} | |
| ${{ env.DOCKER_REGISTRY }}/${{ matrix.service }}:latest | |
| cache-from: type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ matrix.service }}:buildcache | |
| cache-to: type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ matrix.service }}:buildcache,mode=max | |
| # Trivy 스캔 (백그라운드, 블로킹 없음) | |
| - name: Run Trivy scan | |
| uses: aquasecurity/trivy-action@master | |
| continue-on-error: true | |
| with: | |
| image-ref: ${{ env.DOCKER_REGISTRY }}/${{ matrix.service }}:${{ env.IMAGE_TAG }} | |
| format: 'sarif' | |
| output: 'trivy-results-${{ matrix.service }}.sarif' | |
| severity: 'CRITICAL,HIGH' | |
| timeout: '5m0s' | |
| - name: Upload Trivy results | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() | |
| continue-on-error: true | |
| with: | |
| sarif_file: 'trivy-results-${{ matrix.service }}.sarif' | |
| category: 'trivy-${{ matrix.service }}' | |
| # 배포 | |
| deploy-to-ec2: | |
| name: Deploy to EC2 | |
| needs: [build-jars, docker-build-scan-push] | |
| runs-on: ubuntu-latest | |
| if: (github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/heads/feat/')) && needs.build-jars.outputs.has-changes == 'true' # ⬅️ 테스트를 위해 feat/ 브랜치도 허용 (선택 사항) | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Configure AWS Credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ secrets.AWS_REGION }} | |
| - name: Prepare changed services list | |
| id: services | |
| run: | | |
| SERVICES=$(echo '${{ needs.build-jars.outputs.services }}' | jq -r '.[]' | tr '\n' ' ') | |
| echo "list=$SERVICES" >> $GITHUB_OUTPUT | |
| echo "📦 Services to deploy: $SERVICES" | |
| - name: Deploy to EC2 via AWS SSM | |
| id: deploy | |
| run: | | |
| SERVICES="${{ steps.services.outputs.list }}" | |
| cat > deploy.sh << 'DEPLOY_EOF' | |
| #!/bin/bash | |
| set -e | |
| SERVICES="$CHANGED_SERVICES" | |
| IMAGE_TAG="$GITHUB_SHA" | |
| echo "📂 Navigating to project directory..." | |
| cd /home/ubuntu/Raspberry | |
| echo "🔧 Changing file ownership..." | |
| sudo chown -R $USER:$USER . | |
| echo "🔄 Pulling latest code..." | |
| git fetch origin | |
| git reset --hard $GITHUB_SHA # ⬅️ 1. 'origin/main'이 아닌 현재 커밋(SHA)을 체크아웃하도록 수정 | |
| # 스왑 체크 (간소화) | |
| if ! sudo swapon --show | grep -q "/swapfile" && [ ! -f /swapfile ]; then | |
| sudo fallocate -l 2G /swapfile | |
| sudo chmod 600 /swapfile | |
| sudo mkswap /swapfile | |
| sudo swapon /swapfile | |
| echo "/swapfile none swap sw 0 0" | sudo tee -a /etc/fstab | |
| fi | |
| echo "🐳 Docker login..." | |
| echo "$DOCKERHUB_PASSWORD" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin | |
| sudo chmod 666 /var/run/docker.sock | |
| echo "🔧 Updating docker-compose.yml..." | |
| cp docker-compose.yml docker-compose.yml.backup | |
| for service in $SERVICES; do | |
| sed -i "s|image: kwa06001/${service}:.*|image: kwa06001/${service}:${IMAGE_TAG}|g" docker-compose.yml | |
| done | |
| sed -i '/build:/,+2d' docker-compose.yml | |
| echo "📥 Pulling images (parallel)..." | |
| for service in $SERVICES; do | |
| docker pull kwa06001/${service}:${IMAGE_TAG} & | |
| done | |
| wait | |
| echo "🛑 Stopping and removing old services..." # ⬅️ 2. 기존 컨테이너 중지 | |
| docker-compose stop $SERVICES || true # ⬅️ 3. 기존 컨테이너 삭제 | |
| docker-compose rm -f $SERVICES || true # ⬅️ 4. | |
| echo "🚀 Creating new services..." # ⬅️ 5. 새 컨테이너 생성 | |
| docker-compose up -d --no-deps $SERVICES # ⬅️ 6. --force-recreate 플래그 제거 | |
| echo "📊 Ensuring monitoring services are running..." # ⬅️ 7. 모니터링 서비스 실행 | |
| docker-compose up -d prometheus grafana # ⬅️ 8. | |
| echo "⏳ Waiting 15s..." | |
| sleep 15 | |
| docker image prune -a -f || true | |
| echo "✅ Deployment completed!" | |
| docker-compose ps | |
| DEPLOY_EOF | |
| ENCODED_SCRIPT=$(cat deploy.sh | base64 -w 0) | |
| COMMAND_ID=$(aws ssm send-command \ | |
| --instance-ids "${{ secrets.EC2_INSTANCE_ID }}" \ | |
| --document-name "AWS-RunShellScript" \ | |
| --parameters "commands=[\"echo $ENCODED_SCRIPT | base64 -d > /tmp/deploy.sh && chmod +x /tmp/deploy.sh && DOCKERHUB_PASSWORD='${{ secrets.DOCKERHUB_PASSWORD }}' DOCKERHUB_USERNAME='${{ secrets.DOCKERHUB_USERNAME }}' GITHUB_SHA='${{ github.sha }}' CHANGED_SERVICES='$SERVICES' bash /tmp/deploy.sh\"]" \ | |
| --timeout-seconds 300 \ | |
| --output text \ | |
| --query 'Command.Id') | |
| echo "command_id=$COMMAND_ID" >> $GITHUB_OUTPUT | |
| echo "✅ SSM Command ID: $COMMAND_ID" | |
| - name: Wait for deployment | |
| run: | | |
| COMMAND_ID="${{ steps.deploy.outputs.command_id }}" | |
| for i in {1..30}; do | |
| STATUS=$(aws ssm get-command-invocation \ | |
| --command-id "$COMMAND_ID" \ | |
| --instance-id "${{ secrets.EC2_INSTANCE_ID }}" \ | |
| --query 'Status' \ | |
| --output text 2>/dev/null || echo "Pending") | |
| echo "[$i/30] Status: $STATUS" | |
| if [ "$STATUS" = "Success" ]; then | |
| echo "✅ Deployment completed!" | |
| aws ssm get-command-invocation \ | |
| --command-id "$COMMAND_ID" \ | |
| --instance-id "${{ secrets.EC2_INSTANCE_ID }}" \ | |
| --query 'StandardOutputContent' \ | |
| --output text | |
| break | |
| elif [ "$STATUS" = "Failed" ] || [ "$STATUS" = "TimedOut" ]; then | |
| echo "❌ Deployment failed: $STATUS" | |
| aws ssm get-command-invocation \ | |
| --command-id "$COMMAND_ID" \ | |
| --instance-id "${{ secrets.EC2_INSTANCE_ID }}" \ | |
| --query 'StandardErrorContent' \ | |
| --output text | |
| exit 1 | |
| fi | |
| sleep 5 | |
| done | |
| - name: Quick health check | |
| run: | | |
| cat > verify.sh << 'VERIFY_EOF' | |
| #!/bin/bash | |
| cd /home/ubuntu/Raspberry | |
| echo "🔍 Health check..." | |
| if curl -sf http://localhost:8761 > /dev/null 2>&1; then | |
| echo "✅ Eureka OK" | |
| else | |
| echo "❌ Eureka DOWN" | |
| exit 1 | |
| fi | |
| if curl -sf http://localhost:8080/actuator/health > /dev/null 2>&1; then | |
| echo "✅ Gateway OK" | |
| fi | |
| docker-compose ps --format "table {{.Service}}\t{{.Status}}" | |
| VERIFY_EOF | |
| ENCODED_SCRIPT=$(cat verify.sh | base64 -w 0) | |
| VERIFY_CMD=$(aws ssm send-command \ | |
| --instance-ids "${{ secrets.EC2_INSTANCE_ID }}" \ | |
| --document-name "AWS-RunShellScript" \ | |
| --parameters "commands=[\"echo $ENCODED_SCRIPT | base64 -d | bash\"]" \ | |
| --timeout-seconds 120 \ | |
| --output text \ | |
| --query 'Command.Id') | |
| sleep 8 | |
| aws ssm get-command-invocation \ | |
| --command-id "$VERIFY_CMD" \ | |
| --instance-id "${{ secrets.EC2_INSTANCE_ID }}" \ | |
| --query 'StandardOutputContent' \ | |
| --output text | |
| - name: Summary | |
| if: always() | |
| run: | | |
| if [ "${{ job.status }}" == "success" ]; then | |
| echo "✅ Deployment successful!" | |
| echo "🔖 Image tag: ${{ github.sha }}" | |
| echo "📦 Services: ${{ steps.services.outputs.list }}" | |
| else | |
| echo "❌ Deployment failed!" | |
| exit 1 | |
| fi |