Files
2026-06-15 15:51:04 +09:00

324 lines
16 KiB
Bash
Executable File

#!/bin/bash
# ======================================================================
# Golden Chart - 원격 서버 자동 배포 스크립트
# 대상 서버: exdev.co.kr
# 동작: 로컬 소스 압축 전송 → 서버에서 docker compose up --build
# ======================================================================
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# 색상
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
# ── 서버 설정 ──────────────────────────────────────────────────────────
REMOTE_HOST="${REMOTE_HOST:-exdev.co.kr}"
REMOTE_USER="${REMOTE_USER:-aidev}"
REMOTE_PORT="${REMOTE_PORT:-22}"
REMOTE_PROJECT_DIR="${REMOTE_PROJECT_DIR:-/Users/aidev/apps/goldenChart}"
FRONTEND_PORT="${FRONTEND_PORT:-80}"
BACKEND_PORT="${BACKEND_PORT:-8080}"
# ── 옵션 파싱 ──────────────────────────────────────────────────────────
DOWN=false
LOGS=false
STATUS=false
SETUP_SSH=false
SYNC=true
NO_CACHE=true # 기본: --no-cache (항상 최신 코드)
FORGET_PASSWORD=false
SHELL_ONLY=false
SERVICE="" # 특정 서비스만 재빌드 (frontend / backend / all)
print_usage() {
echo "사용법: $0 [옵션]"
echo ""
echo " (기본) 소스 동기화 + 전체 서비스 재빌드 + 재시작"
echo " --use-cache 캐시 사용 빌드 (빠름)"
echo " --no-sync 파일 동기화 생략"
echo " --frontend 프론트엔드만 재빌드"
echo " --backend 백엔드만 재빌드"
echo " --down, -d 모든 서비스 중지"
echo " --logs, -l 전체 로그 스트리밍"
echo " --status, -s 서비스 상태 확인"
echo " --shell 서버 SSH 세션 열기"
echo " --forget-password SSH 패스워드 캐시 삭제"
echo " --help, -h 도움말"
echo ""
echo "서버: ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}"
echo "경로: ${REMOTE_PROJECT_DIR}"
echo "URL: http://${REMOTE_HOST}:${FRONTEND_PORT}"
}
while [[ $# -gt 0 ]]; do
case $1 in
--use-cache) NO_CACHE=false; shift ;;
--no-sync) SYNC=false; shift ;;
--frontend) SERVICE=frontend; shift ;;
--backend) SERVICE=backend; shift ;;
--down|-d) DOWN=true; shift ;;
--logs|-l) LOGS=true; shift ;;
--status|-s) STATUS=true; shift ;;
--shell) SHELL_ONLY=true; shift ;;
--setup-ssh) SETUP_SSH=true; shift ;;
--forget-password) FORGET_PASSWORD=true; shift ;;
--user) REMOTE_USER="$2"; shift 2 ;;
--port-ssh) REMOTE_PORT="$2"; shift 2 ;;
--help|-h) print_usage; exit 0 ;;
*) echo -e "${RED}알 수 없는 옵션: $1${NC}"; print_usage; exit 1 ;;
esac
done
SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}"
# ── 배너 ───────────────────────────────────────────────────────────────
echo -e "${CYAN}${BOLD}"
echo "╔══════════════════════════════════════════════════════════╗"
echo "║ GoldenChart 원격 배포 스크립트 ║"
echo "╚══════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " 서버: ${YELLOW}${SSH_TARGET}:${REMOTE_PORT}${NC}"
echo -e " 경로: ${YELLOW}${REMOTE_PROJECT_DIR}${NC}"
echo -e " URL: ${CYAN}http://${REMOTE_HOST}:${FRONTEND_PORT}${NC}"
echo ""
# ── SSH 패스워드 캐시 ─────────────────────────────────────────────────
PASS_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/golden-chart-deploy"
PASS_CACHE_FILE="${PASS_CACHE_DIR}/sshpass-${REMOTE_USER}@${REMOTE_HOST}-${REMOTE_PORT}.txt"
if [ "$FORGET_PASSWORD" = true ]; then
rm -f "$PASS_CACHE_FILE" && echo -e "${GREEN}✓ 패스워드 캐시 삭제${NC}" || echo -e "${YELLOW}캐시 없음${NC}"
fi
SERVER_PASSWORD=""
PASSWORD_FROM_CACHE=false
apply_ssh_cmds() {
# IdentitiesOnly=yes: ssh-agent의 모든 키를 자동 시도하지 않도록 제한
# → "Too many authentication failures" 방지
if [ -n "$SERVER_PASSWORD" ]; then
export SSHPASS="$SERVER_PASSWORD"
SSH_CMD="sshpass -e ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o PubkeyAuthentication=no -p ${REMOTE_PORT}"
SCP_CMD="sshpass -e scp -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o PubkeyAuthentication=no -P ${REMOTE_PORT}"
else
unset SSHPASS 2>/dev/null || true
SSH_CMD="ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -p ${REMOTE_PORT}"
SCP_CMD="scp -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -P ${REMOTE_PORT}"
fi
}
if command -v sshpass &>/dev/null; then
if [ -f "$PASS_CACHE_FILE" ]; then
IFS= read -r SERVER_PASSWORD < "$PASS_CACHE_FILE" || SERVER_PASSWORD=""
[ -n "$SERVER_PASSWORD" ] && PASSWORD_FROM_CACHE=true && echo -e "${GREEN}✓ 저장된 패스워드 사용 (--forget-password 로 초기화)${NC}"
fi
if [ -z "$SERVER_PASSWORD" ]; then
echo -e "${CYAN}SSH 패스워드 (키 인증 시 Enter):${NC}"
read -s -p "패스워드: " SERVER_PASSWORD; echo ""
PASSWORD_FROM_CACHE=false
fi
else
echo -e "${YELLOW}sshpass 없음 — SSH 키 인증 모드${NC}"
fi
apply_ssh_cmds
# ── SSH 연결 테스트 ───────────────────────────────────────────────────
echo -e "${CYAN}[연결] SSH 테스트 중...${NC}"
# set -e 영향 차단을 위해 || true 사용
SSH_ERR=$( { $SSH_CMD $SSH_TARGET "exit"; } 2>&1 ) || true
SSH_EC=$( { $SSH_CMD $SSH_TARGET "exit" >/dev/null 2>&1; echo $?; } ) || SSH_EC=1
SSH_EC="${SSH_EC:-1}"
if [ "${SSH_EC}" != "0" ] && [ -n "$SERVER_PASSWORD" ] && [ "$PASSWORD_FROM_CACHE" = true ] && command -v sshpass &>/dev/null; then
echo -e "${YELLOW}캐시 패스워드 실패 — 재입력:${NC}"
rm -f "$PASS_CACHE_FILE"
read -s -p "패스워드: " SERVER_PASSWORD; echo ""
PASSWORD_FROM_CACHE=false
apply_ssh_cmds
SSH_ERR=$( { $SSH_CMD $SSH_TARGET "exit"; } 2>&1 ) || true
SSH_EC=$( { $SSH_CMD $SSH_TARGET "exit" >/dev/null 2>&1; echo $?; } ) || SSH_EC=1
SSH_EC="${SSH_EC:-1}"
fi
if [ "${SSH_EC}" != "0" ]; then
echo -e "${RED}✗ SSH 연결 실패 (exit code: ${SSH_EC})${NC}"
[ -n "$SSH_ERR" ] && echo -e "${RED} 오류: $(echo "$SSH_ERR" | head -3)${NC}"
echo ""
echo -e "${YELLOW}해결 방법:${NC}"
echo " 1) SSH 키 등록: ssh-copy-id -p ${REMOTE_PORT} ${SSH_TARGET}"
echo " 2) 직접 테스트: ssh -p ${REMOTE_PORT} ${SSH_TARGET}"
echo " 3) sshpass 설치 (패스워드 인증): brew install hudochenkov/sshpass/sshpass"
exit 1
fi
echo -e "${GREEN}✓ SSH 연결 성공${NC}"
# 패스워드 캐시 저장
if [ -n "$SERVER_PASSWORD" ] && [ "$PASSWORD_FROM_CACHE" = false ] && command -v sshpass &>/dev/null; then
mkdir -p "$PASS_CACHE_DIR"; chmod 700 "$PASS_CACHE_DIR" 2>/dev/null || true
( umask 077 && printf '%s' "$SERVER_PASSWORD" > "$PASS_CACHE_FILE" )
chmod 600 "$PASS_CACHE_FILE" 2>/dev/null || true
echo -e "${GREEN}✓ 패스워드 캐시 저장됨${NC}"
fi
echo ""
# ── 원격 명령 헬퍼 ────────────────────────────────────────────────────
remote_exec() { $SSH_CMD $SSH_TARGET "bash -l -c $(printf '%q' "$*")"; }
# ── 특수 모드 처리 ────────────────────────────────────────────────────
if [ "$SHELL_ONLY" = true ]; then
echo -e "${CYAN}서버 SSH 세션 (exit 로 종료):${NC}"
$SSH_CMD -t $SSH_TARGET "bash -l"; exit 0
fi
if [ "$STATUS" = true ]; then
echo -e "${CYAN}[상태] 서비스 컨테이너:${NC}"
remote_exec "cd ${REMOTE_PROJECT_DIR} && docker compose ps"
echo ""; echo -e "${CYAN}[리소스]:${NC}"
remote_exec "docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'"
exit 0
fi
if [ "$LOGS" = true ]; then
echo -e "${CYAN}[로그] 전체 서비스 로그 (Ctrl+C 종료):${NC}"
$SSH_CMD $SSH_TARGET "cd ${REMOTE_PROJECT_DIR} && docker compose logs -f --tail 100"
exit 0
fi
if [ "$DOWN" = true ]; then
echo -e "${YELLOW}[중지] 모든 서비스 중지 중...${NC}"
remote_exec "cd ${REMOTE_PROJECT_DIR} && docker compose down"
echo -e "${GREEN}✓ 서비스 중지 완료${NC}"; exit 0
fi
# ── [1/3] 파일 동기화 ─────────────────────────────────────────────────
if [ "$SYNC" = true ]; then
echo -e "${CYAN}[1/3] 소스 → 서버 동기화 중...${NC}"
TEMP_ARCHIVE="/tmp/gc-deploy-$$.tar.gz"
echo -e "${YELLOW} 압축 중 (node_modules / .git / dist / target 제외)...${NC}"
tar --exclude='*/node_modules' \
--exclude='*/.git' \
--exclude='*/dist' \
--exclude='*/target' \
--exclude='*/.idea' \
--exclude='*/.vscode' \
--exclude='*/.cursor' \
--exclude='*/__MACOSX' \
--exclude='*/.DS_Store' \
--exclude='*.log' \
--exclude='.env.local' \
--exclude='.env.*.local' \
-czf "$TEMP_ARCHIVE" .
echo -e "${YELLOW} 서버 전송 중...${NC}"
$SCP_CMD "$TEMP_ARCHIVE" "${SSH_TARGET}:/tmp/gc-deploy.tar.gz"
echo -e "${YELLOW} 서버 기존 소스 정리 후 압축 해제 중...${NC}"
remote_exec "ENV_BAK=/tmp/gc-env-backup-\$\$; \
if [ -f ${REMOTE_PROJECT_DIR}/.env ]; then cp ${REMOTE_PROJECT_DIR}/.env \"\$ENV_BAK\"; fi; \
rm -rf ${REMOTE_PROJECT_DIR} && \
mkdir -p ${REMOTE_PROJECT_DIR} && \
cd ${REMOTE_PROJECT_DIR} && \
tar -xzf /tmp/gc-deploy.tar.gz && \
rm -f /tmp/gc-deploy.tar.gz && \
if [ -f \"\$ENV_BAK\" ]; then mv \"\$ENV_BAK\" ${REMOTE_PROJECT_DIR}/.env && chmod 600 ${REMOTE_PROJECT_DIR}/.env; fi"
rm -f "$TEMP_ARCHIVE"
echo -e "${GREEN}✓ 파일 동기화 완료${NC}"
else
echo -e "${CYAN}[1/3] 파일 동기화 생략 — 서버 기존 소스 사용${NC}"
fi
echo ""
# ── Docker 데몬 확인 ──────────────────────────────────────────────────
echo -e "${CYAN}[사전] Docker 데몬 확인 중...${NC}"
if ! remote_exec "docker info >/dev/null 2>&1"; then
echo -e "${YELLOW} Docker Desktop 기동 시도 중...${NC}"
remote_exec "[ -d /Applications/Docker.app ] && open -ga Docker || true"
for i in $(seq 1 40); do
sleep 3
remote_exec "docker info >/dev/null 2>&1" && break
echo -ne "${YELLOW} 대기 중 ${i}...${NC}\r"
done
fi
remote_exec "docker info >/dev/null 2>&1" && echo -e "${GREEN}✓ Docker 데몬 준비됨${NC}" || { echo -e "${RED}✗ Docker 데몬 불가${NC}"; exit 1; }
echo ""
# ── [2/3] 빌드 및 재시작 ──────────────────────────────────────────────
[ "$NO_CACHE" = true ] && CACHE_MSG="캐시 없이 (항상 최신 코드 반영)" || CACHE_MSG="캐시 사용 (빠름)"
[ "$NO_CACHE" = true ] && NO_CACHE_FLAG="--no-cache" || NO_CACHE_FLAG=""
if [ -z "$SERVICE" ]; then
# ── 전체 서비스 재시작 ─────────────────────────────────────────────
echo -e "${CYAN}[2/3] 전체 서비스 빌드 + 재시작 중... (${CACHE_MSG})${NC}"
echo -e "${YELLOW} (mysql, backend, frontend 순서로 시작됨)${NC}"
remote_exec "cd ${REMOTE_PROJECT_DIR} && \
docker compose down --remove-orphans && \
docker compose build ${NO_CACHE_FLAG} && \
docker compose up -d"
else
# ── 특정 서비스만 재빌드 ──────────────────────────────────────────
echo -e "${CYAN}[2/3] ${SERVICE} 서비스만 재빌드 중... (${CACHE_MSG})${NC}"
remote_exec "cd ${REMOTE_PROJECT_DIR} && \
docker compose build ${NO_CACHE_FLAG} ${SERVICE} && \
docker compose up -d ${SERVICE}"
fi
echo -e "${GREEN}✓ 빌드 및 재시작 완료${NC}"
echo ""
# ── [3/3] 서비스 안정화 대기 ──────────────────────────────────────────
echo -e "${CYAN}[3/3] 서비스 기동 대기 중 (최대 90초)...${NC}"
WAIT=0; MAX_WAIT=90
while [ $WAIT -lt $MAX_WAIT ]; do
RUNNING=$(remote_exec "cd ${REMOTE_PROJECT_DIR} && docker compose ps --status running --format json 2>/dev/null | grep -c 'running' || echo 0" 2>/dev/null || echo "0")
# 3개 서비스가 모두 running 이면 완료
TOTAL=$(remote_exec "cd ${REMOTE_PROJECT_DIR} && docker compose ps --format json 2>/dev/null | grep -c 'name' || echo 0" 2>/dev/null || echo "0")
HTTP_CODE=$(remote_exec "curl -s -o /dev/null -w '%{http_code}' http://localhost:${FRONTEND_PORT}/ 2>/dev/null || echo 000" 2>/dev/null || echo "000")
echo -ne " 대기 ${WAIT}s — 컨테이너: ${RUNNING}/${TOTAL}, HTTP: ${HTTP_CODE} \r"
if echo "$HTTP_CODE" | grep -qE "^2[0-9][0-9]$"; then
echo ""
break
fi
sleep 5; WAIT=$((WAIT + 5))
done
echo ""
# ── 최종 상태 ─────────────────────────────────────────────────────────
echo -e "${CYAN}[완료] 서비스 상태:${NC}"
remote_exec "cd ${REMOTE_PROJECT_DIR} && docker compose ps"
echo ""
HTTP_CODE=$(remote_exec "curl -s -o /dev/null -w '%{http_code}' http://localhost:${FRONTEND_PORT}/ 2>/dev/null || echo 000" 2>/dev/null || echo "000")
if echo "$HTTP_CODE" | grep -qE "^2[0-9][0-9]$"; then
echo -e "${GREEN}✓ HTTP ${HTTP_CODE} — 프론트엔드 정상 응답${NC}"
else
echo -e "${YELLOW}⚠ HTTP ${HTTP_CODE} — 아직 초기화 중일 수 있습니다.${NC}"
echo -e "${YELLOW} 로그 확인: ./deploy-home.sh --logs${NC}"
fi
BACKEND_HTTP=$(remote_exec "curl -s -o /dev/null -w '%{http_code}' http://localhost:${BACKEND_PORT}/api/indicators/list 2>/dev/null || echo 000" 2>/dev/null || echo "000")
if echo "$BACKEND_HTTP" | grep -qE "^2[0-9][0-9]$"; then
echo -e "${GREEN}✓ HTTP ${BACKEND_HTTP} — 백엔드 API 정상 응답${NC}"
else
echo -e "${YELLOW}⚠ 백엔드 HTTP ${BACKEND_HTTP} (Flyway 마이그레이션 실행 중일 수 있음)${NC}"
fi
echo ""
echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}${BOLD}║ 배포 완료! 🎉 ║${NC}"
echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " HTTP: ${CYAN}${BOLD}http://${REMOTE_HOST}:${FRONTEND_PORT}${NC}"
echo -e " HTTPS: ${CYAN}${BOLD}https://${REMOTE_HOST}/${NC} ${YELLOW}(인증서: ./scripts/setup-exdev-https.sh)${NC}"
echo ""
echo -e "${YELLOW}자주 쓰는 명령어:${NC}"
echo " ./deploy-home.sh # 전체 재빌드 배포 (기본)"
echo " ./deploy-home.sh --use-cache # 캐시 사용 빌드 (빠름)"
echo " ./deploy-home.sh --frontend # 프론트엔드만 재빌드"
echo " ./deploy-home.sh --backend # 백엔드만 재빌드"
echo " ./deploy-home.sh --no-sync # 서버 기존 소스로 재빌드"
echo " ./deploy-home.sh --status # 서비스 상태 확인"
echo " ./deploy-home.sh --logs # 전체 로그 보기"
echo " ./deploy-home.sh --down # 서비스 중지"
echo " ./deploy-home.sh --shell # 서버 SSH 접속"