fix(infra): harden AI service — graceful shutdown, rate limiting, API key auth, pinned deps, Grafana secrets
- Add dumb-init + --timeout-graceful-shutdown 30 to AI service Dockerfile - Add slowapi rate limiting (configurable via AI_RATE_LIMIT) and X-API-Key auth middleware - Pin all Python dependencies to exact versions for reproducible builds - Move Grafana admin credentials from env vars to Docker secrets in production compose Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -18,6 +18,7 @@ services:
|
|||||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||||
AI_SERVICES_URL: http://ai-services:8000
|
AI_SERVICES_URL: http://ai-services:8000
|
||||||
|
AI_SERVICES_API_KEY: ${AI_API_KEY}
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -136,6 +137,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
AI_DEBUG: 'false'
|
AI_DEBUG: 'false'
|
||||||
AI_LOG_LEVEL: info
|
AI_LOG_LEVEL: info
|
||||||
|
AI_API_KEY: ${AI_API_KEY}
|
||||||
|
AI_RATE_LIMIT: ${AI_RATE_LIMIT:-60/minute}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'python', '-c', 'import httpx; httpx.get("http://localhost:8000/health").raise_for_status()']
|
test: ['CMD', 'python', '-c', 'import httpx; httpx.get("http://localhost:8000/health").raise_for_status()']
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -172,10 +175,13 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '${GRAFANA_PORT:-3002}:3000'
|
- '${GRAFANA_PORT:-3002}:3000'
|
||||||
environment:
|
environment:
|
||||||
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER}
|
GF_SECURITY_ADMIN_USER__FILE: /run/secrets/grafana_admin_user
|
||||||
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
|
GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin_password
|
||||||
GF_USERS_ALLOW_SIGN_UP: 'false'
|
GF_USERS_ALLOW_SIGN_UP: 'false'
|
||||||
GF_SERVER_ROOT_URL: ${GRAFANA_ROOT_URL:-http://localhost:3002}
|
GF_SERVER_ROOT_URL: ${GRAFANA_ROOT_URL:-http://localhost:3002}
|
||||||
|
secrets:
|
||||||
|
- grafana_admin_user
|
||||||
|
- grafana_admin_password
|
||||||
volumes:
|
volumes:
|
||||||
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
|
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
|
||||||
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||||
@@ -206,6 +212,12 @@ volumes:
|
|||||||
grafana_data:
|
grafana_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
grafana_admin_user:
|
||||||
|
environment: GRAFANA_ADMIN_USER
|
||||||
|
grafana_admin_password:
|
||||||
|
environment: GRAFANA_ADMIN_PASSWORD
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
goodgo-net:
|
goodgo-net:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@@ -2,21 +2,22 @@ FROM python:3.12-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system deps for underthesea / numpy
|
# Install system deps for underthesea / numpy + dumb-init for signal handling
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends gcc g++ && \
|
apt-get install -y --no-install-recommends gcc g++ dumb-init && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY pyproject.toml .
|
COPY pyproject.toml .
|
||||||
RUN pip install --no-cache-dir . 2>/dev/null || pip install --no-cache-dir \
|
RUN pip install --no-cache-dir . 2>/dev/null || pip install --no-cache-dir \
|
||||||
"fastapi>=0.115.0" \
|
"fastapi==0.115.0" \
|
||||||
"uvicorn[standard]>=0.32.0" \
|
"uvicorn[standard]==0.32.0" \
|
||||||
"xgboost>=2.1.0" \
|
"xgboost==2.1.0" \
|
||||||
"numpy>=1.26.0" \
|
"numpy==1.26.4" \
|
||||||
"underthesea>=6.8.0" \
|
"underthesea==6.8.0" \
|
||||||
"pydantic>=2.9.0" \
|
"pydantic==2.9.0" \
|
||||||
"pydantic-settings>=2.5.0" \
|
"pydantic-settings==2.5.0" \
|
||||||
"httpx>=0.27.0"
|
"httpx==0.27.0" \
|
||||||
|
"slowapi==0.1.9"
|
||||||
|
|
||||||
COPY app/ ./app/
|
COPY app/ ./app/
|
||||||
|
|
||||||
@@ -28,4 +29,5 @@ EXPOSE 8000
|
|||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||||
CMD python -c "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"
|
CMD python -c "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"
|
||||||
|
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--timeout-graceful-shutdown", "30"]
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import Depends, FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||||
|
from slowapi.errors import RateLimitExceeded
|
||||||
|
from slowapi.util import get_remote_address
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
from app.middleware import verify_api_key
|
||||||
from app.routers import avm, moderation
|
from app.routers import avm, moderation
|
||||||
|
|
||||||
|
limiter = Limiter(key_func=get_remote_address, default_limits=[settings.rate_limit])
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=settings.app_name,
|
title=settings.app_name,
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
|
dependencies=[Depends(verify_api_key)],
|
||||||
)
|
)
|
||||||
|
app.state.limiter = limiter
|
||||||
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||||
|
|
||||||
if not settings.cors_origin_list:
|
if not settings.cors_origin_list:
|
||||||
raise RuntimeError("AI_CORS_ORIGINS must be set (comma-separated list of allowed origins)")
|
raise RuntimeError("AI_CORS_ORIGINS must be set (comma-separated list of allowed origins)")
|
||||||
|
|||||||
23
libs/ai-services/app/middleware.py
Normal file
23
libs/ai-services/app/middleware.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import hmac
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException, Security, status
|
||||||
|
from fastapi.security import APIKeyHeader
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_api_key(
|
||||||
|
api_key: Optional[str] = Security(api_key_header),
|
||||||
|
) -> str:
|
||||||
|
"""Validate X-API-Key header. Skipped when AI_API_KEY is not configured."""
|
||||||
|
if not settings.api_key:
|
||||||
|
return "no-auth"
|
||||||
|
if not api_key or not hmac.compare_digest(api_key, settings.api_key):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid or missing API key",
|
||||||
|
)
|
||||||
|
return api_key
|
||||||
@@ -4,14 +4,15 @@ version = "0.1.0"
|
|||||||
description = "AI/ML services for Goodgo Platform — AVM, feature extraction, moderation"
|
description = "AI/ML services for Goodgo Platform — AVM, feature extraction, moderation"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.115.0",
|
"fastapi==0.115.0",
|
||||||
"uvicorn[standard]>=0.32.0",
|
"uvicorn[standard]==0.32.0",
|
||||||
"xgboost>=2.1.0",
|
"xgboost==2.1.0",
|
||||||
"numpy>=1.26.0",
|
"numpy==1.26.4",
|
||||||
"underthesea>=6.8.0",
|
"underthesea==6.8.0",
|
||||||
"pydantic>=2.9.0",
|
"pydantic==2.9.0",
|
||||||
"pydantic-settings>=2.5.0",
|
"pydantic-settings==2.5.0",
|
||||||
"httpx>=0.27.0",
|
"httpx==0.27.0",
|
||||||
|
"slowapi==0.1.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user