fix #546: add hermes VM deployment script
This commit is contained in:
61
docs/design/546-hermes-vm-deploy/README.md
Normal file
61
docs/design/546-hermes-vm-deploy/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Redmine #546 - 외부 VM 배포 설계
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
VPD Backoffice는 Oracle Database VPD/ORDS 기능을 백오피스 권한 테이블로 제어하는 Spring Boot 관리 도구다. 사용자는 Oracle DB schema user가 아니라 Bearer Token으로 식별되는 application user이며, 사용자/그룹/역할/권한/행 규칙/컬럼 NULL 처리 설정이 VPD policy function과 ORDS 조회 결과에 반영된다.
|
||||
|
||||
## 목표
|
||||
|
||||
외부 VM에 VPD Backoffice를 올려 상시 실행하고, 이후 개선 작업을 같은 방식으로 반복 배포할 수 있게 한다.
|
||||
|
||||
## 초기 확인 결과
|
||||
|
||||
- 초기 확인 시점에는 `~/.ssh/config`에 `Host hermes` 항목이 잡히지 않아 `ssh hermes`가 `Could not resolve hostname hermes`로 실패했다.
|
||||
- 이후 `Host hermes`가 `130.162.134.59`, `User opc`, `IdentityFile /Users/joungminko/devkit/credential/8-31.pem`로 확인됐다.
|
||||
- 로컬 `.env`는 `TNS_ADMIN=/Users/.../Wallet_D8AUKRO81636MON0`처럼 로컬 절대 경로를 사용하므로, VM 배포 시 wallet 디렉토리와 `TNS_ADMIN`을 원격 경로로 재작성해야 한다.
|
||||
|
||||
## 배포 방식
|
||||
|
||||
- 기본 대상 SSH host는 `hermes`로 두되, 스크립트 인자로 다른 SSH alias를 받을 수 있게 한다.
|
||||
- 로컬에서 `mvn -DskipTests package`로 jar를 빌드한다.
|
||||
- 원격 디렉토리 기본값은 `~/apps/vpd-backoffice`다.
|
||||
- 배포 패키지 구성:
|
||||
- Spring Boot executable jar
|
||||
- VM용 `.env`
|
||||
- ADB wallet 디렉토리
|
||||
- `start.sh`, `stop.sh`, `status.sh`
|
||||
- VM용 `.env`는 로컬 `.env`를 기반으로 하되 아래 값을 원격 경로 기준으로 덮어쓴다.
|
||||
- `TNS_ADMIN`
|
||||
- `BACKOFFICE_DB_URL`
|
||||
- `BACKOFFICE_ORDS_DB_URL`
|
||||
- `BACKOFFICE_PORT`
|
||||
- 원격 프로세스는 `nohup java -jar app.jar` 방식으로 기동한다.
|
||||
- systemd 등록은 VM의 sudo 정책과 포트 공개 정책이 확정된 뒤 별도 이슈에서 다룬다.
|
||||
|
||||
## 완료 기준
|
||||
|
||||
- `scripts/deploy-backoffice-vm.sh`가 jar, `.env`, wallet, start/stop/status 스크립트를 원격에 설치할 수 있다.
|
||||
- 대상 alias가 없으면 명확하게 실패한다.
|
||||
- 배포 후 원격 `status.sh`와 HTTP `/login` 헬스체크가 가능하다.
|
||||
- OCI NSG와 VM firewalld에서 운영자 IP 기준 `8082/tcp` 접근을 허용한다.
|
||||
|
||||
## 배포 결과
|
||||
|
||||
- 배포 대상: `hermes` / `opc@130.162.134.59`
|
||||
- 원격 경로: `/home/opc/apps/vpd-backoffice`
|
||||
- Java runtime: `java-21-openjdk-headless`
|
||||
- 기동 방식: `/home/opc/apps/vpd-backoffice/start.sh`
|
||||
- PID 파일: `/home/opc/apps/vpd-backoffice/app.pid`
|
||||
- 로그 파일: `/home/opc/apps/vpd-backoffice/app.log`
|
||||
- VM firewalld: `8082/tcp` open
|
||||
- OCI NSG: `218.39.170.82/32 -> tcp/8082` ingress 추가
|
||||
- 외부 헬스체크: `http://130.162.134.59:8082/login` HTTP 200
|
||||
|
||||
## 검증
|
||||
|
||||
- `mvn test`
|
||||
- `scripts/deploy-backoffice-vm.sh --dry-run`
|
||||
- `ssh hermes` 접속 확인
|
||||
- `scripts/deploy-backoffice-vm.sh --host hermes --skip-build`
|
||||
- 원격 내부 `/login` HTTP 200
|
||||
- 외부 `/login` HTTP 200
|
||||
228
scripts/deploy-backoffice-vm.sh
Executable file
228
scripts/deploy-backoffice-vm.sh
Executable file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deploy VPD Backoffice to an SSH-accessible VM.
|
||||
set -Eeuo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
. "$ROOT/scripts/lib/common.sh"
|
||||
|
||||
HOST="hermes"
|
||||
REMOTE_DIR='$HOME/apps/vpd-backoffice'
|
||||
REMOTE_PORT="${BACKOFFICE_PORT:-8082}"
|
||||
IDENTITY_FILE=""
|
||||
KNOWN_HOSTS_FILE=""
|
||||
ACCEPT_HOST_KEY="N"
|
||||
DRY_RUN="N"
|
||||
SKIP_BUILD="N"
|
||||
START_APP="Y"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $0 [--host SSH_ALIAS] [--identity-file KEY] [--known-hosts-file FILE] [--accept-host-key] [--remote-dir PATH] [--port PORT] [--dry-run] [--skip-build] [--no-start]
|
||||
|
||||
Defaults:
|
||||
--host hermes
|
||||
--remote-dir \$HOME/apps/vpd-backoffice
|
||||
--port 8082
|
||||
|
||||
The script uploads:
|
||||
- target/*.jar as app.jar
|
||||
- .env rewritten for the VM wallet path
|
||||
- TNS_ADMIN wallet directory
|
||||
- start.sh / stop.sh / status.sh
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--host)
|
||||
HOST="${2:?--host requires a value}"
|
||||
shift 2
|
||||
;;
|
||||
--identity-file)
|
||||
IDENTITY_FILE="${2:?--identity-file requires a value}"
|
||||
shift 2
|
||||
;;
|
||||
--known-hosts-file)
|
||||
KNOWN_HOSTS_FILE="${2:?--known-hosts-file requires a value}"
|
||||
shift 2
|
||||
;;
|
||||
--accept-host-key)
|
||||
ACCEPT_HOST_KEY="Y"
|
||||
shift
|
||||
;;
|
||||
--remote-dir)
|
||||
REMOTE_DIR="${2:?--remote-dir requires a value}"
|
||||
shift 2
|
||||
;;
|
||||
--port)
|
||||
REMOTE_PORT="${2:?--port requires a value}"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN="Y"
|
||||
shift
|
||||
;;
|
||||
--skip-build)
|
||||
SKIP_BUILD="Y"
|
||||
shift
|
||||
;;
|
||||
--no-start)
|
||||
START_APP="N"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "알 수 없는 옵션: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -f "$ROOT/.env" ]] || die ".env 파일이 없습니다."
|
||||
# shellcheck disable=SC1091
|
||||
set -a; . "$ROOT/.env"; set +a
|
||||
|
||||
require_env TNS_ADMIN ADB_TNS BACKOFFICE_DB_USERNAME BACKOFFICE_DB_PASSWORD
|
||||
[[ -d "$TNS_ADMIN" ]] || die "TNS_ADMIN 디렉토리가 없습니다: $TNS_ADMIN"
|
||||
[[ -f "$TNS_ADMIN/tnsnames.ora" ]] || die "tnsnames.ora가 없습니다: $TNS_ADMIN/tnsnames.ora"
|
||||
|
||||
REMOTE_WALLET_DIR="${REMOTE_DIR%/}/wallet"
|
||||
REMOTE_ENV_FILE="${REMOTE_DIR%/}/.env"
|
||||
SSH_ARGS=(-o BatchMode=yes -o ConnectTimeout=8)
|
||||
if [[ -n "$IDENTITY_FILE" ]]; then
|
||||
[[ -f "$IDENTITY_FILE" ]] || die "identity file을 찾을 수 없습니다: $IDENTITY_FILE"
|
||||
SSH_ARGS+=(-i "$IDENTITY_FILE")
|
||||
fi
|
||||
if [[ -n "$KNOWN_HOSTS_FILE" ]]; then
|
||||
SSH_ARGS+=(-o "UserKnownHostsFile=$KNOWN_HOSTS_FILE")
|
||||
fi
|
||||
if [[ "$ACCEPT_HOST_KEY" == "Y" ]]; then
|
||||
SSH_ARGS+=(-o StrictHostKeyChecking=no)
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "Y" ]]; then
|
||||
log "dry-run: host=$HOST"
|
||||
[[ -n "$IDENTITY_FILE" ]] && log "dry-run: identity_file=$IDENTITY_FILE"
|
||||
log "dry-run: remote_dir=$REMOTE_DIR"
|
||||
log "dry-run: remote_wallet_dir=$REMOTE_WALLET_DIR"
|
||||
log "dry-run: port=$REMOTE_PORT"
|
||||
log "dry-run: local_wallet=$TNS_ADMIN"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "SSH 대상 확인: $HOST"
|
||||
ssh "${SSH_ARGS[@]}" "$HOST" 'printf "connected %s %s\n" "$(hostname)" "$(whoami)"'
|
||||
|
||||
if [[ "$SKIP_BUILD" != "Y" ]]; then
|
||||
log "jar 빌드"
|
||||
mvn -DskipTests package
|
||||
fi
|
||||
|
||||
JAR_PATH="$(find "$ROOT/target" -maxdepth 1 -type f -name '*.jar' ! -name '*sources.jar' ! -name '*javadoc.jar' | sort | tail -n 1)"
|
||||
[[ -n "$JAR_PATH" && -f "$JAR_PATH" ]] || die "target jar를 찾을 수 없습니다. mvn package를 먼저 실행하세요."
|
||||
|
||||
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/vpd-backoffice-deploy.XXXXXX")"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$TMP_DIR/wallet"
|
||||
cp "$JAR_PATH" "$TMP_DIR/app.jar"
|
||||
cp -R "$TNS_ADMIN"/. "$TMP_DIR/wallet/"
|
||||
|
||||
cp "$ROOT/.env" "$TMP_DIR/.env"
|
||||
cat >> "$TMP_DIR/.env" <<ENVEOF
|
||||
|
||||
# --- VM deployment overrides ---
|
||||
export TNS_ADMIN="${REMOTE_WALLET_DIR}"
|
||||
export BACKOFFICE_DB_URL="jdbc:oracle:thin:@\${ADB_TNS}?TNS_ADMIN=\${TNS_ADMIN}"
|
||||
export BACKOFFICE_ORDS_DB_URL="\${BACKOFFICE_DB_URL}"
|
||||
export BACKOFFICE_PORT="${REMOTE_PORT}"
|
||||
ENVEOF
|
||||
chmod 600 "$TMP_DIR/.env"
|
||||
|
||||
cat > "$TMP_DIR/start.sh" <<'START'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$APP_DIR"
|
||||
set -a
|
||||
. "$APP_DIR/.env"
|
||||
set +a
|
||||
PID_FILE="$APP_DIR/app.pid"
|
||||
LOG_FILE="$APP_DIR/app.log"
|
||||
|
||||
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" >/dev/null 2>&1; then
|
||||
echo "already running pid=$(cat "$PID_FILE")"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
nohup java -jar "$APP_DIR/app.jar" > "$LOG_FILE" 2>&1 &
|
||||
echo $! > "$PID_FILE"
|
||||
echo "started pid=$(cat "$PID_FILE") log=$LOG_FILE"
|
||||
START
|
||||
|
||||
cat > "$TMP_DIR/stop.sh" <<'STOP'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PID_FILE="$APP_DIR/app.pid"
|
||||
|
||||
if [[ ! -f "$PID_FILE" ]]; then
|
||||
echo "not running: pid file not found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PID="$(cat "$PID_FILE")"
|
||||
if kill -0 "$PID" >/dev/null 2>&1; then
|
||||
kill "$PID"
|
||||
for _ in {1..20}; do
|
||||
if ! kill -0 "$PID" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
echo "stopped"
|
||||
STOP
|
||||
|
||||
cat > "$TMP_DIR/status.sh" <<'STATUS'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PID_FILE="$APP_DIR/app.pid"
|
||||
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" >/dev/null 2>&1; then
|
||||
echo "running pid=$(cat "$PID_FILE")"
|
||||
else
|
||||
echo "stopped"
|
||||
fi
|
||||
STATUS
|
||||
|
||||
chmod +x "$TMP_DIR/start.sh" "$TMP_DIR/stop.sh" "$TMP_DIR/status.sh"
|
||||
|
||||
log "원격 디렉토리 생성"
|
||||
ssh "${SSH_ARGS[@]}" "$HOST" "mkdir -p \"$REMOTE_DIR\""
|
||||
|
||||
log "패키지 업로드"
|
||||
tar -C "$TMP_DIR" -czf - app.jar .env wallet start.sh stop.sh status.sh \
|
||||
| ssh "${SSH_ARGS[@]}" "$HOST" "tar -C \"$REMOTE_DIR\" -xzf - && chmod 600 \"$REMOTE_ENV_FILE\" && chmod +x \"$REMOTE_DIR\"/start.sh \"$REMOTE_DIR\"/stop.sh \"$REMOTE_DIR\"/status.sh"
|
||||
|
||||
log "원격 Java 확인"
|
||||
ssh "${SSH_ARGS[@]}" "$HOST" "java -version >/tmp/vpd-backoffice-java-version.out 2>&1 && cat /tmp/vpd-backoffice-java-version.out | head -n 1"
|
||||
|
||||
if [[ "$START_APP" == "Y" ]]; then
|
||||
log "원격 앱 재기동"
|
||||
ssh "${SSH_ARGS[@]}" "$HOST" "\"$REMOTE_DIR\"/stop.sh || true; \"$REMOTE_DIR\"/start.sh; sleep 8; \"$REMOTE_DIR\"/status.sh"
|
||||
|
||||
log "원격 헬스체크"
|
||||
ssh "${SSH_ARGS[@]}" "$HOST" "curl -sS -o /tmp/vpd-backoffice-login.html -w '%{http_code}\n' http://127.0.0.1:${REMOTE_PORT}/login"
|
||||
fi
|
||||
|
||||
ok "배포 완료: $HOST:$REMOTE_DIR"
|
||||
Reference in New Issue
Block a user