feat(lps): P2 시크릿 env 주입 + Chrome 프로필 영속 볼륨

프로덕션에서 시크릿을 이미지에 굽지 않고 env 로 주입, 재시작 시 챌린지 쿠키 유지.

- server_configs: 시크릿 env override 추가(house 의 DB override 패턴 확장) —
  OPENAI_API_KEY/MODEL, DECODO_HOST/USERNAME/PASSWORD/COST_PER_GB, NAVER_KEYS="id:sec,...".
  로컬은 env 미설정 → config.local.toml 그대로.
- worker_main: Chrome 프로필 경로를 LPS_PROFILE_DIR(기본 /tmp)로 env 화 → 영속 볼륨 마운트 가능.
- docker-compose: lps-worker 에 lps-profiles 볼륨(/profiles) + 시크릿 env 주입 주석 예시.
- config.example: 프로덕션 env 주입 목록 문서화.

검증: env override 동작(OpenAI/DECODO/Naver), 프로필이 /profiles 볼륨에 Cookies 포함 저장(cf_clearance 유지). 89 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
민헌 2026-07-09 23:04:37 +09:00
parent 94d4e54d5e
commit 3a8d669970
4 changed files with 45 additions and 2 deletions

View File

@ -143,6 +143,15 @@ services:
DB_HOST: host.docker.internal DB_HOST: host.docker.internal
PYTHONUNBUFFERED: "1" PYTHONUNBUFFERED: "1"
WORKER_CONCURRENCY: "1" # 상품 동시 검색 수(워커별 브라우저 세트, Chrome 4×N) WORKER_CONCURRENCY: "1" # 상품 동시 검색 수(워커별 브라우저 세트, Chrome 4×N)
LPS_PROFILE_DIR: /profiles # Chrome 프로필을 영속 볼륨에 → 재시작해도 cf_clearance 유지(재웜업 회피)
# ── 프로덕션 시크릿 주입(이미지에 안 굽고 env 로). 아래 주석 해제 후 값 채우거나 secrets 매니저 연동 ──
# OPENAI_API_KEY: ${OPENAI_API_KEY}
# DECODO_USERNAME: ${DECODO_USERNAME}
# DECODO_PASSWORD: ${DECODO_PASSWORD}
# DECODO_HOST: ${DECODO_HOST}
# NAVER_KEYS: ${NAVER_KEYS} # "id1:secret1,id2:secret2"
volumes:
- 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 을 많이 씀 — 부족하면 탭 크래시
@ -150,3 +159,6 @@ services:
logging: logging:
driver: json-file driver: json-file
options: { max-size: "10m", max-file: "5" } options: { max-size: "10m", max-file: "5" }
volumes:
lps-profiles:

View File

@ -1,6 +1,14 @@
# 복사해서 사용: cp config.local.toml.example config.local.toml # 복사해서 사용: cp config.local.toml.example config.local.toml
# 실제 config.local.toml 은 시크릿 포함이라 커밋하지 않는다(.gitignore: *.toml). # 실제 config.local.toml 은 시크릿 포함이라 커밋하지 않는다(.gitignore: *.toml).
# 모든 서버는 APP_ENV=local 로 띄우며 이 파일을 읽는다. # 모든 서버는 APP_ENV=local 로 띄우며 이 파일을 읽는다.
#
# ── 프로덕션: 시크릿을 이미지에 굽지 말고 env 로 주입(server_configs 가 override) ──
# DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME
# OPENAI_API_KEY / OPENAI_MODEL
# DECODO_HOST / DECODO_USERNAME / DECODO_PASSWORD / DECODO_COST_PER_GB
# NAVER_KEYS="id1:secret1,id2:secret2"
# LPS_PROFILE_DIR=/profiles (Chrome 프로필 영속 볼륨), LPS_CHROME_EXECUTABLE=/usr/bin/chromium
# → 배포 시엔 아래 시크릿 값을 비워두고 위 env 로 채우면 이미지에 시크릿이 안 남는다.
[WebServerConfig] [WebServerConfig]
server_name = "LpsServer" server_name = "LpsServer"
port = 9600 port = 9600

View File

@ -2,7 +2,7 @@ import os
from config.config_loader import Configs from config.config_loader import Configs
from config.config_models import ( from config.config_models import (
WebServerConfig, LogConfig, MainDBConfig, NaverConfig, OpenAIConfig, DecodoConfig, WebServerConfig, LogConfig, MainDBConfig, NaverConfig, NaverKey, OpenAIConfig, DecodoConfig,
) )
# 실행 환경 결정 (기본 local). 환경변수 APP_ENV 로 변경. # 실행 환경 결정 (기본 local). 환경변수 APP_ENV 로 변경.
@ -41,4 +41,25 @@ def _apply_db_env_override(cfg: MainDBConfig):
cfg.name = os.environ["DB_NAME"] cfg.name = os.environ["DB_NAME"]
# 시크릿 env override — 프로덕션에선 API 키를 이미지에 굽지 않고 env(또는 시크릿매니저)로 주입한다.
# 로컬은 env 미설정 → config.local.toml 값 그대로. (배포 시 toml 의 시크릿은 비워두고 아래 env 로 주입 권장)
def _apply_secret_env_override():
if os.environ.get("OPENAI_API_KEY"):
openai_config.api_key = os.environ["OPENAI_API_KEY"]
if os.environ.get("OPENAI_MODEL"):
openai_config.model = os.environ["OPENAI_MODEL"]
for k in ("host", "username", "password"):
v = os.environ.get(f"DECODO_{k.upper()}")
if v:
setattr(decodo_config, k, v)
if os.environ.get("DECODO_COST_PER_GB"):
decodo_config.cost_per_gb = float(os.environ["DECODO_COST_PER_GB"])
# NAVER_KEYS="id1:secret1,id2:secret2" 형식으로 키 로테이션 주입
nk = os.environ.get("NAVER_KEYS")
if nk:
naver_config.keys = [NaverKey(id=i, secret=s)
for i, s in (p.split(":", 1) for p in nk.split(",") if ":" in p)]
_apply_db_env_override(main_db_config) _apply_db_env_override(main_db_config)
_apply_secret_env_override()

View File

@ -40,7 +40,9 @@ def _build_worker(i: int, concurrency: int, has_openai: bool, neg_cache, history
suffix = f"_w{i}" if concurrency > 1 else "" suffix = f"_w{i}" if concurrency > 1 else ""
def _pf(source): # 워커별 Chrome 프로필 경로(중복 실행 시 ProcessSingleton 충돌 방지) def _pf(source): # 워커별 Chrome 프로필 경로(중복 실행 시 ProcessSingleton 충돌 방지)
return f"/tmp/lps_{source}{suffix}" # LPS_PROFILE_DIR 를 영속 볼륨으로 마운트하면 재시작해도 cf_clearance 등 쿠키 유지(재웜업 회피).
base = os.environ.get("LPS_PROFILE_DIR", "/tmp")
return f"{base}/lps_{source}{suffix}"
adapters = { adapters = {
"coupang": CoupangAdapter(headless=False, user_data_dir=_pf("coupang"), proxy=proxy, on_detect=bot_log.record), "coupang": CoupangAdapter(headless=False, user_data_dir=_pf("coupang"), proxy=proxy, on_detect=bot_log.record),