From ddacd1b7f562ded033232943ea4331dd508a229a Mon Sep 17 00:00:00 2001 From: infraurp Date: Wed, 26 Aug 2026 11:16:10 +0900 Subject: [PATCH] =?UTF-8?q?csv.wise.ai.kr=20=EB=B0=B0=ED=8F=AC=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EC=8B=A0=EC=84=A4=20(PC?= =?UTF-8?q?=20=E2=86=92=20git=20=E2=86=92=20web=20=E2=86=92=20was)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 웹서버(175.118.124.176)의 Gitea 러너가 JDK8 로 WAR 을 빌드해 WAS(27.255.76.2)의 Tomcat·MariaDB 컨테이너로 배포한다. WAS 는 웹서버를 통해서만 닿고, Tomcat 은 127.0.0.1 바인딩이라 웹서버가 SSH 터널로 잇는다. - WAS 는 가용 메모리가 3G 안팎(llama-server 200G+ 점유)이라 빌드를 웹서버에서 한다 - claudeops 가 sudo 를 쓸 수 없어 Tomcat·MariaDB 를 컨테이너로 올렸다 - 배포 스크립트를 매 실행마다 저장소본으로 덮어써 서버 사본 드리프트를 막는다 Co-Authored-By: Claude Opus 5 --- .gitea/workflows/deploy-wise.yml | 132 ++++++++++++++++++++++++++++ deploy/wise/01-was-setup.sh | 65 ++++++++++++++ deploy/wise/README.md | 123 ++++++++++++++++++++++++++ deploy/wise/arko-deploy.sh | 115 ++++++++++++++++++++++++ deploy/wise/arko-runner-config.yaml | 14 +++ deploy/wise/arko-runner.service | 20 +++++ deploy/wise/arko-was-tunnel.service | 40 +++++++++ deploy/wise/csv.wise.ai.kr.conf | 58 ++++++++++++ deploy/wise/mariadb.cnf | 33 +++++++ 9 files changed, 600 insertions(+) create mode 100644 .gitea/workflows/deploy-wise.yml create mode 100644 deploy/wise/01-was-setup.sh create mode 100644 deploy/wise/README.md create mode 100644 deploy/wise/arko-deploy.sh create mode 100644 deploy/wise/arko-runner-config.yaml create mode 100644 deploy/wise/arko-runner.service create mode 100644 deploy/wise/arko-was-tunnel.service create mode 100644 deploy/wise/csv.wise.ai.kr.conf create mode 100644 deploy/wise/mariadb.cnf diff --git a/.gitea/workflows/deploy-wise.yml b/.gitea/workflows/deploy-wise.yml new file mode 100644 index 0000000..b632df5 --- /dev/null +++ b/.gitea/workflows/deploy-wise.yml @@ -0,0 +1,132 @@ +# arko_renual — csv.wise.ai.kr 자동 배포 파이프라인 +# +# [개발 PC] git push → 이 워크플로가 자동 실행 +# [웹서버] 빌드(JDK8/Maven) → WAR 을 WAS 로 전송 +# [WAS] ROOT 재전개 + 환경설정 주입 + Tomcat 재기동 + 헬스체크(실패 시 자동 롤백) +# [웹서버] SSH 터널 경유로 공개 확인 (https://csv.wise.ai.kr) +# +# 그 세 걸음이 곧 지정된 경로다: PC → git → web → was(db) +# +# ┌ 왜 웹서버에서 빌드하나 ────────────────────────────────────────────────┐ +# │ WAS(27.255.76.2)는 64코어지만 llama-server 가 200G+ 를 점유해 가용 │ +# │ 메모리가 3G 안팎이다. 거기서 Maven(힙 1G)을 돌리면 LLM 서비스가 OOM 될 │ +# │ 수 있다. 웹서버(4코어/여유 4G)는 빌드 외 부하가 없어 여기서 빌드한다. │ +# └────────────────────────────────────────────────────────────────────────┘ +# +# ※ Gitea 1.22.3 은 workflow_dispatch(수동 실행 버튼)를 지원하지 않는다 — +# 넣으면 워크플로가 Actions 목록에 아예 뜨지 않는다. 트리거는 push 하나만 둔다. +# 수동 재배포는 WAS 에서 직접: +# ssh -J <웹서버> claudeops@27.255.76.2 \ +# 'bash /opt/arko-app/deploy/arko-deploy.sh /opt/arko-app/incoming/arko.war' +name: deploy-wise + +on: + push: + branches: + - master + # 현재 배포 대상은 이 브랜치다. master 는 7커밋 뒤처져 있고 이관된 DB 스키마와 + # 맞지 않는다. master 로 병합한 뒤에는 아래 줄을 지워도 된다. + - feature/privacy-access-control-policy + paths-ignore: # 문서만 바뀐 커밋으로 재배포하지 않는다 + - '**.md' + - '**.docx' + - '**.pptx' + - '**.hwp' + - 'docs/**' + - '_workspace/**' + - '_workspace_privacy/**' + +concurrency: + group: arko-wise-deploy + cancel-in-progress: false # 배포 도중 취소되면 ROOT 가 반쪽 상태로 남는다 + +env: + JAVA_HOME: /usr/lib/jvm/java-1.8.0-openjdk + MVN: /opt/maven/bin/mvn + WAS_HOST: 27.255.76.2 + WAS_USER: claudeops + WAS_KEY: /root/.ssh/id_ed25519 + APP_HOME: /opt/arko-app + SITE_URL: https://csv.wise.ai.kr/ + +jobs: + deploy: + runs-on: arko-web + timeout-minutes: 25 + + steps: + # ⚠ lfs: true 를 주지 않는다 — 러너에 git-lfs 가 없으면 checkout 자체가 죽는다. + - uses: actions/checkout@v4 + + - name: 사전 점검 + run: | + set -e + df -h /opt | tail -1 + free -h | head -2 + # 터널이 살아 있어야 배포 후 확인이 가능하다 + systemctl is-active arko-was-tunnel || echo "::warning::터널이 죽어 있다 — 배포 후 확인이 실패할 수 있다" + + - name: 빌드 (JDK 8) + run: | + set -e + export JAVA_HOME="$JAVA_HOME" + export MAVEN_OPTS="-Xmx1024m" + # Maven 3.8+ 는 HTTP 저장소를 차단한다. eGov 2.7 은 HTTP 저장소(terracotta 등)를 + # 쓰므로 3.6.3 을 고정해서 쓴다. 3.8 로 올리면 의존성 해석부터 실패한다. + "$MVN" -B clean package -DskipTests + ls -lh target/arko.war + + - name: 산출물 검증 + run: | + set -e + # 바이트코드 major=51(Java 7). JDK 11/17 로 잘못 빌드되면 WAS 에서 못 뜬다. + CLS=$(find target/classes -name '*.class' | head -1) + VER=$(od -An -t d1 -j 7 -N 1 "$CLS" | tr -d ' ') + echo "바이트코드 major=$VER (51이어야 정상)" + [ "$VER" = "51" ] || { echo "::error::JDK 8 이 아닌 컴파일러로 빌드됐다"; exit 1; } + # 파이프 없이 한 번만 읽어 검사한다(파이프+pipefail 조합이 정상 WAR 을 오판한 전례가 있다) + WAR_LIST=$(unzip -l target/arko.war) + grep -q "WEB-INF/web.xml" <<< "$WAR_LIST" || { echo "::error::정상 WAR 이 아니다"; exit 1; } + + - name: WAS 로 전송 + run: | + set -e + SSH="ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -i $WAS_KEY" + $SSH "$WAS_USER@$WAS_HOST" "mkdir -p $APP_HOME/incoming $APP_HOME/deploy" + scp -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -i "$WAS_KEY" \ + target/arko.war "$WAS_USER@$WAS_HOST:$APP_HOME/incoming/arko.war" + # 배포 스크립트도 매번 저장소본으로 덮어쓴다. + # (이전 파이프라인에서 서버 사본이 저장소본과 갈라져 고친 내용이 반영되지 않는 + # 사고가 있었다. 저장소를 유일한 원본으로 둔다.) + scp -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -i "$WAS_KEY" \ + deploy/wise/arko-deploy.sh "$WAS_USER@$WAS_HOST:$APP_HOME/deploy/arko-deploy.sh" + # 잘린 전송을 잡는다 — 전례가 있어 크기를 반드시 대조한다 + LOCAL_MD5=$(md5sum target/arko.war | cut -d' ' -f1) + REMOTE_MD5=$($SSH "$WAS_USER@$WAS_HOST" "md5sum $APP_HOME/incoming/arko.war | cut -d' ' -f1") + echo "local=$LOCAL_MD5 remote=$REMOTE_MD5" + [ "$LOCAL_MD5" = "$REMOTE_MD5" ] || { echo "::error::전송된 WAR 이 손상됐다"; exit 1; } + + - name: WAS 배포 + run: | + set -e + ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -i "$WAS_KEY" \ + "$WAS_USER@$WAS_HOST" "chmod +x $APP_HOME/deploy/arko-deploy.sh && bash $APP_HOME/deploy/arko-deploy.sh $APP_HOME/incoming/arko.war" + + - name: 공개 확인 + run: | + set -e + # 터널 경유(웹서버 → WAS) + TUN=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 http://127.0.0.1:11060/ || echo 000) + echo "터널 127.0.0.1:11060 → HTTP $TUN" + # 공개 URL. 첫 요청은 JSP 컴파일로 15초 이상 걸릴 수 있다. + CODE=000 + for _ in $(seq 1 6); do + CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 60 "$SITE_URL" || echo 000) + case "$CODE" in 200|302) break ;; esac + sleep 5 + done + echo "$SITE_URL → HTTP $CODE" + case "$CODE" in + 200|302) echo "배포 정상" ;; + *) echo "::error::공개 URL 응답 없음 (HTTP $CODE) — 터널(arko-was-tunnel) 상태를 확인할 것"; exit 1 ;; + esac diff --git a/deploy/wise/01-was-setup.sh b/deploy/wise/01-was-setup.sh new file mode 100644 index 0000000..ae856fd --- /dev/null +++ b/deploy/wise/01-was-setup.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# arko WAS(27.255.76.2) 최초 구축 — 컨테이너 2기(MariaDB, Tomcat) 생성 +# +# 실행 위치: WAS 에서 claudeops 계정으로. bash 01-was-setup.sh +# 재실행 안전: 컨테이너를 지우고 다시 만든다. 단 db-data 는 건드리지 않는다. +# +# ┌ 이 서버의 제약 ────────────────────────────────────────────────────────┐ +# │ ① claudeops 는 sudo 를 쓸 수 없다. docker 그룹 소속이라 컨테이너로만 │ +# │ 설치할 수 있다. /opt 쓰기도 컨테이너를 거쳐야 한다. │ +# │ ② llama-server 가 200G+ 를 점유해 가용 메모리가 3G 안팎이다. │ +# │ 두 컨테이너 합쳐 1.75G 로 묶어 둔다. 올리기 전 반드시 free -g 확인. │ +# │ ③ 11000~11050 은 다른 서비스가 쓰고 있다. arko 는 11060/11061 을 쓴다. │ +# └────────────────────────────────────────────────────────────────────────┘ +set -euo pipefail + +APP_HOME=/opt/arko-app +MARIADB_ROOT_PW=${MARIADB_ROOT_PW:?"MARIADB_ROOT_PW 를 지정할 것"} + +# ---------------------------------------------------------- 0. 디렉토리 +# /opt 는 root 소유라 컨테이너를 거쳐 만든다(claudeops 는 sudo 불가). +docker run --rm -v /opt:/host-opt alpine:3 sh -c \ + 'for d in db-data incoming webapps logs deploy conf backup lib upload privacy-download; do mkdir -p /host-opt/arko-app/$d; done; chown -R 1005:1006 /host-opt/arko-app' +mkdir -p "$APP_HOME"/upload/{thumbNail,preView,Synch,bbsTemp} "$APP_HOME"/logs/{mysql,tomcat} + +docker network inspect arko-net >/dev/null 2>&1 || docker network create arko-net + +# ------------------------------------------------------ 1. JDBC 드라이버 +# MariaDB 로 가므로 MariaDB 커넥터를 쓴다. WAR 안의 mysql-connector 는 쓰지 않는다 +# (pom 은 5.1.17 고정인데 properties 는 8.x 전용 com.mysql.cj.jdbc.Driver 를 가리켜 +# 그대로는 맞지 않는다). Tomcat 의 lib 에 두면 WAR 을 건드리지 않고 교체할 수 있다. +[ -f "$APP_HOME/lib/mariadb-java-client-2.7.12.jar" ] || \ + curl -fsSL -o "$APP_HOME/lib/mariadb-java-client-2.7.12.jar" \ + https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/2.7.12/mariadb-java-client-2.7.12.jar + +# ---------------------------------------------------------- 2. MariaDB +# lower_case_table_names=1 은 initdb 시점에만 반영된다(mariadb.cnf 주석 참고). +docker rm -f arko-mariadb >/dev/null 2>&1 || true +docker run -d --name arko-mariadb --restart unless-stopped --network arko-net \ + -p 127.0.0.1:11061:3306 \ + -m 768m --memory-swap 768m \ + -e MARIADB_ROOT_PASSWORD="$MARIADB_ROOT_PW" \ + -e MARIADB_DATABASE=csv -e MARIADB_USER=csv -e MARIADB_PASSWORD=csv \ + -e TZ=Asia/Seoul \ + -v "$APP_HOME/db-data":/var/lib/mysql \ + -v "$APP_HOME/conf/mariadb.cnf":/etc/mysql/conf.d/arko.cnf:ro \ + -v "$APP_HOME/logs/mysql":/var/log/mysql \ + mariadb:11.4 + +# ----------------------------------------------------------- 3. Tomcat +# arko 는 source/target 1.7 이라 JDK 8 로만 뜬다(11/17 불가). +# 컨테이너는 만들어만 두고 기동은 arko-deploy.sh 가 한다(ROOT 가 없으면 의미가 없다). +docker rm -f arko-tomcat >/dev/null 2>&1 || true +docker create --name arko-tomcat --restart unless-stopped --network arko-net \ + -p 127.0.0.1:11060:8080 \ + -m 1g --memory-swap 1g \ + -e TZ=Asia/Seoul \ + -e JAVA_OPTS="-Xms256m -Xmx768m -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Seoul -Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom" \ + -v "$APP_HOME/webapps":/usr/local/tomcat/webapps \ + -v "$APP_HOME/lib/mariadb-java-client-2.7.12.jar":/usr/local/tomcat/lib/mariadb-java-client-2.7.12.jar:ro \ + -v "$APP_HOME/logs/tomcat":/usr/local/tomcat/logs \ + -v "$APP_HOME/upload":/app/upload \ + -v "$APP_HOME/privacy-download":/app/privacy-download \ + tomcat:9.0-jdk8-temurin + +echo "완료. 다음: config/globals-{dev,live}.properties 를 두고 arko-deploy.sh 를 실행할 것." diff --git a/deploy/wise/README.md b/deploy/wise/README.md new file mode 100644 index 0000000..ebd8c5a --- /dev/null +++ b/deploy/wise/README.md @@ -0,0 +1,123 @@ +# csv.wise.ai.kr 배포 구성 + +## 전체 구조 + +``` + [개발 PC] + │ git push + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 웹서버 175.118.124.176 (utimsurpai.cafe24.com, Rocky 8, SSH :9271) │ +│ Gitea git.wise.ai.kr (127.0.0.1:9003) │ +│ act_runner `arko-web:host` → JDK8+Maven 3.6.3 으로 WAR 빌드 │ +│ Apache httpd → https://csv.wise.ai.kr │ +└───────────┬──────────────────────────────────┬──────────────────────┘ + │ ① scp WAR + 배포 스크립트 (SSH) │ ② SSH 터널 :11060 + ▼ ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ WAS 27.255.76.2 (Ubuntu 22.04, 64코어) │ +│ arko-tomcat 컨테이너 127.0.0.1:11060 ← ROOT 전개 │ +│ arko-mariadb 컨테이너 127.0.0.1:11061 DB `csv` │ +│ /opt/arko-app (webapps · config · upload · backup · logs) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +WAS 는 27 → 인터넷 방향으로만 열려 있고, 웹서버를 거쳐야 닿는다. +Tomcat 은 127.0.0.1 에만 바인딩되며 **인터넷에 직접 노출되지 않는다** — 웹서버가 +아웃바운드 SSH 터널로 자기 쪽 11060 을 WAS 의 Tomcat 에 잇는다. + +## 배포 흐름 + +```bash +git push # 이하 전부 자동 +``` + +``` +push → 웹서버에서 빌드(JDK8) → 바이트코드 major=51 검증 → WAR md5 대조 전송 + → WAS 에서 ROOT 재전개 + 환경설정 주입 + Tomcat 재기동 + 헬스체크 + → 실패 시 직전 백업으로 자동 롤백 + → https://csv.wise.ai.kr 공개 확인 +``` + +## 왜 이런 구조인가 + +**빌드를 WAS 에서 하지 않는 이유** — WAS 는 64코어지만 llama-server 가 200G+ 를 +점유해 **가용 메모리가 3G 안팎**이다. 여기서 Maven(힙 1G)을 돌리면 LLM 서비스가 +OOM 될 수 있다. 웹서버(4코어 / 여유 4G)는 빌드 외 부하가 없다. + +**컨테이너로 설치한 이유** — WAS 의 `claudeops` 계정은 **sudo 를 쓸 수 없고** +docker 그룹에만 속한다. 패키지 설치가 불가능해 Tomcat·MariaDB 를 컨테이너로 올렸다. +같은 서버의 다른 서비스(urpai 등)와 같은 관행이다. + +**배포 스크립트를 매번 덮어쓰는 이유** — 이전 파이프라인에서 서버 사본과 저장소본이 +갈라져, 저장소에서 고친 내용이 배포에 반영되지 않는 사고가 있었다. 워크플로가 매 실행마다 +`deploy/wise/arko-deploy.sh` 를 WAS 로 복사하므로 **저장소가 유일한 원본**이다. + +## 포트 + +| 포트 | 위치 | 용도 | +|---|---|---| +| 9271 | 웹서버 | SSH | +| 9003 | 웹서버 127.0.0.1 | Gitea | +| 11060 | WAS 127.0.0.1 | Tomcat (웹서버는 터널로 접근) | +| 11061 | WAS 127.0.0.1 | MariaDB | + +11000~11050 은 같은 서버의 다른 서비스가 쓰고 있다. 포트를 새로 잡을 때 겹치지 않게 할 것. + +## 파일 + +| 파일 | 설치 위치 | 역할 | +|---|---|---| +| `01-was-setup.sh` | WAS | 컨테이너 2기 최초 생성 (재실행 안전) | +| `arko-deploy.sh` | WAS `/opt/arko-app/deploy/` | 재전개·설정주입·기동·헬스체크·롤백 | +| `mariadb.cnf` | WAS `/opt/arko-app/conf/` | MariaDB 설정 (`lower_case_table_names=1` 주의) | +| `csv.wise.ai.kr.conf` | 웹 `/etc/httpd/conf.d/` | Apache vhost (80→443, 터널로 프록시) | +| `arko-was-tunnel.service` | 웹 `/etc/systemd/system/` | SSH 터널 (자동 재연결) | +| `arko-runner.service` | 웹 `/etc/systemd/system/` | arko 전용 act_runner | +| `arko-runner-config.yaml` | 웹 `/opt/arko-runner/config.yaml` | 러너 설정 | + +## 환경설정 (저장소에 없음) + +`globals-dev.properties` / `globals-live.properties` 는 `.gitignore` 대상이라 +저장소에도 WAR 에도 들어가지 않는다. **WAS 의 `/opt/arko-app/config/` 에만 두고** +배포 때 `WEB-INF/classes/egovframework/egovProps/` 로 주입한다. +`context-datasource.xml` 이 `globals-dev.properties` 를 읽으므로 이 주입이 없으면 기동 실패한다. + +WAS 값에서 로컬과 다른 부분: + +```properties +Globals.OsType =UNIX +Globals.DriverClassName =org.mariadb.jdbc.Driver +Globals.Url =jdbc:mariadb://arko-mariadb:3306/csv?useUnicode=true&characterEncoding=UTF-8&useSSL=false +Globals.fileStorePath =/app/upload/ +Globals.home.url =https://csv.wise.ai.kr +Globals.home.base =/usr/local/tomcat/webapps/ROOT +Globals.privacyDownloadPath=/app/privacy-download/ +``` + +## 운영 명령 + +```bash +# 웹서버 (ssh -p 9271 root@175.118.124.176) +systemctl status arko-runner arko-was-tunnel # 502 가 나면 터널부터 확인 +tail -f /var/log/httpd/csv_error.log + +# WAS (ssh 는 웹서버를 경유한다) +docker ps --filter name=arko- +docker logs -f --tail 100 arko-tomcat +tail -f /opt/arko-app/logs/deploy.log +docker exec -it arko-mariadb mariadb -ucsv -pcsv csv + +# 수동 재배포 (Gitea 1.22 는 수동 실행 버튼이 없다) +bash /opt/arko-app/deploy/arko-deploy.sh /opt/arko-app/incoming/arko.war +``` + +## 알려진 제약 + +- **Gitea 1.22.3 은 `workflow_dispatch` 를 지원하지 않는다.** 넣으면 워크플로가 Actions + 목록에 아예 뜨지 않는다. 재배포는 실제 파일을 바꿔 push 하거나 WAS 에서 직접 실행한다. +- **Maven 은 3.6.3 을 쓴다.** 3.8+ 는 HTTP 저장소를 차단하는데 eGov 2.7 은 HTTP 저장소 + (terracotta 등)에 의존해 의존성 해석부터 실패한다. +- **메모리가 빠듯하다.** 두 컨테이너 합쳐 1.75G 상한이다. 올리기 전 `free -g` 로 확인할 것. +- **DB 는 MariaDB 다.** 원본(MySQL 8)의 `utf8mb4_0900_ai_ci` 는 MariaDB 에 없어 + `utf8mb4_general_ci` 로 변환해 이관했다. 한글 정렬 결과는 같다(코드포인트 순). diff --git a/deploy/wise/arko-deploy.sh b/deploy/wise/arko-deploy.sh new file mode 100644 index 0000000..1d3d4b9 --- /dev/null +++ b/deploy/wise/arko-deploy.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# arko(문화품앗이) WAS 배포 스크립트 — 27.255.76.2 +# +# 설치 위치: /opt/arko-app/deploy/arko-deploy.sh +# 호출 주체: Gitea Actions(웹서버 러너)가 ssh 로, 또는 수동 실행 +# 사용법 : bash arko-deploy.sh /opt/arko-app/incoming/arko.war +# +# ┌ 이 서버의 제약 ────────────────────────────────────────────────────────┐ +# │ claudeops 는 sudo 를 쓸 수 없다. 대신 docker 그룹에 속한다. │ +# │ 그래서 Tomcat·MariaDB 는 패키지 설치가 아니라 컨테이너로 돌린다. │ +# │ 또한 llama-server 가 200G+ 를 점유해 가용 메모리가 3G 안팎이다. │ +# │ 컨테이너에 메모리 상한을 걸어 두었으니 함부로 올리지 말 것. │ +# └────────────────────────────────────────────────────────────────────────┘ +# +# 동작: WAR 검증 → 현재 ROOT 백업(3개 유지) → 컨테이너 정지 → 재전개 +# → 환경설정 주입 → 기동 → 헬스체크 (실패 시 직전 백업으로 자동 롤백) +set -euo pipefail + +APP_HOME=/opt/arko-app +WEBAPPS=$APP_HOME/webapps +ROOT_DIR=$WEBAPPS/ROOT +CONFIG_DIR=$APP_HOME/config +BACKUP_DIR=$APP_HOME/backup +LOG_DIR=$APP_HOME/logs +CONTAINER=arko-tomcat +HEALTH_URL=http://127.0.0.1:11060/ +HEALTH_TIMEOUT=240 # 초. arko 는 첫 기동에서 JSP 컴파일로 60~120초 걸린다 +KEEP_BACKUPS=3 + +WAR_SRC=${1:-$APP_HOME/incoming/arko.war} +TS=$(date +%Y%m%d_%H%M%S) + +mkdir -p "$BACKUP_DIR" "$LOG_DIR" "$WEBAPPS" +log() { echo "[$(date +'%F %T')] $*" | tee -a "$LOG_DIR/deploy.log"; } +die() { log "ERROR: $*"; exit 1; } + +# ------------------------------------------------------------- 1. WAR 검증 +[ -f "$WAR_SRC" ] || die "WAR 파일 없음: $WAR_SRC" +# unzip -l 을 파이프로 grep 하면 set -o pipefail 과 맞물려 정상 WAR 을 오판한다 +# (2026-07-30 개발 파이프라인에서 실제로 배포가 멈춘 적이 있다). 한 번만 읽어 변수에 담는다. +WAR_LIST=$(unzip -l "$WAR_SRC") || die "WAR 손상: $WAR_SRC" +grep -q "WEB-INF/web.xml" <<< "$WAR_LIST" || die "WEB-INF/web.xml 없음 — 정상 WAR 아님" +log "배포 시작: $WAR_SRC ($(du -h "$WAR_SRC" | cut -f1))" + +# 전개본 + 백업까지 WAR 3배의 여유가 필요하다 +NEED_KB=$(( $(du -k "$WAR_SRC" | cut -f1) * 3 )) +FREE_KB=$(df -k "$APP_HOME" | awk 'NR==2 {print $4}') +[ "$FREE_KB" -gt "$NEED_KB" ] || die "디스크 부족: 여유 $((FREE_KB/1024))MB, 필요 $((NEED_KB/1024))MB" + +# --------------------------------------------------------- 2. 현재본 백업 +BACKUP_FILE="" +if [ -d "$ROOT_DIR" ]; then + BACKUP_FILE="$BACKUP_DIR/ROOT_$TS.tar.gz" + log "현재 ROOT 백업 → $BACKUP_FILE" + tar -czf "$BACKUP_FILE" -C "$WEBAPPS" ROOT + ls -1t "$BACKUP_DIR"/ROOT_*.tar.gz 2>/dev/null | tail -n +$((KEEP_BACKUPS+1)) | xargs -r rm -f +fi + +# --------------------------------------------------------- 3. 컨테이너 정지 +if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then + log "Tomcat 정지" + docker stop "$CONTAINER" >/dev/null 2>&1 || true +fi + +# ------------------------------------------------------------- 4. 재전개 +log "ROOT 재전개" +rm -rf "$ROOT_DIR" "$WEBAPPS/ROOT.tmp" +mkdir -p "$WEBAPPS/ROOT.tmp" +unzip -q "$WAR_SRC" -d "$WEBAPPS/ROOT.tmp" +mv "$WEBAPPS/ROOT.tmp" "$ROOT_DIR" + +# ------------------------------------------------- 5. 환경설정 주입 (필수) +# globals-{dev,live}.properties 는 .gitignore 대상이라 저장소에도 WAR 에도 없다. +# context-datasource.xml 이 globals-dev.properties 를 읽으므로 이 주입이 없으면 기동 실패한다. +PROPS_DEST="$ROOT_DIR/WEB-INF/classes/egovframework/egovProps" +[ -d "$PROPS_DEST" ] || die "egovProps 경로 없음: $PROPS_DEST" +for f in globals-dev.properties globals-live.properties; do + [ -f "$CONFIG_DIR/$f" ] || die "서버 보관 환경설정 없음: $CONFIG_DIR/$f" + cp -f "$CONFIG_DIR/$f" "$PROPS_DEST/$f" + log "환경설정 주입: $f" +done +chmod 640 "$PROPS_DEST"/globals-*.properties + +# --------------------------------------------------------- 6. 컨테이너 기동 +log "Tomcat 기동" +docker start "$CONTAINER" >/dev/null || die "컨테이너 기동 실패 — 'docker logs $CONTAINER' 확인" + +# ------------------------------------------------------------- 7. 헬스체크 +log "헬스체크 (최대 ${HEALTH_TIMEOUT}초)" +CODE=000 +for _ in $(seq 1 $((HEALTH_TIMEOUT/5))); do + CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$HEALTH_URL" || echo 000) + case "$CODE" in 200|302) break ;; esac + sleep 5 +done + +case "$CODE" in + 200|302) + log "배포 완료 — HTTP $CODE" + ;; + *) + log "헬스체크 실패 (HTTP $CODE)" + if [ -n "$BACKUP_FILE" ] && [ -f "$BACKUP_FILE" ]; then + log "롤백: $BACKUP_FILE 복구" + docker stop "$CONTAINER" >/dev/null 2>&1 || true + rm -rf "$ROOT_DIR" + tar -xzf "$BACKUP_FILE" -C "$WEBAPPS" + docker start "$CONTAINER" >/dev/null 2>&1 || true + log "롤백 완료 — 기동 로그: docker logs --tail 100 $CONTAINER" + else + log "복구할 백업이 없다(최초 배포). 컨테이너 로그를 확인할 것." + fi + exit 1 + ;; +esac diff --git a/deploy/wise/arko-runner-config.yaml b/deploy/wise/arko-runner-config.yaml new file mode 100644 index 0000000..be86e52 --- /dev/null +++ b/deploy/wise/arko-runner-config.yaml @@ -0,0 +1,14 @@ +# arko 전용 act_runner 설정 — /opt/arko-runner/config.yaml +# 이 서버에는 러너가 셋(wise/urpai/arko) 상주한다. arko 빌드는 1~3분이 걸리고 +# WAR 이 192MB 라, 다른 서비스 배포를 막지 않도록 러너를 분리해 둔다. +log: + level: info +runner: + capacity: 1 # arko 배포는 ROOT 를 교체하므로 절대 병렬로 돌리지 않는다 + timeout: 30m + labels: + - arko-web:host # host 모드 — 러너가 웹서버에서 직접 명령을 실행한다 +cache: + enabled: true +container: + network: host diff --git a/deploy/wise/arko-runner.service b/deploy/wise/arko-runner.service new file mode 100644 index 0000000..169dd68 --- /dev/null +++ b/deploy/wise/arko-runner.service @@ -0,0 +1,20 @@ +# arko 전용 Gitea Actions 러너 — /etc/systemd/system/arko-runner.service +# 라벨 arko-web:host 로 .gitea/workflows/deploy-wise.yml 을 받는다. +[Unit] +Description=Gitea Actions runner (arko / csv.wise.ai.kr) +After=network-online.target gitea.service +Wants=network-online.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/arko-runner +# nginx/Apache 를 거치지 않고 Gitea 에 직결한다(127.0.0.1:9003). +ExecStart=/opt/arko-runner/act_runner daemon --config /opt/arko-runner/config.yaml +Environment=HOME=/root +Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/wise/arko-was-tunnel.service b/deploy/wise/arko-was-tunnel.service new file mode 100644 index 0000000..403a83c --- /dev/null +++ b/deploy/wise/arko-was-tunnel.service @@ -0,0 +1,40 @@ +# arko(문화품앗이) WAS 터널 — 운영 웹(175.118.124.176) → WAS(27.255.76.2) +# 설치 위치: /etc/systemd/system/arko-was-tunnel.service +# +# 왜 터널인가: +# WAS 의 Tomcat 은 컨테이너 포트를 127.0.0.1:11060 에만 바인딩한다. +# 같은 서버의 urpai 처럼 공인 고포트로 열 수도 있지만, arko 는 개인정보보호 +# 과제 대상 시스템이라 WAS 를 인터넷에 직접 노출하지 않는다. +# 웹서버가 아웃바운드 SSH 로 자기 쪽 127.0.0.1:11060 을 WAS 의 Tomcat 에 잇는다. +# +# 키 제한: WAS 의 authorized_keys 에서 +# restrict,port-forwarding,permitopen="127.0.0.1:11060" +# 으로 묶여 있어 셸 접근이 불가능하고 이 포트 외에는 열 수 없다. +# +# 502 가 나면 여기부터 확인한다: systemctl status arko-was-tunnel +[Unit] +Description=SSH tunnel to arko WAS (27.255.76.2:11060) +After=network-online.target +Wants=network-online.target +Before=httpd.service + +[Service] +Type=simple +ExecStart=/usr/bin/ssh -N -T \ + -o ExitOnForwardFailure=yes \ + -o ServerAliveInterval=30 \ + -o ServerAliveCountMax=3 \ + -o StrictHostKeyChecking=accept-new \ + -o UserKnownHostsFile=/root/.ssh/known_hosts \ + -o IdentitiesOnly=yes \ + -i /root/.ssh/id_arko_tunnel \ + -L 127.0.0.1:11060:127.0.0.1:11060 \ + claudeops@27.255.76.2 + +Restart=always +RestartSec=10 +User=root +Group=root + +[Install] +WantedBy=multi-user.target diff --git a/deploy/wise/csv.wise.ai.kr.conf b/deploy/wise/csv.wise.ai.kr.conf new file mode 100644 index 0000000..6acc07c --- /dev/null +++ b/deploy/wise/csv.wise.ai.kr.conf @@ -0,0 +1,58 @@ +# csv.wise.ai.kr — arko(문화품앗이) 웹 티어 +# 설치 위치: /etc/httpd/conf.d/csv.wise.ai.kr.conf (이 서버는 Apache httpd 를 쓴다) +# +# 경로: 브라우저 → httpd 443 → [SSH 터널] 127.0.0.1:11060 → WAS(27.255.76.2) Tomcat +# DB(MariaDB)는 WAS 안에서만 접근한다. 이 서버는 DB 에 붙지 않는다. +# +# 127.0.0.1:11060 은 arko-was-tunnel.service 가 만들어 준다. +# 502 가 나면 `systemctl status arko-was-tunnel` 부터 확인할 것. + + + ServerName csv.wise.ai.kr + + # 인증서 갱신(webroot)은 리다이렉트보다 먼저 처리되어야 한다 + Alias /.well-known/acme-challenge/ /var/www/html/.well-known/acme-challenge/ + + Require all granted + Options None + + + RewriteEngine On + RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/ + RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L] + + + + ServerName csv.wise.ai.kr + + SSLEngine on + SSLCertificateFile /etc/letsencrypt/live/csv.wise.ai.kr/fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live/csv.wise.ai.kr/privkey.pem + SSLProtocol -all +TLSv1.2 +TLSv1.3 + SSLCipherSuite HIGH:!aNULL:!MD5 + + # 자원봉사 활동사진·첨부파일 업로드가 있어 기본값으로는 부족하다 + LimitRequestBody 104857600 + + Header always set X-Frame-Options "SAMEORIGIN" + Header always set X-Content-Type-Options "nosniff" + Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" + # 운영(csv.culture.go.kr)이 따로 있는 검증 환경이므로 색인을 막는다 + Header always set X-Robots-Tag "noindex, nofollow" + + # Host 를 그대로 넘겨야 WAS 가 csv.wise.ai.kr 기준으로 링크를 만든다 + ProxyPreserveHost On + # arko 는 첫 요청에서 JSP 를 컴파일하고, 통계·엑셀 다운로드가 오래 걸린다 + ProxyTimeout 300 + + # 접속기록(개인정보보호 SFR-004)이 실제 접속자 IP 를 남겨야 한다. + # 프록시를 한 단 거치므로 원본 IP 를 헤더로 전달한다. + RequestHeader set X-Forwarded-Proto "https" + + ProxyPass /.well-known/acme-challenge/ ! + ProxyPass / http://127.0.0.1:11060/ + ProxyPassReverse / http://127.0.0.1:11060/ + + ErrorLog /var/log/httpd/csv_error.log + CustomLog /var/log/httpd/csv_access.log combined + diff --git a/deploy/wise/mariadb.cnf b/deploy/wise/mariadb.cnf new file mode 100644 index 0000000..9c592bf --- /dev/null +++ b/deploy/wise/mariadb.cnf @@ -0,0 +1,33 @@ +# arko(문화품앗이) MariaDB — /opt/arko-app/conf/mariadb.cnf +# +# 이 서버(27.255.76.2)는 llama-server가 200G+를 쓰는 GPU 추론 서버다. +# 가용 메모리가 3G 안팎이라 버퍼풀을 384M로 묶고 컨테이너에도 768m 상한을 건다. +# 늘리기 전에 반드시 `free -g` 로 여유를 확인할 것. +[mysqld] +# 원본은 Windows MySQL 8 (lower_case_table_names=1). Linux 기본값 0으로 두면 +# iBatis SQL의 테이블명 대소문자가 어긋나 "Table doesn't exist"가 난다. +# 이 값은 초기화(initdb) 시점에만 반영되므로 db-data를 비우고 다시 만들어야 바뀐다. +lower_case_table_names = 1 + +character-set-server = utf8mb4 +collation-server = utf8mb4_general_ci + +innodb_buffer_pool_size = 384M +innodb_log_file_size = 128M +innodb_flush_log_at_trx_commit = 2 +max_connections = 150 +max_allowed_packet = 64M +table_open_cache = 400 +tmp_table_size = 32M +max_heap_table_size = 32M + +# 원본 MySQL 8은 ONLY_FULL_GROUP_BY가 켜진 상태로 운영된다. 동작을 맞춰 둔다. +sql_mode = ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION + +default_time_zone = '+09:00' +slow_query_log = 1 +slow_query_log_file = /var/log/mysql/slow.log +long_query_time = 3 + +[client] +default-character-set = utf8mb4