csv.wise.ai.kr 배포 파이프라인 신설 (PC → git → web → was)
Some checks failed
deploy-wise / deploy (push) Failing after 11s

웹서버(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 <noreply@anthropic.com>
This commit is contained in:
infraurp 2026-08-26 11:16:10 +09:00
parent 7a8ec86642
commit ddacd1b7f5
9 changed files with 600 additions and 0 deletions

View File

@ -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

View File

@ -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 를 실행할 것."

123
deploy/wise/README.md Normal file
View File

@ -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` 로 변환해 이관했다. 한글 정렬 결과는 같다(코드포인트 순).

115
deploy/wise/arko-deploy.sh Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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` 부터 확인할 것.
<VirtualHost *:80>
ServerName csv.wise.ai.kr
# 인증서 갱신(webroot)은 리다이렉트보다 먼저 처리되어야 한다
Alias /.well-known/acme-challenge/ /var/www/html/.well-known/acme-challenge/
<Directory "/var/www/html/.well-known/acme-challenge">
Require all granted
Options None
</Directory>
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>
<VirtualHost *:443>
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
</VirtualHost>

33
deploy/wise/mariadb.cnf Normal file
View File

@ -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