feat(lps): docker-compose 에 lps-admin 추가 + 환경설정 config.local.toml 단일화
- lps-admin 컨테이너화: Vite 정적 빌드 → nginx, /v1·/healthz·/readyz 를 lps-api:9600 으로 동일출처 프록시(빌드 타임 API URL 주입 불필요) - lps-api/worker: APP_ENV=local 고정 + DB_HOST override(도메인 backend·negodata·agent 와 동일 패턴), config.<env>.toml 마운트 방식 폐기 - server_configs 에 DB 접속 env override(_apply_db_env_override) 복원 - config.dev/prod.toml.example 제거 — 환경 구분 없이 config.local.toml 하나 (prod 서버도 그 서버의 config.local.toml + docker compose up -d) - run_docker.sh 환경 선택 제거·lps-admin 포함, README·operations 문서 갱신 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d111610555
commit
dc8c288f68
@ -9,6 +9,8 @@
|
|||||||
# negodata 서버: http://localhost:9400/docs
|
# negodata 서버: http://localhost:9400/docs
|
||||||
# agent 서버: http://localhost:9500/docs
|
# agent 서버: http://localhost:9500/docs
|
||||||
# anchoring 배치: 포트 없음 — 상주 스케줄러(격주 토 00:00 KST), docker logs anchoring 으로 확인
|
# anchoring 배치: 포트 없음 — 상주 스케줄러(격주 토 00:00 KST), docker logs anchoring 으로 확인
|
||||||
|
# lps API: http://localhost:9600/docs
|
||||||
|
# lps admin: http://localhost:3400 (nginx → /v1·/healthz 는 lps-api 로 프록시)
|
||||||
#
|
#
|
||||||
# DB 준비(최초 1회): postgres-init 의 SQL 을 대상 DB 에 적용한다.
|
# DB 준비(최초 1회): postgres-init 의 SQL 을 대상 DB 에 적용한다.
|
||||||
# psql -h <host> -p <port> -U <user> -f postgres-init/00-init.sql (스키마 전체: negosium_db + 도메인·learning·anchoring schema)
|
# psql -h <host> -p <port> -U <user> -f postgres-init/00-init.sql (스키마 전체: negosium_db + 도메인·learning·anchoring schema)
|
||||||
@ -118,26 +120,20 @@ services:
|
|||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "5"
|
max-file: "5"
|
||||||
|
|
||||||
# ── LPS (인터넷 최저가 검색) ──────────────────────────────────
|
# LPS API (인터넷 최저가 검색 — 요청 접수/조회, lean). 설정·시크릿은 config.local.toml 하나(미커밋, 마운트).
|
||||||
# API(요청 접수, lean) + 워커(크롤, 헤드풀 Chromium+Xvfb). DB 는 외부(host.docker.internal).
|
|
||||||
# ── 설정은 TOML 하나(2026-07-13 협의 — .env/env 주입 제거) ──
|
|
||||||
# 환경(dev/prod)은 APP_ENV 로 선택: ./lps/config/config.<APP_ENV>.toml(미커밋, example 복사)을 마운트해 읽는다.
|
|
||||||
# 실행은 ./lps/run_docker.sh(대화형) 권장 — 설정 파일 검증·prod 안전장치까지 처리.
|
|
||||||
# 이미지에는 시크릿이 없고, 마운트를 잊으면 기동 시 FileNotFoundError 로 즉시 실패한다.
|
|
||||||
# 값 변경 = toml 수정 + docker compose restart (스케일·알림·guard 키 전부 toml 섹션에서).
|
|
||||||
lps-api:
|
lps-api:
|
||||||
build:
|
build:
|
||||||
context: ./lps
|
context: ./lps
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: lps-api
|
container_name: lps-api
|
||||||
environment:
|
environment:
|
||||||
APP_ENV: ${APP_ENV:-dev}
|
APP_ENV: local
|
||||||
|
DB_HOST: ${LPS_DB_HOST-host.docker.internal} # 컨테이너→호스트 DB (config.local.toml 의 127.0.0.1 override)
|
||||||
PYTHONUNBUFFERED: "1"
|
PYTHONUNBUFFERED: "1"
|
||||||
volumes:
|
volumes:
|
||||||
- ./lps/config/config.${APP_ENV:-dev}.toml:/app/config/config.${APP_ENV:-dev}.toml:ro
|
- ./lps/config/config.local.toml:/app/config/config.local.toml:ro
|
||||||
ports:
|
ports:
|
||||||
# prod 는 LPS_API_BIND=127.0.0.1 로 호스트 내부(리버스프록시)만 접근 허용(run_docker.sh 가 자동 설정)
|
- "${LPS_API_BIND:-0.0.0.0}:9600:9600" # prod 는 LPS_API_BIND=127.0.0.1 로 내부만 개방(리버스프록시 뒤)
|
||||||
- "${LPS_API_BIND:-0.0.0.0}:9600:9600"
|
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
labels:
|
labels:
|
||||||
@ -147,23 +143,40 @@ services:
|
|||||||
driver: json-file
|
driver: json-file
|
||||||
options: { max-size: "10m", max-file: "5" }
|
options: { max-size: "10m", max-file: "5" }
|
||||||
|
|
||||||
|
# LPS 워커 (크롤 — 헤드풀 Chromium+Xvfb, headless 는 안티봇에 탐지됨). config.local.toml 공유.
|
||||||
lps-worker:
|
lps-worker:
|
||||||
build:
|
build:
|
||||||
context: ./lps
|
context: ./lps
|
||||||
dockerfile: Dockerfile.worker # Chromium + Xvfb (headless 는 안티봇에 탐지됨)
|
dockerfile: Dockerfile.worker
|
||||||
container_name: lps-worker
|
container_name: lps-worker
|
||||||
environment:
|
environment:
|
||||||
APP_ENV: ${APP_ENV:-dev} # → 마운트된 config.<APP_ENV>.toml 사용(동시성·폴백·예산·알림 전부 toml)
|
APP_ENV: local
|
||||||
|
DB_HOST: ${LPS_DB_HOST-host.docker.internal} # 컨테이너→호스트 DB (config.local.toml 의 127.0.0.1 override)
|
||||||
PYTHONUNBUFFERED: "1"
|
PYTHONUNBUFFERED: "1"
|
||||||
volumes:
|
volumes:
|
||||||
- ./lps/config/config.${APP_ENV:-dev}.toml:/app/config/config.${APP_ENV:-dev}.toml:ro
|
- ./lps/config/config.local.toml:/app/config/config.local.toml:ro
|
||||||
- lps-profiles:/profiles # Chrome 프로필(쿠키) 영속 — [WorkerConfig].profile_dir=/profiles
|
- lps-profiles:/profiles # Chrome 프로필(쿠키) 영속
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
shm_size: "1gb" # Chrome 는 /dev/shm 을 많이 씀 — 부족하면 탭 크래시
|
shm_size: "1gb" # Chrome 는 /dev/shm 을 많이 씀 — 부족하면 탭 크래시
|
||||||
stop_grace_period: 75s # graceful 종료 유예([WorkerConfig].shutdown_grace_sec=60 + 정리 여유) — 기본 10s 면 하던 잡 마무리 전에 SIGKILL
|
stop_grace_period: 75s # graceful 종료 유예(worker shutdown_grace_sec=60 + 여유)
|
||||||
labels:
|
labels:
|
||||||
autoheal: "true" # 하트비트 HEALTHCHECK 실패(행/좀비) 시 autoheal 이 재시작
|
autoheal: "true" # 하트비트 실패(행/좀비) 시 autoheal 재시작
|
||||||
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options: { max-size: "10m", max-file: "5" }
|
||||||
|
|
||||||
|
# LPS 관리자 UI (정적 React → nginx). /v1·/healthz·/readyz 는 nginx 가 lps-api:9600 으로 프록시(앱은 상대경로 호출).
|
||||||
|
lps-admin:
|
||||||
|
build:
|
||||||
|
context: ./lps-admin
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: lps-admin
|
||||||
|
ports:
|
||||||
|
- "3400:80"
|
||||||
|
depends_on:
|
||||||
|
- lps-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
|
|||||||
5
lps-admin/.dockerignore
Normal file
5
lps-admin/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
*.local
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
26
lps-admin/Dockerfile
Normal file
26
lps-admin/Dockerfile
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# LPS 관리자 UI 이미지 — Vite(React) 정적 빌드 → nginx 서빙.
|
||||||
|
# nginx 가 /v1·/healthz·/readyz 를 lps-api:9600 으로 프록시(동일출처)하므로,
|
||||||
|
# 앱은 상대경로로 API 를 호출한다(client.ts baseUrl 빈값) → 빌드 타임 API URL 주입 불필요.
|
||||||
|
# 빌드는 slim(glibc) — alpine(musl)+esbuild 의 ETXTBSY 회피(negodata/front 선례).
|
||||||
|
FROM node:22-slim AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 의존성 먼저 (레이어 캐시). package-lock 고정 설치.
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build # tsc -b && vite build → dist/
|
||||||
|
|
||||||
|
# ── 런타임: 정적 서빙 + API 프록시 ──
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# UI 자체 도달성만 확인(index.html) — API 상태는 앱 대시보드가 lps-api 를 직접 폴링한다.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1/ >/dev/null 2>&1 || exit 1
|
||||||
51
lps-admin/nginx.conf
Normal file
51
lps-admin/nginx.conf
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# LPS 관리자 UI — 정적 SPA 서빙 + LPS API 동일출처 프록시.
|
||||||
|
# 브라우저는 이 오리진(:3400)만 보고, /v1·/healthz·/readyz 는 여기서 lps-api:9600 으로 넘긴다.
|
||||||
|
# (vite dev 서버의 proxy 설정과 동일한 대상·경로 — 개발/운영 동작 일치)
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# gzip — 번들·JSON 응답 전송량 절감
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript application/json image/svg+xml;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
|
||||||
|
# ── API 프록시 (lps-api:9600) ──
|
||||||
|
# 도커 임베디드 DNS(127.0.0.11)로 매 요청 재해석 → lps-api 재시작(새 IP)에도 502 없이 따라간다.
|
||||||
|
# proxy_pass 에 경로를 두지 않아 원본 요청 URI 를 그대로 전달한다(/v1/lps/ops → 동일 경로).
|
||||||
|
resolver 127.0.0.11 valid=30s ipv6=off;
|
||||||
|
|
||||||
|
location /v1/ {
|
||||||
|
set $lps_api http://lps-api:9600;
|
||||||
|
proxy_pass $lps_api;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 120s; # 테스트 검색 등 긴 폴링 여유
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /healthz {
|
||||||
|
set $lps_api http://lps-api:9600;
|
||||||
|
proxy_pass $lps_api;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /readyz {
|
||||||
|
set $lps_api http://lps-api:9600;
|
||||||
|
proxy_pass $lps_api;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── SPA 라우팅 — 정적 파일 없으면 index.html 로 폴백 ──
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 정적 자산 캐시(해시 파일명이라 장기 캐시 안전). index.html 은 캐시 안 함.
|
||||||
|
location /assets/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -86,12 +86,18 @@ curl -X POST localhost:9600/v1/lps/search -H 'Content-Type: application/json' \
|
|||||||
-d '{"data":[{"product_code":"T1","product_name":"맥심 커피","specification":"1박스, 160개입"}]}'
|
-d '{"data":[{"product_code":"T1","product_name":"맥심 커피","specification":"1박스, 160개입"}]}'
|
||||||
```
|
```
|
||||||
|
|
||||||
**서버(dev/prod) 실행 — Docker (대화형)**
|
**서버 실행 — Docker**
|
||||||
```bash
|
```bash
|
||||||
./run_docker.sh # 환경(dev/prod) 선택 → config.<env>.toml 검증(없으면 example 복사 제안) → 기동/재시작/중지/로그
|
# 리포 루트에서 전체 스택과 함께 (권장)
|
||||||
|
docker compose up -d # negosium 스택 + lps-api·lps-worker·lps-admin 모두 기동
|
||||||
|
|
||||||
|
# lps 서브셋만 (대화형: 설정 검증·guard 키 안전장치 + admin 포함)
|
||||||
|
./run_docker.sh
|
||||||
```
|
```
|
||||||
> 환경은 `APP_ENV` 가 고르는 `config.<env>.toml` 하나로 결정됩니다 — local(호스트)/dev/prod 모두 같은 구조.
|
> 환경 구분이 없습니다(도메인 backend·negodata·agent 와 동일) — 항상 `config.local.toml`.
|
||||||
> prod 선택 시 guard 키 미설정 경고 + API 포트 127.0.0.1 바인드(외부는 리버스프록시 경유)까지 자동 처리.
|
> **prod 서버**도 그 서버의 `config.local.toml` 에 prod 값(시크릿·guard 키·스케일)을 채우고 그냥 `docker compose up -d`.
|
||||||
|
> DB 는 컨테이너에서 `host.docker.internal`(호스트 DB)로 접속하고, 관리형 DB 면 `LPS_DB_HOST=` 로 override 를 끄고 toml 의 호스트를 씁니다.
|
||||||
|
> 운영에서 API 를 외부에 열지 않으려면 `export LPS_API_BIND=127.0.0.1`(리버스프록시 뒤).
|
||||||
|
|
||||||
> 자세한 실행/설정은 [운영 가이드](docs/operations.md) 참고.
|
> 자세한 실행/설정은 [운영 가이드](docs/operations.md) 참고.
|
||||||
|
|
||||||
@ -118,7 +124,7 @@ lps/
|
|||||||
├── Dockerfile # API 이미지(lean) · Dockerfile.worker # 워커(Chromium+Xvfb)
|
├── Dockerfile # API 이미지(lean) · Dockerfile.worker # 워커(Chromium+Xvfb)
|
||||||
├── run_local_server.sh # 로컬 API 실행 (대화형)
|
├── run_local_server.sh # 로컬 API 실행 (대화형)
|
||||||
├── run_local_worker.sh # 로컬 워커 실행 (대화형: 동시성·프로필)
|
├── run_local_worker.sh # 로컬 워커 실행 (대화형: 동시성·프로필)
|
||||||
├── run_docker.sh # dev/prod Docker 실행 (대화형: 환경 선택·설정 검증·prod 안전장치)
|
├── run_docker.sh # lps 서브셋 Docker 실행 (대화형: 설정 검증·guard 안전장치 + admin 포함)
|
||||||
├── run_loadtest_gui.sh # 부하 테스트 Locust 웹 UI(:8089) 실행 (대화형)
|
├── run_loadtest_gui.sh # 부하 테스트 Locust 웹 UI(:8089) 실행 (대화형)
|
||||||
├── config/ # 설정(config.local.toml — 포트/DB/API키, 미커밋; 배포는 env 주입)
|
├── config/ # 설정(config.local.toml — 포트/DB/API키, 미커밋; 배포는 env 주입)
|
||||||
├── common/ # 공통(enums, DB 세션, 모델, 로거, alerts=임계 알림 관리자)
|
├── common/ # 공통(enums, DB 세션, 모델, 로거, alerts=임계 알림 관리자)
|
||||||
|
|||||||
@ -1,80 +0,0 @@
|
|||||||
# dev(개발 서버, Docker) 설정 — 복사해서 사용: cp config.dev.toml.example config.dev.toml
|
|
||||||
# 실제 config.dev.toml 은 시크릿 포함이라 커밋하지 않는다(.gitignore: *.toml).
|
|
||||||
#
|
|
||||||
# 실행: ./lps/run_docker.sh 에서 dev 선택(권장) 또는 APP_ENV=dev docker compose up -d lps-api lps-worker
|
|
||||||
# compose 가 이 파일을 컨테이너에 마운트하고 APP_ENV 로 선택한다. 이미지에는 시크릿이 없다 —
|
|
||||||
# 마운트를 잊으면 기동 시 FileNotFoundError 로 즉시 실패(조용한 오동작 없음).
|
|
||||||
# local 과의 차이만 주석으로 표시 — 나머지 의미는 config.local.toml.example 참고.
|
|
||||||
|
|
||||||
[WebServerConfig]
|
|
||||||
server_name = "LpsServer"
|
|
||||||
port = 9600
|
|
||||||
process_count = 1 # API 병목은 드묾(단일로 ~1,100 RPS 실측). 부하 대비 시에만 코어 수만큼
|
|
||||||
is_ssl = false
|
|
||||||
is_test = false
|
|
||||||
cors_origins = []
|
|
||||||
api_keys = [] # dev 는 개방 모드(협의: guard 는 prod 전용)
|
|
||||||
|
|
||||||
[LogConfig]
|
|
||||||
print_console = true
|
|
||||||
log_level = "info"
|
|
||||||
|
|
||||||
[MainDBConfig]
|
|
||||||
db_type = "postgresql"
|
|
||||||
name = "lps_db"
|
|
||||||
write_host = "host.docker.internal" # 컨테이너 → 호스트 DB (전용 DB 서버면 그 호스트로)
|
|
||||||
write_port = 5432
|
|
||||||
write_id = "<DB_USER>"
|
|
||||||
write_pw = "<DB_PASSWORD>"
|
|
||||||
read_host = "host.docker.internal"
|
|
||||||
read_port = 5432
|
|
||||||
read_id = "<DB_USER>"
|
|
||||||
read_pw = "<DB_PASSWORD>"
|
|
||||||
show_log = false
|
|
||||||
pool_size = 10
|
|
||||||
max_overflow = 20
|
|
||||||
connection_budget = 40 # 전용 PG(max_connections≈100)면 90 근처로 상향
|
|
||||||
sslmode = ""
|
|
||||||
|
|
||||||
[WorkerConfig]
|
|
||||||
concurrency = 1
|
|
||||||
fallbacks = []
|
|
||||||
profile_dir = "/profiles" # compose 의 영속 볼륨(lps-profiles) — 재시작에도 cf_clearance 유지
|
|
||||||
job_deadline_sec = 300
|
|
||||||
shutdown_grace_sec = 60
|
|
||||||
chrome_channel = "chrome"
|
|
||||||
chrome_executable = "/usr/bin/chromium" # 컨테이너는 시스템 chromium + Xvfb(headful)
|
|
||||||
heartbeat_file = "/tmp/lps_worker_heartbeat"
|
|
||||||
|
|
||||||
[AlertConfig]
|
|
||||||
webhook = "" # Slack 호환 웹훅 — 채우면 임계 알림 전송(docs/operations.md)
|
|
||||||
cooldown_min = 30
|
|
||||||
dead_1h = 20
|
|
||||||
blocks_1h = 80
|
|
||||||
queue_lag_sec = 300
|
|
||||||
pool_pct = 90
|
|
||||||
source_fail_30m = 5
|
|
||||||
deadline_1h = 5
|
|
||||||
cost_1h_usd = 1.0
|
|
||||||
ports_low_pct = 30
|
|
||||||
block_sessions_6h = 1
|
|
||||||
|
|
||||||
[NaverConfig]
|
|
||||||
[[NaverConfig.keys]]
|
|
||||||
id = "<NAVER_CLIENT_ID>"
|
|
||||||
secret = "<NAVER_CLIENT_SECRET>"
|
|
||||||
|
|
||||||
[OpenAIConfig]
|
|
||||||
api_key = "<OPENAI_API_KEY>" # 비면 AI 판정 OFF
|
|
||||||
model = "gpt-4o-mini"
|
|
||||||
|
|
||||||
[DecodoConfig]
|
|
||||||
host = "" # 4종(호스트·계정·포트범위) 비면 프록시 미사용(직접 연결)
|
|
||||||
username = ""
|
|
||||||
password = ""
|
|
||||||
port_start = 0
|
|
||||||
port_end = 0
|
|
||||||
session_minutes = 10
|
|
||||||
cost_per_gb = 0.0
|
|
||||||
ip_request_budget = 3
|
|
||||||
port_cooldown_sec = 0
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
# prod(운영 서버, Docker) 설정 — 복사해서 사용: cp config.prod.toml.example config.prod.toml
|
|
||||||
# 실제 config.prod.toml 은 시크릿 포함이라 커밋하지 않는다(.gitignore: *.toml).
|
|
||||||
#
|
|
||||||
# 실행: ./lps/run_docker.sh 에서 prod 선택(권장) — guard 키 미설정 경고 + API 포트 127.0.0.1 바인드까지 처리
|
|
||||||
# (수동: APP_ENV=prod LPS_API_BIND=127.0.0.1 docker compose up -d lps-api lps-worker)
|
|
||||||
# prod 필수 체크: ① api_keys 채움 ② negodata 쪽 lps_api_key 에 같은 키 ③ 알림 webhook 권장
|
|
||||||
# compose 가 이 파일을 컨테이너에 마운트하고 APP_ENV 로 선택한다. 이미지에는 시크릿이 없다 —
|
|
||||||
# 마운트를 잊으면 기동 시 FileNotFoundError 로 즉시 실패(조용한 오동작 없음).
|
|
||||||
# local 과의 차이만 주석으로 표시 — 나머지 의미는 config.local.toml.example 참고.
|
|
||||||
|
|
||||||
[WebServerConfig]
|
|
||||||
server_name = "LpsServer"
|
|
||||||
port = 9600
|
|
||||||
process_count = 1 # API 병목은 드묾(단일로 ~1,100 RPS 실측). 부하 대비 시에만 코어 수만큼
|
|
||||||
is_ssl = false
|
|
||||||
is_test = false
|
|
||||||
cors_origins = []
|
|
||||||
api_keys = ["<openssl rand -hex 32 로 생성>"] # prod 필수 — 비우면 개방 모드(위험). 복수 등록=무중단 교체
|
|
||||||
|
|
||||||
[LogConfig]
|
|
||||||
print_console = true
|
|
||||||
log_level = "info" # prod 는 debug 금지(로그량·민감정보)
|
|
||||||
|
|
||||||
[MainDBConfig]
|
|
||||||
db_type = "postgresql"
|
|
||||||
name = "lps_db"
|
|
||||||
write_host = "host.docker.internal" # 컨테이너 → 호스트 DB (전용 DB 서버면 그 호스트로)
|
|
||||||
write_port = 5432
|
|
||||||
write_id = "<DB_USER>"
|
|
||||||
write_pw = "<DB_PASSWORD>"
|
|
||||||
read_host = "host.docker.internal"
|
|
||||||
read_port = 5432
|
|
||||||
read_id = "<DB_USER>"
|
|
||||||
read_pw = "<DB_PASSWORD>"
|
|
||||||
show_log = false
|
|
||||||
pool_size = 10
|
|
||||||
max_overflow = 20
|
|
||||||
connection_budget = 40 # 전용 PG(max_connections≈100)면 90 근처로 상향. 관리형 DB 면 sslmode 도 확인
|
|
||||||
sslmode = ""
|
|
||||||
|
|
||||||
[WorkerConfig]
|
|
||||||
concurrency = 1
|
|
||||||
fallbacks = []
|
|
||||||
profile_dir = "/profiles" # compose 의 영속 볼륨(lps-profiles) — 재시작에도 cf_clearance 유지
|
|
||||||
job_deadline_sec = 300
|
|
||||||
shutdown_grace_sec = 60
|
|
||||||
chrome_channel = "chrome"
|
|
||||||
chrome_executable = "/usr/bin/chromium" # 컨테이너는 시스템 chromium + Xvfb(headful)
|
|
||||||
heartbeat_file = "/tmp/lps_worker_heartbeat"
|
|
||||||
|
|
||||||
[AlertConfig]
|
|
||||||
webhook = "" # prod 는 채우기를 권장 — 없으면 임계 알림이 로그로만 남는다(docs/operations.md)
|
|
||||||
cooldown_min = 30
|
|
||||||
dead_1h = 20
|
|
||||||
blocks_1h = 80
|
|
||||||
queue_lag_sec = 300
|
|
||||||
pool_pct = 90
|
|
||||||
source_fail_30m = 5
|
|
||||||
deadline_1h = 5
|
|
||||||
cost_1h_usd = 1.0
|
|
||||||
ports_low_pct = 30
|
|
||||||
block_sessions_6h = 1
|
|
||||||
|
|
||||||
[NaverConfig]
|
|
||||||
[[NaverConfig.keys]]
|
|
||||||
id = "<NAVER_CLIENT_ID>"
|
|
||||||
secret = "<NAVER_CLIENT_SECRET>"
|
|
||||||
|
|
||||||
[OpenAIConfig]
|
|
||||||
api_key = "<OPENAI_API_KEY>" # 비면 AI 판정 OFF
|
|
||||||
model = "gpt-4o-mini"
|
|
||||||
|
|
||||||
[DecodoConfig]
|
|
||||||
host = "" # 4종(호스트·계정·포트범위) 비면 프록시 미사용(직접 연결)
|
|
||||||
username = ""
|
|
||||||
password = ""
|
|
||||||
port_start = 0
|
|
||||||
port_end = 0
|
|
||||||
session_minutes = 10
|
|
||||||
cost_per_gb = 0.0
|
|
||||||
ip_request_budget = 3
|
|
||||||
port_cooldown_sec = 0
|
|
||||||
@ -6,13 +6,16 @@ from config.config_models import (
|
|||||||
WorkerConfig, AlertConfig,
|
WorkerConfig, AlertConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── 설정 소스는 TOML 하나다(2026-07-13 협의 — env/.env 이중 관리 제거). ──
|
# ── 설정 소스는 config.local.toml 하나다(도메인 backend 와 동일 — 서버마다 그 서버의 값, 미커밋). ──
|
||||||
# env 는 부트스트랩·실행 입력만 남는다:
|
# 운영 전제: 항상 APP_ENV=local 로 띄운다 → config.local.toml (호스트 실행·pytest·docker 모두 동일).
|
||||||
# APP_ENV : 어떤 toml 을 읽을지 — local(호스트 실행) / dev·prod(컨테이너에 마운트된 config.<env>.toml)
|
# prod 서버도 그 서버의 config.local.toml 에 prod 값(시크릿·guard키·스케일)을 채우고 `docker compose up -d`.
|
||||||
|
# env 는 실행 입력과 '환경별로 바뀌는 접속점'만 담당한다:
|
||||||
|
# DB_HOST/PORT/… : DB 접속 override — config.local.toml 은 127.0.0.1(호스트 실행·pytest) 유지하고,
|
||||||
|
# 컨테이너에선 compose 가 DB_HOST=host.docker.internal 로 호스트 DB 를 가리킨다(_apply_db_env_override).
|
||||||
# PROCESS_COUNT : 실행 스크립트·부하벤치의 대화형 입력(uvicorn 워커 수 임시 override)
|
# PROCESS_COUNT : 실행 스크립트·부하벤치의 대화형 입력(uvicorn 워커 수 임시 override)
|
||||||
# WORKER_CONCURRENCY : 워커 실행 스크립트의 대화형 입력(worker_main 이 읽음)
|
# WORKER_CONCURRENCY : 워커 실행 스크립트의 대화형 입력(worker_main 이 읽음)
|
||||||
# LPS_LIVE : 라이브 스모크 테스트 옵트인(설정이 아니라 실행 스위치)
|
# LPS_LIVE : 라이브 스모크 테스트 옵트인(설정이 아니라 실행 스위치)
|
||||||
# 시크릿 포함 실값은 환경별 config.<APP_ENV>.toml(미커밋)에 두고, Docker 는 파일을 마운트한다.
|
# 시크릿(네이버·OpenAI·DECODO)은 env 로 넣지 않는다 — config.local.toml 에 두고 컨테이너엔 마운트한다.
|
||||||
APP_ENV = os.environ.get("APP_ENV", "local")
|
APP_ENV = os.environ.get("APP_ENV", "local")
|
||||||
|
|
||||||
_config_dir = os.path.dirname(__file__)
|
_config_dir = os.path.dirname(__file__)
|
||||||
@ -21,7 +24,7 @@ _config_file = os.path.join(_config_dir, f"config.{APP_ENV}.toml")
|
|||||||
if not os.path.exists(_config_file):
|
if not os.path.exists(_config_file):
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"설정 파일이 없습니다: {_config_file} (APP_ENV={APP_ENV}). "
|
f"설정 파일이 없습니다: {_config_file} (APP_ENV={APP_ENV}). "
|
||||||
"호스트 실행은 config.local.toml 준비, 컨테이너(dev/prod)는 config.<APP_ENV>.toml 마운트를 확인하세요."
|
"config.local.toml 을 준비하세요(config.local.toml.example 복사). 컨테이너는 compose 가 이 파일을 마운트합니다."
|
||||||
)
|
)
|
||||||
|
|
||||||
configs = Configs(_config_file)
|
configs = Configs(_config_file)
|
||||||
@ -37,6 +40,26 @@ worker_config: WorkerConfig = configs.get(WorkerConfig) or WorkerConfig()
|
|||||||
alert_config: AlertConfig = configs.get(AlertConfig) or AlertConfig()
|
alert_config: AlertConfig = configs.get(AlertConfig) or AlertConfig()
|
||||||
|
|
||||||
|
|
||||||
|
# DB 접속 env override — config.local.toml 의 접속 정보를 '환경별로 바뀌는 값만' 덮는다.
|
||||||
|
# 로컬(호스트 uvicorn·pytest)은 env 미설정 → toml(127.0.0.1) 그대로. 컨테이너는 compose 가
|
||||||
|
# DB_HOST=host.docker.internal 을 넣어 호스트 DB 를 가리킨다(도메인 backend 와 동일한 패턴).
|
||||||
|
# 값이 빈 문자열이면 override 하지 않는다 → 관리형 DB 는 LPS_DB_HOST= 로 비워 config.local.toml 의 호스트를 쓴다.
|
||||||
|
def _apply_db_env_override(cfg: MainDBConfig):
|
||||||
|
if os.environ.get("DB_HOST"):
|
||||||
|
cfg.write_host = cfg.read_host = os.environ["DB_HOST"]
|
||||||
|
if os.environ.get("DB_PORT"):
|
||||||
|
cfg.write_port = cfg.read_port = int(os.environ["DB_PORT"])
|
||||||
|
if os.environ.get("DB_USER"):
|
||||||
|
cfg.write_id = cfg.read_id = os.environ["DB_USER"]
|
||||||
|
if os.environ.get("DB_PASSWORD"):
|
||||||
|
cfg.write_pw = cfg.read_pw = os.environ["DB_PASSWORD"]
|
||||||
|
if os.environ.get("DB_NAME"):
|
||||||
|
cfg.name = os.environ["DB_NAME"]
|
||||||
|
# 커넥션 예산도 env 로 조정 가능(자동 산정 입력) — 전용/공유 PG 에 맞춰 서버별로.
|
||||||
|
if os.environ.get("DB_CONNECTION_BUDGET"):
|
||||||
|
cfg.connection_budget = int(os.environ["DB_CONNECTION_BUDGET"])
|
||||||
|
|
||||||
|
|
||||||
def _autosize_pool(cfg: MainDBConfig, process_count: int):
|
def _autosize_pool(cfg: MainDBConfig, process_count: int):
|
||||||
"""process_count 기반 커넥션 풀 자동 산정.
|
"""process_count 기반 커넥션 풀 자동 산정.
|
||||||
|
|
||||||
@ -59,6 +82,9 @@ def _autosize_pool(cfg: MainDBConfig, process_count: int):
|
|||||||
return cfg.pool_size, cfg.max_overflow
|
return cfg.pool_size, cfg.max_overflow
|
||||||
|
|
||||||
|
|
||||||
|
# DB 접속점 override(호스트↔컨테이너) — 자동 산정(예산) 전에 적용해야 budget override 가 반영된다.
|
||||||
|
_apply_db_env_override(main_db_config)
|
||||||
|
|
||||||
# uvicorn 워커 수 — 실행 스크립트/부하벤치의 대화형 입력만 env 로 임시 override(설정은 toml 이 소스).
|
# uvicorn 워커 수 — 실행 스크립트/부하벤치의 대화형 입력만 env 로 임시 override(설정은 toml 이 소스).
|
||||||
if os.environ.get("PROCESS_COUNT"):
|
if os.environ.get("PROCESS_COUNT"):
|
||||||
web_server_config.process_count = int(os.environ["PROCESS_COUNT"])
|
web_server_config.process_count = int(os.environ["PROCESS_COUNT"])
|
||||||
|
|||||||
@ -18,12 +18,12 @@ cp config/config.local.toml.example config/config.local.toml
|
|||||||
| `[OpenAIConfig]` | `api_key` (AI 판정·검색어 생성용) |
|
| `[OpenAIConfig]` | `api_key` (AI 판정·검색어 생성용) |
|
||||||
| `[DecodoConfig]` | 프록시 정보(비워두면 프록시 미사용) |
|
| `[DecodoConfig]` | 프록시 정보(비워두면 프록시 미사용) |
|
||||||
|
|
||||||
> **설정 소스는 TOML 하나다**(2026-07-13 협의 — env/.env 이중 관리 제거). 호스트 실행은
|
> **설정 소스는 `config.local.toml` 하나다**(도메인 backend·negodata·agent 와 동일). 환경 구분이 없다 —
|
||||||
> `config.local.toml`, Docker(dev/prod 서버)는 `config.dev.toml`/`config.prod.toml`(각 example 복사)을
|
> 호스트 실행·Docker·prod 서버 모두 같은 파일명을 쓰고, **서버마다 그 서버의 값**(시크릿·guard 키·스케일)을 담는다(미커밋).
|
||||||
> 컨테이너에 마운트하고 `APP_ENV` 로 선택해 읽는다(`./run_docker.sh` 가 대화형으로 처리).
|
> 컨테이너는 compose 가 이 파일을 마운트한다 — 이미지엔 시크릿이 없고(빌드 시 `.dockerignore` 제외), 마운트를
|
||||||
> 이미지에는 시크릿이 없고(빌드 시 `.dockerignore` 제외), 마운트를
|
> 잊으면 기동 시 FileNotFoundError 로 즉시 실패한다. env 로는 '환경별로 바뀌는 접속점'만 준다:
|
||||||
> 잊으면 기동 시 FileNotFoundError 로 즉시 실패한다. env 는 `APP_ENV`·실행 스크립트의 대화형
|
> `DB_HOST`(컨테이너→호스트 DB, compose 가 `host.docker.internal` 주입 — 관리형 DB 면 `LPS_DB_HOST=` 로 끔),
|
||||||
> 입력(`PROCESS_COUNT`/`WORKER_CONCURRENCY`)·`LPS_LIVE`(테스트)만 남는다.
|
> 그리고 실행 스크립트의 대화형 입력(`PROCESS_COUNT`/`WORKER_CONCURRENCY`)·`LPS_LIVE`(테스트).
|
||||||
|
|
||||||
**DB 준비**: `lps_db` 생성 후 최초 실행 시 테이블 자동 생성.
|
**DB 준비**: `lps_db` 생성 후 최초 실행 시 테이블 자동 생성.
|
||||||
```bash
|
```bash
|
||||||
@ -33,13 +33,15 @@ psql -h 127.0.0.1 -U postgres -d lps_db -c "CREATE EXTENSION IF NOT EXISTS pgcry
|
|||||||
|
|
||||||
## 2. 실행
|
## 2. 실행
|
||||||
|
|
||||||
**환경 개요** — 환경은 `APP_ENV` 가 고르는 `config.<env>.toml` 하나로 결정된다:
|
**환경 개요** — 환경 구분이 없다. 어디서든 `config.local.toml` 하나(서버마다 그 서버의 값):
|
||||||
|
|
||||||
| 환경 | 실행 위치 | 설정 파일 | 실행 방법 |
|
| 실행 위치 | 설정 파일 | 실행 방법 |
|
||||||
|------|----------|-----------|----------|
|
|----------|-----------|----------|
|
||||||
| local | 개발자 호스트 | `config.local.toml` | `./run_local_server.sh` + `./run_local_worker.sh` |
|
| 개발자 호스트(비도커) | `config.local.toml` | `./run_local_server.sh` + `./run_local_worker.sh` |
|
||||||
| dev | 개발 서버(Docker) | `config.dev.toml` (example 복사) | `./run_docker.sh` → dev 선택 |
|
| 개발/운영 서버(Docker) | `config.local.toml` (그 서버 값) | `docker compose up -d` (또는 `./run_docker.sh` — lps 서브셋·안전장치) |
|
||||||
| prod | 운영 서버(Docker) | `config.prod.toml` (example 복사) | `./run_docker.sh` → prod 선택 (guard 키 검사·API 127.0.0.1 바인드 자동) |
|
|
||||||
|
> **prod**도 별도 환경이 아니다 — prod 서버의 `config.local.toml` 에 prod 값(시크릿·`api_keys` guard·스케일)을 채우고
|
||||||
|
> `docker compose up -d`. 외부 노출 차단은 `export LPS_API_BIND=127.0.0.1`(리버스프록시 뒤).
|
||||||
|
|
||||||
**API 서버** (요청 접수)
|
**API 서버** (요청 접수)
|
||||||
```bash
|
```bash
|
||||||
@ -227,9 +229,9 @@ docker ps # lps-worker "(healthy)" 확인
|
|||||||
([DecodoConfig].port_start/end) 자동 반영 — 코드에 포트 수 하드코딩 없음. 튜닝은 `ip_session` 분석 쿼리(database.md) 참고.
|
([DecodoConfig].port_start/end) 자동 반영 — 코드에 포트 수 하드코딩 없음. 튜닝은 `ip_session` 분석 쿼리(database.md) 참고.
|
||||||
|
|
||||||
- **API guard**: `[WebServerConfig].api_keys` 설정 시 `/v1/*` 전체에 X-API-Key 검증(복수 키 —
|
- **API guard**: `[WebServerConfig].api_keys` 설정 시 `/v1/*` 전체에 X-API-Key 검증(복수 키 —
|
||||||
무중단 교체). 개발(local/dev)은 빈값=개방 모드. **prod 체크리스트**: ① config.prod.toml 에
|
무중단 교체). 개발기는 빈값=개방 모드. **prod 체크리스트**: ① prod 서버의 config.local.toml 에
|
||||||
`api_keys` 채움(negodata 쪽은 `lps_api_key` 에 같은 키 — 헤더 자동 첨부) ② lps-api 포트 공개
|
`api_keys` 채움(negodata 쪽은 `lps_api_key` 에 같은 키 — 헤더 자동 첨부) ② lps-api 외부 노출 차단
|
||||||
제거(내부 네트워크만, `ports:` 삭제) ③ 기동 로그에서 `API guard ON` 확인.
|
(`export LPS_API_BIND=127.0.0.1` 또는 compose `ports:` 삭제) ③ 기동 로그에서 `API guard ON` 확인.
|
||||||
|
|
||||||
**남은 배포 과제**: 레이트리밋(키별 요청량 제한), 다중 레플리카 시 분산 레이트리밋/프록시 IP 조정.
|
**남은 배포 과제**: 레이트리밋(키별 요청량 제한), 다중 레플리카 시 분산 레이트리밋/프록시 IP 조정.
|
||||||
**비용**: 대역폭이 원가의 대부분(오픈마켓 크롤) — 같은 상품 재크롤을 줄이는 **TTL 캐시**가 다음 절감 후보.
|
**비용**: 대역폭이 원가의 대부분(오픈마켓 크롤) — 같은 상품 재크롤을 줄이는 **TTL 캐시**가 다음 절감 후보.
|
||||||
|
|||||||
@ -1,49 +1,40 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# LPS Docker 실행(대화형) — dev/prod 환경을 골라 lps-api/lps-worker 를 관리한다.
|
# LPS Docker 실행(대화형) — lps-api/lps-worker/lps-admin 을 관리한다.
|
||||||
# 환경 = APP_ENV 가 고르는 config.<env>.toml (미커밋, example 복사 후 값 채움).
|
# 환경 구분 없음(도메인 backend·negodata·agent 와 동일): 항상 config.local.toml.
|
||||||
# 호스트(로컬) 실행은 run_local_server.sh / run_local_worker.sh 를 사용.
|
# 어느 서버든 그 서버의 config.local.toml 값으로 뜬다 — prod 서버엔 prod 값을 채워두면 그만.
|
||||||
|
# 호스트(비도커) 실행은 run_local_server.sh / run_local_worker.sh 를 사용.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")/.." # 리포 루트(docker-compose.yml 위치)
|
cd "$(dirname "$0")/.." # 리포 루트(docker-compose.yml 위치)
|
||||||
|
|
||||||
echo "── LPS Docker 실행 ──"
|
echo "── LPS Docker 실행 ──"
|
||||||
read -rp "환경 선택 [1] dev(기본) [2] prod : " env_choice
|
|
||||||
case "${env_choice:-1}" in
|
|
||||||
1) ENV=dev ;;
|
|
||||||
2) ENV=prod ;;
|
|
||||||
*) echo "[error] 알 수 없는 선택: $env_choice"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CFG="lps/config/config.${ENV}.toml"
|
CFG="lps/config/config.local.toml"
|
||||||
if [[ ! -f "$CFG" ]]; then
|
if [[ ! -f "$CFG" ]]; then
|
||||||
echo "[warn] $CFG 가 없습니다 (환경별 설정은 미커밋 — 서버마다 직접 준비)."
|
echo "[warn] $CFG 가 없습니다 (시크릿 포함, 미커밋 — 서버마다 직접 준비)."
|
||||||
read -rp "example 을 복사해 생성할까요? [Y/n] " yn
|
read -rp "example 을 복사해 생성할까요? [Y/n] " yn
|
||||||
if [[ "${yn:-Y}" =~ ^[Yy] ]]; then
|
if [[ "${yn:-Y}" =~ ^[Yy] ]]; then
|
||||||
cp "lps/config/config.${ENV}.toml.example" "$CFG"
|
cp "lps/config/config.local.toml.example" "$CFG"
|
||||||
echo "[ok] $CFG 생성 — DB 접속·API 키 등 값을 채운 뒤 다시 실행하세요."
|
echo "[ok] $CFG 생성 — DB 접속·API 키 등 값을 채운 뒤 다시 실행하세요."
|
||||||
fi
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# prod 안전 점검: guard 키가 비어 있으면 API 가 개방 모드로 뜬다(위험) — 명시적 확인을 받는다.
|
# prod 서버 안전 점검: guard 키가 비어 있으면 API 가 개방 모드로 뜬다 — 명시적 확인.
|
||||||
if [[ "$ENV" == "prod" ]] && grep -qE '^\s*api_keys\s*=\s*\[\s*\]' "$CFG"; then
|
if grep -qE '^\s*api_keys\s*=\s*\[\s*\]' "$CFG"; then
|
||||||
echo "[warn] prod 인데 [WebServerConfig].api_keys 가 비어 있습니다 → API 개방 모드(무인증)."
|
echo "[warn] [WebServerConfig].api_keys 가 비어 있습니다 → API 개방 모드(무인증). prod 라면 키를 채우세요."
|
||||||
read -rp "그래도 계속할까요? [y/N] " go
|
read -rp "그래도 계속할까요? [y/N] " go
|
||||||
[[ "${go:-N}" =~ ^[Yy] ]] || { echo "중단합니다."; exit 1; }
|
[[ "${go:-N}" =~ ^[Yy] ]] || { echo "중단합니다."; exit 1; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export APP_ENV="$ENV"
|
# 운영(리버스프록시 뒤)에서 API 를 외부에 열지 않으려면 실행 전에: export LPS_API_BIND=127.0.0.1
|
||||||
if [[ "$ENV" == "prod" ]]; then
|
|
||||||
export LPS_API_BIND=127.0.0.1 # prod 는 API 포트를 호스트 내부만 바인드(외부는 리버스프록시 경유)
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "환경: $ENV · 설정: $CFG$([[ "$ENV" == "prod" ]] && echo ' · API 바인드: 127.0.0.1')"
|
echo "설정: $CFG · API 바인드: ${LPS_API_BIND:-0.0.0.0}"
|
||||||
read -rp "동작 선택 [1] 시작/재빌드(기본) [2] 재시작 [3] 중지 [4] 로그 : " act
|
read -rp "동작 선택 [1] 시작/재빌드(기본) [2] 재시작 [3] 중지 [4] 로그 : " act
|
||||||
case "${act:-1}" in
|
case "${act:-1}" in
|
||||||
1) docker compose up -d --build lps-api lps-worker autoheal
|
1) docker compose up -d --build lps-api lps-worker lps-admin autoheal
|
||||||
echo "[ok] 기동 — 로그의 'API guard ON/개방 모드' · 'DECODO 프리플라이트' 확인: docker compose logs -f lps-api lps-worker" ;;
|
echo "[ok] 기동 — admin: http://localhost:3400 · 로그의 'API guard ON/개방 모드' · 'DECODO 프리플라이트' 확인: docker compose logs -f lps-api lps-worker lps-admin" ;;
|
||||||
2) docker compose restart lps-api lps-worker ;;
|
2) docker compose restart lps-api lps-worker lps-admin ;;
|
||||||
3) docker compose stop lps-api lps-worker ;;
|
3) docker compose stop lps-api lps-worker lps-admin ;;
|
||||||
4) docker compose logs -f lps-api lps-worker ;;
|
4) docker compose logs -f lps-api lps-worker lps-admin ;;
|
||||||
*) echo "[error] 알 수 없는 선택: $act"; exit 1 ;;
|
*) echo "[error] 알 수 없는 선택: $act"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user