# Redmine #546 - 외부 VM 배포 설계

## 현재 기준 배포 대상

이 문서의 최신 운영 기준은 아래와 같다. 과거 `hermes`/`130.162.134.59` 기록은 초기 개발·실험 배포 이력으로만 본다.

| 구분 | 값 |
| --- | --- |
| 개발·빌드 VM | `hermes` |
| 공개 서비스 배포 VM | `opc@161.33.6.45` (`vnic-aidp-poc`) |
| 공개 주소 | `https://kb.cloud-handson.com` |
| DNS | `kb.cloud-handson.com → 161.33.6.45` |
| VM 서비스 | `vpd-backoffice.service` |
| 앱 수신 주소 | `127.0.0.1:8080` |
| 공개 프록시 | Nginx `80/443 → 127.0.0.1:8080` |
| 인증서 | Let’s Encrypt / Certbot Nginx plugin |
| 앱 디렉터리 | `/home/opc/apps/vpd-backoffice` |
| Wallet 디렉터리 | `/home/opc/apps/vpd-backoffice/wallet` |

반복 배포 시 완료 판정은 반드시 공개 주소 기준으로 한다.

```bash
curl -k -sS https://kb.cloud-handson.com/login
```

`hermes` 내부의 `8082` 응답만 확인하고 완료 처리하지 않는다. `8082`는 과거/개발 배포 경로에 해당할 수 있다.

## 프로젝트 개요

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`을 원격 경로로 재작성해야 한다.

## 배포 방식

- 기본 개발/빌드 host는 `hermes`일 수 있으나, 공개 서비스 배포 대상은 `opc@161.33.6.45`다.
- 배포 스크립트의 기본값이 `hermes`인 경우, 운영 배포에서는 반드시 `--host` 또는 SSH alias가 `161.33.6.45`를 가리키는지 확인한다.
- 로컬에서 `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`
- 원격 프로세스는 systemd `vpd-backoffice.service`로 기동한다.
  - `User=opc`, `Restart=on-failure`, `NoNewPrivileges=true`, `PrivateTmp=true`를 적용한다.
  - 환경 파일은 `/etc/vpd-backoffice/vpd-backoffice.env`에 두고 `root:opc`, `640` 권한으로 제한한다.
  - Wallet과 초기 관리자 자격증명 파일은 앱 디렉터리에 두고 각각 `700` 디렉터리와 `600` 파일 권한으로 제한한다.

## 완료 기준

- `scripts/deploy-backoffice-vm.sh`가 jar, `.env`, wallet, start/stop/status 스크립트를 원격에 설치할 수 있다.
- 대상 alias가 없으면 명확하게 실패한다.
- 배포 후 원격 `status.sh`와 HTTP `/login` 헬스체크가 가능하다.
- 현재 공개 운영에서는 Nginx 80/443만 외부에 열고, Spring Boot 애플리케이션 포트는 VM 내부 loopback으로 제한한다.

## 과거 hermes 배포 결과

- 배포 대상: `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

## 2026-07-07 KBAIPOC VM 배포 결과

- 배포 대상: `opc@161.33.6.45` (`vnic-aidp-poc`), 서비스: `vpd-backoffice.service`.
- Java: Oracle Linux 9의 `java-21-openjdk-headless`를 설치했다.
- 앱은 Nginx 뒤의 `127.0.0.1:8080`에서만 수신한다.
- ADB Wallet은 앱 디렉터리의 `wallet/`에 배치하고, 앱은 `kbaipoc_high` 서비스 별칭으로 접속한다.
- 새로운 ADB에 표준 VPD POC 스키마, `CB_ORDS`, 백오피스 지원 객체, 동적 VPD 필터를 VM SQLcl로 초기화했다. 핵심 지원 테이블 5개와 `CB_AGENT_CTX_PKG`, `CB_AGENT_DOC_VPD_FILTER`의 `VALID` 상태를 확인했다.
- 초기화 SQL은 현재 스키마의 `CB_PERMISSION_RULE` 5열 모델에 맞춰 seed 열을 명시하고, 지원 객체 생성은 재실행 가능하도록 `ORA-00955`를 처리한다. Autonomous Database의 parallel DML 충돌을 피하기 위해 보조 SQL은 parallel DML을 비활성화하고 seed 전 트랜잭션을 끝낸다.
- VM 내부 로그인과 인증된 대시보드에서 ADB 조회·도움말 SVG를 확인했다.

## 2026-07-07 HTTPS 전환 결과

- 공개 주소: `https://kb.cloud-handson.com`.
- DNS A 레코드 `kb.cloud-handson.com → 161.33.6.45`와 Nginx HTTP-01 경로를 외부에서 검증했다.
- Nginx는 80/443에서 수신하고 `127.0.0.1:8080`의 Spring Boot 서비스로 프록시한다. HTTP 요청은 HTTPS로 `301` 리다이렉트한다.
- Let’s Encrypt 인증서를 Certbot Nginx 플러그인으로 발급했다. 인증서 CN은 `kb.cloud-handson.com`, 만료일은 2026-10-05다.
- `certbot-renew.timer`를 enable해 자동 갱신을 설정했다.
- SELinux enforcing 환경에서 `httpd_can_network_connect`를 영구 허용해 Nginx의 localhost upstream 연결을 허용했다.
- 백오피스 환경은 `BACKOFFICE_REQUIRE_HTTPS=true`, secure session cookie로 전환했다.

## 검증

- `mvn test`
- `scripts/deploy-backoffice-vm.sh --dry-run`
- 운영 배포 대상 SSH alias가 `opc@161.33.6.45`를 가리키는지 확인
- 운영 배포 대상의 `systemctl status vpd-backoffice` 확인
- 원격 내부 `/login` HTTP 200
- 외부 `/login` HTTP 200
- `systemctl status vpd-backoffice`가 `active (running)`이고 앱 로그에서 `vpd-backoffice-pool` 연결이 성공한다.
- VM SQLcl에서 VPD context package body와 dynamic filter가 `VALID`이다.
- `https://kb.cloud-handson.com/login`이 외부에서 HTTP `200`을 반환하고 인증서 발급자가 Let’s Encrypt인지 확인한다.
- `http://kb.cloud-handson.com/login`은 HTTPS 주소로 `301` 리다이렉트한다.
