diff --git a/Jenkinsfile b/Jenkinsfile index f849517..41c2d38 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,10 +1,226 @@ - pipeline { - agent any - stages { - stage('stage 1') { - steps { - echo "Hello world" - } - } - } - } \ No newline at end of file +pipeline { + agent any // 배포 서버에 직접 SSH 접속하는 경우, 특정 Jenkins 에이전트 레이블을 지정할 수 있음 + + 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 'https://git.frovide.com/kegorii/frovide.com.git' // 본인의 Git 저장소 URL로 변경 + // credentialsId: 'your-git-credential-id' // Git Credential 필요시 + } + } + + stage('Install Dependencies and Build (SvelteKit)') { + steps { + sh 'npm install --prefix . --cache ${env.REMOTE_NPM_CACHE_DIR}' // Jenkins 에이전트에서 의존성 설치 + sh 'npm run build' // SvelteKit 빌드 (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!" + } + } +} \ No newline at end of file diff --git a/src/routes/health/+server.js b/src/routes/health/+server.js new file mode 100644 index 0000000..2ef0d1a --- /dev/null +++ b/src/routes/health/+server.js @@ -0,0 +1,9 @@ +// src/routes/health/+server.js +export async function GET() { + return new Response('OK', { + status: 200, + headers: { + 'Content-Type': 'text/plain' + } + }); +} \ No newline at end of file