diff --git a/.github/workflows/backend-cicd_ArgoCD.yaml b/.github/workflows/backend-cicd_ArgoCD.yaml new file mode 100644 index 0000000..2cfd9fb --- /dev/null +++ b/.github/workflows/backend-cicd_ArgoCD.yaml @@ -0,0 +1,254 @@ +name: Backend Services CI/CD + +on: + push: + branches: [ main, develop ] + paths: + - 'api-gateway/**' + - 'user-service/**' + - 'bill-service/**' + - 'product-service/**' + - 'kos-mock/**' + - 'common/**' + - '.github/**' + pull_request: + branches: [ main ] + workflow_dispatch: + inputs: + ENVIRONMENT: + description: 'Target environment' + required: true + default: 'dev' + type: choice + options: + - dev + - staging + - prod + SKIP_SONARQUBE: + description: 'Skip SonarQube Analysis' + required: false + default: 'true' + type: choice + options: + - 'true' + - 'false' + +env: + REGISTRY: acrdigitalgarage01.azurecr.io + IMAGE_ORG: phonebill + RESOURCE_GROUP: rg-digitalgarage-01 + AKS_CLUSTER: aks-digitalgarage-01 + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.set_outputs.outputs.image_tag }} + environment: ${{ steps.set_outputs.outputs.environment }} + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + cache: 'gradle' + + - name: Determine environment + id: determine_env + run: | + # Use input parameter or default to 'dev' + ENVIRONMENT="${{ github.event.inputs.ENVIRONMENT || 'dev' }}" + echo "environment=$ENVIRONMENT" >> $GITHUB_OUTPUT + + - name: Load environment variables + id: env_vars + run: | + ENV=${{ steps.determine_env.outputs.environment }} + + # Initialize variables with defaults + REGISTRY="acrdigitalgarage01.azurecr.io" + IMAGE_ORG="phonebill" + RESOURCE_GROUP="rg-digitalgarage-01" + AKS_CLUSTER="aks-digitalgarage-01" + NAMESPACE="phonebill-dg0500" + + # Read environment variables from .github/config file + if [[ -f ".github/config/deploy_env_vars_${ENV}" ]]; then + while IFS= read -r line || [[ -n "$line" ]]; do + # Skip comments and empty lines + [[ "$line" =~ ^#.*$ ]] && continue + [[ -z "$line" ]] && continue + + # Extract key-value pairs + key=$(echo "$line" | cut -d '=' -f1) + value=$(echo "$line" | cut -d '=' -f2-) + + # Override defaults if found in config + case "$key" in + "resource_group") RESOURCE_GROUP="$value" ;; + "cluster_name") AKS_CLUSTER="$value" ;; + esac + done < ".github/config/deploy_env_vars_${ENV}" + fi + + # Export for other jobs + echo "REGISTRY=$REGISTRY" >> $GITHUB_ENV + echo "IMAGE_ORG=$IMAGE_ORG" >> $GITHUB_ENV + echo "RESOURCE_GROUP=$RESOURCE_GROUP" >> $GITHUB_ENV + echo "AKS_CLUSTER=$AKS_CLUSTER" >> $GITHUB_ENV + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: | + ./gradlew build -x test + + - name: SonarQube Analysis & Quality Gate + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + run: | + # Check if SonarQube should be skipped + SKIP_SONARQUBE="${{ github.event.inputs.SKIP_SONARQUBE || 'true' }}" + + if [[ "$SKIP_SONARQUBE" == "true" ]]; then + echo "⏭️ Skipping SonarQube Analysis (SKIP_SONARQUBE=$SKIP_SONARQUBE)" + exit 0 + fi + + # Define services array + services=(api-gateway user-service bill-service product-service kos-mock) + + # Run tests, coverage reports, and SonarQube analysis for each service + for service in "${services[@]}"; do + ./gradlew :$service:test :$service:jacocoTestReport :$service:sonar \ + -Dsonar.projectKey=phonebill-$service-dg0500 \ + -Dsonar.projectName=phonebill-$service-dg0500 \ + -Dsonar.host.url=$SONAR_HOST_URL \ + -Dsonar.token=$SONAR_TOKEN \ + -Dsonar.java.binaries=build/classes/java/main \ + -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml \ + -Dsonar.exclusions=**/config/**,**/entity/**,**/dto/**,**/*Application.class,**/exception/** + done + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: app-builds + path: | + api-gateway/build/libs/*.jar + user-service/build/libs/*.jar + bill-service/build/libs/*.jar + product-service/build/libs/*.jar + kos-mock/build/libs/*.jar + + - name: Set outputs + id: set_outputs + run: | + # Generate timestamp for image tag + #IMAGE_TAG=$(date +%Y%m%d%H%M%S) + IMAGE_TAG=dg0500 + echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "environment=${{ steps.determine_env.outputs.environment }}" >> $GITHUB_OUTPUT + + release: + name: Build and Push Docker Images + needs: build + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: app-builds + + - name: Set environment variables from build job + run: | + echo "REGISTRY=${{ env.REGISTRY }}" >> $GITHUB_ENV + echo "IMAGE_ORG=${{ env.IMAGE_ORG }}" >> $GITHUB_ENV + echo "ENVIRONMENT=${{ needs.build.outputs.environment }}" >> $GITHUB_ENV + echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub (prevent rate limit) + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Login to Azure Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.ACR_USERNAME }} + password: ${{ secrets.ACR_PASSWORD }} + + - name: Build and push Docker images for all services + run: | + # Define services array + services=(api-gateway user-service bill-service product-service kos-mock) + + # Build and push each service image + for service in "${services[@]}"; do + echo "Building and pushing $service..." + docker build \ + --build-arg BUILD_LIB_DIR="$service/build/libs" \ + --build-arg ARTIFACTORY_FILE="$service.jar" \ + -f deployment/container/Dockerfile-backend \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/$service:${{ needs.build.outputs.environment }}-${{ needs.build.outputs.image_tag }} . + + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/$service:${{ needs.build.outputs.environment }}-${{ needs.build.outputs.image_tag }} + done + + update-manifest: + name: Update Manifest Repository + needs: [build, release] + runs-on: ubuntu-latest + + steps: + - name: Set image tag environment variable + run: | + echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV + echo "ENVIRONMENT=${{ needs.build.outputs.environment }}" >> $GITHUB_ENV + + - name: Update Manifest Repository + run: | + # 매니페스트 레포지토리 클론 + REPO_URL=$(echo "https://github.com/cna-bootcamp/phonebill-manifest.git" | sed 's|https://||') + git clone https://${{ secrets.GIT_USERNAME }}:${{ secrets.GIT_PASSWORD }}@${REPO_URL} manifest-repo + cd manifest-repo + + # Kustomize 설치 + curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash + sudo mv kustomize /usr/local/bin/ + + # 매니페스트 업데이트 + cd phonebill/kustomize/overlays/${{ env.ENVIRONMENT }} + + # 각 서비스별 이미지 태그 업데이트 + services="api-gateway user-service bill-service product-service kos-mock" + for service in $services; do + kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/$service:${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }} + done + + # Git 설정 및 푸시 + cd ../../../.. + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git add . + git commit -m "🚀 Update phonebill ${{ env.ENVIRONMENT }} images to ${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }}" + git push origin main + + echo "✅ 매니페스트 업데이트 완료. ArgoCD가 자동으로 배포합니다." \ No newline at end of file diff --git a/deployment/cicd/Jenkinsfile_ArgoCD b/deployment/cicd/Jenkinsfile_ArgoCD new file mode 100644 index 0000000..4a91a31 --- /dev/null +++ b/deployment/cicd/Jenkinsfile_ArgoCD @@ -0,0 +1,244 @@ +def PIPELINE_ID = "${env.BUILD_NUMBER}" + +def getImageTag() { + def dateFormat = new java.text.SimpleDateFormat('yyyyMMddHHmmss') + def currentDate = new Date() + return dateFormat.format(currentDate) +} + +podTemplate( + label: "${PIPELINE_ID}", + serviceAccount: 'jenkins', + slaveConnectTimeout: 300, + idleMinutes: 1, + activeDeadlineSeconds: 3600, + podRetention: never(), // 파드 자동 정리 옵션: never(), onFailure(), always(), default() + yaml: ''' + spec: + terminationGracePeriodSeconds: 3 + restartPolicy: Never + tolerations: + - effect: NoSchedule + key: dedicated + operator: Equal + value: cicd + ''', + containers: [ + containerTemplate( + name: 'podman', + image: "mgoltzsche/podman", + ttyEnabled: true, + command: 'cat', + privileged: true, + resourceRequestCpu: '500m', + resourceRequestMemory: '2Gi', + resourceLimitCpu: '2000m', + resourceLimitMemory: '4Gi' + ), + containerTemplate( + name: 'gradle', + image: 'gradle:jdk21', + ttyEnabled: true, + command: 'cat', + resourceRequestCpu: '500m', + resourceRequestMemory: '1Gi', + resourceLimitCpu: '1000m', + resourceLimitMemory: '2Gi', + envVars: [ + envVar(key: 'DOCKER_HOST', value: 'unix:///run/podman/podman.sock'), + envVar(key: 'TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE', value: '/run/podman/podman.sock'), + envVar(key: 'TESTCONTAINERS_RYUK_DISABLED', value: 'true') + ] + ), + containerTemplate( + name: 'azure-cli', + image: 'hiondal/azure-kubectl:latest', + command: 'cat', + ttyEnabled: true, + resourceRequestCpu: '200m', + resourceRequestMemory: '512Mi', + resourceLimitCpu: '500m', + resourceLimitMemory: '1Gi' + ), + containerTemplate( + name: 'git', + image: 'alpine/git:latest', + command: 'cat', + ttyEnabled: true, + resourceRequestCpu: '100m', + resourceRequestMemory: '256Mi', + resourceLimitCpu: '300m', + resourceLimitMemory: '512Mi' + ) + ], + volumes: [ + emptyDirVolume(mountPath: '/home/gradle/.gradle', memory: false), + emptyDirVolume(mountPath: '/root/.azure', memory: false), + emptyDirVolume(mountPath: '/run/podman', memory: false) + ] +) { + node(PIPELINE_ID) { + def props + //def imageTag = getImageTag() + def imageTag = "dg0500" + def environment = params.ENVIRONMENT ?: 'dev' + def skipSonarQube = (params.SKIP_SONARQUBE?.toLowerCase() == 'true') + def services = ['api-gateway', 'user-service', 'bill-service', 'product-service', 'kos-mock'] + + try { + stage("Get Source") { + checkout scm + props = readProperties file: "deployment/cicd/config/deploy_env_vars_${environment}" + } + + stage("Setup AKS") { + container('azure-cli') { + withCredentials([azureServicePrincipal('azure-credentials')]) { + sh """ + az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID + az aks get-credentials --resource-group ${props.resource_group} --name ${props.cluster_name} --overwrite-existing + kubectl create namespace phonebill-dg0500 --dry-run=client -o yaml | kubectl apply -f - + """ + } + } + } + + stage('Build') { + container('gradle') { + sh """ + chmod +x gradlew + ./gradlew build -x test + """ + } + } + + stage('SonarQube Analysis & Quality Gate') { + if (skipSonarQube) { + echo "⏭️ Skipping SonarQube Analysis (SKIP_SONARQUBE=${params.SKIP_SONARQUBE})" + } else { + container('gradle') { + withSonarQubeEnv('SonarQube') { + // 각 서비스별 테스트 및 SonarQube 분석 + services.each { service -> + sh """ + ./gradlew :${service}:test :${service}:jacocoTestReport :${service}:sonar \\ + -Dsonar.projectKey=phonebill-${service}-dg0500 \\ + -Dsonar.projectName=phonebill-${service}-dg0500 \\ + -Dsonar.java.binaries=build/classes/java/main \\ + -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml \\ + -Dsonar.exclusions=**/config/**,**/entity/**,**/dto/**,**/*Application.class,**/exception/** + """ + } + + // Quality Gate 확인 + timeout(time: 10, unit: 'MINUTES') { + def qg = waitForQualityGate() + if (qg.status != 'OK') { + error "Pipeline aborted due to quality gate failure: ${qg.status}" + } + } + } + } + } + } + + stage('Build & Push Images') { + timeout(time: 30, unit: 'MINUTES') { + container('podman') { + withCredentials([ + usernamePassword( + credentialsId: 'acr-credentials', + usernameVariable: 'ACR_USERNAME', + passwordVariable: 'ACR_PASSWORD' + ), + usernamePassword( + credentialsId: 'dockerhub-credentials', + usernameVariable: 'DOCKERHUB_USERNAME', + passwordVariable: 'DOCKERHUB_PASSWORD' + ) + ]) { + // Docker Hub 로그인 (rate limit 해결) + sh "podman login docker.io --username \$DOCKERHUB_USERNAME --password \$DOCKERHUB_PASSWORD" + + // ACR 로그인 + sh "podman login acrdigitalgarage01.azurecr.io --username \$ACR_USERNAME --password \$ACR_PASSWORD" + + services.each { service -> + sh """ + podman build \\ + --build-arg BUILD_LIB_DIR="${service}/build/libs" \\ + --build-arg ARTIFACTORY_FILE="${service}.jar" \\ + -f deployment/container/Dockerfile-backend \\ + -t acrdigitalgarage01.azurecr.io/phonebill/${service}:${environment}-${imageTag} . + + podman push acrdigitalgarage01.azurecr.io/phonebill/${service}:${environment}-${imageTag} + """ + } + } + } + } + } + + stage('Update Manifest Repository') { + container('git') { + withCredentials([usernamePassword( + credentialsId: 'github-credentials-dg0500', + usernameVariable: 'GIT_USERNAME', + passwordVariable: 'GIT_TOKEN' + )]) { + sh """ + # 매니페스트 레포지토리 클론 + REPO_URL=\$(echo "https://github.com/cna-bootcamp/phonebill-manifest.git" | sed 's|https://||') + git clone https://\${GIT_USERNAME}:\${GIT_TOKEN}@\${REPO_URL} manifest-repo + cd manifest-repo + + # Kustomize 설치 + curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash + mkdir -p \$HOME/bin && mv kustomize \$HOME/bin/ + export PATH=\$PATH:\$HOME/bin + + # 환경별 매니페스트 업데이트 + cd phonebill/kustomize/overlays/${environment} + + # 각 서비스별 이미지 태그 업데이트 + services="api-gateway user-service bill-service product-service kos-mock" + for service in \$services; do + \$HOME/bin/kustomize edit set image acrdigitalgarage01.azurecr.io/phonebill/\$service:${environment}-${imageTag} + done + + # Git 설정 및 푸시 + cd ../../../.. + git config user.name "Jenkins CI" + git config user.email "jenkins@example.com" + git add . + git commit -m "🚀 Update phonebill ${environment} images to ${environment}-${imageTag}" + git push origin main + + echo "✅ 매니페스트 업데이트 완료. ArgoCD가 자동으로 배포합니다." + """ + } + } + } + + // 파이프라인 완료 로그 (Scripted Pipeline 방식) + stage('Pipeline Complete') { + echo "🧹 Pipeline completed. Pod cleanup handled by Jenkins Kubernetes Plugin." + + // 성공/실패 여부 로깅 + if (currentBuild.result == null || currentBuild.result == 'SUCCESS') { + echo "✅ Pipeline completed successfully!" + } else { + echo "❌ Pipeline failed with result: ${currentBuild.result}" + } + } + + } catch (Exception e) { + currentBuild.result = 'FAILURE' + echo "❌ Pipeline failed with exception: ${e.getMessage()}" + throw e + } finally { + echo "🧹 Cleaning up resources and preparing for pod termination..." + echo "Pod will be terminated in 3 seconds due to terminationGracePeriodSeconds: 3" + } + } +} \ No newline at end of file