From 8fb54be830d6cb7e387f67d0e7d4d1ef0202f402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AF=BC=ED=97=8C?= Date: Mon, 13 Jul 2026 21:22:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(lps):=20local/dev/prod=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=84=20=EC=8B=A4=ED=96=89=20=EC=B2=B4=EA=B3=84=20?= =?UTF-8?q?=E2=80=94=20=ED=99=98=EA=B2=BD=20toml=202=EC=A2=85=20+=20?= =?UTF-8?q?=EB=8C=80=ED=99=94=ED=98=95=20run=5Fdocker.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다른 개발자가 어느 환경이든 같은 방식으로 띄울 수 있게 한다. 환경 = APP_ENV 가 고르는 config..toml 하나(구조 동일, 값만 다름). - config.docker.toml → config.dev.toml 개명, config.prod.toml.example 신설 (prod 성격 반영: api_keys 필수 표기·debug 로그 금지·웹훅 권장). - compose 의 lps 서비스 APP_ENV/마운트를 ${APP_ENV:-dev} 로 파라미터화, API 포트를 ${LPS_API_BIND:-0.0.0.0} 바인드로 노출 제어. - run_docker.sh(대화형) 신설: 환경 선택 → 설정 파일 검증(없으면 example 복사 제안) → 기동/재시작/중지/로그. prod 는 guard 키 비면 경고 후 확인받고, LPS_API_BIND=127.0.0.1 자동 설정(외부는 리버스프록시 경유). - 운영 가이드에 환경 개요 표, README 빠른 시작에 Docker 실행 추가. - 검증: 스크립트 문법·compose(dev/prod) 파싱·APP_ENV=dev 로딩·145 passed. Co-Authored-By: Claude Fable 5 --- docker-compose.yml | 15 ++-- lps/README.md | 8 ++ ...r.toml.example => config.dev.toml.example} | 12 +-- lps/config/config.local.toml.example | 3 +- lps/config/config.prod.toml.example | 82 +++++++++++++++++++ lps/config/server_configs.py | 4 +- lps/docs/operations.md | 19 +++-- lps/run_docker.sh | 49 +++++++++++ 8 files changed, 171 insertions(+), 21 deletions(-) rename lps/config/{config.docker.toml.example => config.dev.toml.example} (75%) create mode 100644 lps/config/config.prod.toml.example create mode 100755 lps/run_docker.sh diff --git a/docker-compose.yml b/docker-compose.yml index 272dca7..ce1c601 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -120,9 +120,9 @@ services: # ── LPS (인터넷 최저가 검색) ────────────────────────────────── # API(요청 접수, lean) + 워커(크롤, 헤드풀 Chromium+Xvfb). DB 는 외부(host.docker.internal). - # 이미지엔 시크릿이 없다(example config 로 빌드) — 실값은 아래 env 로 주입. # ── 설정은 TOML 하나(2026-07-13 협의 — .env/env 주입 제거) ── - # ./lps/config/config.docker.toml(미커밋, example 복사 후 값 채움)을 마운트하고 APP_ENV=docker 로 읽는다. + # 환경(dev/prod)은 APP_ENV 로 선택: ./lps/config/config..toml(미커밋, example 복사)을 마운트해 읽는다. + # 실행은 ./lps/run_docker.sh(대화형) 권장 — 설정 파일 검증·prod 안전장치까지 처리. # 이미지에는 시크릿이 없고, 마운트를 잊으면 기동 시 FileNotFoundError 로 즉시 실패한다. # 값 변경 = toml 수정 + docker compose restart (스케일·알림·guard 키 전부 toml 섹션에서). lps-api: @@ -131,12 +131,13 @@ services: dockerfile: Dockerfile container_name: lps-api environment: - APP_ENV: docker + APP_ENV: ${APP_ENV:-dev} PYTHONUNBUFFERED: "1" volumes: - - ./lps/config/config.docker.toml:/app/config/config.docker.toml:ro + - ./lps/config/config.${APP_ENV:-dev}.toml:/app/config/config.${APP_ENV:-dev}.toml:ro ports: - - "9600:9600" + # prod 는 LPS_API_BIND=127.0.0.1 로 호스트 내부(리버스프록시)만 접근 허용(run_docker.sh 가 자동 설정) + - "${LPS_API_BIND:-0.0.0.0}:9600:9600" extra_hosts: - "host.docker.internal:host-gateway" labels: @@ -152,10 +153,10 @@ services: dockerfile: Dockerfile.worker # Chromium + Xvfb (headless 는 안티봇에 탐지됨) container_name: lps-worker environment: - APP_ENV: docker # → 마운트된 config.docker.toml 사용(동시성·폴백·예산·알림 전부 toml) + APP_ENV: ${APP_ENV:-dev} # → 마운트된 config..toml 사용(동시성·폴백·예산·알림 전부 toml) PYTHONUNBUFFERED: "1" volumes: - - ./lps/config/config.docker.toml:/app/config/config.docker.toml:ro + - ./lps/config/config.${APP_ENV:-dev}.toml:/app/config/config.${APP_ENV:-dev}.toml:ro - lps-profiles:/profiles # Chrome 프로필(쿠키) 영속 — [WorkerConfig].profile_dir=/profiles extra_hosts: - "host.docker.internal:host-gateway" diff --git a/lps/README.md b/lps/README.md index 3b2f979..9a7a591 100644 --- a/lps/README.md +++ b/lps/README.md @@ -86,6 +86,13 @@ curl -X POST localhost:9600/v1/lps/search -H 'Content-Type: application/json' \ -d '{"data":[{"product_code":"T1","product_name":"맥심 커피","specification":"1박스, 160개입"}]}' ``` +**서버(dev/prod) 실행 — Docker (대화형)** +```bash +./run_docker.sh # 환경(dev/prod) 선택 → config..toml 검증(없으면 example 복사 제안) → 기동/재시작/중지/로그 +``` +> 환경은 `APP_ENV` 가 고르는 `config..toml` 하나로 결정됩니다 — local(호스트)/dev/prod 모두 같은 구조. +> prod 선택 시 guard 키 미설정 경고 + API 포트 127.0.0.1 바인드(외부는 리버스프록시 경유)까지 자동 처리. + > 자세한 실행/설정은 [운영 가이드](docs/operations.md) 참고. --- @@ -111,6 +118,7 @@ lps/ ├── Dockerfile # API 이미지(lean) · Dockerfile.worker # 워커(Chromium+Xvfb) ├── run_local_server.sh # 로컬 API 실행 (대화형) ├── run_local_worker.sh # 로컬 워커 실행 (대화형: 동시성·프로필) +├── run_docker.sh # dev/prod Docker 실행 (대화형: 환경 선택·설정 검증·prod 안전장치) ├── run_loadtest_gui.sh # 부하 테스트 Locust 웹 UI(:8089) 실행 (대화형) ├── config/ # 설정(config.local.toml — 포트/DB/API키, 미커밋; 배포는 env 주입) ├── common/ # 공통(enums, DB 세션, 모델, 로거, alerts=임계 알림 관리자) diff --git a/lps/config/config.docker.toml.example b/lps/config/config.dev.toml.example similarity index 75% rename from lps/config/config.docker.toml.example rename to lps/config/config.dev.toml.example index 86e0d86..6fdfa2f 100644 --- a/lps/config/config.docker.toml.example +++ b/lps/config/config.dev.toml.example @@ -1,9 +1,9 @@ -# Docker(컨테이너) 실행용 설정 — 복사해서 사용: cp config.docker.toml.example config.docker.toml -# 실제 config.docker.toml 은 시크릿 포함이라 커밋하지 않는다(.gitignore: *.toml). +# dev(개발 서버, Docker) 설정 — 복사해서 사용: cp config.dev.toml.example config.dev.toml +# 실제 config.dev.toml 은 시크릿 포함이라 커밋하지 않는다(.gitignore: *.toml). # -# 사용법: docker-compose 가 이 파일을 컨테이너에 마운트하고 APP_ENV=docker 로 띄운다. -# ./lps/config/config.docker.toml → /app/config/config.docker.toml (ro) -# 이미지에는 시크릿이 없다 — 마운트를 잊으면 기동 시 FileNotFoundError 로 즉시 실패(조용한 오동작 없음). +# 실행: ./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] @@ -13,7 +13,7 @@ process_count = 1 # API 병목은 드묾(단일로 ~1,100 RPS 실 is_ssl = false is_test = false cors_origins = [] -api_keys = [] # prod 는 반드시 채운다(openssl rand -hex 32) + lps-api 포트 비공개 +api_keys = [] # dev 는 개방 모드(협의: guard 는 prod 전용) [LogConfig] print_console = true diff --git a/lps/config/config.local.toml.example b/lps/config/config.local.toml.example index 78dce20..b287f7f 100644 --- a/lps/config/config.local.toml.example +++ b/lps/config/config.local.toml.example @@ -3,7 +3,8 @@ # # ── 설정 소스는 TOML 하나다(2026-07-13 협의 — env/.env 이중 관리 제거) ── # 호스트 실행: APP_ENV=local(기본) → 이 파일 -# Docker : APP_ENV=docker → config.docker.toml 을 컨테이너에 마운트(config.docker.toml.example 참고) +# Docker : APP_ENV=dev|prod → config.dev.toml / config.prod.toml 을 컨테이너에 마운트 +# (각 example 참고, 실행은 ../run_docker.sh 대화형 권장) # env 는 APP_ENV·PROCESS_COUNT/WORKER_CONCURRENCY(실행 스크립트 대화형 입력)·LPS_LIVE(테스트)만 남는다. [WebServerConfig] server_name = "LpsServer" diff --git a/lps/config/config.prod.toml.example b/lps/config/config.prod.toml.example new file mode 100644 index 0000000..1732334 --- /dev/null +++ b/lps/config/config.prod.toml.example @@ -0,0 +1,82 @@ +# 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 = [""] # 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 = "" +write_pw = "" +read_host = "host.docker.internal" +read_port = 5432 +read_id = "" +read_pw = "" +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 = "" +secret = "" + +[OpenAIConfig] +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 diff --git a/lps/config/server_configs.py b/lps/config/server_configs.py index 600b952..712634a 100644 --- a/lps/config/server_configs.py +++ b/lps/config/server_configs.py @@ -8,7 +8,7 @@ from config.config_models import ( # ── 설정 소스는 TOML 하나다(2026-07-13 협의 — env/.env 이중 관리 제거). ── # env 는 부트스트랩·실행 입력만 남는다: -# APP_ENV : 어떤 toml 을 읽을지(local=호스트 실행 / docker=컨테이너에 마운트된 config.docker.toml) +# APP_ENV : 어떤 toml 을 읽을지 — local(호스트 실행) / dev·prod(컨테이너에 마운트된 config..toml) # PROCESS_COUNT : 실행 스크립트·부하벤치의 대화형 입력(uvicorn 워커 수 임시 override) # WORKER_CONCURRENCY : 워커 실행 스크립트의 대화형 입력(worker_main 이 읽음) # LPS_LIVE : 라이브 스모크 테스트 옵트인(설정이 아니라 실행 스위치) @@ -21,7 +21,7 @@ _config_file = os.path.join(_config_dir, f"config.{APP_ENV}.toml") if not os.path.exists(_config_file): raise FileNotFoundError( f"설정 파일이 없습니다: {_config_file} (APP_ENV={APP_ENV}). " - "호스트 실행은 config.local.toml 준비, 컨테이너는 config.docker.toml 마운트를 확인하세요." + "호스트 실행은 config.local.toml 준비, 컨테이너(dev/prod)는 config..toml 마운트를 확인하세요." ) configs = Configs(_config_file) diff --git a/lps/docs/operations.md b/lps/docs/operations.md index 8e736eb..04599a2 100644 --- a/lps/docs/operations.md +++ b/lps/docs/operations.md @@ -19,8 +19,9 @@ cp config/config.local.toml.example config/config.local.toml | `[DecodoConfig]` | 프록시 정보(비워두면 프록시 미사용) | > **설정 소스는 TOML 하나다**(2026-07-13 협의 — env/.env 이중 관리 제거). 호스트 실행은 -> `config.local.toml`, Docker 는 `config.docker.toml`(example 복사)을 컨테이너에 마운트하고 -> `APP_ENV=docker` 로 읽는다. 이미지에는 시크릿이 없고(빌드 시 `.dockerignore` 제외), 마운트를 +> `config.local.toml`, Docker(dev/prod 서버)는 `config.dev.toml`/`config.prod.toml`(각 example 복사)을 +> 컨테이너에 마운트하고 `APP_ENV` 로 선택해 읽는다(`./run_docker.sh` 가 대화형으로 처리). +> 이미지에는 시크릿이 없고(빌드 시 `.dockerignore` 제외), 마운트를 > 잊으면 기동 시 FileNotFoundError 로 즉시 실패한다. env 는 `APP_ENV`·실행 스크립트의 대화형 > 입력(`PROCESS_COUNT`/`WORKER_CONCURRENCY`)·`LPS_LIVE`(테스트)만 남는다. @@ -32,6 +33,14 @@ psql -h 127.0.0.1 -U postgres -d lps_db -c "CREATE EXTENSION IF NOT EXISTS pgcry ## 2. 실행 +**환경 개요** — 환경은 `APP_ENV` 가 고르는 `config..toml` 하나로 결정된다: + +| 환경 | 실행 위치 | 설정 파일 | 실행 방법 | +|------|----------|-----------|----------| +| local | 개발자 호스트 | `config.local.toml` | `./run_local_server.sh` + `./run_local_worker.sh` | +| dev | 개발 서버(Docker) | `config.dev.toml` (example 복사) | `./run_docker.sh` → dev 선택 | +| prod | 운영 서버(Docker) | `config.prod.toml` (example 복사) | `./run_docker.sh` → prod 선택 (guard 키 검사·API 127.0.0.1 바인드 자동) | + **API 서버** (요청 접수) ```bash ./run_local_server.sh # → http://localhost:9600/docs @@ -206,8 +215,8 @@ docker ps # lps-worker "(healthy)" 확인 - **워커 = 헤드풀 Chromium + Xvfb**(`Dockerfile.worker`): **headless 는 Akamai·Cloudflare Turnstile 에 탐지됨**(실측). Xvfb 가상 디스플레이로 headful 실행. - **API = lean**(`Dockerfile`, 브라우저 불필요). - **시크릿은 이미지에 없음(강제)**: 이미지는 example config 로 빌드된다(`.dockerignore` 가 - config.local.toml·`.profiles/` 제외). 실값은 **`lps/config/config.docker.toml`**(example 복사, - 미커밋)을 compose 가 마운트해 주입(`APP_ENV=docker`). 마운트를 잊으면 기동 시 즉시 실패. 기동 로그의 + config.local.toml·`.profiles/` 제외). 실값은 **`lps/config/config..toml`**(dev/prod example 복사, + 미커밋)을 compose 가 마운트해 주입(`APP_ENV` 선택). 마운트를 잊으면 기동 시 즉시 실패. 기동 로그의 `AI: ON/OFF`·`DECODO 프록시: ON/OFF` 로 주입 성공을 반드시 확인할 것. - **Chrome 프로필 영속 볼륨**(`lps-profiles:/profiles`, `[WorkerConfig].profile_dir`): 재시작해도 cf_clearance 유지 → 재웜업 회피. - **워커 헬스**: HEALTHCHECK(하트비트<120s)로 행 워커 감지. compose 의 `restart` 는 unhealthy 를 @@ -218,7 +227,7 @@ docker ps # lps-worker "(healthy)" 확인 ([DecodoConfig].port_start/end) 자동 반영 — 코드에 포트 수 하드코딩 없음. 튜닝은 `ip_session` 분석 쿼리(database.md) 참고. - **API guard**: `[WebServerConfig].api_keys` 설정 시 `/v1/*` 전체에 X-API-Key 검증(복수 키 — - 무중단 교체). 개발(local/dev)은 빈값=개방 모드. **prod 체크리스트**: ① config.docker.toml 에 + 무중단 교체). 개발(local/dev)은 빈값=개방 모드. **prod 체크리스트**: ① config.prod.toml 에 `api_keys` 채움(negodata 쪽은 `lps_api_key` 에 같은 키 — 헤더 자동 첨부) ② lps-api 포트 공개 제거(내부 네트워크만, `ports:` 삭제) ③ 기동 로그에서 `API guard ON` 확인. diff --git a/lps/run_docker.sh b/lps/run_docker.sh new file mode 100755 index 0000000..bec5199 --- /dev/null +++ b/lps/run_docker.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# LPS Docker 실행(대화형) — dev/prod 환경을 골라 lps-api/lps-worker 를 관리한다. +# 환경 = APP_ENV 가 고르는 config..toml (미커밋, example 복사 후 값 채움). +# 호스트(로컬) 실행은 run_local_server.sh / run_local_worker.sh 를 사용. +set -euo pipefail +cd "$(dirname "$0")/.." # 리포 루트(docker-compose.yml 위치) + +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" +if [[ ! -f "$CFG" ]]; then + echo "[warn] $CFG 가 없습니다 (환경별 설정은 미커밋 — 서버마다 직접 준비)." + read -rp "example 을 복사해 생성할까요? [Y/n] " yn + if [[ "${yn:-Y}" =~ ^[Yy] ]]; then + cp "lps/config/config.${ENV}.toml.example" "$CFG" + echo "[ok] $CFG 생성 — DB 접속·API 키 등 값을 채운 뒤 다시 실행하세요." + fi + exit 0 +fi + +# prod 안전 점검: guard 키가 비어 있으면 API 가 개방 모드로 뜬다(위험) — 명시적 확인을 받는다. +if [[ "$ENV" == "prod" ]] && grep -qE '^\s*api_keys\s*=\s*\[\s*\]' "$CFG"; then + echo "[warn] prod 인데 [WebServerConfig].api_keys 가 비어 있습니다 → API 개방 모드(무인증)." + read -rp "그래도 계속할까요? [y/N] " go + [[ "${go:-N}" =~ ^[Yy] ]] || { echo "중단합니다."; exit 1; } +fi + +export APP_ENV="$ENV" +if [[ "$ENV" == "prod" ]]; then + export LPS_API_BIND=127.0.0.1 # prod 는 API 포트를 호스트 내부만 바인드(외부는 리버스프록시 경유) +fi + +echo "" +echo "환경: $ENV · 설정: $CFG$([[ "$ENV" == "prod" ]] && echo ' · API 바인드: 127.0.0.1')" +read -rp "동작 선택 [1] 시작/재빌드(기본) [2] 재시작 [3] 중지 [4] 로그 : " act +case "${act:-1}" in + 1) docker compose up -d --build lps-api lps-worker autoheal + echo "[ok] 기동 — 로그의 'API guard ON/개방 모드' · 'DECODO 프리플라이트' 확인: docker compose logs -f lps-api lps-worker" ;; + 2) docker compose restart lps-api lps-worker ;; + 3) docker compose stop lps-api lps-worker ;; + 4) docker compose logs -f lps-api lps-worker ;; + *) echo "[error] 알 수 없는 선택: $act"; exit 1 ;; +esac