외부에서 API 를 함부로 호출(비용 발생 enqueue 등)하지 못하도록 정적 키 guard 를 추가한다. '키의 존재'가 토글 — 개발(local/dev)은 env 를 비워 개방 모드(기동 시 WARN), prod 만 키를 주입한다(협의 결정). - router/v1/validator/auth.py: X-API-Key 의존성 — secrets.compare_digest 상수시간 비교, 콤마 구분 복수 키(무중단 키 교체), 매 요청 env 조회 (재기동 없이 테스트 가능). /v1 라우터 전체에 적용. - /healthz·/readyz 는 라우터 밖이라 항상 개방(LB 프로브). - negodata lps_sync_service: LPS_API_KEY env 있으면 헤더 자동 첨부(한 곳). - compose(lps-api·negodata-backend) LPS_API_KEY 패스스루 + .env.example. - prod 체크리스트(operations.md): 키 주입 + lps-api 포트 비공개 + 기동 로그 'API guard ON' 확인. api.md 인증 섹션 추가. - 라이브 스모크: 무헤더/오키 401 · 정키 2종 200 · healthz 200 확인. - 테스트 6건 추가, 전체 141 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
import asyncio
|
|
import time
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import Depends, FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.middleware.gzip import GZipMiddleware
|
|
|
|
from common.alerts import run_pool_monitor
|
|
from common.database.db_session_manager import DB_SESSION_MNG
|
|
from common.logger import LOG
|
|
from common.utils.gtime import GTime
|
|
from config.server_configs import web_server_config
|
|
from router.v1.validator.auth import configured_keys, require_api_key
|
|
import router.v1.lps.search
|
|
|
|
API_SERVER_START_TIME = GTime.UTCStr()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# startup: API 자신의 DB 풀 포화 감시(경량) — 대량 폴링으로 풀을 고갈시키는 주범이 API 일 수 있다.
|
|
stop = asyncio.Event()
|
|
pool_monitor = asyncio.create_task(run_pool_monitor(stop))
|
|
yield
|
|
# shutdown: 모니터 정지 후 DB 엔진 커넥션 풀 정리
|
|
stop.set()
|
|
pool_monitor.cancel()
|
|
try:
|
|
await pool_monitor
|
|
except asyncio.CancelledError:
|
|
pass
|
|
await DB_SESSION_MNG.dispose_all()
|
|
|
|
|
|
app = FastAPI(title="LPS Api Server", lifespan=lifespan)
|
|
|
|
# CORS: config 의 cors_origins 가 있을 때만 적용(브라우저 프론트 호출 허용).
|
|
# 명시적 오리진을 쓰므로 allow_credentials=True 가능(쿠키/Authorization 헤더 허용).
|
|
if web_server_config.cors_origins:
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=web_server_config.cors_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Accept-Encoding: gzip 요청에 대해 1000 bytes 이상 응답을 압축.
|
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
|
|
|
|
|
@app.middleware("http")
|
|
async def log_time(request: Request, call_next):
|
|
start_time = time.time()
|
|
response = await call_next(request)
|
|
elapsed = time.time() - start_time
|
|
LOG.d(f"took: {elapsed:.4f} - {request.url.path}")
|
|
return response
|
|
|
|
|
|
@app.get(
|
|
path="/healthz",
|
|
summary="헬스체크(liveness)",
|
|
description="서버 기동 시각을 반환. 프로세스가 살아있는지만 확인(DB 무관).",
|
|
responses={404: {"description": "Not found"}},
|
|
)
|
|
async def healthz():
|
|
return API_SERVER_START_TIME
|
|
|
|
|
|
@app.get(
|
|
path="/readyz",
|
|
summary="레디니스(readiness)",
|
|
description="DB 도달성까지 확인. 오케스트레이터/LB 가 트래픽 라우팅 여부 판단에 사용. 실패 시 503.",
|
|
)
|
|
async def readyz():
|
|
from fastapi import Response
|
|
from crud.job_crud import JobQueue
|
|
try:
|
|
await JobQueue().ping()
|
|
return {"ready": True}
|
|
except Exception as ex:
|
|
return Response(content=f'{{"ready": false, "error": "{type(ex).__name__}"}}',
|
|
media_type="application/json", status_code=503)
|
|
|
|
|
|
# 각 도메인 라우터를 등록한다. 새 기능 추가 시 router.v1.<domain>.<file> 를 import 후 include.
|
|
# guard: LPS_API_KEY 설정 시 /v1 전체에 X-API-Key 검증(개발은 미설정=개방 — auth.py 참고).
|
|
app.include_router(router.v1.lps.search.router, dependencies=[Depends(require_api_key)])
|
|
|
|
if configured_keys():
|
|
LOG.i(f"API guard ON — X-API-Key 검증({len(configured_keys())}개 키)")
|
|
else:
|
|
LOG.w("LPS_API_KEY 미설정 — API 개방 모드(개발용). prod 배포 시 키 주입 + 포트 비공개 필수")
|