fix(lps): Docker 이미지 무시크릿화 + 배포 견고화 — 리뷰 발견사항 일괄 적용
시크릿 유출 차단(핵심): - config.local.toml(OpenAI·DECODO·네이버 키)이 COPY . . 로 이미지에 구워지던 문제 — .dockerignore 제외 + 빌드 시 example(플레이스홀더) 복사로 대체. 실값은 compose env 주입(리포 루트 .env, 템플릿 .env.example) - DECODO_PORT_START/END/SESSION_MINUTES env override 추가 — 포트가 toml(플레이스홀더 0)에만 있으면 자격증명을 넣어도 프록시가 조용히 꺼지는 구멍 봉합 배포 견고화: - API Dockerfile 에 HEALTHCHECK(/healthz) 추가 - autoheal 컨테이너 추가 — compose restart 는 unhealthy 를 재시작하지 않으므로 라벨(autoheal=true) 기반 자동 재시작 담당 - 이미지 python 3.12→3.14 정렬(로컬 개발·테스트 환경과 일치) - API 이미지 경량화: requirements-api.txt 분리(크롤 의존성 제거, 330MB) 검증: 양 이미지 빌드 성공, 이미지 내 시크릿·.profiles 부재 확인, 무시크릿 API 이미지 스모크(healthz/readyz/HEALTHCHECK healthy) 통과 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c075dfbcfb
commit
a267326bf7
17
.env.example
Normal file
17
.env.example
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# docker compose 용 환경변수 템플릿 — 복사해서 사용: cp .env.example .env
|
||||||
|
# 실제 값(.env)은 커밋하지 않는다(.gitignore). 이미지에는 시크릿이 없으므로(lps 는 example
|
||||||
|
# config 로 빌드) 아래 값이 없으면 해당 기능이 꺼진 채 뜬다(주석 참고).
|
||||||
|
|
||||||
|
# ── LPS DB (미설정 시 postgres/postgres) ──
|
||||||
|
LPS_DB_USER=postgres
|
||||||
|
LPS_DB_PASSWORD=postgres
|
||||||
|
|
||||||
|
# ── LPS 워커 시크릿 ──
|
||||||
|
OPENAI_API_KEY= # 비면 AI 유사도 판정 OFF
|
||||||
|
NAVER_KEYS= # "id1:secret1,id2:secret2" — 네이버 쇼핑 오픈API 키(여러 개면 로테이션)
|
||||||
|
DECODO_HOST= # 예: gate.decodo.com — DECODO 4종이 비면 프록시 미사용(직접 연결)
|
||||||
|
DECODO_USERNAME=
|
||||||
|
DECODO_PASSWORD=
|
||||||
|
DECODO_PORT_START=0 # 예: 10001
|
||||||
|
DECODO_PORT_END=0 # 예: 10010
|
||||||
|
DECODO_COST_PER_GB=0 # 요금($/GB) — 검색 원가 계측용(예: 3.0)
|
||||||
@ -115,6 +115,8 @@ services:
|
|||||||
|
|
||||||
# ── LPS (인터넷 최저가 검색) ──────────────────────────────────
|
# ── LPS (인터넷 최저가 검색) ──────────────────────────────────
|
||||||
# API(요청 접수, lean) + 워커(크롤, 헤드풀 Chromium+Xvfb). DB 는 외부(host.docker.internal).
|
# API(요청 접수, lean) + 워커(크롤, 헤드풀 Chromium+Xvfb). DB 는 외부(host.docker.internal).
|
||||||
|
# 이미지엔 시크릿이 없다(example config 로 빌드) — 실값은 아래 env 로 주입.
|
||||||
|
# 시크릿 값은 리포 루트 .env 파일에 채운다(.env.example 참고, .env 는 미커밋).
|
||||||
lps-api:
|
lps-api:
|
||||||
build:
|
build:
|
||||||
context: ./lps
|
context: ./lps
|
||||||
@ -122,7 +124,9 @@ services:
|
|||||||
container_name: lps-api
|
container_name: lps-api
|
||||||
environment:
|
environment:
|
||||||
APP_ENV: local
|
APP_ENV: local
|
||||||
DB_HOST: host.docker.internal # 컨테이너→호스트 DB (config.local.toml의 127.0.0.1 override)
|
DB_HOST: host.docker.internal # 컨테이너→호스트 DB (example toml 의 127.0.0.1 override)
|
||||||
|
DB_USER: ${LPS_DB_USER:-postgres}
|
||||||
|
DB_PASSWORD: ${LPS_DB_PASSWORD:-postgres}
|
||||||
PYTHONUNBUFFERED: "1"
|
PYTHONUNBUFFERED: "1"
|
||||||
# 멀티코어: PROCESS_COUNT(uvicorn 워커=코어수)를 올리면 커넥션 풀은 자동 산정된다.
|
# 멀티코어: PROCESS_COUNT(uvicorn 워커=코어수)를 올리면 커넥션 풀은 자동 산정된다.
|
||||||
# (pool+overflow)×2엔진×PROCESS_COUNT ≤ DB_CONNECTION_BUDGET 를 config 가 스스로 보장.
|
# (pool+overflow)×2엔진×PROCESS_COUNT ≤ DB_CONNECTION_BUDGET 를 config 가 스스로 보장.
|
||||||
@ -132,6 +136,8 @@ services:
|
|||||||
- "9600:9600"
|
- "9600:9600"
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
|
labels:
|
||||||
|
autoheal: "true" # HEALTHCHECK 실패 시 autoheal 이 재시작
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
@ -145,22 +151,45 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
APP_ENV: local
|
APP_ENV: local
|
||||||
DB_HOST: host.docker.internal
|
DB_HOST: host.docker.internal
|
||||||
|
DB_USER: ${LPS_DB_USER:-postgres}
|
||||||
|
DB_PASSWORD: ${LPS_DB_PASSWORD:-postgres}
|
||||||
PYTHONUNBUFFERED: "1"
|
PYTHONUNBUFFERED: "1"
|
||||||
WORKER_CONCURRENCY: "1" # 상품 동시 검색 수(워커별 브라우저 세트, Chrome 4×N)
|
WORKER_CONCURRENCY: "1" # 상품 동시 검색 수(워커별 브라우저 세트, Chrome 4×N)
|
||||||
LPS_PROFILE_DIR: /profiles # Chrome 프로필을 영속 볼륨에 → 재시작해도 cf_clearance 유지(재웜업 회피)
|
LPS_PROFILE_DIR: /profiles # Chrome 프로필을 영속 볼륨에 → 재시작해도 cf_clearance 유지(재웜업 회피)
|
||||||
# LPS_FALLBACKS: "gmarket,auction,st11" # 오픈마켓 폴백(기본 OFF — 켜기 전 라이브 스모크로 셀렉터 점검)
|
# LPS_FALLBACKS: "gmarket,auction,st11" # 오픈마켓 폴백(기본 OFF — 켜기 전 라이브 스모크로 셀렉터 점검)
|
||||||
# ── 프로덕션 시크릿 주입(이미지에 안 굽고 env 로). 아래 주석 해제 후 값 채우거나 secrets 매니저 연동 ──
|
# LPS_JOB_DEADLINE_SEC: "300" # 잡 1건 처리 상한(행 방어) — 기본 300s
|
||||||
# OPENAI_API_KEY: ${OPENAI_API_KEY}
|
# ── 시크릿 주입(이미지엔 없음 — 필수). 리포 루트 .env 에 값 채움(.env.example 참고) ──
|
||||||
# DECODO_USERNAME: ${DECODO_USERNAME}
|
OPENAI_API_KEY: ${OPENAI_API_KEY:-} # 비면 AI 판정 OFF
|
||||||
# DECODO_PASSWORD: ${DECODO_PASSWORD}
|
NAVER_KEYS: ${NAVER_KEYS:-} # "id1:secret1,id2:secret2" — 비면 네이버 검색 실패
|
||||||
# DECODO_HOST: ${DECODO_HOST}
|
DECODO_HOST: ${DECODO_HOST:-} # DECODO 4종 비면 프록시 미사용(직접 연결)
|
||||||
# NAVER_KEYS: ${NAVER_KEYS} # "id1:secret1,id2:secret2"
|
DECODO_USERNAME: ${DECODO_USERNAME:-}
|
||||||
|
DECODO_PASSWORD: ${DECODO_PASSWORD:-}
|
||||||
|
DECODO_PORT_START: ${DECODO_PORT_START:-0}
|
||||||
|
DECODO_PORT_END: ${DECODO_PORT_END:-0}
|
||||||
|
DECODO_COST_PER_GB: ${DECODO_COST_PER_GB:-0}
|
||||||
volumes:
|
volumes:
|
||||||
- lps-profiles:/profiles # Chrome 프로필(쿠키) 영속
|
- 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 종료 유예(LPS_SHUTDOWN_GRACE_SEC=60 + 정리 여유) — 기본 10s 면 하던 잡 마무리 전에 SIGKILL
|
stop_grace_period: 75s # graceful 종료 유예(LPS_SHUTDOWN_GRACE_SEC=60 + 정리 여유) — 기본 10s 면 하던 잡 마무리 전에 SIGKILL
|
||||||
|
labels:
|
||||||
|
autoheal: "true" # 하트비트 HEALTHCHECK 실패(행/좀비) 시 autoheal 이 재시작
|
||||||
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options: { max-size: "10m", max-file: "5" }
|
||||||
|
|
||||||
|
# HEALTHCHECK 실패 컨테이너 자동 재시작 — compose 의 restart 는 '프로세스 종료'만 다루고
|
||||||
|
# unhealthy 는 표시만 하므로, autoheal 라벨 붙은 컨테이너(lps-api/lps-worker)를 감시해 재시작한다.
|
||||||
|
# docker.sock 마운트 = 도커 제어 권한이므로 신뢰 환경에서만 사용.
|
||||||
|
autoheal:
|
||||||
|
image: willfarrell/autoheal:latest
|
||||||
|
container_name: autoheal
|
||||||
|
environment:
|
||||||
|
AUTOHEAL_CONTAINER_LABEL: autoheal
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
|
|||||||
@ -10,3 +10,6 @@ loadtest/
|
|||||||
# Chrome 프로필(쿠키·cf_clearance 세션) — 이미지에 구우면 세션 유출 + 149MB 비대.
|
# Chrome 프로필(쿠키·cf_clearance 세션) — 이미지에 구우면 세션 유출 + 149MB 비대.
|
||||||
# 컨테이너는 빈 프로필로 시작해 웜업으로 쿠키를 만들고 volume(/profiles)에 영속한다.
|
# 컨테이너는 빈 프로필로 시작해 웜업으로 쿠키를 만들고 volume(/profiles)에 영속한다.
|
||||||
.profiles/
|
.profiles/
|
||||||
|
# 시크릿(OpenAI·DECODO·네이버 키) — 이미지에 굽지 않는다. Dockerfile 이 example 을
|
||||||
|
# 복사해 넣고, 실값은 compose env 로 주입(server_configs 의 env override).
|
||||||
|
config/config.local.toml
|
||||||
|
|||||||
@ -1,18 +1,27 @@
|
|||||||
# LPS API 서버 이미지 — 요청 접수/조회만(브라우저 불필요, lean).
|
# LPS API 서버 이미지 — 요청 접수/조회만(브라우저 불필요, lean).
|
||||||
# 크롤은 별도 워커 이미지(Dockerfile.worker, Chromium+Xvfb)가 담당한다.
|
# 크롤은 별도 워커 이미지(Dockerfile.worker, Chromium+Xvfb)가 담당한다.
|
||||||
FROM python:3.12-slim
|
# 시크릿은 이미지에 굽지 않는다 — config 는 example(플레이스홀더)로 대체되고,
|
||||||
|
# 실제 값은 compose 의 env(DB_USER/DB_PASSWORD 등)로 주입된다(server_configs override).
|
||||||
|
FROM python:3.14-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 의존성 먼저 설치 (레이어 캐시 활용)
|
# 의존성 먼저 설치 (레이어 캐시 활용) — API 전용 경량 세트
|
||||||
COPY requirements.txt .
|
COPY requirements-api.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements-api.txt
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# 항상 APP_ENV=local 로 실행 → config.local.toml 사용.
|
# 시크릿 든 config.local.toml 은 .dockerignore 로 제외됨 → example(플레이스홀더)로 대체.
|
||||||
|
# 실값은 env 주입: DB_HOST/DB_USER/DB_PASSWORD/DB_NAME, PROCESS_COUNT, DB_CONNECTION_BUDGET …
|
||||||
|
RUN cp config/config.local.toml.example config/config.local.toml
|
||||||
|
|
||||||
|
# 항상 APP_ENV=local 로 실행 → config.local.toml(=example 사본) + env override.
|
||||||
ENV APP_ENV=local
|
ENV APP_ENV=local
|
||||||
|
|
||||||
EXPOSE 9600
|
EXPOSE 9600
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:9600/healthz', timeout=4).status==200 else 1)"
|
||||||
|
|
||||||
CMD ["python", "web_main.py"]
|
CMD ["python", "web_main.py"]
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
# docker compose build lps-worker && docker compose up -d lps-worker
|
# docker compose build lps-worker && docker compose up -d lps-worker
|
||||||
# 스텔스 참고: 로컬 Mac 은 실제 Chrome(channel=chrome), 컨테이너는 시스템 chromium(executable_path).
|
# 스텔스 참고: 로컬 Mac 은 실제 Chrome(channel=chrome), 컨테이너는 시스템 chromium(executable_path).
|
||||||
# 프로덕션에서 stealth 가 부족하면 amd64 이미지에 google-chrome-stable 설치 후 LPS_CHROME_CHANNEL=chrome 로 전환.
|
# 프로덕션에서 stealth 가 부족하면 amd64 이미지에 google-chrome-stable 설치 후 LPS_CHROME_CHANNEL=chrome 로 전환.
|
||||||
FROM python:3.12-slim
|
FROM python:3.14-slim
|
||||||
|
|
||||||
# chromium(런타임 의존성 apt 가 자동 해결) + Xvfb + 한글 폰트
|
# chromium(런타임 의존성 apt 가 자동 해결) + Xvfb + 한글 폰트
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
@ -19,6 +19,10 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# 시크릿 든 config.local.toml 은 .dockerignore 로 제외됨 → example(플레이스홀더)로 대체.
|
||||||
|
# 실값은 compose env 로 주입: DB_*, OPENAI_API_KEY, DECODO_*(포트 포함), NAVER_KEYS.
|
||||||
|
RUN cp config/config.local.toml.example config/config.local.toml
|
||||||
|
|
||||||
ENV APP_ENV=local \
|
ENV APP_ENV=local \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
LPS_CHROME_EXECUTABLE=/usr/bin/chromium \
|
LPS_CHROME_EXECUTABLE=/usr/bin/chromium \
|
||||||
|
|||||||
@ -85,6 +85,12 @@ def _apply_secret_env_override():
|
|||||||
v = os.environ.get(f"DECODO_{k.upper()}")
|
v = os.environ.get(f"DECODO_{k.upper()}")
|
||||||
if v:
|
if v:
|
||||||
setattr(decodo_config, k, v)
|
setattr(decodo_config, k, v)
|
||||||
|
# 포트 범위도 시크릿과 함께 env 주입 — example(플레이스홀더 0) 기반 이미지에서 이게 없으면
|
||||||
|
# 자격증명을 넣어도 enabled=False(포트 0)로 프록시가 조용히 꺼진다.
|
||||||
|
for k in ("port_start", "port_end", "session_minutes"):
|
||||||
|
v = os.environ.get(f"DECODO_{k.upper()}")
|
||||||
|
if v:
|
||||||
|
setattr(decodo_config, k, int(v))
|
||||||
if os.environ.get("DECODO_COST_PER_GB"):
|
if os.environ.get("DECODO_COST_PER_GB"):
|
||||||
decodo_config.cost_per_gb = float(os.environ["DECODO_COST_PER_GB"])
|
decodo_config.cost_per_gb = float(os.environ["DECODO_COST_PER_GB"])
|
||||||
# NAVER_KEYS="id1:secret1,id2:secret2" 형식으로 키 로테이션 주입
|
# NAVER_KEYS="id1:secret1,id2:secret2" 형식으로 키 로테이션 주입
|
||||||
|
|||||||
@ -18,7 +18,10 @@ cp config/config.local.toml.example config/config.local.toml
|
|||||||
| `[OpenAIConfig]` | `api_key` (AI 판정·검색어 생성용) |
|
| `[OpenAIConfig]` | `api_key` (AI 판정·검색어 생성용) |
|
||||||
| `[DecodoConfig]` | 프록시 정보(비워두면 프록시 미사용) |
|
| `[DecodoConfig]` | 프록시 정보(비워두면 프록시 미사용) |
|
||||||
|
|
||||||
> **한 파일에 설정+시크릿 통합** 관리. 배포 시엔 이 파일을 마운트하거나, 환경별로 바뀌는 값(DB_HOST 등)만 환경변수로 덮어씁니다.
|
> **한 파일에 설정+시크릿 통합** 관리(로컬). **Docker 이미지에는 이 파일이 들어가지 않는다** —
|
||||||
|
> 빌드 시 `.dockerignore` 로 제외되고 example(플레이스홀더)이 대신 들어가며, 실값은 compose 의
|
||||||
|
> env 로 주입한다(리포 루트 `.env`, 템플릿 `.env.example`). `server_configs` 의 env override 가
|
||||||
|
> DB 접속·`OPENAI_API_KEY`·`DECODO_*`(포트 포함)·`NAVER_KEYS` 를 모두 덮는다.
|
||||||
|
|
||||||
**DB 준비**: `lps_db` 생성 후 최초 실행 시 테이블 자동 생성.
|
**DB 준비**: `lps_db` 생성 후 최초 실행 시 테이블 자동 생성.
|
||||||
```bash
|
```bash
|
||||||
@ -180,9 +183,14 @@ docker ps # lps-worker "(healthy)" 확인
|
|||||||
```
|
```
|
||||||
- **워커 = 헤드풀 Chromium + Xvfb**(`Dockerfile.worker`): **headless 는 Akamai·Cloudflare Turnstile 에 탐지됨**(실측). Xvfb 가상 디스플레이로 headful 실행.
|
- **워커 = 헤드풀 Chromium + Xvfb**(`Dockerfile.worker`): **headless 는 Akamai·Cloudflare Turnstile 에 탐지됨**(실측). Xvfb 가상 디스플레이로 headful 실행.
|
||||||
- **API = lean**(`Dockerfile`, 브라우저 불필요).
|
- **API = lean**(`Dockerfile`, 브라우저 불필요).
|
||||||
- **시크릿은 이미지에 안 굽고 env 주입**: `OPENAI_API_KEY`·`DECODO_*`·`NAVER_KEYS` (compose 주석 참고). 로컬은 config.local.toml.
|
- **시크릿은 이미지에 없음(강제)**: 이미지는 example config 로 빌드된다(`.dockerignore` 가
|
||||||
|
config.local.toml·`.profiles/` 제외). 실값은 **리포 루트 `.env`**(템플릿 `.env.example`)에서
|
||||||
|
compose env 로 주입. `.env` 없이 뜨면 AI OFF·프록시 미사용으로 조용히 동작하니, 기동 로그의
|
||||||
|
`AI: ON/OFF`·`DECODO 프록시: ON/OFF` 로 주입 성공을 반드시 확인할 것.
|
||||||
- **Chrome 프로필 영속 볼륨**(`lps-profiles:/profiles`, `LPS_PROFILE_DIR`): 재시작해도 cf_clearance 유지 → 재웜업 회피.
|
- **Chrome 프로필 영속 볼륨**(`lps-profiles:/profiles`, `LPS_PROFILE_DIR`): 재시작해도 cf_clearance 유지 → 재웜업 회피.
|
||||||
- **워커 헬스**: HEALTHCHECK(하트비트<120s)로 행 워커 감지. k8s 는 liveness probe 로 자동 재시작 연결.
|
- **워커 헬스**: HEALTHCHECK(하트비트<120s)로 행 워커 감지. compose 의 `restart` 는 unhealthy 를
|
||||||
|
재시작하지 않으므로 **autoheal 컨테이너**(라벨 `autoheal=true` 감시)가 재시작 담당. k8s 는 liveness probe 로 대체.
|
||||||
|
- **잡 데드라인**: 잡 1건 300s 상한(`LPS_JOB_DEADLINE_SEC`) — 크롤 행이 워커 슬롯을 영구 점유하지 못하게 함.
|
||||||
|
|
||||||
**남은 배포 과제**: API 인증·레이트리밋(비용 남용 방지), 다중 레플리카 시 분산 레이트리밋/프록시 IP 조정.
|
**남은 배포 과제**: API 인증·레이트리밋(비용 남용 방지), 다중 레플리카 시 분산 레이트리밋/프록시 IP 조정.
|
||||||
**비용**: 대역폭이 원가의 대부분(오픈마켓 크롤) — 같은 상품 재크롤을 줄이는 **TTL 캐시**가 다음 절감 후보.
|
**비용**: 대역폭이 원가의 대부분(오픈마켓 크롤) — 같은 상품 재크롤을 줄이는 **TTL 캐시**가 다음 절감 후보.
|
||||||
|
|||||||
11
lps/requirements-api.txt
Normal file
11
lps/requirements-api.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# LPS API 서버 전용 의존성 — 요청 접수/조회만(크롤 없음).
|
||||||
|
# 크롤 의존성(patchright·curl_cffi·selectolax·openai)은 워커 전용(requirements.txt).
|
||||||
|
# web_main import 체인 정적 추적으로 확인(2026-07-10): fastapi/uvicorn/sqlalchemy/pydantic 만 필요.
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
sqlalchemy>=2.0
|
||||||
|
greenlet # SQLAlchemy async 의 sync/async 브리지에 필수
|
||||||
|
asyncpg
|
||||||
|
orjson
|
||||||
|
pydantic>=2.0
|
||||||
|
httpx # (얇음) 향후 API→워커 헬스 프록시 등 대비 유지
|
||||||
Loading…
Reference in New Issue
Block a user