pipeline { agent any environment { // Docker Image Configuration APP_IMAGE = "localtest/soso-server:latest" COMPOSE_PROJECT_NAME = "soso-server" // Deployment Configuration DEPLOY_TIMEOUT = "300" HEALTH_CHECK_RETRIES = "30" HEALTH_CHECK_INTERVAL = "10" } options { timeout(time: 45, unit: 'MINUTES') timestamps() buildDiscarder(logRotator(numToKeepStr: '30', daysToKeepStr: '7')) skipDefaultCheckout(false) ansiColor('xterm') } triggers { githubPush() } stages { stage('πŸ—οΈ Prepare') { steps { script { // Clean workspace and checkout cleanWs() checkout scm // Display build information sh ''' echo "πŸš€ SOSO Server CI/CD Pipeline Started" echo "πŸ“‹ Build Information:" echo " β€’ Branch: ${GIT_BRANCH}" echo " β€’ Commit: ${GIT_COMMIT}" echo " β€’ Build: ${BUILD_NUMBER}" echo " β€’ Date: $(date '+%Y-%m-%d %H:%M:%S %Z')" echo " β€’ Image: ${APP_IMAGE}" echo "" ''' // Set dynamic variables env.BUILD_TIMESTAMP = sh(script: "date +%Y%m%d-%H%M%S", returnStdout: true).trim() env.GIT_SHORT_COMMIT = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() } } } stage('πŸ§ͺ Unit Tests') { steps { sh ''' echo "πŸ§ͺ Running Unit Tests..." set -eux # Test Environment Configuration export SPRING_PROFILES_ACTIVE=test export SPRING_DATASOURCE_URL="jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE" export SPRING_DATASOURCE_DRIVER_CLASS_NAME="org.h2.Driver" export SPRING_DATASOURCE_USERNAME="sa" export SPRING_DATASOURCE_PASSWORD="" export SPRING_JPA_HIBERNATE_DDL_AUTO="create-drop" export SPRING_JPA_DATABASE_PLATFORM="org.hibernate.dialect.H2Dialect" export SPRING_SESSION_STORE_TYPE="none" echo "πŸ“Š Test Configuration:" echo " β€’ Profile: $SPRING_PROFILES_ACTIVE" echo " β€’ Database: H2 In-Memory" echo " β€’ Java: $(java -version 2>&1 | head -1)" echo "" # ============================================================= # μ•ˆμ „ν•œ λΉŒλ“œ μ΅œμ ν™” μ „λž΅ # ============================================================= # λͺ©ν‘œ: μ•ˆμ •μ„±μ„ μœ μ§€ν•˜λ©΄μ„œ λΉŒλ“œ μ‹œκ°„ 단좕 # # 1단계: ν…ŒμŠ€νŠΈ 결과만 μ‚­μ œ (컴파일 μΊμ‹œ 보쑴) # - 전체 build μ‚­μ œ λŒ€μ‹  ν…ŒμŠ€νŠΈ 좜λ ₯만 정리 # - μ˜ˆμƒ 효과: 컴파일 μ‹œκ°„ 30-50% μ ˆμ•½ # # 2단계: Gradle 데λͺ¬ μž¬μ‹œμž‘ (κΉ¨λ—ν•œ μƒνƒœ + 속도 ν–₯상) # - 맀번 데λͺ¬ 쀑지 ν›„ μž¬μ‹œμž‘μœΌλ‘œ μΊμ‹œ μ˜€μ—Ό λ°©μ§€ # - μ˜ˆμƒ 효과: 데λͺ¬ μž¬μ‚¬μš©μœΌλ‘œ 5-10초 μ ˆμ•½ # # 3단계: 병렬 ν…ŒμŠ€νŠΈ μ‹€ν–‰ (λ©€ν‹°μ½”μ–΄ ν™œμš©) # - 독립적인 ν…ŒμŠ€νŠΈλ₯Ό λ™μ‹œ μ‹€ν–‰ # - μ˜ˆμƒ 효과: ν…ŒμŠ€νŠΈ μ‹œκ°„ 30-40% 단좕 # ============================================================= # ν…ŒμŠ€νŠΈ κ²°κ³Ό 파일만 μ‚­μ œ (컴파일 μΊμ‹œλŠ” μœ μ§€) echo "🧹 Cleaning test output files..." rm -rf build/test-results || true rm -rf build/reports/tests || true # μ†μƒλœ Gradle μΊμ‹œ 파일만 정리 rm -rf .gradle/8.*/fileHashes/fileHashes.lock || true rm -rf .gradle/buildOutputCleanup/buildOutputCleanup.lock || true # Gradle 데λͺ¬ μž¬μ‹œμž‘ (κΉ¨λ—ν•œ μƒνƒœ 보μž₯) echo "πŸ”„ Restarting Gradle daemon for clean state..." ./gradlew --stop || true # μ΅œμ ν™”λœ ν…ŒμŠ€νŠΈ μ‹€ν–‰ echo "πŸš€ Running tests with optimizations..." ./gradlew test \ -Dspring.profiles.active=test \ --parallel \ --max-workers=4 \ --build-cache \ --info \ --stacktrace echo "βœ… Tests completed successfully" ''' } post { always { junit testResults: 'build/test-results/test/*.xml', allowEmptyResults: true // publishTestResults testResultsPattern: 'build/test-results/test/*.xml' } } } stage('πŸ—οΈ Build Application') { steps { sh ''' echo "πŸ—οΈ Building Application JAR..." set -eux # Build the application ./gradlew bootJar \ --info \ --parallel # Display build results echo "πŸ“¦ Build Results:" ls -la build/libs/ # Extract version information JAR_FILE=$(find build/libs -name "*.jar" -not -name "*plain*" | head -1) if [ -f "$JAR_FILE" ]; then JAR_SIZE=$(du -h "$JAR_FILE" | cut -f1) echo " β€’ JAR File: $(basename "$JAR_FILE")" echo " β€’ Size: $JAR_SIZE" fi ''' archiveArtifacts artifacts: 'build/libs/*.jar', allowEmptyArchive: false, onlyIfSuccessful: true } } stage('🐳 Build Docker Image') { steps { sh ''' echo "🐳 Building Docker Image..." set -eux # Build Docker image with multiple tags docker build \ -t "${APP_IMAGE}" \ -t "${APP_IMAGE%:*}:${BUILD_TIMESTAMP}" \ -t "${APP_IMAGE%:*}:${GIT_SHORT_COMMIT}" \ --label "version=${BUILD_TIMESTAMP}" \ --label "commit=${GIT_SHORT_COMMIT}" \ --label "build-number=${BUILD_NUMBER}" \ . echo "πŸ“Š Docker Image Information:" docker images | grep "${APP_IMAGE%:*}" | head -5 # Clean up old images docker image prune -f --filter "until=72h" || true ''' } } stage('πŸš€ Deploy to Production') { steps { script { withCredentials([file(credentialsId: 'soso-env', variable: 'ENV_FILE')]) { sh ''' echo "πŸš€ Deploying to Production..." set -eux # ============================================================= # 영ꡬ 배포 디렉토리 μ‚¬μš© (Jenkins workspace와 독립적) # ============================================================= # # μ™œ μ΄λ ‡κ²Œ ν•˜λŠ”κ°€? # 1. Jenkins workspaceλŠ” cleanWs()둜 μ‚­μ œλ¨ # 2. Docker λ³Όλ₯¨ λ§ˆμš΄νŠΈκ°€ workspace 경둜λ₯Ό μ°Έμ‘°ν•˜λ©΄ μ•ˆ 됨 # 3. μ„œλ²„μ˜ 영ꡬ 디렉토리λ₯Ό 배포 κΈ°μ€€μ μœΌλ‘œ μ‚¬μš© # 4. 레포 μ€‘μ‹¬μ˜ 인프라 배포 (Infrastructure as Code) # # μž₯점: # - Caddyfile λ“± μ„€μ • 파일 κ²½λ‘œκ°€ 항상 유효 # - μ»¨ν…Œμ΄λ„ˆ μž¬μ‹œμž‘ μ‹œμ—λ„ 마운트 경둜 보쑴 # - Git 기반 버전 관리 및 λ‘€λ°± κ°€λŠ₯ # ============================================================= DEPLOY_DIR=/srv/soso/app/SOSO-Server echo "πŸ“‚ 배포 디렉토리: $DEPLOY_DIR" # 배포 디렉토리가 μ—†μœΌλ©΄ 생성 if [ ! -d "$DEPLOY_DIR" ]; then echo "⚠️ 배포 디렉토리가 μ—†μŠ΅λ‹ˆλ‹€. 생성 쀑..." mkdir -p "$DEPLOY_DIR" cd "$DEPLOY_DIR" git clone https://github.com/B2A5/SOSO-Server.git . fi # 배포 λ””λ ‰ν† λ¦¬λ‘œ 이동 cd "$DEPLOY_DIR" # μ΅œμ‹  μ½”λ“œλ‘œ μ—…λ°μ΄νŠΈ (ν˜„μž¬ 브랜치 κΈ°μ€€) echo "πŸ”„ μ΅œμ‹  μ½”λ“œλ‘œ μ—…λ°μ΄νŠΈ 쀑..." git fetch origin git reset --hard origin/${GIT_BRANCH##*/} echo "βœ… ν˜„μž¬ 컀밋:" git log -1 --oneline echo "" # Copy environment file to deployment directory cp "$ENV_FILE" "$DEPLOY_DIR/.env" # Set the API image in environment echo "API_IMAGE=${APP_IMAGE}" >> "$DEPLOY_DIR/.env" echo "πŸ“‹ Deployment Configuration:" echo " β€’ Deploy Dir: $DEPLOY_DIR" echo " β€’ Image: ${APP_IMAGE}" echo " β€’ Branch: ${GIT_BRANCH##*/}" echo " β€’ Compose Project: ${COMPOSE_PROJECT_NAME}" echo " β€’ Environment: Production" echo "" # ============================================================= # 무쀑단 배포 (Zero Downtime Deployment) # ============================================================= # # μ „λž΅: # 1. μ˜μ‘΄μ„± μ„œλΉ„μŠ€(DB, Redis)λŠ” 이미 μ‹€ν–‰ 쀑이면 μŠ€ν‚΅ # 2. APIλŠ” 둀링 μ—…λ°μ΄νŠΈ (μƒˆ μ»¨ν…Œμ΄λ„ˆ μ‹œμž‘ β†’ ν—¬μŠ€μ²΄ν¬ β†’ 이전 μ»¨ν…Œμ΄λ„ˆ 제거) # 3. ProxyλŠ” μ ˆλŒ€ μž¬μ‹œμž‘ν•˜μ§€ μ•ŠμŒ (영ꡬ μœ μ§€) # # λ‹€μš΄νƒ€μž„: 0초 # ============================================================= # μ˜μ‘΄μ„± μ„œλΉ„μŠ€ 확인 및 μ‹œμž‘ echo "πŸ” μ˜μ‘΄μ„± μ„œλΉ„μŠ€ 확인 쀑..." docker compose up -d --no-deps db redis # μ˜μ‘΄μ„± μ„œλΉ„μŠ€λ“€μ΄ 정상 μƒνƒœκ°€ 될 λ•ŒκΉŒμ§€ λŒ€κΈ° echo "⏳ μ˜μ‘΄μ„± μ„œλΉ„μŠ€ ν—¬μŠ€μ²΄ν¬..." timeout ${DEPLOY_TIMEOUT} bash -c ' until docker compose ps db | grep -q "healthy"; do echo " β€’ λ°μ΄ν„°λ² μ΄μŠ€ μ€€λΉ„ 쀑..." sleep 5 done until docker compose ps redis | grep -q "healthy"; do echo " β€’ Redis μ€€λΉ„ 쀑..." sleep 5 done ' echo "βœ… μ˜μ‘΄μ„± μ„œλΉ„μŠ€ 정상" # ============================================================= # API 무쀑단 배포 (Rolling Update) # ============================================================= # # docker compose up -d --no-deps api λ™μž‘: # 1. μƒˆ API μ»¨ν…Œμ΄λ„ˆ 생성 (이전 μ»¨ν…Œμ΄λ„ˆλŠ” 계속 μ‹€ν–‰) # 2. μƒˆ μ»¨ν…Œμ΄λ„ˆ μ‹œμž‘ 및 ν—¬μŠ€μ²΄ν¬ λŒ€κΈ° # 3. ν—¬μŠ€μ²΄ν¬ 톡과 μ‹œ λ„€νŠΈμ›Œν¬μ— μΆ”κ°€ # 4. Proxyκ°€ μžλ™μœΌλ‘œ μƒˆ μ»¨ν…Œμ΄λ„ˆλ‘œ λΌμš°νŒ… μ‹œμž‘ # 5. 이전 μ»¨ν…Œμ΄λ„ˆ graceful shutdown 및 제거 # # μ‚¬μš©μž μž…μž₯: μ„œλΉ„μŠ€ 쀑단 μ—†μŒ βœ… # ============================================================= echo "πŸš€ API 무쀑단 배포 μ‹œμž‘..." echo " β€’ ν˜„μž¬ μ‹€ν–‰ 쀑인 API: $(docker compose ps api --format '{{.Status}}' 2>/dev/null || echo 'μ—†μŒ')" # --no-deps: μ˜μ‘΄μ„± μ„œλΉ„μŠ€λŠ” κ±΄λ“œλ¦¬μ§€ μ•ŠμŒ # --wait: ν—¬μŠ€μ²΄ν¬ ν†΅κ³ΌκΉŒμ§€ λŒ€κΈ° # --wait-timeout: μ΅œλŒ€ λŒ€κΈ° μ‹œκ°„ docker compose up -d --no-deps --wait --wait-timeout 180 api # μ΅œμ’… ν—¬μŠ€μ²΄ν¬ 확인 (μΆ”κ°€ μ•ˆμ „μž₯치) echo "πŸ₯ API μ΅œμ’… ν—¬μŠ€μ²΄ν¬..." RETRY_COUNT=0 until [ $RETRY_COUNT -eq 10 ]; do if docker compose ps api | grep -q "healthy"; then echo "βœ… API 무쀑단 배포 μ™„λ£Œ!" echo " β€’ μƒˆ API μ»¨ν…Œμ΄λ„ˆ: $(docker compose ps api --format '{{.ID}}' | head -1)" break elif [ $RETRY_COUNT -eq 9 ]; then echo "❌ API ν—¬μŠ€μ²΄ν¬ μ‹€νŒ¨" echo "πŸ“‹ μ»¨ν…Œμ΄λ„ˆ μƒνƒœ:" docker compose ps api echo "πŸ“‹ 졜근 둜그:" docker compose logs api --tail 50 exit 1 else echo " β€’ ν—¬μŠ€μ²΄ν¬ λŒ€κΈ° 쀑... ($((RETRY_COUNT+1))/10)" sleep 5 fi RETRY_COUNT=$((RETRY_COUNT+1)) done # ============================================================= # Proxy 확인 및 μ‹œμž‘ (졜초 1회만 λ˜λŠ” ν•„μš” μ‹œ) # ============================================================= # # ProxyλŠ” 영ꡬ μœ μ§€λ©λ‹ˆλ‹€: # - 이미 μ‹€ν–‰ 쀑이면 κ·ΈλŒ€λ‘œ μœ μ§€ # - μ—†μœΌλ©΄ μ‹œμž‘ # - 좩돌 μ‹œ κΈ°μ‘΄ μ»¨ν…Œμ΄λ„ˆ 제거 ν›„ μ‹œμž‘ # ============================================================= echo "🌐 Proxy 배포 쀑..." # Proxyκ°€ μ—†μœΌλ©΄ 생성, 있으면 μ„€μ • λ¦¬λ‘œλ“œ if docker compose ps proxy | grep -q "proxy"; then echo "πŸ”„ Proxy μ„€μ • λ¦¬λ‘œλ“œ 쀑 (무쀑단)..." # Caddy μ„€μ • 무쀑단 λ¦¬λ‘œλ“œ docker exec soso-proxy caddy reload --config /etc/caddy/Caddyfile --force 2>&1 || { echo "⚠️ λ¦¬λ‘œλ“œ μ‹€νŒ¨, Proxy μž¬μ‹œμž‘ 쀑..." docker compose restart proxy sleep 3 } echo "βœ… Proxy μ„€μ • μ—…λ°μ΄νŠΈ μ™„λ£Œ" else echo "πŸ”„ Proxy μ‹œμž‘ 쀑..." # ν˜Ήμ‹œ λͺ¨λ₯Ό 좩돌 λ°©μ§€ (μˆ˜λ™ μ‹€ν–‰ μ»¨ν…Œμ΄λ„ˆ λ“±) docker rm -f soso-proxy 2>/dev/null || true # Proxy μ‹œμž‘ (μ˜μ‘΄μ„±: API healthy) docker compose up -d --no-deps proxy # Proxy ν—¬μŠ€μ²΄ν¬ sleep 3 if docker compose ps proxy | grep -q "healthy"; then echo "βœ… Proxy 정상 μ‹œμž‘ μ™„λ£Œ" else echo "⚠️ Proxy ν—¬μŠ€μ²΄ν¬ λŒ€κΈ° 쀑..." fi fi # μ΅œμ’… μ‹œμŠ€ν…œ μƒνƒœ 확인 echo "πŸ” μ΅œμ’… μ‹œμŠ€ν…œ μƒνƒœ 확인..." docker compose ps echo "βœ… 배포 μ™„λ£Œ!" echo "" echo "🌐 Service URLs:" echo " β€’ Main Site: https://soso.dreampaste.com" echo " β€’ API Docs: https://soso.dreampaste.com/swagger-ui/" echo " β€’ Jenkins: https://soso.dreampaste.com/jenkins/" echo "" ''' } } } post { failure { script { sh ''' echo "❌ Deployment failed - Rolling back..." # 배포 λ””λ ‰ν† λ¦¬λ‘œ 이동 DEPLOY_DIR=/srv/soso/app/SOSO-Server cd "$DEPLOY_DIR" # Show current status echo "πŸ“‹ Current Status:" docker compose ps || true # Show logs for debugging echo "πŸ“‹ Service Logs:" docker compose logs api --tail 100 || true # Stop failed services echo "πŸ›‘ Stopping failed services..." docker compose stop api || true docker compose rm -f api || true echo "πŸ”„ Rollback completed" ''' } } success { script { sh ''' echo "πŸŽ‰ Deployment Success!" # 배포 λ””λ ‰ν† λ¦¬λ‘œ 이동 DEPLOY_DIR=/srv/soso/app/SOSO-Server cd "$DEPLOY_DIR" echo "πŸ“Š Final Status:" docker compose ps echo "" echo "πŸ’Ύ Cleaning up old images..." docker image prune -f --filter "until=24h" || true ''' } } } } } post { always { script { sh ''' echo "🧹 Pipeline Cleanup..." # Clean up temporary files rm -f .env || true ''' cleanWs() } } success { echo 'πŸŽ‰ Pipeline completed successfully!' } failure { echo '❌ Pipeline failed!' } unstable { echo '⚠️ Pipeline completed with warnings' } } }