#!/usr/bin/env bash # ============================================================================= # 파일 배포 스크립트 (rsync/SCP) # Jenkins Stage 5: Deploy에서 호출 # 사용: deploy.sh --server <서버명> --env --ssh-key <키파일> # --ssh-user <사용자> --artifact <파일경로> [--rollback true] # ============================================================================= set -euo pipefail # ── 기본값 ──────────────────────────────────────────────────────────────────── SERVER="" DEPLOY_ENV="dev" SSH_KEY="" SSH_USER="deploy" ARTIFACT="" SSH_PORT="${SSH_PORT:-22}" IS_ROLLBACK="false" LOG_DIR="${WORKSPACE:-/tmp}/build-logs" DEPLOY_LOG="${LOG_DIR}/deploy-$(date +%Y%m%d-%H%M%S).log" # 배포 설정 (환경변수로 재정의 가능) DEPLOY_BASE_PATH="${DEPLOY_BASE_PATH:-/opt/apps}" BACKUP_COUNT="${BACKUP_COUNT:-5}" # 이전 버전 보관 개수 DEPLOY_TIMEOUT="${DEPLOY_TIMEOUT:-300}" # rsync 타임아웃 (초) GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m' log() { echo -e "${GREEN}[DEPLOY]${NC} $*" | tee -a "${DEPLOY_LOG}"; } err() { echo -e "${RED}[ERROR]${NC} $*" | tee -a "${DEPLOY_LOG}"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*" | tee -a "${DEPLOY_LOG}"; } mkdir -p "${LOG_DIR}" # ── 파라미터 파싱 ──────────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case $1 in --server) SERVER="$2"; shift 2 ;; --env) DEPLOY_ENV="$2"; shift 2 ;; --ssh-key) SSH_KEY="$2"; shift 2 ;; --ssh-user) SSH_USER="$2"; shift 2 ;; --artifact) ARTIFACT="$2"; shift 2 ;; --rollback) IS_ROLLBACK="$2";shift 2 ;; *) shift ;; esac done # ── 필수값 확인 ─────────────────────────────────────────────────────────────── [[ -z "${SERVER}" ]] && { err "--server 파라미터가 필요합니다"; exit 1; } [[ -z "${SSH_KEY}" ]] && { err "--ssh-key 파라미터가 필요합니다"; exit 1; } [[ -z "${ARTIFACT}" ]] && { err "--artifact 파라미터가 필요합니다"; exit 1; } [[ ! -f "${ARTIFACT}" ]] && { err "아티팩트 파일을 찾을 수 없습니다: ${ARTIFACT}"; exit 1; } [[ ! -f "${SSH_KEY}" ]] && { err "SSH 키 파일을 찾을 수 없습니다: ${SSH_KEY}"; exit 1; } chmod 600 "${SSH_KEY}" # ── 서버 접속 설정 ──────────────────────────────────────────────────────────── SSH_OPTS="-i ${SSH_KEY} -p ${SSH_PORT} -o StrictHostKeyChecking=no -o ConnectTimeout=10" # TARGET_HOST는 환경변수 또는 서버명으로부터 resolv (실제 IP는 Jenkins Credentials에서 관리) TARGET_HOST="${TARGET_HOST:-${SERVER}}" SSH_CMD="ssh ${SSH_OPTS} ${SSH_USER}@${TARGET_HOST}" # ── 앱 이름 추출 ────────────────────────────────────────────────────────────── APP_NAME=$(basename "${ARTIFACT}" | sed 's/[-_][0-9].*//' | sed 's/\.\(jar\|war\|tar\.gz\)//') DEPLOY_PATH="${DEPLOY_BASE_PATH}/${APP_NAME}" ARTIFACT_FILENAME=$(basename "${ARTIFACT}") TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_PATH="${DEPLOY_PATH}/backups/${TIMESTAMP}" log "배포 정보:" log " 서버: ${TARGET_HOST} (${DEPLOY_ENV})" log " 아티팩트: ${ARTIFACT_FILENAME}" log " 배포 경로: ${DEPLOY_PATH}" log " 롤백: ${IS_ROLLBACK}" # ── 서버 디렉터리 준비 ──────────────────────────────────────────────────────── prepare_remote_dirs() { log "원격 디렉터리 준비..." ${SSH_CMD} " mkdir -p '${DEPLOY_PATH}/current' \ '${DEPLOY_PATH}/releases' \ '${DEPLOY_PATH}/backups' " } # ── 기존 배포 백업 ──────────────────────────────────────────────────────────── backup_current() { log "현재 배포 백업..." ${SSH_CMD} " if ls '${DEPLOY_PATH}/current'/*.jar \ '${DEPLOY_PATH}/current'/*.war \ '${DEPLOY_PATH}/current'/*.tar.gz 2>/dev/null | head -1 | grep -q .; then mkdir -p '${BACKUP_PATH}' cp '${DEPLOY_PATH}/current'/. '${BACKUP_PATH}/' 2>/dev/null || true echo '백업 완료: ${BACKUP_PATH}' else echo '백업 대상 없음 (초기 배포)' fi # 오래된 백업 정리 (최신 N개만 유지) ls -dt '${DEPLOY_PATH}/backups'/*/ 2>/dev/null | \ tail -n +$((${BACKUP_COUNT} + 1)) | \ xargs rm -rf 2>/dev/null || true " } # ── 아티팩트 전송 (SCP) ─────────────────────────────────────────────────────── transfer_artifact() { log "아티팩트 전송 중: ${ARTIFACT_FILENAME}..." local remote_releases="${DEPLOY_PATH}/releases/${TIMESTAMP}" ${SSH_CMD} "mkdir -p '${remote_releases}'" scp -i "${SSH_KEY}" \ -P "${SSH_PORT}" \ -o StrictHostKeyChecking=no \ -o ConnectTimeout=10 \ "${ARTIFACT}" \ "${SSH_USER}@${TARGET_HOST}:${remote_releases}/${ARTIFACT_FILENAME}" # 체크섬 검증 (원본과 비교) local local_sha local_sha=$(sha256sum "${ARTIFACT}" | awk '{print $1}') local remote_sha remote_sha=$(${SSH_CMD} "sha256sum '${remote_releases}/${ARTIFACT_FILENAME}'" | awk '{print $1}') if [[ "${local_sha}" != "${remote_sha}" ]]; then err "체크섬 불일치: 전송 오류" err " 로컬: ${local_sha}" err " 원격: ${remote_sha}" exit 1 fi log "체크섬 검증 통과: ${local_sha:0:16}..." # current 심링크 업데이트 ${SSH_CMD} " ln -sfn '${remote_releases}' '${DEPLOY_PATH}/latest' # current에 아티팩트 복사 rm -f '${DEPLOY_PATH}/current'/*.jar \ '${DEPLOY_PATH}/current'/*.war \ '${DEPLOY_PATH}/current'/*.tar.gz 2>/dev/null || true cp '${remote_releases}/${ARTIFACT_FILENAME}' '${DEPLOY_PATH}/current/' " log "배포 완료: ${DEPLOY_PATH}/current/${ARTIFACT_FILENAME}" } # ── tar.gz 압축 해제 (Node.js 등) ──────────────────────────────────────────── extract_if_needed() { if [[ "${ARTIFACT_FILENAME}" == *.tar.gz ]]; then log "압축 해제 중..." ${SSH_CMD} " cd '${DEPLOY_PATH}/current' tar xzf '${ARTIFACT_FILENAME}' --overwrite echo '압축 해제 완료' " fi } # ── 배포 기록 ──────────────────────────────────────────────────────────────── record_deploy() { local build_num="${BUILD_NUMBER:-0}" ${SSH_CMD} " echo '${TIMESTAMP}|${ARTIFACT_FILENAME}|${build_num}|${IS_ROLLBACK}' >> \ '${DEPLOY_PATH}/deploy-history.log' " log "배포 이력 기록 완료" } # ── 메인 ───────────────────────────────────────────────────────────────────── main() { local start_time=$SECONDS log "=== 배포 시작: $(date) ===" prepare_remote_dirs backup_current transfer_artifact extract_if_needed record_deploy local elapsed=$((SECONDS - start_time)) log "=== 배포 완료: ${elapsed}초 ===" } main "$@"