pipeline { agent any environment { // Jenkins Credentials ID (SSH 접속용) JENKINS_SSH_CREDENTIAL_ID = 'frovide-ssh-credential-id' // Jenkins Credentials에 등록된 ID로 변경 // 배포 서버 정보 DEPLOY_USER = 'kegorii' // 배포 서버 SSH 사용자 DEPLOY_HOST = 'frovide.com' // 배포 서버 IP 또는 호스트명 APP_NAME = 'frovide-production' // PM2에서 사용할 애플리케이션 이름 // 사용할 두 개의 포트 PORT_A = 7051 PORT_B = 7052 // 배포 서버 내 프로젝트 경로 REMOTE_APP_ROOT_DIR = "/home/${env.DEPLOY_USER}/${env.APP_NAME}" // 프로젝트 루트 디렉토리 REMOTE_BUILD_DIR = "${env.REMOTE_APP_ROOT_DIR}/build" // SvelteKit 빌드 결과물이 위치할 디렉토리 // 배포 서버 내 Node.js 설치 및 PM2 캐시 경로 REMOTE_NPM_CACHE_DIR = "/home/${env.DEPLOY_USER}/.npm" // npm 캐시 경로 (최적화용) REMOTE_SCRIPTS_DIR = "/opt/scripts" // switch_nginx_port.sh 스크립트 경로 // 헬스 체크 URL HEALTH_CHECK_URL = "http://${env.DEPLOY_HOST}" // Nginx 80포트로 접근하여 헬스 체크 HEALTH_CHECK_PATH = "/health" // SvelteKit 앱에 구현할 헬스 체크 경로 (아래 참고) HEALTH_CHECK_TIMEOUT_SECONDS = 30 HEALTH_CHECK_INTERVAL_SECONDS = 5 } stages { stage('Checkout Source Code') { steps { git branch: 'main', url: 'https://git.frovide.com/kegorii/frovide.com.git' // 만약 main 브랜치가 아닌 다른 브랜치라면 해당 브랜치 이름으로 변경해주세요. // 예: git branch: 'develop', url: '...' } } stage('Install Dependencies and Build (SvelteKit)') { steps { // nvm 활성화를 위해 . (점) 명령을 사용 sh ''' . /var/lib/jenkins/.nvm/nvm.sh nvm use 18 # 사용하려는 Node.js 버전 npm install npm run build ''' echo "SvelteKit project built successfully." } } stage('Determine Active Port') { steps { sshagent([env.JENKINS_SSH_CREDENTIAL_ID]) { script { // 현재 Nginx가 바라보고 있는 포트를 배포 서버에서 가져옴 def nginxConfPath = "/etc/nginx/conf.d/svelte_app_upstream.conf" // Nginx 설정 파일 경로 def currentActivePortOutput = sh(script: "ssh ${env.DEPLOY_USER}@${env.DEPLOY_HOST} 'grep -oP \"server 127.0.0.1:\\\\K[0-9]+\" ${nginxConfPath} | head -n 1'", returnStdout: true).trim() if (currentActivePortOutput == "") { // 초기 배포이거나 Nginx 설정에 활성화된 포트가 없는 경우 env.ACTIVE_PORT = env.PORT_A // 첫 배포 시 PORT_A를 활성 포트로 가정 env.INACTIVE_PORT = env.PORT_B // 다음 배포 대상은 PORT_B echo "Initial deployment or no active port found. Setting active port to ${env.ACTIVE_PORT} and inactive to ${env.INACTIVE_PORT}." } else if (currentActivePortOutput == "${env.PORT_A}") { env.ACTIVE_PORT = env.PORT_A env.INACTIVE_PORT = env.PORT_B echo "Current active port is ${env.ACTIVE_PORT}. Next deployment target port will be ${env.INACTIVE_PORT}." } else if (currentActivePortOutput == "${env.PORT_B}") { env.ACTIVE_PORT = env.PORT_B env.INACTIVE_PORT = env.PORT_A echo "Current active port is ${env.ACTIVE_PORT}. Next deployment target port will be ${env.INACTIVE_PORT}." } else { error "Unexpected active port found: ${currentActivePortOutput}. Please check Nginx configuration and app_upstream.conf." } } } } } stage('Transfer Built Application') { steps { sshagent([env.JENKINS_SSH_CREDENTIAL_ID]) { script { // 배포 서버에 프로젝트 루트 디렉토리 생성 sh "ssh ${env.DEPLOY_USER}@${env.DEPLOY_HOST} 'mkdir -p ${env.REMOTE_APP_ROOT_DIR}'" // SvelteKit 빌드 결과물 (build 디렉토리)과 package.json, package-lock.json (PM2 의존성 확인용) 전송 echo "Transferring SvelteKit build artifacts and package files to ${env.REMOTE_APP_ROOT_DIR}..." // rsync를 사용하여 변경된 파일만 전송하여 효율성을 높입니다. sh "rsync -avz ./build/ ${env.DEPLOY_USER}@${env.DEPLOY_HOST}:${env.REMOTE_BUILD_DIR}" sh "rsync -avz ./package.json ${env.DEPLOY_USER}@${env.DEPLOY_HOST}:${env.REMOTE_APP_ROOT_DIR}" sh "rsync -avz ./package-lock.json ${env.DEPLOY_USER}@${env.DEPLOY_HOST}:${env.REMOTE_APP_ROOT_DIR}" } } } } stage('Deploy New Version (Inactive Port)') { steps { sshagent([env.JENKINS_SSH_CREDENTIAL_ID]) { script { // PM2를 사용하여 새로운 버전의 애플리케이션을 Inactive Port에 시작 echo "Starting new version of ${env.APP_NAME} on port ${env.INACTIVE_PORT} using PM2..." // SvelteKit adapter-node의 기본 실행 파일은 build/index.js 입니다. // PM2는 프로젝트 루트에서 실행되어야 package.json을 찾을 수 있습니다. def pm2StartCommand = "cd ${env.REMOTE_APP_ROOT_DIR} && NODE_ENV=production PORT=${env.INACTIVE_PORT} pm2 start build/index.js --name \"${env.APP_NAME}-${env.INACTIVE_PORT}\" --output /dev/null --error /dev/null --time --watch --ignore-watch=\"node_modules\" --max-memory-restart 500M" // 이전 프로세스가 남아있을 경우를 대비하여 stop/delete 후 시작 (선택 사항) sh "ssh ${env.DEPLOY_USER}@${env.DEPLOY_HOST} 'pm2 stop ${env.APP_NAME}-${env.INACTIVE_PORT} || true'" sh "ssh ${env.DEPLOY_USER}@${env.DEPLOY_HOST} 'pm2 delete ${env.APP_NAME}-${env.INACTIVE_PORT} || true'" sh "ssh ${env.DEPLOY_USER}@${env.DEPLOY_HOST} '${pm2StartCommand}'" echo "PM2 process started for ${env.APP_NAME}-${env.INACTIVE_PORT}." } } } } stage('Health Check New Version') { steps { sshagent([env.JENKINS_SSH_CREDENTIAL_ID]) { script { echo "Performing health check on new version at port ${env.INACTIVE_PORT}..." def isHealthy = false def attempts = 0 def maxAttempts = env.HEALTH_CHECK_TIMEOUT_SECONDS.toInteger() / env.HEALTH_CHECK_INTERVAL_SECONDS.toInteger() while (!isHealthy && attempts < maxAttempts) { try { // SvelteKit 앱이 직접 해당 포트에서 헬스 체크 엔드포인트에 응답하는지 확인 def healthCheckTarget = "http://localhost:${env.INACTIVE_PORT}${env.HEALTH_CHECK_PATH}" def responseCode = sh(script: "ssh ${env.DEPLOY_USER}@${env.DEPLOY_HOST} 'curl -s -o /dev/null -w \"%{http_code}\" ${healthCheckTarget}'", returnStdout: true).trim() if (responseCode == "200") { isHealthy = true echo "Health check passed for new version on port ${env.INACTIVE_PORT}." } else { echo "Health check failed (HTTP ${responseCode}) for new version on port ${env.INACTIVE_PORT}. Retrying..." sleep env.HEALTH_CHECK_INTERVAL_SECONDS.toInteger() } } catch (e) { echo "Error during health check: ${e}. Retrying..." sleep env.HEALTH_CHECK_INTERVAL_SECONDS.toInteger() } attempts++ } if (!isHealthy) { error "Health check failed for new version on port ${env.INACTIVE_PORT} after ${attempts} attempts. Aborting deployment." } } } } } stage('Switch Nginx (Traffic Shift)') { steps { sshagent([env.JENKINS_SSH_CREDENTIAL_ID]) { script { echo "Switching Nginx to new version on port ${env.INACTIVE_PORT}..." // 배포 서버에서 Nginx 스위치 스크립트 실행 sh "ssh ${env.DEPLOY_USER}@${env.DEPLOY_HOST} '${env.REMOTE_SCRIPTS_DIR}/switch_nginx_port.sh ${env.INACTIVE_PORT}'" echo "Nginx successfully switched to port ${env.INACTIVE_PORT}." // 최종적으로 Nginx 80포트로 헬스 체크하여 새로운 버전이 정상 서비스되는지 확인 echo "Final health check via Nginx (port 80)..." def isNginxHealthy = false def nginxAttempts = 0 def maxNginxAttempts = 10 // Nginx 스위치 후 짧게 확인 while (!isNginxHealthy && nginxAttempts < maxNginxAttempts) { try { def responseCode = sh(script: "curl -s -o /dev/null -w \"%{http_code}\" ${env.HEALTH_CHECK_URL}${env.HEALTH_CHECK_PATH}", returnStdout: true).trim() if (responseCode == "200") { isNginxHealthy = true echo "Nginx health check passed. Traffic successfully shifted." } else { echo "Nginx health check failed (HTTP ${responseCode}). Retrying..." sleep 5 } } catch (e) { echo "Error during Nginx health check: ${e}. Retrying..." sleep 5 } nginxAttempts++ } if (!isNginxHealthy) { error "Final Nginx health check failed after ${nginxAttempts} attempts. Potential issue with Nginx switch." } } } } } stage('Stop Old Version') { steps { sshagent([env.JENKINS_SSH_CREDENTIAL_ID]) { script { echo "Stopping old version of ${env.APP_NAME} on port ${env.ACTIVE_PORT}..." // PM2를 사용하여 이전 버전의 애플리케이션 종료 sh "ssh ${env.DEPLOY_USER}@${env.DEPLOY_HOST} 'pm2 stop ${env.APP_NAME}-${env.ACTIVE_PORT} || true'" sh "ssh ${env.DEPLOY_USER}@${env.DEPLOY_HOST} 'pm2 delete ${env.APP_NAME}-${env.ACTIVE_PORT} || true'" echo "Old version on port ${env.ACTIVE_PORT} stopped." } } } } } post { always { cleanWs() // Jenkins 워크스페이스 정리 echo "Deployment pipeline finished." } success { echo "Deployment successful!" // 알림 보내기 (예: Slack) // slackSend channel: '#deploy-notifications', message: "Deployment of ${env.APP_NAME} to ${env.DEPLOY_HOST} successful!" } failure { echo "Deployment failed! Rolling back or investigate manually." // 롤백 로직 추가 (선택 사항) // 예: Nginx를 이전 포트로 다시 전환하는 스크립트 실행 // ssh ${env.DEPLOY_USER}@${env.DEPLOY_HOST} '${env.REMOTE_SCRIPTS_DIR}/switch_nginx_port.sh ${env.ACTIVE_PORT}' // 알림 보내기 // slackSend channel: '#deploy-notifications', message: "Deployment of ${env.APP_NAME} to ${env.DEPLOY_HOST} FAILED!" } } }