diff --git a/docs/design/546-hermes-vm-deploy/README.md b/docs/design/546-hermes-vm-deploy/README.md new file mode 100644 index 0000000..2bfe88f --- /dev/null +++ b/docs/design/546-hermes-vm-deploy/README.md @@ -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 diff --git a/scripts/deploy-backoffice-vm.sh b/scripts/deploy-backoffice-vm.sh new file mode 100755 index 0000000..15c6c69 --- /dev/null +++ b/scripts/deploy-backoffice-vm.sh @@ -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 <> "$TMP_DIR/.env" < "$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"