324 lines
16 KiB
Bash
Executable File
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 접속"
|