콘텐츠로 건너뛰기
Home » Blog » scp 기반 배포 자동화

scp 기반 배포 자동화

  • cicd

배경

Docker, Jenkins 기반의 CI/CD 와는 달리, 몇몇 서버 환경은 여전히 파일 전송 기반의 배포 방식을 사용한다.
현재 회사의 경우, 몇몇 프로젝트들은 여전히 AWS가 아닌 IDC 서버에서 운영되고 있다.
이러한 프로젝트들은 보통 빌드된 jar 파일을 FileZilla를 통해 서버에 업로드하고, 이를 스크립트로 실행하는 방식으로 배포한다.
그러나 FileZilla의 경우, 사용성 측면에서 상당히 버벅였고, 디렉토리 조회 시 새로고침 버튼을 자주 눌러주어야 한다는 점에서 큰 피로감을 주었다.

이러한 불편을 계기로, scp 명령어를 활용하여 젠킨스의 stage를 정의하는 것처럼(?) 스크립트를 작성해 보았다.
jar 파일을 수동으로 직접 배포해야하고, github-action이나 gitlab-ci 등을 사용할 수 없고, 파이프라인을 구축하기 애매한 상황이라면, 이와같이 스크립트를 기반으로 로컬 상에서의 배포 자동화를 구축하는 것도 하나의 방법이 될 수 있다.
(CI 툴이 등장하기 전으로 돌아가보자)


scp란?

scpSecure Copy Protocol의 약자로, 리눅스(또는 유닉스 계열 OS)에서 원격 서버 간 파일을 안전하게 복사(전송)할 수 있도록 도와주는 명령어이다. 내부적으로 SSH 프로토콜을 기반으로 하여, 암호화된 채널을 통해 파일을 복사한다.
(+ scp는 점차적으로 최신 OpenSSH 버전에서 deprecated되고 있고, 대체로 sftp 또는 rsync over SSH가 추천된다.)


환경

현재 회사의 IDC 서버 내 배포된 WAS의 디렉토리의 구조와 배포 플로우는 다음과 같다.

  1. 기존의 JAR 파일을 서버 내에서 shutdown 스크립트를 통해 종료한다.
  2. 로컬 빌드 후 생성된 JAR 파일을 FileZilla 등을 통해 타겟 디렉토리에 배치한다.
  3. 서버 내의 startup 스크립트 통해 새로 배포한 WAS를 실행한다.

이 과정을 자동화하기 위해 몇 가지 조건을 설정했다.

  1. 서버 내에서도 스크립트를 통해 WAS를 핸들링할 수 있어야 하므로, startup 및 shutdown 스크립트가 배치되어 있어야 한다.
  2. 새로운 jar를 띄우기 전, 기존의 jar파일을 백업 디렉토리에 버저닝을 진행해야한다.
  3. 배포 스크립트 내 환경변수는 /conf 디렉토리에 위치해야하며, 이를 참조할 수 있어야한다.

사전 작업

우선 로컬 프로젝트에 파일을 배치한다.

  1. application.yml에 주입할 환경변수 파일(.env)는 프로젝트 루트에 배치
  2. 스크립트 파일들(deploy.sh, startup_application.sh, shutdown_application.sh, check_status.sh)과 스크립트에 사용할 환경변수 파일(.deploy.env)은 /scripts 디렉토리에 배치

배포 스크립트 플로우

이후 deploy.sh 스크립트의 배포 플로우는 다음과 같다.

  1. 환경변수(.deploy.env) 읽기 및 경로 변수 선언
  2. sshpass 명령어 유무 확인
  3. jar clear build
  4. .deploy.env에 배치한 remote server로 scp를 통해 파일 전송 (타겟 폴더로 전체 전송)
    1. startup 스크립트
    2. shutdown 스크립트
    3. check 스크립트
    4. env 파일
    5. deploy env 파일
  5. 기존 프로세스 종료
    1. 파일 마다의 타겟 포인트로 파일(환경변수 파일 및 스크립트 파일) 이동 후 덮어쓰기
    2. 파일 실행 및 읽기 권한 부여
    3. shutdown 스크립트 실행
      1. 환경변수 읽기 및 경로 설정
      2. 기존 프로세스 종료
      3. 버저닝 후 백업
      4. 기존 jar 파일 삭제
  6. 빌드한 JAR 파일 전송
  7. 새로운 프로세스 시작
    1. startup 스크립트 실행
      1. 환경변수 읽기 및 경로 설정 (jdk 경로 등)
      2. 로그 디렉토리 경로 지정
      3. 기존 프로세스 종료 (2차 체크)
      4. 실행
    2. 실행된 WAS에 health check
    3. ping 결과를 통해 최종 서버상태 점검 후 푸시알람 전송

결과

api-server deploy

status check


스크립트 상세

/api-server/scripts/deploy.sh

#!/bin/bash
set -e

# 1. Load deployment environment variables
DEPLOY_ENV="$(dirname "$0")/.deploy.env"
if [ ! -f "$DEPLOY_ENV" ]; then
  echo "❌ Error: $DEPLOY_ENV file not found."
  exit 1
fi
source "$DEPLOY_ENV"

# 2. Define paths
JAR_FILE="build/libs/${JAR_NAME}"
STARTUP_SCRIPT="scripts/startup_application.sh"
SHUTDOWN_SCRIPT="scripts/shutdown_application.sh"
CHECK_SCRIPT="scripts/check_status.sh"
ENV_FILE=".env"
CONF_DIR="$REMOTE_DIR/conf"
DEPLOY_ENV_PATH="scripts/.deploy.env"

# 3. Check sshpass availability
if ! command -v sshpass &> /dev/null; then
  echo "❌ Error: sshpass is not installed."
  exit 1
fi
echo "✅ 1. Environment loaded and sshpass is available."

# 4. Build project
cd ..
./gradlew clean build
echo "✅ 2. Project built successfully."

# 5. Transfer .env and scripts
sshpass -p "$REMOTE_PASS" scp \
  "$STARTUP_SCRIPT" \
  "$SHUTDOWN_SCRIPT" \
  "$CHECK_SCRIPT" \
  "$ENV_FILE" \
  "$DEPLOY_ENV_PATH" \
  "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
echo "✅ 3. .env and scripts transferred."

# 6. Prepare environment and shutdown existing app
sshpass -p "$REMOTE_PASS" ssh -T "$REMOTE_USER@$REMOTE_HOST" <<EOF
  mkdir -p $CONF_DIR
  mv $REMOTE_DIR/.env $CONF_DIR/.env
  mv $REMOTE_DIR/.deploy.env $CONF_DIR/.deploy.env
  chmod +x $REMOTE_DIR/startup_application.sh
  chmod +x $REMOTE_DIR/shutdown_application.sh
  chmod +x $REMOTE_DIR/check_status.sh
  chmod 644 $CONF_DIR/.env
  chmod 644 $CONF_DIR/.deploy.env
  bash $REMOTE_DIR/shutdown_application.sh
EOF
echo "✅ 4. Shutdown completed and configuration applied."

# 7. Transfer JAR
sshpass -p "$REMOTE_PASS" scp \
  "$JAR_FILE" \
  "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
echo "✅ 5. JAR file transferred."

# 8. Start application
sshpass -p "$REMOTE_PASS" ssh -T "$REMOTE_USER@$REMOTE_HOST" <<EOF
  bash $REMOTE_DIR/startup_application.sh
EOF
echo "✅ 6. Application started on remote server."

# 9. Health check and alert
echo "✅ 7. Checking server health on remote..."

sshpass -p "$REMOTE_PASS" ssh -T "$REMOTE_USER@$REMOTE_HOST" <<EOF
  MAX_RETRIES=20
  RETRY_DELAY=5
  SUCCESS=false

  for (( i=1; i<=MAX_RETRIES; i++ ))
  do
    if curl -s "$DEPLOY_URL/ping" | grep -q "pong"; then
      echo "🚀 Server is alive. Deploy succeeded!"
      curl -X POST -H "Content-Type: application/json" \
        -d '{"title":"Deployment success","body":"The server responded to /ping ✅"}' \
        "$PUSH_ALERT"
      SUCCESS=true
      break
    fi

    echo "Waiting for server... (\$i/$MAX_RETRIES)"
    sleep "\$RETRY_DELAY"
  done

  if [ "\$SUCCESS" != true ]; then
    echo "❌ Server failed to respond."
    curl -X POST -H "Content-Type: application/json" \
      -d '{"title":"Deployment failed","body":"The server did not respond to /ping ❌"}' \
      "$PUSH_ALERT"
    exit 1
  fi
EOF

/api-server/scripts/startup_application.sh

#!/bin/bash
set -e

echo "  > Starting server..."

DEPLOY_ENV="$(dirname "$0")/conf/.deploy.env"
if [ -f "$DEPLOY_ENV" ]; then
  source "$DEPLOY_ENV"
fi

JAR_PATH="${BASE_DIR}/${JAR_NAME}"
ENV_FILE="${BASE_DIR}/conf/.env"
LOG_FILE="${BASE_DIR}/logs/app.log"
JAVA_HOME="/data/autobond/config/jdk17"
JAVA_CMD="$JAVA_HOME/bin/java"

mkdir -p "${BASE_DIR}/logs"

if [ ! -f "$ENV_FILE" ]; then
  echo "  > .env not found: $ENV_FILE"
  exit 1
fi

set +o histexpand
source "$ENV_FILE"
echo "  > ENV loaded: $SERVER_PORT"

PID=$(pgrep -f "$JAR_PATH" || true)
if [ -n "$PID" ]; then
  echo "  > Stopping existing process (PID: $PID)..."
  kill -9 "$PID"
fi

nohup "$JAVA_CMD" -Xms1024m -Xmx1024m -jar "$JAR_PATH" >> "$LOG_FILE" 2>&1 < /dev/null &
echo "  > Started. Logs: $LOG_FILE"

/api-server/scripts/shutdown_application.sh

#!/bin/bash
set -e

echo "  > Stopping server and creating backup..."

# 0. Load environment variables
DEPLOY_ENV="$(dirname "$0")/conf/.deploy.env"
if [ -f "$DEPLOY_ENV" ]; then
  source "$DEPLOY_ENV"
fi

JAR_PATH="$BASE_DIR/$JAR_NAME"
LOG_PATH="$BASE_DIR/logs/app.log"
BACKUP_DIR="$BASE_DIR/backup"
mkdir -p "$BACKUP_DIR"

# 1. Shutdown existing application gracefully
PID=$(pgrep -f "$JAR_PATH" || true)
if [ -n "$PID" ]; then
  echo "  > Sending SIGTERM to process (PID: $PID)..."
  kill "$PID"

  # Wait up to 30 seconds for graceful shutdown
  for i in {1..30}; do
    if ps -p "$PID" > /dev/null; then
      echo "  > Waiting for process to terminate... ($i/30)"
      sleep 1
    else
      echo "  > Graceful shutdown complete."
      break
    fi
  done

  # Force kill if still alive
  if ps -p "$PID" > /dev/null; then
    echo "  > Process still alive. Forcing kill."
    kill -9 "$PID"
  fi
else
  echo "  > No running application found."
fi

# 2. Create timestamped archive backup
if [ -f "$JAR_PATH" ]; then
  TIMESTAMP=$(date +%Y%m%d%H%M%S)
  ARCHIVE_DIR="$BACKUP_DIR/$TIMESTAMP"
  ARCHIVE_PATH="$BACKUP_DIR/${TIMESTAMP}.tar.gz"

  mkdir -p "$ARCHIVE_DIR"

  # Copy JAR file
  cp "$JAR_PATH" "$ARCHIVE_DIR/$JAR_NAME"

  # Copy and truncate log file if it exists
  if [ -f "$LOG_PATH" ]; then
    cp "$LOG_PATH" "$ARCHIVE_DIR/app.log"
    > "$LOG_PATH"
  fi

  # Compress archive directory
  tar -czf "$ARCHIVE_PATH" -C "$BACKUP_DIR" "$TIMESTAMP"
  echo "  > Backup archive created: $ARCHIVE_PATH"

  # Clean up
  rm -rf "$ARCHIVE_DIR"
  rm -f "$JAR_PATH"
  echo "  > Original JAR deleted: $JAR_PATH"
else
  echo "  > No JAR file found to back up: $JAR_PATH"
fi

/api-server/scripts/check_status.sh

#!/bin/bash
set -e

echo "> Checking server status..."

# Load deployment environment
DEPLOY_ENV="$(dirname "$0")/conf/.deploy.env"
if [ ! -f "$DEPLOY_ENV" ]; then
  echo "❌ Error: $DEPLOY_ENV not found"
  exit 1
fi
source "$DEPLOY_ENV"

# Validate required variables
if [ -z "$BASE_DIR" ] || [ -z "$JAR_NAME" ] || [ -z "$SERVER_PORT" ]; then
  echo "❌ BASE_DIR, JAR_NAME, or SERVER_PORT is not set in .deploy.env"
  exit 1
fi

# Define paths
JAR_PATH="$BASE_DIR/$JAR_NAME"
LOG_FILE="$BASE_DIR/logs/app.log"
CONF_DIR="$BASE_DIR/conf"

# Check JAR process
SERVER_PID=$(pgrep -f "$JAR_NAME" || echo "")

if [ -n "$SERVER_PID" ]; then
  echo "> ✅ Server is running (PID: $SERVER_PID)"

  # Port check
  if netstat -tlnp 2>/dev/null | grep -q ":$SERVER_PORT"; then
    echo "> ✅ Serving on port $SERVER_PORT"
  else
    echo "> ⚠️  Port $SERVER_PORT not active"
  fi

  # Memory usage
  if command -v ps >/dev/null 2>&1; then
    MEMORY=$(ps -o rss= -p "$SERVER_PID" 2>/dev/null | awk '{print $1/1024 " MB"}' || echo "Unknown")
    echo "> 📊 Memory usage: $MEMORY"
  fi
else
  echo "> ❌ Server is not running"
fi

# Log file check
if [ -f "$LOG_FILE" ]; then
  LOG_SIZE=$(du -h "$LOG_FILE" | cut -f1)
  echo "> 📝 Log file: $LOG_FILE (Size: $LOG_SIZE)"
  echo "> 📋 Recent logs (last 5 lines):"
  tail -5 "$LOG_FILE" 2>/dev/null || echo "Could not read logs"
else
  echo "> 📝 Log file not found"
fi

# Config directory check
if [ -d "$CONF_DIR" ]; then
  CONF_SIZE=$(du -sh "$CONF_DIR" | cut -f1)
  echo "> ⚙️ Config directory: $CONF_DIR (Size: $CONF_SIZE)"
else
  echo "> ⚙️ Config directory not found"
fi

/api-server/scripts/.deploy.env

REMOTE_HOST=
REMOTE_USER=
REMOTE_PASS=
REMOTE_DIR=

SERVER_PORT=
BASE_DIR=
JAR_NAME=
DEPLOY_URL=
PUSH_ALERT=