시크릿 유출 차단(핵심): - 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>
115 lines
5.5 KiB
Python
115 lines
5.5 KiB
Python
import os
|
||
|
||
from config.config_loader import Configs
|
||
from config.config_models import (
|
||
WebServerConfig, LogConfig, MainDBConfig, NaverConfig, NaverKey, OpenAIConfig, DecodoConfig,
|
||
)
|
||
|
||
# 실행 환경 결정 (기본 local). 환경변수 APP_ENV 로 변경.
|
||
APP_ENV = os.environ.get("APP_ENV", "local")
|
||
|
||
_config_dir = os.path.dirname(__file__)
|
||
_config_file = os.path.join(_config_dir, f"config.{APP_ENV}.toml")
|
||
|
||
# 운영 전제: 항상 APP_ENV=local 로 띄운다 → config.local.toml 사용 (test/docker 도 local 로 실행).
|
||
if not os.path.exists(_config_file):
|
||
raise FileNotFoundError(f"설정 파일이 없습니다: {_config_file} (APP_ENV={APP_ENV}). APP_ENV=local 로 실행하세요.")
|
||
|
||
configs = Configs(_config_file)
|
||
|
||
web_server_config: WebServerConfig = configs.get(WebServerConfig)
|
||
log_config: LogConfig = configs.get(LogConfig)
|
||
main_db_config: MainDBConfig = configs.get(MainDBConfig)
|
||
# 시크릿 포함 설정도 TOML 로 통합. 섹션이 없으면 기본값(빈/비활성).
|
||
naver_config: NaverConfig = configs.get(NaverConfig) or NaverConfig()
|
||
openai_config: OpenAIConfig = configs.get(OpenAIConfig) or OpenAIConfig()
|
||
decodo_config: DecodoConfig = configs.get(DecodoConfig) or DecodoConfig()
|
||
|
||
|
||
# DB 접속 env override (config.local.toml 유지, 도커에서 host 만 교체). 로컬은 env 미설정 → toml 그대로.
|
||
def _apply_db_env_override(cfg: MainDBConfig):
|
||
h = os.environ.get("DB_HOST")
|
||
if h:
|
||
cfg.write_host = cfg.read_host = h
|
||
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"]
|
||
# 커넥션 예산 override (자동 산정용). env DB_CONNECTION_BUDGET.
|
||
if os.environ.get("DB_CONNECTION_BUDGET"):
|
||
cfg.connection_budget = int(os.environ["DB_CONNECTION_BUDGET"])
|
||
|
||
|
||
def _autosize_pool(cfg: MainDBConfig, process_count: int):
|
||
"""process_count 기반 커넥션 풀 자동 산정.
|
||
|
||
실제 동시 커넥션 = (pool_size + max_overflow) × 2엔진(R/W) × process_count.
|
||
이 값이 connection_budget 를 넘지 않도록 pool_size/max_overflow 를 역산한다.
|
||
budget<=0 이면 비활성(toml 의 pool_size/max_overflow 그대로 사용).
|
||
|
||
반환: 자동 산정을 수행했으면 (pool_size, max_overflow), 아니면 None.
|
||
"""
|
||
budget = cfg.connection_budget
|
||
if budget <= 0:
|
||
return None
|
||
pc = max(1, process_count)
|
||
# 워커·엔진당 커넥션 = 예산 / (2엔진 × process_count). 최소 1(pool_size>=1) 보장.
|
||
# (예산 준수가 최우선 — process_count 가 너무 크면 엔진당 1커넥션까지 줄여서라도 예산을 넘지 않는다)
|
||
per_engine = max(1, budget // (2 * pc))
|
||
# 정상(pool) 60% + 버스트(overflow) 40% 로 분할.
|
||
cfg.pool_size = max(1, round(per_engine * 0.6))
|
||
cfg.max_overflow = max(0, per_engine - cfg.pool_size)
|
||
return cfg.pool_size, cfg.max_overflow
|
||
|
||
|
||
def _apply_pool_env_override(cfg: MainDBConfig):
|
||
"""명시적 풀 override — 자동 산정보다 우선(테스트·특수 배포용)."""
|
||
if os.environ.get("DB_POOL_SIZE"):
|
||
cfg.pool_size = int(os.environ["DB_POOL_SIZE"])
|
||
if os.environ.get("DB_MAX_OVERFLOW"):
|
||
cfg.max_overflow = int(os.environ["DB_MAX_OVERFLOW"])
|
||
|
||
|
||
# 시크릿 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)
|
||
# 포트 범위도 시크릿과 함께 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"):
|
||
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_secret_env_override()
|
||
|
||
# uvicorn 워커 수(멀티코어) env override — 부하테스트에서 1↔N 비교용(코드/toml 수정 없이).
|
||
if os.environ.get("PROCESS_COUNT"):
|
||
web_server_config.process_count = int(os.environ["PROCESS_COUNT"])
|
||
|
||
# 커넥션 풀 자동 산정: process_count(위에서 확정) 기준으로 예산 안에 맞춘다.
|
||
# → 멀티워커 배포 시 풀 오버서브스크립션(→커넥션 고갈)을 config 가 스스로 방지.
|
||
_pool_autosized = _autosize_pool(main_db_config, web_server_config.process_count)
|
||
# 명시적 DB_POOL_SIZE/DB_MAX_OVERFLOW 는 자동 산정보다 우선(최종 override).
|
||
_apply_pool_env_override(main_db_config)
|