diff --git a/Jenkinsfile b/Jenkinsfile index cbbe5e7..6f98d9d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,12 @@ pipeline { agent any + parameters { + choice(name: 'ACTION', choices: ['deploy', 'rollback'], description: '배포 또는 롤백') + choice(name: 'ROLLBACK_TARGET', choices: ['production', 'stage'], description: '롤백 대상 환경') + string(name: 'ROLLBACK_RELEASE', defaultValue: '', description: '롤백할 릴리스 ID (예: 20260310_120000). 비워두면 직전 릴리스로 롤백') + } + options { disableConcurrentBuilds() } @@ -8,10 +14,73 @@ pipeline { environment { DEPLOY_USER = 'hskwon' RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') + PROD_SERVER = '211.117.60.189' } stages { + + // ── 롤백: 릴리스 목록 조회 ── + stage('Rollback: List Releases') { + when { expression { params.ACTION == 'rollback' } } + steps { + script { + def basePath = params.ROLLBACK_TARGET == 'production' ? '/home/webservice/api' : '/home/webservice/api-stage' + sshagent(credentials: ['deploy-ssh-key']) { + def releases = sh(script: "ssh ${DEPLOY_USER}@${PROD_SERVER} 'ls -1dt ${basePath}/releases/*/ | head -6 | xargs -I{} basename {}'", returnStdout: true).trim() + def current = sh(script: "ssh ${DEPLOY_USER}@${PROD_SERVER} 'basename \$(readlink -f ${basePath}/current)'", returnStdout: true).trim() + echo "=== ${params.ROLLBACK_TARGET} 릴리스 목록 ===" + echo "현재 활성: ${current}" + echo "사용 가능:\n${releases}" + } + } + } + } + + // ── 롤백: symlink 전환 ── + stage('Rollback: Switch Release') { + when { expression { params.ACTION == 'rollback' } } + steps { + script { + def basePath = params.ROLLBACK_TARGET == 'production' ? '/home/webservice/api' : '/home/webservice/api-stage' + + sshagent(credentials: ['deploy-ssh-key']) { + def targetRelease = params.ROLLBACK_RELEASE + if (!targetRelease?.trim()) { + // 비워두면 직전 릴리스로 롤백 + targetRelease = sh(script: "ssh ${DEPLOY_USER}@${PROD_SERVER} 'ls -1dt ${basePath}/releases/*/ | sed -n 2p | xargs basename'", returnStdout: true).trim() + } + + // 릴리스 존재 여부 확인 + sh "ssh ${DEPLOY_USER}@${PROD_SERVER} 'test -d ${basePath}/releases/${targetRelease}'" + + slackSend channel: '#deploy_api', color: '#FF9800', tokenCredentialId: 'slack-token', + message: "🔄 *api* ${params.ROLLBACK_TARGET} 롤백 시작 → ${targetRelease}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + + sh """ + ssh ${DEPLOY_USER}@${PROD_SERVER} ' + ln -sfn ${basePath}/releases/${targetRelease} ${basePath}/current && + cd ${basePath}/current && + php artisan config:cache && + php artisan route:cache && + php artisan view:cache && + sudo systemctl reload php8.4-fpm + ' + """ + + if (params.ROLLBACK_TARGET == 'production') { + sh "ssh ${DEPLOY_USER}@${PROD_SERVER} 'sudo supervisorctl restart sam-queue-worker:*'" + } + + slackSend channel: '#deploy_api', color: 'good', tokenCredentialId: 'slack-token', + message: "✅ *api* ${params.ROLLBACK_TARGET} 롤백 완료 → ${targetRelease}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + } + } + } + } + + // ── 일반 배포: Checkout ── stage('Checkout') { + when { expression { params.ACTION == 'deploy' } } steps { checkout scm script { @@ -24,17 +93,22 @@ pipeline { // ── main → 운영서버 Stage 배포 ── stage('Deploy Stage') { - when { branch 'main' } + when { + allOf { + branch 'main' + expression { params.ACTION == 'deploy' } + } + } steps { sshagent(credentials: ['deploy-ssh-key']) { sh """ - ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/api-stage/releases/${RELEASE_ID}' + ssh ${DEPLOY_USER}@${PROD_SERVER} 'mkdir -p /home/webservice/api-stage/releases/${RELEASE_ID}' rsync -az --delete \ --exclude='.git' --exclude='.env' \ --exclude='storage/app' --exclude='storage/logs' \ --exclude='storage/framework/sessions' --exclude='storage/framework/cache' \ - . ${DEPLOY_USER}@211.117.60.189:/home/webservice/api-stage/releases/${RELEASE_ID}/ - ssh ${DEPLOY_USER}@211.117.60.189 ' + . ${DEPLOY_USER}@${PROD_SERVER}:/home/webservice/api-stage/releases/${RELEASE_ID}/ + ssh ${DEPLOY_USER}@${PROD_SERVER} ' cd /home/webservice/api-stage/releases/${RELEASE_ID} && mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs && sudo chown -R www-data:webservice storage bootstrap/cache && @@ -71,17 +145,22 @@ pipeline { // ── main → 운영서버 Production 배포 ── stage('Deploy Production') { - when { branch 'main' } + when { + allOf { + branch 'main' + expression { params.ACTION == 'deploy' } + } + } steps { sshagent(credentials: ['deploy-ssh-key']) { sh """ - ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/api/releases/${RELEASE_ID}' + ssh ${DEPLOY_USER}@${PROD_SERVER} 'mkdir -p /home/webservice/api/releases/${RELEASE_ID}' rsync -az --delete \ --exclude='.git' --exclude='.env' \ --exclude='storage/app' --exclude='storage/logs' \ --exclude='storage/framework/sessions' --exclude='storage/framework/cache' \ - . ${DEPLOY_USER}@211.117.60.189:/home/webservice/api/releases/${RELEASE_ID}/ - ssh ${DEPLOY_USER}@211.117.60.189 ' + . ${DEPLOY_USER}@${PROD_SERVER}:/home/webservice/api/releases/${RELEASE_ID}/ + ssh ${DEPLOY_USER}@${PROD_SERVER} ' cd /home/webservice/api/releases/${RELEASE_ID} && mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs && sudo chown -R www-data:webservice storage bootstrap/cache && @@ -109,23 +188,32 @@ pipeline { post { success { - slackSend channel: '#deploy_api', color: 'good', tokenCredentialId: 'slack-token', - message: "✅ *api* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + script { + if (params.ACTION == 'deploy') { + slackSend channel: '#deploy_api', color: 'good', tokenCredentialId: 'slack-token', + message: "✅ *api* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + } + } } failure { - slackSend channel: '#deploy_api', color: 'danger', tokenCredentialId: 'slack-token', - message: "❌ *api* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" script { - if (env.BRANCH_NAME == 'main') { - sshagent(credentials: ['deploy-ssh-key']) { - sh """ - ssh ${DEPLOY_USER}@211.117.60.189 ' - PREV=\$(ls -1dt /home/webservice/api/releases/*/ | sed -n "2p" | xargs basename 2>/dev/null) && - [ -n "\$PREV" ] && ln -sfn /home/webservice/api/releases/\$PREV /home/webservice/api/current && - sudo systemctl reload php8.4-fpm - ' || true - """ + if (params.ACTION == 'deploy') { + slackSend channel: '#deploy_api', color: 'danger', tokenCredentialId: 'slack-token', + message: "❌ *api* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" + if (env.BRANCH_NAME == 'main') { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@${PROD_SERVER} ' + PREV=\$(ls -1dt /home/webservice/api/releases/*/ | sed -n "2p" | xargs basename 2>/dev/null) && + [ -n "\$PREV" ] && ln -sfn /home/webservice/api/releases/\$PREV /home/webservice/api/current && + sudo systemctl reload php8.4-fpm + ' || true + """ + } } + } else { + slackSend channel: '#deploy_api', color: 'danger', tokenCredentialId: 'slack-token', + message: "❌ *api* ${params.ROLLBACK_TARGET} 롤백 실패\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } } }