# GUARDiA 운영·CI/CD 지식 문서

> **출처:** `workspace/guardia-docs/` 운영·배포 가이드(19·20·21·43·48번 등) + 루트 `CLAUDE.md` 하네스 변경이력 + `docs/가디아_운영_노하우_전수.md`
> **작성:** 2026-07-12 | **대상:** GUARDiA 전 솔루션 운영·배포 담당 에이전트/개발자
> **보안:** 이 문서에는 비밀번호·API 키·webhook secret·SSH 자격증명을 기재하지 않는다. 서버는 "GUARDiA 인프라 서버"(zioinfo.co.kr) 로 표기한다.

---

## 1. 배포 파이프라인 전체 흐름

### 1.1 표준 흐름 (한 줄 요약)

```
workspace 소스 수정 → 모노레포 git commit
    ↓ .git/hooks/post-commit 자동 실행
git archive (추적 파일만 추출) → repos/{시스템}/ 동기화
    ↓
repos/{시스템} → Gitea(git.zioinfo.co.kr) push
    ↓ Gitea webhook (POST http://127.0.0.1:9999)
deploy_server.py (GUARDiA 인프라 서버, 포트 9999)
    ↓ 레포별 배포 블록 실행
서버 /opt/{시스템}/src git pull → 빌드(npm/mvn/pip) → 산출물 복사
    ↓
systemctl restart {서비스} → health 게이트 확인
```

### 1.2 단계별 상세

| 단계 | 위치 | 내용 |
|------|------|------|
| ① 소스 수정 | `C:\GUARDiA\workspace\<시스템>\` | 모든 개발은 workspace에서만. repos/ 직접 수정 금지 |
| ② 모노레포 커밋 | `C:\GUARDiA` (.git) | commit 시 post-commit 훅이 자동 발동 |
| ③ repos 동기화 | `C:\GUARDiA\repos\<시스템>\` | `git archive HEAD workspace/{시스템}/ \| tar -x` — **추적 파일만** 추출되므로 node_modules 유입 원천 차단 |
| ④ Gitea push | `git.zioinfo.co.kr/zio/<repo>` | repos/는 각각 독립 git repo (모노레포 .gitignore 처리) |
| ⑤ webhook 수신 | 서버 `/opt/zioinfo/deploy_server.py` :9999 | systemd 서비스 `zioinfo-deploy`. 로그: `/var/log/zioinfo/deploy.log` |
| ⑥ 배포 실행 | 서버 `/opt/<시스템>/` | 레포별 블록: git pull → 빌드 → 산출물 복사 → 서비스 재시작 |
| ⑦ health 게이트 | 각 서비스 health 엔드포인트 | 200/UP 확인 후 배포 완료 판정. 실패 시 롤백 검토 |

### 1.3 시스템별 배포 블록 (deploy_server.py)

- **zioinfo-web(홈페이지):** git pull → `frontend npm run build`(Vite outDir = `backend/src/main/resources/static/`) → 정적 파일 `/var/www/zioinfo/` 복사 → `mvn package` → app.jar 교체 → `systemctl restart zioinfo`
- **guardia-itsm(FastAPI):** git pull → `rsync -a --delete`(\_\_pycache\_\_·.git 제외) `/opt/guardia/src/ → /opt/guardia/app/` → venv pip install → `systemctl restart guardia`
- **guardia-manager:** git pull → npm build → dist를 `/var/www/manager/` 복사 → 백엔드 rsync → 재시작 (★한때 프론트만 배포되는 결함이 있어 백엔드 rsync 단계가 추가됨 — 2026-07-04)
- **Spring Boot 단일 jar 솔루션(ERP·CRM·OCR·BI·PMS·RPA·Groupware·Portal·Mall·CMS·MES·ESN 등):** frontend → backend static 번들 → 단일 jar 빌드 → jar 교체 → systemd 재시작
- **guardia-rag(Python):** git pull → pip → restart → health (2026-07-04 파이프라인 완성)

### 1.4 수동 배포 (자동 배포 불가 시)

```bash
# 개발 PC에서 — 특정 시스템/전체
python scripts/deploy/manual_deploy.py guardia-itsm   # 별칭 itsm/web 가능
python scripts/deploy/manual_deploy.py                # 전체

# 서버에서 직접 (가장 확실) — 예: zioinfo-web
SRC=/opt/zioinfo/src
git -C $SRC fetch origin main && git -C $SRC reset --hard origin/main
cd $SRC/frontend && npm run build --silent
cp -rf $SRC/backend/src/main/resources/static/. /var/www/zioinfo/
cd $SRC/backend && mvn clean package -DskipTests -q
cp $SRC/backend/target/zioinfo-web-*.jar /opt/zioinfo/app/app.jar
systemctl restart zioinfo && sleep 5 && systemctl is-active zioinfo
```

- ITSM 웹 UI에서도 배포 트리거 가능: `POST /api/cicd/deploy`(JWT 인증), `GET /api/cicd/status`
- Windows Task Scheduler: `GUARDiA-AutoDeploy-Hourly`(1시간마다 manual_deploy 전체), `GUARDiA-DailyParent`(매일 09:00 건강검진·성장일지)

---

## 2. 배포 함정·체크리스트 (실사고 기반)

### 2.1 파이프라인 인프라 함정

| # | 함정 | 증상 | 해법 |
|---|------|------|------|
| 1 | **deploy_server.py 서버 사본 미반영** | 로컬에서 deploy_server.py를 고쳐도 서버는 구버전 실행 | 수정 시 반드시 서버 `/opt/zioinfo/deploy_server.py`에 반영 + `systemctl restart zioinfo-deploy`. 백업본(.bak) 생성 후 교체 |
| 2 | **배포 블록 부재 = 1ms no-op** | 배포 로그에 '완료'가 찍혀도 처리 시간이 1ms 수준이면 해당 repo 블록이 deploy_server.py에 없는 것 | 로그의 소요 시간 확인. 신규 솔루션 추가 시 deploy_server.py에 배포 블록 추가 필수 (guardia-rag 사례: 웹훅 수신은 됐지만 블록 부재로 무동작) |
| 3 | **Gitea webhook 자체 부재** | push해도 :9999에 요청이 안 옴 | 신규 repo는 Gitea에 webhook 등록 필수 (URL=localhost:9999, push 이벤트, main 브랜치, secret 일치). hook test API로 검증 |
| 4 | **webhook 로컬 차단** | webhook 발송 실패 | `/etc/gitea/app.ini` `[webhook] ALLOW_LOCAL_NETWORKS = true` 필수. loopback을 ALLOWED_HOST_LIST에 포함 |
| 5 | **webhook URL이 외부 도메인** | NAT 헤어핀으로 전달 실패/지연 | webhook URL은 `http://localhost:9999`(서버 내부 루프백)로 설정 |
| 6 | **webhook secret 불일치 (403)** | deploy_server가 403 반환 | Gitea webhook secret과 deploy_server 설정값 일치 확인 (값은 문서에 기재 금지) |
| 7 | **서버 /opt 소스가 git 체크아웃이 아님** | webhook이 와도 git pull이 no-op | `/opt/<시스템>/src`는 반드시 Gitea repo의 git 체크아웃이어야 함 (guardia-rag 사례: 미git화 → git 체크아웃 전환) |
| 8 | **remote에 구버전 자격증명** | git pull 인증 실패로 자동배포 중단 | 서버 /opt/*/src remote URL의 자격증명 일관성 점검 (2026-06-12 itsm/web/manager/docs 복구 사례) |

### 2.2 빌드·소스 함정

| # | 함정 | 해법 |
|---|------|------|
| 9 | **수동 배포 후 자동 배포 미작동** — 서버 커밋이 Gitea와 어긋나 이후 webhook이 '변경 없음' 처리 | `git fetch origin main && git reset --hard origin/main`으로 서버 소스를 origin에 강제 정렬 |
| 10 | **node_modules 실수 커밋** | .gitignore에 node_modules/·dist/·build/ 추가. post-commit이 git archive를 쓰므로 추적 파일만 넘어가는 구조 유지 |
| 11 | **nginx/브라우저 정적 캐시** — 배포해도 화면 미반영 | 정적 파일 강제 복사 + `systemctl reload nginx`; JS/CSS는 Vite 해시로 자동 무효화; 최종적으로 Ctrl+Shift+R |
| 12 | **Vite outDir 착각** — dist/를 찾다 파일 없음 | zioinfo-web은 outDir이 `backend/src/main/resources/static/` — 그 경로에서 복사 |
| 13 | **로컬 rollup win32 크래시** | 로컬 빌드 실패해도 서버 빌드(npm+mvn)를 신뢰하는 경로로 진행 (esbuild 대체 검증 병행) |
| 14 | **공유 트리 교차 커밋** | 여러 트랙이 같은 트리를 만질 때는 **파일 단위 커밋**으로 교차 오염 방지 (kintex 표준) |

### 2.3 DB·스키마 함정

| # | 함정 | 해법 |
|---|------|------|
| 15 | **Flyway/DDL 마이그레이션 배포 사고** | 배포 전 라이브 DB에 **dry-run(BEGIN…ROLLBACK)** 필수 (kintex 표준화 교훈) |
| 16 | **`sql.init.mode: never` + schema.sql 후행 확장 = 누락 테이블** | 초기 배포 후 schema.sql에 테이블을 추가해도 재적용 안 됨 → 런타임 `relation does not exist` 500. 표준 수복: 시드 멱등화(유니크 인덱스) + `mode=always` + `continue-on-error` + DataAccessException 핸들러(스택트레이스 누출 차단) |
| 17 | **MyBatis 매퍼 빈 누락 크래시** | `@MapperScan(annotationClass = Mapper.class)` 사용 (OCR TemplateMapper 사례) |
| 18 | **시크릿 미주입 기동 실패** | 배포 전 시크릿 **fail-fast 프로파일** 확인 — env(systemd drop-in EnvironmentFile) 주입 여부 점검 |

### 2.4 배포 전 체크리스트 (요약)

```
□ workspace에서만 수정했고 repos/ 직접 수정 없음
□ 파일 단위 커밋 (공유 트리 교차 방지)
□ DDL 변경 시 라이브 Flyway dry-run(BEGIN…ROLLBACK) 통과
□ deploy_server.py 변경 시 서버 사본 반영 + zioinfo-deploy 재시작
□ 신규 repo면: Gitea webhook 등록 + deploy_server 배포 블록 + /opt git 체크아웃 3종 세트
□ push 후 /var/log/zioinfo/deploy.log 에서 소요 시간 확인 (1ms면 블록 부재 의심)
□ 서비스 health 엔드포인트 200/UP 확인 (health 게이트)
□ 배포 후 회귀: run_full_test.py 통과
```

---

## 3. 서비스 운영 맵

### 3.1 접속 체계

- 공개 접근은 **서브도메인 + nginx 리버스 프록시**: `zioinfo.co.kr`(홈페이지) / `itsm.` / `manager.` / `mail.` / `git.`(Gitea) / `jenkins.` / `docs.` / `kintex.`(킨텍스) 등
- nginx 설정: `/etc/nginx/sites-available/{subdomain}.zioinfo.co.kr`
- SSL: Let's Encrypt(certbot 자동 갱신) 기본, 일부 ZeroSSL(acme.sh). 신규 서브도메인은 `certbot --nginx -d {sub}.zioinfo.co.kr` 발급

### 3.2 포트 맵 (GUARDiA 인프라 서버 내부)

| 포트 | 서비스 | 비고 |
|------|--------|------|
| 9001 | GUARDiA ITSM (FastAPI) | 허브. 전 솔루션 연동 기준점 |
| 8002/8090 | GUARDiA Manager | ITSM JWT 재사용 |
| 8082 | zioinfo-web (홈페이지 Spring Boot) | 정적은 /var/www/zioinfo + nginx |
| 8003~8013 | ERP·CRM·OCR·BI·PMS·RPA·Groupware·Portal·Mall·CMS·MES | Spring Boot 단일 jar, DB `{sol}_db` |
| 8015/8016 | zioinfo-ESN / GUARDiA ESN | ESL 플랫폼 |
| 8018/8019 | MRO / Signage(e-SignBoard) | |
| 8021 | KINTEX | kintex.zioinfo.co.kr |
| 8025/8026 | 웹메일 (SMTP UI/백엔드) | Postfix/Dovecot 연동 |
| 8080 | Jenkins | 보조 CI (주력은 deploy_server) |
| 9003 | Gitea 내부 | 외부는 git.zioinfo.co.kr(443) 경유 |
| 9999 | deploy_server.py (webhook) | systemd `zioinfo-deploy` |
| 11434 | Ollama | 온프레미스 AI. RAM 제약으로 소형 모델(qwen3:1.7b·llama3.2:1b·moondream) 운용 |
| — | guardia-rag (중앙 RAG) | 전 솔루션 AI 질의 경유점 (LangChain+ChromaDB) |

### 3.3 systemd 서비스 구성 개요

- 명명: `guardia`(ITSM)·`zioinfo`(홈페이지)·`guardia-manager`·`zioinfo-mail`·`zioinfo-deploy`(webhook)·`gitea`·`jenkins`·`postgresql`·`ollama` + 솔루션별 `guardia-{sol}.service` (예: guardia-ocr) — 총 17개+ active
- 경로 규약: 소스 `/opt/{sol}/src/`, 실행 `/opt/{sol}/app/`(또는 jar), 정적 `/var/www/{sol}/`
- **AI env 주입:** 전 서비스에 systemd **drop-in**(`ai-env.conf`, EnvironmentFile 추가) 방식 — 기존 ExecStart 불변. 시크릿은 `/opt/guardia/secrets/`(root 600) + env 파일에서만 로드, 코드·커밋·로그 기재 금지
- 로그: `journalctl -u {서비스} -n 100 --no-pager` / 배포 로그 `tail -50 /var/log/zioinfo/deploy.log`

```bash
# 전체 상태 빠른 점검
for svc in nginx zioinfo guardia guardia-manager zioinfo-mail gitea jenkins postgresql ollama; do
  printf "%-22s %s\n" $svc "$(systemctl is-active $svc 2>/dev/null)"
done
```

### 3.4 health 엔드포인트 패턴

| 계열 | 패턴 | 판정 |
|------|------|------|
| FastAPI (ITSM·mail 등) | `GET /health` 또는 `GET /api/health` | JSON `status` 필드 |
| Spring Boot 솔루션 | `GET /api/health` (또는 actuator health) | `UP`/200 |
| 배포 게이트 | 재시작 후 sleep 4~5초 → health 200 확인 | 실패 시 배포 실패 처리 |

- **health 게이트 원칙:** 직렬 배포(솔루션 다수 동시 배포 시)에서 각 단계마다 health 통과 후 다음 진행. RAM이 빠듯한 서버 특성상 동시 재시작 금지·직렬 빌드가 표준.

### 3.5 DB 운영 요점

- PostgreSQL: 솔루션별 DB 격리(`erp_db`·`crm_db`·`ocr_db`·… , 각 `{sol}_user`), Hikari max 3 (RAM 제약)
- 홈페이지는 SQLite(`/opt/zioinfo/app/data/zioinfo.db`)
- 백업: `pg_dump`(PostgreSQL) / 파일 복사(SQLite) — 일일 백업 루틴 대상

---

## 4. 테스트 체계

### 4.1 상시 전체 테스트 — run_full_test.py

- 실행: `python3 C:\GUARDiA\scripts\check\run_full_test.py` (paramiko로 서버 내부에서 curl 실행)
- 규모: 22개 그룹 — **현재 126개 테스트**(초기 69 → 93 → 126으로 성장). service_health 그룹이 각 서비스 health를 우선 검사
- 인증: admin 비밀번호를 **서버 암호화 저장소(root 전용 복호 헬퍼)에서 서버 내부 셸 변수로만 조달** — 값을 로컬로 가져오지 않고 출력·저장하지 않음
- 결과 저장: `.claude/agents/_workspace/test_results/latest.json`
- 트리거: "테스트 해줘"·"검증해줘"·"배포 확인"·"회귀 테스트" → `test-orchestrator` 스킬 (에이전트 api-tester)

### 4.2 배포와의 결합

- **배포 후 필수 회귀:** 배포 작업(특히 다중 솔루션·deploy_server 변경·스키마 변경) 뒤에는 반드시 126/126 통과를 확인하고 종료 (2026-07-04 WISE 전 솔루션 배포·홈페이지 리뉴얼 모두 이 게이트로 마감)
- 스모크: 각 솔루션에 개별 스모크 스크립트가 있으면 우선 실행 후 전체 테스트 (guardia-rag 스모크 3종 사례)
- E2E 검증 표준: push → webhook 수신 → git pull → 빌드 → restart → health → HEAD 커밋 자동 갱신 확인까지가 "자동배포 검증 완료"의 정의

---

## 5. 운영 점검 루틴 — guardia-ops-check

정기/요청 시 수행하는 통합 헬스체크. 트리거: "운영 점검"·"서버 기동 확인"·"전체 상태 확인"·"테이블 누락 확인" → `guardia-ops-check-orchestrator` 스킬.

### 5.1 3단 점검 구성

| 단계 | 에이전트 | 내용 |
|------|----------|------|
| ① Git 정합 | git-ops-dev | Gitea 자격증명 설정 상태·repo clone/원격 정합·workspace↔repos↔Gitea↔서버 4-way 동기화 (system-sync-orchestrator와 연계: deploy-verifier/deploy-fixer) |
| ② 서비스 기동 | server-health-checker | 전 solution systemd active + health 엔드포인트 응답 전수 확인 |
| ③ 스키마 무결성 | schema-audit-dev (schema-integrity 재사용) | 매퍼가 참조하는 테이블/컬럼 vs 라이브 DB 전수 대조 → `relation does not exist` 예방. 수복은 schema-fix-dev(멱등 ALTER/CREATE + mode=always + 누출 차단 패턴) |

### 5.2 일상 운영 루틴 요약

```bash
# 1) 서비스 전수 상태
systemctl is-active {서비스들}          # 3.3 스니펫

# 2) 배포 최신성
tail -20 /var/log/zioinfo/deploy.log    # 최근 배포 + 소요시간(1ms 의심)
git -C /opt/{sol}/src log --oneline -3  # 서버 HEAD가 Gitea와 일치하는지

# 3) webhook 경로 생존
ss -tlnp | grep 9999                    # deploy_server 리스닝
curl -s -X POST http://127.0.0.1:9999 -H 'Content-Type: application/json' \
  -d '{"repository":{"name":"zioinfo-web"},"ref":"refs/heads/main"}'   # "Deploy queued" 기대

# 4) 리소스 (RAM 제약 서버)
free -h                                 # Ollama 모델 로드 여부에 민감 — avail 감시

# 5) 전체 회귀
python3 scripts/check/run_full_test.py  # 126/126
```

### 5.3 장애 대응 우선순위

1. health 실패 서비스 → `journalctl -u {svc} -n 100`으로 원인 (기동 실패 최다 원인: 시크릿 env 미주입·스키마 누락 테이블·RAM 부족 OOM)
2. 자동배포 불통 → §2.1 함정 표 순서대로 (블록 부재 → webhook 부재 → ALLOW_LOCAL_NETWORKS → secret → /opt git화 → remote 자격증명)
3. AI 기능 불능 → Ollama RAM 제약 우선 의심 (7B+ 모델은 서버에서 로드 불가 — 소형 모델 폴백 체인: Claude→Qwen3→소형 Ollama). `ollama-health-orchestrator` 하네스 참조
4. 화면 미반영 → 캐시/정적 복사(§2.2 #11) → 그래도 안 되면 서버 HEAD 확인(§2.2 #9)

### 5.4 부속 정기 루틴

- **부모 역할 하네스(guardia-parent):** 매일 건강검진(테스트·자가 수복)·성장일지 기록 — Task Scheduler `GUARDiA-DailyParent` 09:00
- **SSL:** certbot 자동 갱신 + 만료 임박 도메인 월 1회 `certbot certificates` 확인
- **백업:** DB pg_dump/파일 백업 일일, 배포 전 산출물(.bak) 백업 습관화

---

## 부록 A. 신규 솔루션 배포 온보딩 절차 (표준)

1. `workspace/<sol>/` 소스 완성 → repos/<sol> fresh init → Gitea `zio/<sol>` repo 생성 + push
2. 서버: `/opt/<sol>/src` git clone(체크아웃), DB `{sol}_db`/`{sol}_user` 생성, systemd `guardia-<sol>.service` 등록(setup_<sol>_service.py 패턴)
3. AI env drop-in(`ai-env.conf`) 적용 (시크릿은 env 파일에서만)
4. Gitea webhook 등록 (localhost:9999·push·main·secret) + hook test
5. `deploy_server.py`에 <sol> 배포 블록 추가 → **서버 사본 반영 + zioinfo-deploy 재시작**
6. push→자동배포 E2E 검증 (HEAD 갱신·health 200) → run_full_test.py 회귀
7. `guardia-docs` 운영가이드 + 루트 CLAUDE.md 하네스 변경이력 갱신

## 부록 B. 보안 불변 규칙 (운영 문서 공통)

- 자격증명(비밀번호·API 키·secret·SSH 계정)은 문서·코드·커밋·로그·API 응답에 절대 기재/노출 금지 — 서버 env 및 암호화 저장소(AES-256-GCM)에서만 조달
- 에러 응답에 스택트레이스 미노출 (DataAccessException 핸들러 등 누출 차단)
- 외부 API 금지 원칙 유지 (예외: Anthropic Claude API 단일 경로 — 소유자 승인, 키는 서버 env에서만 로드, 실패 시 Ollama 폴백)
- Gitea 전용 운영 — GitHub push 금지
