From 63386b62aa3ac30abbc095d10a3508b4ca8a4796 Mon Sep 17 00:00:00 2001 From: zio Date: Sat, 11 Jul 2026 19:44:30 +0900 Subject: [PATCH] =?UTF-8?q?fix(devops):=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EB=AF=B8=EB=B0=B0=ED=8F=AC=20=EA=B7=BC=EB=B3=B8=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=E2=80=94=20deploy=5Fkintex.sh=20=EB=8B=A8=EC=9D=BC?= =?UTF-8?q?=20=EB=B9=8C=EB=93=9C=20=EC=A7=84=EC=8B=A4=EC=9B=90=20+=20SIGPI?= =?UTF-8?q?PE=20=EA=B2=80=EC=A6=9D=20=EB=B2=84=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상: push 후 프론트만 배포, 백엔드 jar 스테일(V6 고정, V7/V8 미적용). 근본원인 3종: 1) deploy_kintex.sh가 백엔드 빌드 안 함(복사만) → 이제 프론트+백엔드 직접 빌드(단일 진실원) 2) gradlew mode 100644(비실행) → git +x(100755) + sh ./gradlew(실행권한 불요) 3) jar 검증 'unzip -l | grep -q'가 set -o pipefail 하에서 grep 조기종료→unzip SIGPIPE(141) →메인클래스 발견에도 오탐 실패→die→jar 미복사. → grep -q 제거(전량 소비) + 선택 tail(파이프 조기종료 회피) - deploy_server kintex 블록: npm/gradle/verify 개별단계 제거 → git pull → deploy_kintex.sh(빌드+배포) → health - Jenkinsfile: 동일 스크립트 위임(경로 함정 회피, WORKSPACE 일치) - 서버 /opt/zioinfo/deploy_server.py 반영·재시작 완료(백업) Co-Authored-By: Claude Opus 4.8 (1M context) --- Jenkinsfile | 36 ++--------- deploy/deploy_kintex.sh | 95 ++++++++++++++++------------ deploy/deploy_server_kintex_block.py | 19 ++---- src/backend/gradlew | 0 4 files changed, 66 insertions(+), 84 deletions(-) mode change 100644 => 100755 deploy/deploy_kintex.sh mode change 100644 => 100755 src/backend/gradlew diff --git a/Jenkinsfile b/Jenkinsfile index 6afcb7d..187443f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -18,37 +18,13 @@ pipeline { stage('Checkout') { steps { checkout scm } } - stage('Frontend Build') { + stage('Build & Deploy') { 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"' + // 빌드+배포 단일 진실원 — deploy_kintex.sh 가 프론트(npm)·백엔드(sh ./gradlew bootJar) 빌드→검증→배포. + // webhook 경로(deploy_server.py)와 동일 스크립트 사용(경로 함정·SIGPIPE 검증 버그 회피). + // 권한 필요한 배포는 sudo NOPASSWD 권장. 체크아웃 사본을 실행(설치본 스테일 회피). + sh 'chmod +x "$WORKSPACE/deploy/deploy_kintex.sh"' + sh 'sudo bash "$WORKSPACE/deploy/deploy_kintex.sh" "$WORKSPACE"' } } stage('Health Check') { diff --git a/deploy/deploy_kintex.sh b/deploy/deploy_kintex.sh old mode 100644 new mode 100755 index 522d1a9..cfc78b5 --- a/deploy/deploy_kintex.sh +++ b/deploy/deploy_kintex.sh @@ -1,67 +1,82 @@ #!/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 패턴). +# 인자: $1 = 소스 체크아웃 루트(기본 /opt/kintex/src). git pull 이후의 실제 경로와 반드시 일치. +# 동작(단일 진실원): 프론트 빌드 + 백엔드 빌드 → 검증 → 원자적 교체 → 서비스 재기동 → 헬스체크 → 실패 시 롤백. +# +# 재발방지(2026-07-11): +# ① 백엔드 미빌드 버그 — 과거엔 build/libs jar를 "복사만" 했다. 이제 이 스크립트가 직접 bootJar 빌드(단일 진실원). +# ② gradlew 실행권한 — 리포가 mode 100644로 커밋될 수 있어 `sh ./gradlew`로 실행(+x 불요) + chmod 병행. +# ③ jar 검증 SIGPIPE — `unzip -l | grep -q` 는 pipefail 하에서 grep 조기 종료→unzip SIGPIPE(141)→오탐 실패. +# → grep 을 -q 없이 전량 소비(>/dev/null)해 SIGPIPE 회피. jar 선택도 head 대신 tail(파이프 조기종료 방지). set -euo pipefail SRC="${1:-/opt/kintex/src}" WEB_ROOT=/var/www/kintex APP_JAR=/opt/kintex/app/app.jar BACKEND_PORT=8021 +MAIN_CLASS_PATH='BOOT-INF/classes/com/zioinfo/kintex/KintexApplication.class' log(){ echo "[deploy-kintex] $*"; } die(){ echo "[deploy-kintex] ERROR: $*" >&2; exit 1; } -# ── 1. 프론트: 빌드 완전성 검증(라이브 건드리기 전) → 스테이징 → 원자적 rename ── +[ -d "$SRC/src/backend" ] || die "소스 경로 불일치: $SRC/src/backend 없음(인자 \$1=$SRC 확인)" + +# ── 1. 프론트 빌드(Vite) ────────────────────────────────────────────────────── +log "프론트 빌드 시작" +( cd "$SRC/src/frontend" && (npm ci 2>/dev/null || npm install) && \ + NODE_OPTIONS=--max-old-space-size=4096 npm run build ) || die "프론트 빌드 실패" + +# ── 2. 백엔드 빌드(bootJar) — `sh ./gradlew`(+x 불요), clean 으로 스테일 jar 제거 ── +log "백엔드 빌드 시작(bootJar)" +sed -i 's/\r$//' "$SRC/src/backend/gradlew" 2>/dev/null || true +chmod +x "$SRC/src/backend/gradlew" 2>/dev/null || true +( cd "$SRC/src/backend" && sh ./gradlew clean bootJar -x test --no-daemon ) \ + || die "백엔드 bootJar 빌드 실패" + +# ── 3. 프론트: 완전성 검증 → 스테이징 → 원자적 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)" +[ -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 없음 — 프론트 배포 건너뜀(백엔드만)" +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" -# ── 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 없음 — 백엔드 교체 건너뜀" +# ── 4. 백엔드 jar: 선택(tail — SIGPIPE 회피) → 검증(grep 전량소비 — SIGPIPE 회피) → 교체 ── +JAR=$(ls -1 "$SRC"/src/backend/build/libs/kintex-backend-*.jar 2>/dev/null | grep -v plain | tail -1 || true) +[ -n "$JAR" ] || die "bootJar 없음($SRC/src/backend/build/libs) — 빌드 산출물 확인" +# grep 을 -q 없이 실행해 unzip 출력을 끝까지 소비(pipefail+SIGPIPE 오탐 방지). 발견 시 rc=0. +if ! unzip -l "$JAR" | grep -F "$MAIN_CLASS_PATH" >/dev/null; then + die "유효하지 않은 jar(메인 클래스 없음: $JAR) — 배포 중단" fi +[ -f "$APP_JAR" ] && cp "$APP_JAR" "${APP_JAR}.bak" || true +cp "$JAR" "$APP_JAR" +log "백엔드 jar 교체 → $APP_JAR ($(basename "$JAR"), $(stat -c%s "$JAR") bytes)" -# ── 3. 워커 의존성 갱신(venv) — requirements 변경 대비(멱등) ── +# ── 5. 워커 의존성 갱신(멱등) ── 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. 서비스 재기동 ── +# ── 6. 서비스 재기동 ── systemctl restart kintex.service systemctl restart kintex-nanobanana.service 2>/dev/null || log "WARN: 워커 재기동 스킵" -# ── 5. 헬스체크(백엔드 /health 200 = success:true) — 실패 시 jar 롤백 ── +# ── 7. 헬스체크(/health 200) — 실패 시 jar 롤백 ── ok=0 for i in $(seq 1 20); do sleep 3 @@ -74,4 +89,4 @@ if [ "$ok" != "1" ]; then systemctl is-active kintex.service || true die "배포 헬스체크 실패(롤백 수행)" fi -log "배포 완료 — 백엔드 8021 UP, 워커 재기동" +log "배포 완료 — 백엔드 8021 UP(jar 갱신), 프론트 배포, 워커 재기동" diff --git a/deploy/deploy_server_kintex_block.py b/deploy/deploy_server_kintex_block.py index 90cb005..8088379 100644 --- a/deploy/deploy_server_kintex_block.py +++ b/deploy/deploy_server_kintex_block.py @@ -2,12 +2,15 @@ # (deploy_server.py 는 루트 인프라 파일이므로 kintex 리포엔 이 참조 사본만 둔다. 서버 사본이 실행 권위.) # 삽입 위치: 마지막 `elif repo == "uiws":` 블록 다음, `logging.info(f"=== {repo} 배포 완료 ===")` 앞. # Gitea owner=zio, 기본 브랜치=main, 백엔드 포트 8021. +# ★빌드는 deploy_kintex.sh 단일 진실원(프론트 npm + 백엔드 sh ./gradlew bootJar). 블록은 git pull → deploy_kintex.sh → health. ''' 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 가 검증→원자교체→헬스체크→롤백. + # Flyway baseline-on-migrate=true → 기동 시 스키마 자동 적용. + # ★빌드는 deploy_kintex.sh 가 단일 진실원으로 수행(프론트 npm + 백엔드 sh ./gradlew bootJar) + # → 검증(SIGPIPE-safe)→원자교체→jar 교체→재기동→헬스체크→실패 시 롤백. (2026-07-11 백엔드 미배포 버그 근본수정) SRC = "/opt/kintex/src" ok = run_steps(repo, [ ("git pull", ["bash", "-c", @@ -17,19 +20,7 @@ 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", + ("build+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", diff --git a/src/backend/gradlew b/src/backend/gradlew old mode 100644 new mode 100755