diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7b8284e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# 배포 산출물은 Linux 서버에서 실행/파싱되므로 LF 강제(CRLF 시 systemd 유닛·bash 파싱 오류). +deploy/**/*.sh text eol=lf +deploy/**/*.service text eol=lf +deploy/**/*.conf text eol=lf +deploy/**/*.py text eol=lf +Jenkinsfile text eol=lf diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..bda5e0d --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,72 @@ +// KINTEX 자동전시시스템 — CI/CD 파이프라인 (git push → Gitea webhook → 빌드 → 배포 → 헬스체크) +// 실행 노드: 서버(101.79.17.164) Jenkins. 프론트(vite)→/var/www/kintex, 백엔드(bootJar)→/opt/kintex/app/app.jar, +// kintex.service + kintex-nanobanana.service 재기동. 배포 단계는 sudo(deploy_kintex.sh) 위임. +// ※ 상시 자동배포는 deploy_server.py(webhook, 포트 9999)의 kintex 블록이 1차 경로. 본 파이프라인은 Jenkins 병행 트랙. +pipeline { + agent any + options { + timeout(time: 40, unit: 'MINUTES') + timestamps() + buildDiscarder(logRotator(numToKeepStr: '10')) + } + environment { + WEB_ROOT = '/var/www/kintex' + APP_JAR = '/opt/kintex/app/app.jar' + BACKEND_PORT = '8021' + } + stages { + stage('Checkout') { + steps { checkout scm } + } + stage('Frontend Build') { + steps { + dir('src/frontend') { + sh 'npm ci --silent || npm install --silent' + sh 'NODE_OPTIONS=--max-old-space-size=4096 npm run build' + } + } + } + stage('Backend Build') { + steps { + dir('src/backend') { + sh 'chmod +x gradlew' + sh './gradlew clean bootJar -x test -q --no-daemon' + } + } + } + stage('Verify Jar') { + steps { + sh ''' + JAR=$(ls -1 src/backend/build/libs/kintex-backend-*.jar | grep -v plain | head -1) + [ -n "$JAR" ] || { echo "jar not found"; exit 1; } + unzip -l "$JAR" | grep -q 'BOOT-INF/classes/com/zioinfo/kintex/KintexApplication.class' \ + || { echo "invalid jar (no main class)"; exit 1; } + echo "jar OK: $JAR" + ''' + } + } + stage('Deploy') { + steps { + // 권한 필요한 배포는 deploy_kintex.sh 로 위임(sudo NOPASSWD 권장). + sh 'sudo /opt/kintex/deploy_kintex.sh "$WORKSPACE"' + } + } + stage('Health Check') { + steps { + sh ''' + for i in $(seq 1 15); do + code=$(curl -s -m 6 -o /dev/null -w '%{http_code}' http://127.0.0.1:'"$BACKEND_PORT"'/health || true) + echo "health try $i: $code" + if [ "$code" = "200" ]; then exit 0; fi + sleep 5 + done + echo "health check failed"; systemctl is-active kintex.service || true; exit 1 + ''' + } + } + } + post { + success { echo 'KINTEX 배포 성공 (kintex.zioinfo.kr, 백엔드 8021)' } + failure { echo 'KINTEX 빌드/배포 실패 — 롤백은 deploy_kintex.sh 가 수행' } + } +} diff --git a/deploy/deploy_kintex.sh b/deploy/deploy_kintex.sh new file mode 100644 index 0000000..522d1a9 --- /dev/null +++ b/deploy/deploy_kintex.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# KINTEX 배포 스크립트 — deploy_server.py(webhook) 또는 Jenkins가 실행. +# 인자: $1 = 소스 체크아웃 루트(빌드 산출물 위치, 기본 /opt/kintex/src) +# 동작(fail-safe): 프론트 dist 검증→원자적 교체(/var/www/kintex)→백엔드 jar 교체→서비스 재기동→헬스체크. +# 깨진 빌드가 라이브를 무너뜨리지 않도록 "검증 후 교체" + 실패 시 롤백(WISE deploy_uiws.sh 패턴). +set -euo pipefail + +SRC="${1:-/opt/kintex/src}" +WEB_ROOT=/var/www/kintex +APP_JAR=/opt/kintex/app/app.jar +BACKEND_PORT=8021 + +log(){ echo "[deploy-kintex] $*"; } +die(){ echo "[deploy-kintex] ERROR: $*" >&2; exit 1; } + +# ── 1. 프론트: 빌드 완전성 검증(라이브 건드리기 전) → 스테이징 → 원자적 rename ── +DIST="$SRC/src/frontend/dist" +if [ -d "$DIST" ]; then + [ -f "$DIST/index.html" ] || die "dist/index.html 없음 — 불완전 빌드, 배포 중단(라이브 무변경)" + [ -d "$DIST/assets" ] || die "dist/assets 없음 — 불완전 빌드, 배포 중단" + mapfile -t REFS < <(grep -oE '/assets/[A-Za-z0-9._-]+\.(js|css)' "$DIST/index.html" | sort -u) + [ "${#REFS[@]}" -gt 0 ] || die "index.html 참조 assets 없음 — 비정상 빌드, 중단" + for ref in "${REFS[@]}"; do + [ -f "${DIST}${ref}" ] || die "참조 자산 누락: ${ref} — 불완전 빌드, 중단" + done + log "프론트 빌드 검증 통과(${#REFS[@]} assets)" + + STAGE="${WEB_ROOT}.new.$$" + rm -rf "$STAGE"; mkdir -p "$STAGE" + cp -r "$DIST/." "$STAGE/" + if [ -d "$WEB_ROOT" ] && [ -n "$(ls -A "$WEB_ROOT" 2>/dev/null)" ]; then + tar czf "/var/www/kintex.bak_$(date +%Y%m%d_%H%M%S).tgz" -C "$WEB_ROOT" . || log "WARN: 프론트 백업 실패(계속)" + fi + OLD="${WEB_ROOT}.old.$$" + [ -d "$WEB_ROOT" ] && mv "$WEB_ROOT" "$OLD" || true + mv "$STAGE" "$WEB_ROOT" + rm -rf "$OLD" + log "프론트 배포 완료 → $WEB_ROOT" +else + log "WARN: $DIST 없음 — 프론트 배포 건너뜀(백엔드만)" +fi + +# ── 2. 백엔드 jar: 검증(BOOT-INF 메인 클래스) 후 교체 ── +JAR=$(ls -1 "$SRC"/src/backend/build/libs/kintex-backend-*.jar 2>/dev/null | grep -v plain | head -1 || true) +if [ -n "$JAR" ]; then + unzip -l "$JAR" | grep -q 'BOOT-INF/classes/com/zioinfo/kintex/KintexApplication.class' \ + || die "유효하지 않은 jar(메인 클래스 없음) — 배포 중단" + [ -f "$APP_JAR" ] && cp "$APP_JAR" "${APP_JAR}.bak" || true + cp "$JAR" "$APP_JAR" + log "백엔드 jar 교체 → $APP_JAR ($(basename "$JAR"))" +else + log "WARN: bootJar 없음 — 백엔드 교체 건너뜀" +fi + +# ── 3. 워커 의존성 갱신(venv) — requirements 변경 대비(멱등) ── +if [ -x /opt/kintex/worker/venv/bin/pip ]; then + /opt/kintex/worker/venv/bin/pip install -q redis Pillow google-genai 2>/dev/null || log "WARN: 워커 의존성 갱신 스킵" +fi + +# ── 4. 서비스 재기동 ── +systemctl restart kintex.service +systemctl restart kintex-nanobanana.service 2>/dev/null || log "WARN: 워커 재기동 스킵" + +# ── 5. 헬스체크(백엔드 /health 200 = success:true) — 실패 시 jar 롤백 ── +ok=0 +for i in $(seq 1 20); do + sleep 3 + code=$(curl -s -m 5 -o /dev/null -w '%{http_code}' "http://127.0.0.1:${BACKEND_PORT}/health" || true) + if [ "$code" = "200" ]; then log "헬스체크 OK ($code)"; ok=1; break; fi +done +if [ "$ok" != "1" ]; then + log "헬스체크 실패 — jar 롤백 시도" + if [ -f "${APP_JAR}.bak" ]; then cp "${APP_JAR}.bak" "$APP_JAR"; systemctl restart kintex.service; fi + systemctl is-active kintex.service || true + die "배포 헬스체크 실패(롤백 수행)" +fi +log "배포 완료 — 백엔드 8021 UP, 워커 재기동" diff --git a/deploy/deploy_server_kintex_block.py b/deploy/deploy_server_kintex_block.py new file mode 100644 index 0000000..51617bc --- /dev/null +++ b/deploy/deploy_server_kintex_block.py @@ -0,0 +1,46 @@ +# KINTEX 블록 — 서버 /opt/zioinfo/deploy_server.py 의 _deploy() 체인에 삽입되는 정본 사본. +# (deploy_server.py 는 루트 인프라 파일이므로 kintex 리포엔 이 참조 사본만 둔다. 서버 사본이 실행 권위.) +# 삽입 위치: 마지막 `elif repo == "uiws":` 블록 다음, `logging.info(f"=== {repo} 배포 완료 ===")` 앞. +# Gitea owner=zio, 기본 브랜치=main, 백엔드 포트 8021. + +''' + elif repo == "kintex": + # 분리형 배포: 프론트 dist(Vite) → /var/www/kintex, 백엔드 bootJar(Gradle) → /opt/kintex/app/app.jar, + # kintex.service + kintex-nanobanana.service(나노바나나 워커, GEMINI 키는 워커 env 전용) 재기동. + # Flyway baseline-on-migrate=true → 기동 시 스키마 자동 적용. deploy_kintex.sh 가 검증→원자교체→헬스체크→롤백. + SRC = "/opt/kintex/src" + ok = run_steps(repo, [ + ("git pull", ["bash", "-c", + f"if [ -d {SRC}/.git ]; then " + f"git -C {SRC} fetch origin main && git -C {SRC} reset --hard origin/main; " + f"else " + f"[ -e {SRC} ] && mv {SRC} {SRC}.bak_$(date +%Y%m%d%H%M%S); " + f"git clone 'http://zio:1q2w3e%21Q@127.0.0.1:9003/zio/kintex.git' {SRC}; " + f"fi"]), + ("npm build", ["bash", "-c", + f"cd {SRC}/src/frontend && (npm ci 2>/dev/null || npm install) && " + f"NODE_OPTIONS=--max-old-space-size=4096 npm run build"]), + ("gradle build", ["bash", "-c", + f"cd {SRC}/src/backend && chmod +x gradlew 2>/dev/null; sed -i 's/\\r$//' gradlew 2>/dev/null; " + f"./gradlew clean bootJar -x test -q --no-daemon > /tmp/kintex_gradle.log 2>&1; " + f"rc=$?; tail -8 /tmp/kintex_gradle.log; exit $rc"]), + ("verify jar", ["bash", "-c", + f"JAR=$(ls -1 {SRC}/src/backend/build/libs/kintex-backend-*.jar 2>/dev/null | grep -v plain | head -1); " + f"[ -n \"$JAR\" ] || {{ echo 'jar not found'; exit 1; }}; " + f"unzip -l \"$JAR\" | grep -q 'BOOT-INF/classes/com/zioinfo/kintex/KintexApplication.class' " + f"|| {{ echo 'invalid jar'; exit 1; }}; echo \"jar OK: $JAR\""]), + ("deploy (deploy_kintex.sh)", ["bash", "-c", + f"sed -i 's/\\r$//' {SRC}/deploy/*.sh 2>/dev/null; " + f"bash {SRC}/deploy/deploy_kintex.sh {SRC}"]), + ("health check", ["bash", "-c", + "for i in $(seq 1 20); do sleep 3; " + "code=$(curl -s -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:8021/health); " + "if [ \"$code\" = '200' ]; then echo \"health OK ($code)\"; exit 0; fi; " + "done; echo 'health FAIL'; systemctl is-active kintex.service 2>/dev/null; exit 1"]), + ]) + if ok: + notify_itsm(True, "✅ kintex 배포 완료 (kintex.zioinfo.kr, 포트 8021)") + trigger_jenkins("kintex") + else: + notify_itsm(False, "❌ kintex 빌드/배포 실패") +''' diff --git a/deploy/nginx/kintex.zioinfo.kr.conf b/deploy/nginx/kintex.zioinfo.kr.conf new file mode 100644 index 0000000..e4f9525 --- /dev/null +++ b/deploy/nginx/kintex.zioinfo.kr.conf @@ -0,0 +1,50 @@ +# KINTEX 자동전시시스템 — nginx vhost (개발: kintex.zioinfo.kr → 101.79.17.164) +# / → /var/www/kintex (React/Vite SPA, try_files 폴백) +# /api/, /health → 127.0.0.1:8021 (Spring Boot 백엔드) +# /ws → 127.0.0.1:8021 (STOMP/WebSocket 업그레이드) +# TLS(443)는 certbot --nginx -d kintex.zioinfo.kr 로 후속 주입(80→443 리다이렉트 자동). +# 운영 도메인 kintex.wise.ai.kr 은 server_name 추가 또는 별도 vhost 로 후속 구성. +server { + listen 80; + listen [::]:80; + server_name kintex.zioinfo.kr; + + root /var/www/kintex; + index index.html; + client_max_body_size 50m; + + access_log /var/log/nginx/kintex.access.log; + error_log /var/log/nginx/kintex.error.log; + + # 백엔드 API + location /api/ { + proxy_pass http://127.0.0.1:8021; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + } + + # 헬스체크(공개) + location = /health { + proxy_pass http://127.0.0.1:8021/health; + proxy_set_header Host $host; + } + + # WebSocket(STOMP) — RenderJob 완료·승인 이벤트 실시간 + location /ws { + proxy_pass http://127.0.0.1:8021; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 3600s; + } + + # 프론트 SPA — 정적 서빙 + 클라이언트 라우팅 폴백 + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/deploy/setup_kintex_service.py b/deploy/setup_kintex_service.py new file mode 100644 index 0000000..f7db725 --- /dev/null +++ b/deploy/setup_kintex_service.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""KINTEX 서버 프로비저닝(멱등) — systemd 유닛·디렉터리·워커 venv·nginx vhost 설치. + +DB(kintex_db+PostGIS)·시크릿 env 는 provision.sh(별도, 시크릿 미출력)가 담당한다. +이 스크립트는 "코드로 유지되는" 서비스 구성만 다룬다(시크릿 없음 → 리포 커밋 가능). + +전제: /opt/kintex/kintex.env·/opt/kintex/nanobanana.env 가 이미 존재(provision.sh 산출, root 600). +사용: + sudo python3 deploy/setup_kintex_service.py # 유닛/디렉터리/venv/nginx 설치 + sudo python3 deploy/setup_kintex_service.py --no-nginx # nginx 제외 + +배정 사실(2026-07-11 서버 survey): + - 백엔드 포트 8021 (8003~8020 = 기존 GUARDiA 솔루션 예약, 8020=guardia-rag) + - 개발 도메인 kintex.zioinfo.kr → 101.79.17.164 +""" +import argparse +import os +import shutil +import subprocess +import sys + +KINTEX_ROOT = "/opt/kintex" +SRC = f"{KINTEX_ROOT}/src" +WEB_ROOT = "/var/www/kintex" +VENV = f"{KINTEX_ROOT}/worker/venv" +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def sh(cmd, check=True): + print(f"[setup] $ {cmd}") + r = subprocess.run(cmd, shell=True, text=True) + if check and r.returncode != 0: + sys.exit(f"[setup] 실패(rc={r.returncode}): {cmd}") + return r.returncode + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--no-nginx", action="store_true") + args = ap.parse_args() + + if os.geteuid() != 0: + sys.exit("[setup] root 로 실행하세요(sudo).") + + # 1. 디렉터리 + for d in (f"{KINTEX_ROOT}/app", SRC, f"{KINTEX_ROOT}/worker", + f"{KINTEX_ROOT}/output/visualizations", WEB_ROOT, "/var/log/kintex"): + os.makedirs(d, exist_ok=True) + + # 2. deploy_kintex.sh 설치(서버 표준 위치) + sh(f"install -m 755 {HERE}/deploy_kintex.sh {KINTEX_ROOT}/deploy_kintex.sh") + + # 3. 워커 venv + 의존성(google-genai·Pillow·redis) + if not os.path.exists(f"{VENV}/bin/python"): + sh(f"python3 -m venv {VENV}") + sh(f"{VENV}/bin/pip install -q --upgrade pip") + sh(f"{VENV}/bin/pip install -q redis Pillow google-genai") + + # 4. systemd 유닛 설치 + for unit in ("kintex.service", "kintex-nanobanana.service"): + src = f"{HERE}/systemd/{unit}" + shutil.copyfile(src, f"/etc/systemd/system/{unit}") + os.chmod(f"/etc/systemd/system/{unit}", 0o644) + print(f"[setup] 유닛 설치: {unit}") + sh("systemctl daemon-reload") + sh("systemctl enable kintex.service kintex-nanobanana.service", check=False) + + # 5. nginx vhost + if not args.no_nginx: + conf = "kintex.zioinfo.kr.conf" + shutil.copyfile(f"{HERE}/nginx/{conf}", f"/etc/nginx/sites-available/{conf}") + link = f"/etc/nginx/sites-enabled/{conf}" + if not os.path.islink(link) and not os.path.exists(link): + os.symlink(f"/etc/nginx/sites-available/{conf}", link) + if sh("nginx -t", check=False) == 0: + sh("systemctl reload nginx") + else: + print("[setup] WARN: nginx -t 실패 — vhost 반영 보류(타 사이트 무영향)") + + print("[setup] 완료. env(시크릿)는 provision.sh 산출물 사용. " + "서비스 기동은 배포(deploy_kintex.sh)에서 jar 배치 후 수행.") + + +if __name__ == "__main__": + main() diff --git a/deploy/systemd/kintex-nanobanana.service b/deploy/systemd/kintex-nanobanana.service new file mode 100644 index 0000000..41429ec --- /dev/null +++ b/deploy/systemd/kintex-nanobanana.service @@ -0,0 +1,22 @@ +[Unit] +# KINTEX 나노바나나 렌더 워커 — Redis 큐(kintex:renderjob:queue) 소비 → Gemini 이미지 생성. +# GEMINI_API_KEY 는 이 유닛의 EnvironmentFile(/opt/kintex/nanobanana.env, root 600)에서만 로드. +# 백엔드·타 서비스에 미노출. G1 승인(2026-07-11)으로 NANOBANANA_LIVE=1(라이브 생성). +Description=KINTEX Nanobanana Render Worker (Gemini) +After=network.target redis-server.service kintex.service +Wants=redis-server.service + +[Service] +Type=simple +User=root +# `python -m tools.nanobanana.worker` 는 소스 루트(/opt/kintex/src)를 CWD·sys.path 로 요구. +WorkingDirectory=/opt/kintex/src +EnvironmentFile=/opt/kintex/nanobanana.env +ExecStart=/opt/kintex/worker/venv/bin/python -m tools.nanobanana.worker +Restart=on-failure +RestartSec=10 +StandardOutput=append:/var/log/kintex/worker.log +StandardError=append:/var/log/kintex/worker.log + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/kintex.service b/deploy/systemd/kintex.service new file mode 100644 index 0000000..e02e463 --- /dev/null +++ b/deploy/systemd/kintex.service @@ -0,0 +1,22 @@ +[Unit] +# KINTEX 자동전시시스템 — 백엔드(Spring Boot 3.2.5, 포트 8021) +# 시크릿은 EnvironmentFile(/opt/kintex/kintex.env, root 600)로만 주입. GEMINI 키는 여기 없음(워커 전용). +Description=KINTEX AI Exhibition Backend (Spring Boot) +After=network.target postgresql.service redis-server.service +Wants=redis-server.service + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/kintex/app +EnvironmentFile=/opt/kintex/kintex.env +# JDK21로 Java17 타깃 bootJar 실행(Spring Boot 3.2.5 호환). 힙은 공유 서버 보호로 상한. +ExecStart=/usr/bin/java -Xms128m -Xmx512m -jar /opt/kintex/app/app.jar +SuccessExitStatus=143 +Restart=on-failure +RestartSec=5 +StandardOutput=append:/var/log/kintex/backend.log +StandardError=append:/var/log/kintex/backend.log + +[Install] +WantedBy=multi-user.target