CI/CD Pipeline for Microservices (Ultra-Fast) #145
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: softbank2025 | |
| 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 | |
| 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 }} | |
| - 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 | |
| type=gha | |
| cache-to: | | |
| type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ matrix.service }}:buildcache,mode=max | |
| type=gha,mode=max | |
| - 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' | |
| 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 | |
| # 스왑 체크 (간소화) | |
| 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: softbank2025/${service}:.*|image: softbank2025/${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 softbank2025/${service}:${IMAGE_TAG} & | |
| done | |
| wait | |
| echo "🛑 Stopping and removing old services..." | |
| docker-compose stop $SERVICES || true | |
| docker-compose rm -f $SERVICES || true | |
| echo "🚀 Creating new services..." | |
| docker-compose up -d --no-deps $SERVICES | |
| echo "📊 Ensuring monitoring services are running..." | |
| docker-compose up -d prometheus grafana | |
| 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.CommandId') | |
| 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.CommandId') | |
| 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 |