第9章:デプロイと運用

更新日:2025年12月9日

本章では、Pythonアプリケーションの本番デプロイと運用について解説する。FastAPIによるAPI開発、機械学習モデルのサービング、Dockerによるコンテナ化、Prometheus/Grafanaによる監視、セキュリティのベストプラクティスについて学ぶ。開発から本番運用へのスムーズな移行は、プロジェクト成功の鍵である。

1. FastAPI

FastAPIは、モダンで高速なPython Webフレームワークである[1]。型ヒントを活用した自動ドキュメント生成と高いパフォーマンスが特徴。

1.1 基本構造

from fastapi import FastAPI, HTTPException, Depends, status
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import Optional
import uvicorn

app = FastAPI(
    title="My API",
    description="サンプルAPI",
    version="1.0.0",
)

# CORS設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://example.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# リクエスト/レスポンスモデル
class ItemCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    price: float = Field(..., gt=0)
    description: Optional[str] = None

class ItemResponse(BaseModel):
    id: int
    name: str
    price: float
    description: Optional[str]

    class Config:
        from_attributes = True

# エンドポイント
@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.post("/items", response_model=ItemResponse, status_code=status.HTTP_201_CREATED)
async def create_item(item: ItemCreate):
    # DB保存処理(省略)
    return ItemResponse(id=1, **item.model_dump())

@app.get("/items/{item_id}", response_model=ItemResponse)
async def get_item(item_id: int):
    # DB取得処理(省略)
    if item_id <= 0:
        raise HTTPException(status_code=404, detail="Item not found")
    return ItemResponse(id=item_id, name="Sample", price=100.0, description=None)

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

1.2 依存性注入

FastAPIの依存性注入システムにより、共通処理を再利用可能。

from fastapi import Depends, Header, HTTPException
from sqlalchemy.orm import Session
from typing import Annotated

# データベースセッション
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# 認証
async def verify_token(authorization: str = Header(...)):
    if not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Invalid token")
    token = authorization.replace("Bearer ", "")
    # トークン検証ロジック
    return {"user_id": 1, "role": "admin"}

# 型エイリアス(Python 3.9+)
DbSession = Annotated[Session, Depends(get_db)]
CurrentUser = Annotated[dict, Depends(verify_token)]

# エンドポイントで使用
@app.get("/protected")
async def protected_route(db: DbSession, user: CurrentUser):
    return {"user": user, "message": "Access granted"}

1.3 非同期処理とバックグラウンドタスク

from fastapi import BackgroundTasks
import asyncio
import httpx

# 非同期HTTP呼び出し
async def fetch_external_data(url: str) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        return response.json()

# バックグラウンドタスク
def send_notification(email: str, message: str):
    # メール送信処理(時間のかかる処理)
    print(f"Sending email to {email}: {message}")

@app.post("/orders")
async def create_order(
    order: OrderCreate,
    background_tasks: BackgroundTasks,
    db: DbSession,
):
    # 注文処理
    order_id = save_order(db, order)

    # バックグラウンドで通知送信
    background_tasks.add_task(
        send_notification,
        order.customer_email,
        f"Order {order_id} confirmed"
    )

    return {"order_id": order_id, "status": "processing"}

2. モデルサービング

機械学習モデルをAPIとして提供するパターンを解説する。

2.1 FastAPIでのモデルサービング

from fastapi import FastAPI
from pydantic import BaseModel
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from contextlib import asynccontextmanager

# グローバル変数でモデルを保持
model = None
tokenizer = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    # 起動時にモデルをロード
    global model, tokenizer
    model_name = "bert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForSequenceClassification.from_pretrained(model_name)
    model.eval()
    print("Model loaded successfully")
    yield
    # シャットダウン時のクリーンアップ
    del model, tokenizer

app = FastAPI(lifespan=lifespan)

class PredictionRequest(BaseModel):
    text: str

class PredictionResponse(BaseModel):
    label: str
    confidence: float

@app.post("/predict", response_model=PredictionResponse)
async def predict(request: PredictionRequest):
    # 推論
    inputs = tokenizer(
        request.text,
        return_tensors="pt",
        truncation=True,
        max_length=512,
    )

    with torch.no_grad():
        outputs = model(**inputs)
        probs = torch.softmax(outputs.logits, dim=-1)
        confidence, predicted = torch.max(probs, dim=-1)

    labels = ["negative", "positive"]
    return PredictionResponse(
        label=labels[predicted.item()],
        confidence=confidence.item(),
    )

2.2 バッチ推論

複数リクエストをバッチ処理してスループット向上。

from fastapi import FastAPI
import asyncio
from collections import deque
from typing import List
import time

class BatchProcessor:
    def __init__(self, batch_size: int = 32, max_wait_ms: int = 100):
        self.batch_size = batch_size
        self.max_wait_ms = max_wait_ms
        self.queue = deque()
        self.lock = asyncio.Lock()

    async def add_request(self, data: dict) -> dict:
        future = asyncio.Future()
        async with self.lock:
            self.queue.append((data, future))

            if len(self.queue) >= self.batch_size:
                await self._process_batch()

        # タイムアウト待機
        try:
            return await asyncio.wait_for(future, timeout=self.max_wait_ms / 1000 + 5)
        except asyncio.TimeoutError:
            return {"error": "Timeout"}

    async def _process_batch(self):
        if not self.queue:
            return

        batch = []
        futures = []
        while self.queue and len(batch) < self.batch_size:
            data, future = self.queue.popleft()
            batch.append(data)
            futures.append(future)

        # バッチ推論実行
        results = await self._run_inference(batch)

        # 結果を各futureに設定
        for future, result in zip(futures, results):
            future.set_result(result)

    async def _run_inference(self, batch: List[dict]) -> List[dict]:
        # 実際のモデル推論
        # ...
        return [{"prediction": "result"} for _ in batch]

batch_processor = BatchProcessor()

@app.post("/predict/batch")
async def predict_batch(request: PredictionRequest):
    return await batch_processor.add_request({"text": request.text})

Table 1. モデルサービング手法の比較

手法 特徴 用途
FastAPI + uvicorn シンプル、柔軟 中小規模、カスタム処理
TorchServe PyTorch公式、管理機能 PyTorchモデル本番運用
Triton NVIDIA製、高性能 GPU推論、マルチモデル
vLLM LLM特化、高スループット LLMサービング

3. コンテナ化

Dockerによるコンテナ化で、環境の再現性と可搬性を確保する[2]。

3.1 本番用Dockerfile

# Dockerfile
FROM python:3.12-slim as builder

WORKDIR /app

# 依存関係のインストール
RUN pip install uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev

FROM python:3.12-slim as runtime

WORKDIR /app

# 非rootユーザー作成
RUN useradd --create-home --shell /bin/bash appuser

# ビルドステージから仮想環境をコピー
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"

# アプリケーションコード
COPY --chown=appuser:appuser ./src ./src

USER appuser

# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

EXPOSE 8000

CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

3.2 docker-compose

# docker-compose.yml
version: '3.8'

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - api

volumes:
  postgres_data:
  redis_data:

Fig. 1にデプロイアーキテクチャを示す。

4. 監視とロギング

4.1 構造化ロギング

JSON形式のログにより、ログ集約・分析が容易になる。

import logging
import json
from datetime import datetime
from typing import Any

class JSONFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        log_data = {
            "timestamp": datetime.utcnow().isoformat(),
            "level": record.levelname,
            "message": record.getMessage(),
            "logger": record.name,
            "module": record.module,
            "function": record.funcName,
            "line": record.lineno,
        }

        # 追加のコンテキスト
        if hasattr(record, 'request_id'):
            log_data['request_id'] = record.request_id
        if hasattr(record, 'user_id'):
            log_data['user_id'] = record.user_id

        # 例外情報
        if record.exc_info:
            log_data['exception'] = self.formatException(record.exc_info)

        return json.dumps(log_data)

# ロガー設定
def setup_logging():
    handler = logging.StreamHandler()
    handler.setFormatter(JSONFormatter())

    logger = logging.getLogger()
    logger.addHandler(handler)
    logger.setLevel(logging.INFO)

# FastAPIミドルウェアでリクエストID追加
from fastapi import Request
import uuid

@app.middleware("http")
async def add_request_id(request: Request, call_next):
    request_id = str(uuid.uuid4())
    request.state.request_id = request_id

    # ログにリクエストIDを追加
    logger = logging.getLogger()
    old_factory = logging.getLogRecordFactory()

    def record_factory(*args, **kwargs):
        record = old_factory(*args, **kwargs)
        record.request_id = request_id
        return record

    logging.setLogRecordFactory(record_factory)
    response = await call_next(request)
    response.headers["X-Request-ID"] = request_id
    return response

4.2 Prometheus メトリクス

アプリケーションメトリクスをPrometheusで収集[3]。

from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
from fastapi import FastAPI, Request, Response
import time

# メトリクス定義
REQUEST_COUNT = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']
)

REQUEST_LATENCY = Histogram(
    'http_request_duration_seconds',
    'HTTP request latency',
    ['method', 'endpoint'],
    buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 5.0]
)

MODEL_INFERENCE_TIME = Histogram(
    'model_inference_seconds',
    'Model inference time',
    ['model_name'],
    buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0]
)

# ミドルウェア
@app.middleware("http")
async def metrics_middleware(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    duration = time.time() - start_time

    REQUEST_COUNT.labels(
        method=request.method,
        endpoint=request.url.path,
        status=response.status_code
    ).inc()

    REQUEST_LATENCY.labels(
        method=request.method,
        endpoint=request.url.path
    ).observe(duration)

    return response

# メトリクスエンドポイント
@app.get("/metrics")
async def metrics():
    return Response(
        content=generate_latest(),
        media_type=CONTENT_TYPE_LATEST
    )

Table 2. 監視すべき主要メトリクス

カテゴリ メトリクス アラート閾値例
レイテンシ p50, p95, p99応答時間 p99 > 1秒
スループット リクエスト/秒 急激な変動
エラー率 5xx / 総リクエスト > 1%
リソース CPU、メモリ、GPU使用率 > 80%
キュー 待機タスク数 増加傾向

5. セキュリティ

5.1 認証と認可

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from pydantic import BaseModel

# 設定
SECRET_KEY = "your-secret-key"  # 環境変数から取得
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    username: str | None = None
    scopes: list[str] = []

def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        return TokenData(username=username, scopes=payload.get("scopes", []))
    except JWTError:
        raise credentials_exception

# ロールベースアクセス制御
def require_role(required_role: str):
    async def role_checker(user: TokenData = Depends(get_current_user)):
        if required_role not in user.scopes:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Insufficient permissions"
            )
        return user
    return role_checker

@app.get("/admin")
async def admin_only(user: TokenData = Depends(require_role("admin"))):
    return {"message": f"Welcome admin {user.username}"}

5.2 入力バリデーションとサニタイズ

from pydantic import BaseModel, field_validator, Field
from typing import Annotated
import re
import bleach

class UserInput(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: str
    bio: str = Field(..., max_length=1000)

    @field_validator('username')
    @classmethod
    def username_alphanumeric(cls, v):
        if not re.match(r'^[a-zA-Z0-9_]+$', v):
            raise ValueError('Username must be alphanumeric')
        return v

    @field_validator('email')
    @classmethod
    def email_valid(cls, v):
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if not re.match(pattern, v):
            raise ValueError('Invalid email format')
        return v.lower()

    @field_validator('bio')
    @classmethod
    def sanitize_bio(cls, v):
        # HTMLタグを除去
        return bleach.clean(v, tags=[], strip=True)

5.3 セキュリティヘッダー

from fastapi import FastAPI
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware

app = FastAPI()

# HTTPS強制
app.add_middleware(HTTPSRedirectMiddleware)

# 信頼するホスト
app.add_middleware(
    TrustedHostMiddleware,
    allowed_hosts=["example.com", "*.example.com"]
)

# セキュリティヘッダー
@app.middleware("http")
async def add_security_headers(request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
    response.headers["Content-Security-Policy"] = "default-src 'self'"
    return response

Fig. 2にセキュリティレイヤーを示す。

Table 3. セキュリティチェックリスト

カテゴリ 項目
認証 JWT/OAuth2実装、トークン有効期限、リフレッシュトークン
認可 RBAC/ABAC、最小権限原則
入力 バリデーション、サニタイズ、SQLインジェクション対策
通信 HTTPS強制、セキュリティヘッダー
秘密情報 環境変数管理、シークレットマネージャー
監査 アクセスログ、変更履歴、アラート

References

[1] FastAPI, "FastAPI Documentation," fastapi.tiangolo.com, 2024.

[2] Docker, "Docker Documentation," docs.docker.com, 2024.

[3] Prometheus, "Prometheus Documentation," prometheus.io/docs, 2024.

[4] OWASP, "OWASP Top Ten," owasp.org, 2024.

免責事項
本コンテンツは2025年12月時点の情報に基づいて作成されている。セキュリティ要件は常に変化するため、最新のベストプラクティスと脆弱性情報を確認されたい。本番環境では専門家によるセキュリティレビューを推奨する。

← 前章:パフォーマンス最適化トップページへ戻る →