#!/usr/bin/env bash ############################################################################### # [폐쇄망 안에서 실행] 뜬 스택이 실제로 동작하는지 확인한다. # # ./bin/verify.sh # 엔진 자동 감지 # ./bin/verify.sh podman # ENGINE="sudo podman" ./bin/verify.sh # # compose 로 띄웠든 docker/podman run 으로 띄웠든 같은 스크립트를 쓴다. 컨테이너는 # 이름 규칙(-1 / _1 / 접미사 없음)과 compose 라벨을 함께 뒤져 찾는다. # 실제 점검 로직은 bin/checks/*.py 에 있고, web 컨테이너 stdin 으로 넣어 실행한다. # # 1. 컨테이너 상태 5. MinIO 버킷 읽기·쓰기 # 2. Django check 6. nginx → 정적 파일 · SPA # 3. PostgreSQL + pgvector 7. alpha-processing 연동 (PDF 추출 왕복) # 4. Redis + RQ 워커 ############################################################################### . "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh" resolve_engine "${1:-}" [ -f "${ENV_FILE}" ] && load_env check_replica_counts PROJECT="${COMPOSE_PROJECT_NAME:-pi-continuum}" CHECKS="${DEPLOY_DIR}/bin/checks" PASS=0; FAIL=0 ok() { printf ' PASS %s\n' "$*"; PASS=$((PASS+1)); } bad() { printf ' FAIL %s\n' "$*"; FAIL=$((FAIL+1)); } info() { printf ' %s\n' "$*"; } lines() { printf '%s\n' "$1" | grep -E "$2" | head -"${3:-8}" | while IFS= read -r l; do info "${l}"; done || true; } field() { printf '%s\n' "$1" | sed -n "s/^$2=//p" | head -1; } find_container() { # 서비스 이름·복제 번호 → 컨테이너 이름. local svc="$1" replica="${2:-1}" c if [ "${replica}" -gt 1 ]; then for c in "${PROJECT}-${svc}-${replica}" "${PROJECT}_${svc}_${replica}"; do engine container inspect "${c}" >/dev/null 2>&1 && { printf '%s\n' "${c}"; return 0; } done return 1 fi for c in "${PROJECT}-${svc}-1" "${PROJECT}_${svc}_1" "${PROJECT}-${svc}"; do engine container inspect "${c}" >/dev/null 2>&1 && { printf '%s\n' "${c}"; return 0; } done c="$(engine ps -aq --filter "label=com.docker.compose.service=${svc}" \ --filter "label=com.docker.compose.project=${PROJECT}" 2>/dev/null | head -1)" [ -z "${c}" ] && c="$(engine ps -aq --filter "label=io.podman.compose.service=${svc}" 2>/dev/null | head -1)" [ -n "${c}" ] || return 1 printf '%s\n' "${c}" } # 앱 컨테이너의 venv python. 이미지 PATH 에 있지만 sh -c 경로를 타므로 절대경로로 부른다. PY=/opt/venv/bin/python echo echo "PI Continuum 배포 확인 엔진=${ENGINE} 프로젝트=${PROJECT}" echo "─────────────────────────────────────────────────────────────────────" echo "자동 PostgreSQL 백업 (활성화된 배포만)" if out="$(bash "${DEPLOY_DIR}/bin/auto-backup-db.sh" status --if-enabled --check 2>&1)"; then ok "자동 백업 상태 (미설치/비활성은 점검 제외)" else bad "자동 백업 점검 실패"; printf '%s\n' "${out}" fi # ── 1. 컨테이너 상태 ──────────────────────────────────────────────────────── echo "1. 컨테이너 상태" WEB=""; PROC=""; NGINX="" SERVICES=(postgres redis minio web worker-default worker-high worker-log scheduler alpha-processing nginx) [ "${LITELLM_ENABLED:-false}" = true ] && SERVICES+=(litellm) for svc in "${SERVICES[@]}"; do c="$(find_container "${svc}" || true)" if [ -z "${c}" ]; then bad "${svc}: 컨테이너 없음"; continue; fi state="$(engine inspect -f '{{.State.Status}}' "${c}")" health="$(engine inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}-{{end}}' "${c}")" # Podman은 healthcheck가 없는 컨테이너에 빈 값을 돌려줄 수 있다. health="${health:--}" case "${svc}" in web) WEB="${c}" ;; alpha-processing) PROC="${c}" ;; nginx) NGINX="${c}" ;; esac if [ "${state}" = running ] && { [ "${health}" = healthy ] || [ "${health}" = "-" ]; }; then ok "$(printf '%-18s %s' "${svc}" "${state}$([ "${health}" != - ] && printf ', %s' "${health}")")" else bad "$(printf '%-18s state=%s health=%s' "${svc}" "${state}" "${health}")" fi done # 첫 컨테이너는 위에서 확인했다. 설정한 추가 복제도 모두 살아 있는지 확인한다. for spec in "web:${WEB_REPLICAS:-4}" "worker-default:${RQ_DEFAULT_WORKERS:-1}" \ "worker-high:${RQ_HIGH_WORKERS:-1}" "worker-log:${RQ_LOG_WORKERS:-1}"; do svc="${spec%%:*}" replicas="${spec#*:}" for ((replica=2; replica<=replicas; replica++)); do c="$(find_container "${svc}" "${replica}" || true)" if [ -z "${c}" ]; then bad "${svc} #${replica}: 컨테이너 없음"; continue; fi state="$(engine inspect -f '{{.State.Status}}' "${c}")" health="$(engine inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}-{{end}}' "${c}")" health="${health:--}" if [ "${state}" = running ] && { [ "${health}" = healthy ] || [ "${health}" = "-" ]; }; then if [ "${health}" = "-" ]; then ok "${svc} #${replica}: ${state}" else ok "${svc} #${replica}: ${state}, ${health}"; fi else bad "${svc} #${replica}: state=${state} health=${health}" fi done done # bootstrap 은 1회성이라 exited 0 이 정상이다. if c="$(find_container bootstrap || true)"; [ -n "${c}" ]; then code="$(engine inspect -f '{{.State.ExitCode}}' "${c}")" [ "${code}" = 0 ] && ok "$(printf '%-18s exit 0 (1회성)' bootstrap)" || bad "bootstrap exit ${code}" fi [ -n "${WEB}" ] || die "web 컨테이너를 찾지 못해 이후 점검을 진행할 수 없다." in_web() { engine exec -i "${WEB}" sh -c "cd /app && $*"; } in_web_file() { engine exec -i "${WEB}" sh -c "cd /app && ${PY} -" < "$1"; } # ── 2. Django ─────────────────────────────────────────────────────────────── echo echo "2. Django" if out="$(in_web "${PY} manage.py check 2>&1")"; then ok "manage.py check" else bad "manage.py check 실패"; lines "${out}" '.' 10 fi if out="$(in_web "${PY} manage.py migrate --check --noinput 2>&1")"; then ok "미적용 migration 없음" else bad "미적용 migration 있음 — bootstrap 을 다시 돌린다"; lines "${out}" '.' 5 fi # ── 3. PostgreSQL ─────────────────────────────────────────────────────────── echo echo "3. PostgreSQL (pgvector)" if out="$(in_web_file "${CHECKS}/db.py" 2>&1)"; then ok "접속: $(field "${out}" VER)" v="$(field "${out}" VECTOR)" [ -n "${v}" ] && [ "${v}" != MISSING ] \ && ok "pgvector 확장 ${v}" \ || bad "pgvector 확장 없음 — conf/postgres/init.sql 이 초기화 때 실행됐는지 확인한다" m="$(field "${out}" MIGRATIONS)" if [ "${m:-0}" -gt 0 ] 2>/dev/null; then ok "migration ${m} 건 적용, public 테이블 $(field "${out}" TABLES) 개" else bad "migration 이 적용되지 않았다"; fi else bad "PostgreSQL 점검 실패"; lines "${out}" '.' 10 fi # ── 4. Redis · RQ ─────────────────────────────────────────────────────────── echo echo "4. Redis · RQ 워커" if out="$(in_web_file "${CHECKS}/redis_rq.py" 2>&1)"; then printf '%s\n' "${out}" | grep -q 'FILE_STORAGE=ok' \ && ok "FILE_STORAGE db (문서 blob 을 alpha-processing 과 공유)" \ || bad "FILE_STORAGE db 연결 실패" printf '%s\n' "${out}" | grep -q 'WEB_CACHE=ok' \ && ok "web cache db" \ || bad "web cache db 연결 실패" n="$(field "${out}" WORKERS)" expected=$(( ${RQ_DEFAULT_WORKERS:-1} + ${RQ_HIGH_WORKERS:-1} + ${RQ_LOG_WORKERS:-1} )) [ "${n:-0}" -ge "${expected}" ] 2>/dev/null \ && ok "RQ 워커 ${n} 개 등록" \ || bad "RQ 워커가 ${n:-0} 개다 (설정값 ${expected} 개 이상 기대)" lines "${out}" '^(QUEUE| )' 10 else bad "Redis 점검 실패"; lines "${out}" '.' 10 fi # ── 5. MinIO ──────────────────────────────────────────────────────────────── echo echo "5. MinIO" if out="$(in_web_file "${CHECKS}/minio_rw.py" 2>&1)"; then case "$(field "${out}" RW)" in ok) ok "버킷 $(field "${out}" TARGET) 읽기·쓰기" ;; no-bucket) bad "버킷 $(field "${out}" TARGET) 이 없다 (있는 것: $(field "${out}" BUCKETS))" ;; *) bad "버킷 읽기·쓰기 불일치" ;; esac else bad "MinIO 점검 실패"; lines "${out}" '.' 10 fi # ── 6. nginx ──────────────────────────────────────────────────────────────── echo echo "6. nginx (엣지)" if [ -n "${NGINX}" ]; then engine exec -i "${NGINX}" nginx -t >/dev/null 2>&1 && ok "설정 문법 (nginx -t)" || bad "nginx -t 실패" # nginx 컨테이너 안에서 자기 자신을 친다 — 호스트 포트 매핑·방화벽과 분리해서 본다. http_code() { engine exec -i "${NGINX}" sh -c "wget -qS -O /dev/null 'http://127.0.0.1$1' 2>&1 | awk '/HTTP\//{c=\$2} END{print c}'" 2>/dev/null || true; } code="$(http_code /)" [ "${code}" = 200 ] && ok "GET / → 200 (SPA index)" || bad "GET / → ${code:-실패}" idx="$(engine exec -i "${NGINX}" sh -c 'wget -qO- http://127.0.0.1/ 2>/dev/null | head -c 8000' || true)" asset="$(printf '%s' "${idx}" | grep -oE '/assets/[A-Za-z0-9._-]+\.js' | head -1 || true)" if [ -n "${asset}" ]; then acode="$(http_code "${asset}")" [ "${acode}" = 200 ] && ok "GET ${asset} → 200 (collectstatic 산출물)" || bad "GET ${asset} → ${acode:-실패}" else bad "index.html 에서 /assets/*.js 를 찾지 못했다 (collectstatic 확인)" fi # 호스트 포트 매핑까지 if command -v curl >/dev/null 2>&1; then hp="${HTTP_PORT:-80}" hcode="$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "http://127.0.0.1:${hp}/" 2>/dev/null || true)" [ "${hcode}" = 200 ] && ok "호스트 127.0.0.1:${hp} → 200" || bad "호스트 127.0.0.1:${hp} → ${hcode:-실패}" fi else bad "nginx 컨테이너 없음" fi # ── 7. alpha-processing 연동 ──────────────────────────────────────────────── echo echo "7. alpha-processing 연동" if [ -z "${PROC}" ]; then bad "alpha-processing 컨테이너 없음" else info "이미지: $(engine inspect -f '{{.Config.Image}}' "${PROC}")" if in_web "${PY} -c \"import socket; socket.create_connection(('alpha-processing', 50051), 5).close()\"" >/dev/null 2>&1; then ok "web → alpha-processing:50051 TCP" else bad "web 에서 alpha-processing:50051 에 붙지 못한다 (네트워크 alias 확인)" fi # 앱이 실제로 쓰는 경로 그대로: Redis blob → gRPC → Docling → 텍스트 # 긴 Docling 호출은 실시간으로 보여준다. VM이 종료돼도 마지막 진행 로그가 남는다. probe_log="$(mktemp)" probe_rc=0 engine exec -i "${WEB}" sh -c "cd /app && ${PY} -u -" < "${CHECKS}/processing.py" 2>&1 \ | tee "${probe_log}" || probe_rc=$? out="$(cat "${probe_log}")" rm -f -- "${probe_log}" if [ "${probe_rc}" -eq 0 ]; then ok "PDF 텍스트 추출 왕복 (Redis blob → gRPC → Docling)" ok "임베딩 왕복 (gRPC → ONNX, 768차원)" else printf '%s\n' "${out}" | grep -q "PDF 텍스트 추출 왕복 성공" \ && { ok "PDF 텍스트 추출 왕복 (Redis blob → gRPC → Docling)"; bad "임베딩 왕복 실패"; } \ || bad "PDF 텍스트 추출 왕복 실패" fi fi # ── 요약 ──────────────────────────────────────────────────────────────────── echo echo "─────────────────────────────────────────────────────────────────────" printf '결과: %d PASS / %d FAIL\n' "${PASS}" "${FAIL}" [ "${FAIL}" -eq 0 ] || exit 1