frovide.com/Jenkinsfile
Insub Kim 370cbfaaff
Some checks failed
main-branch-frovide/pipeline/head There was a failure building this commit
Install Dependencies and Build (SvelteKit) 수정
2025-06-05 06:07:45 +00:00

229 lines
13 KiB
Groovy

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 branch: 'main', url: 'https://git.frovide.com/kegorii/frovide.com.git'
// 만약 main 브랜치가 아닌 다른 브랜치라면 해당 브랜치 이름으로 변경해주세요.
// 예: git branch: 'develop', url: '...'
}
}
stage('Install Dependencies and Build (SvelteKit)') {
steps {
// Groovy 문자열 내에서 ${} 구문을 사용하여 Jenkins 환경 변수를 직접 삽입합니다.
// sh 명령은 이중 따옴표로 감싸서 Jenkins가 내부의 Groovy 변수를 먼저 치환하게 합니다.
sh "npm install --prefix . --cache ${env.REMOTE_NPM_CACHE_DIR}"
sh "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!"
}
}
}