FastAPIのデプロイメント完全ガイド – Docker・Kubernetes・クラウド運用

FastAPIのデプロイメント完全ガイド – Docker・Kubernetes・クラウド運用

FastAPIアプリケーションを本格的に運用するためには、適切なデプロイメント戦略が必要です。この記事では、Docker、Kubernetes、各種クラウドプラットフォームでのデプロイメント方法を詳しく解説します。

デプロイメントの基礎知識

デプロイメント方式の比較

  1. 従来型デプロイメント: 物理サーバーまたはVMに直接デプロイ
  2. コンテナ化デプロイメント: Dockerコンテナを使用
  3. オーケストレーション: Kubernetes等によるコンテナ管理
  4. サーバーレス: AWS Lambda、Google Cloud Functions等
  5. PaaS: Heroku、Railway、Render等

本番環境の考慮事項

  • スケーラビリティ: 負荷に応じた自動スケーリング
  • 可用性: 高可用性とフェイルオーバー
  • セキュリティ: HTTPS、ファイアウォール、セキュリティ更新
  • 監視: ログ、メトリクス、アラート
  • パフォーマンス: レスポンス時間、スループット

Dockerによるコンテナ化

本番用Dockerfile

# Dockerfile
FROM python:3.11-slim as base

# システムの依存関係をインストール
RUN apt-get update && apt-get install -y 
    gcc 
    g++ 
    && rm -rf /var/lib/apt/lists/*

# 作業ディレクトリの設定
WORKDIR /app

# 依存関係のインストール(キャッシュ効率化)
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && 
    pip install --no-cache-dir -r requirements.txt

# アプリケーションファイルのコピー
COPY ./app /app

# 非rootユーザーの作成(セキュリティ向上)
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN chown -R appuser:appuser /app
USER appuser

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

# ポートの公開
EXPOSE 8000

# アプリケーションの起動
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

マルチステージビルド(最適化版)

# Dockerfile.prod
# ビルドステージ
FROM python:3.11-slim as builder

WORKDIR /app

# 依存関係のインストール
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 本番ステージ
FROM python:3.11-slim as production

# セキュリティ更新
RUN apt-get update && apt-get upgrade -y && 
    apt-get clean && rm -rf /var/lib/apt/lists/*

# ユーザー作成
RUN groupadd -r appuser && useradd -r -g appuser appuser

# 作業ディレクトリ
WORKDIR /app

# ビルドステージから依存関係をコピー
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser ./app /app

# パスの設定
ENV PATH=/home/appuser/.local/bin:$PATH

USER appuser

EXPOSE 8000

CMD ["gunicorn", "main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]

Docker Compose(開発・テスト環境)

# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/fastapi_db
      - SECRET_KEY=your-secret-key
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./app:/app  # 開発時のホットリロード用
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: fastapi_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - web

volumes:
  postgres_data:
  redis_data:

本番用Docker Compose

# docker-compose.prod.yml
version: '3.8'

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.prod
    restart: unless-stopped
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - SECRET_KEY=${SECRET_KEY}
      - ALLOWED_HOSTS=${ALLOWED_HOSTS}
    depends_on:
      - db
      - redis
    networks:
      - app-network

  db:
    image: postgres:15
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/ssl:/etc/nginx/ssl
      - ./nginx/logs:/var/log/nginx
    depends_on:
      - web
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  postgres_data:
  redis_data:

Nginx設定

リバースプロキシ設定

# nginx/nginx.conf
events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # ログ設定
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log warn;

    # Gzip圧縮
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript 
               application/x-javascript application/xml+rss application/json;

    # アップストリーム設定
    upstream fastapi_backend {
        server web:8000;
        # 複数インスタンスの場合
        # server web1:8000;
        # server web2:8000;
    }

    # HTTPからHTTPSへのリダイレクト
    server {
        listen 80;
        server_name your-domain.com www.your-domain.com;
        return 301 https://$server_name$request_uri;
    }

    # HTTPS設定
    server {
        listen 443 ssl http2;
        server_name your-domain.com www.your-domain.com;

        # SSL証明書
        ssl_certificate /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;

        # SSL設定
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers off;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 10m;

        # セキュリティヘッダー
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        add_header X-Content-Type-Options nosniff;
        add_header X-Frame-Options DENY;
        add_header X-XSS-Protection "1; mode=block";

        # 静的ファイル配信
        location /static/ {
            alias /app/static/;
            expires 1y;
            add_header Cache-Control "public, immutable";
        }

        # API プロキシ
        location / {
            proxy_pass http://fastapi_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            # タイムアウト設定
            proxy_connect_timeout 30s;
            proxy_send_timeout 30s;
            proxy_read_timeout 30s;

            # WebSocket サポート
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }

        # ヘルスチェック
        location /health {
            proxy_pass http://fastapi_backend/health;
            access_log off;
        }
    }
}

Kubernetesでの運用

Deployment設定

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fastapi-app
  labels:
    app: fastapi-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: fastapi-app
  template:
    metadata:
      labels:
        app: fastapi-app
    spec:
      containers:
      - name: fastapi
        image: your-registry/fastapi-app:latest
        ports:
        - containerPort: 8000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url
        - name: SECRET_KEY
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: secret-key
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 5
        volumeMounts:
        - name: config-volume
          mountPath: /app/config
      volumes:
      - name: config-volume
        configMap:
          name: app-config

---

apiVersion: v1
kind: Service
metadata:
  name: fastapi-service
spec:
  selector:
    app: fastapi-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8000
  type: ClusterIP

ConfigMapとSecret

# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  app_name: "FastAPI App"
  environment: "production"
  log_level: "INFO"

---

apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  database-url: <base64-encoded-database-url>
  secret-key: <base64-encoded-secret-key>
  redis-password: <base64-encoded-redis-password>

Ingress設定

# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: fastapi-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: 10m
spec:
  tls:
  - hosts:
    - your-domain.com
    secretName: fastapi-tls
  rules:
  - host: your-domain.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: fastapi-service
            port:
              number: 80

Horizontal Pod Autoscaler

# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: fastapi-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: fastapi-app
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

AWS EKSでのデプロイメント

EKSクラスター作成

# eksctl を使用したクラスター作成
eksctl create cluster 
  --name fastapi-cluster 
  --version 1.27 
  --region ap-northeast-1 
  --nodegroup-name standard-workers 
  --node-type t3.medium 
  --nodes 3 
  --nodes-min 1 
  --nodes-max 4 
  --managed

Application Load Balancer設定

# k8s/aws-load-balancer.yaml
apiVersion: v1
kind: Service
metadata:
  name: fastapi-service-alb
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: nlb
    service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:region:account:certificate/cert-id
    service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http
spec:
  type: LoadBalancer
  selector:
    app: fastapi-app
  ports:
  - name: http
    port: 80
    targetPort: 8000
  - name: https
    port: 443
    targetPort: 8000

Google Cloud Platform (GKE) デプロイメント

GKEクラスター作成

# GKEクラスターの作成
gcloud container clusters create fastapi-cluster 
    --zone=asia-northeast1-a 
    --num-nodes=3 
    --machine-type=e2-standard-2 
    --enable-autoscaling 
    --min-nodes=1 
    --max-nodes=5

Cloud SQLとの連携

# k8s/cloudsql-proxy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fastapi-app-with-cloudsql
spec:
  template:
    spec:
      containers:
      - name: fastapi
        image: gcr.io/your-project/fastapi-app:latest
        env:
        - name: DATABASE_URL
          value: "postgresql://user:password@127.0.0.1:5432/dbname"
      - name: cloudsql-proxy
        image: gcr.io/cloudsql-docker/gce-proxy:1.33.2
        command:
          - "/cloud_sql_proxy"
          - "-instances=your-project:region:instance=tcp:5432"
        securityContext:
          runAsNonRoot: true

サーバーレスデプロイメント

AWS Lambda (Mangum)

# lambda_handler.py
from mangum import Mangum
from app.main import app

# FastAPIアプリをLambda対応に変換
handler = Mangum(app, lifespan="off")
# serverless.yml
service: fastapi-serverless

provider:
  name: aws
  runtime: python3.9
  region: ap-northeast-1
  environment:
    DATABASE_URL: ${env:DATABASE_URL}
    SECRET_KEY: ${env:SECRET_KEY}

functions:
  api:
    handler: lambda_handler.handler
    events:
      - http:
          path: /{proxy+}
          method: ANY
          cors: true
      - http:
          path: /
          method: ANY
          cors: true
    timeout: 30

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    dockerizePip: true

Vercel デプロイメント

# vercel_app.py
from app.main import app

# Vercel用のハンドラー
def handler(request):
    return app(request.environ, request.start_response)
{
  "_comment": "vercel.json",
  "builds": [
    {
      "src": "vercel_app.py",
      "use": "@vercel/python"
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "vercel_app.py"
    }
  ]
}

CI/CDパイプライン

GitHub Actions(完全版)

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.11'

    - name: Install dependencies
      run: |
        pip install -r requirements.txt
        pip install -r requirements-test.txt

    - name: Run tests
      run: |
        pytest --cov=app --cov-report=xml

    - name: Upload coverage
      uses: codecov/codecov-action@v3

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
    - uses: actions/checkout@v3

    - name: Log in to Container Registry
      uses: docker/login-action@v2
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v4
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=sha,prefix={{branch}}-
          type=raw,value=latest,enable={{is_default_branch}}

    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        file: ./Dockerfile.prod
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: production

    steps:
    - uses: actions/checkout@v3

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v2
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: Update kubeconfig
      run: |
        aws eks update-kubeconfig --region ap-northeast-1 --name fastapi-cluster

    - name: Deploy to Kubernetes
      run: |
        # イメージタグを更新
        sed -i "s|your-registry/fastapi-app:latest|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}|g" k8s/deployment.yaml

        # デプロイメント実行
        kubectl apply -f k8s/

        # デプロイメント完了まで待機
        kubectl rollout status deployment/fastapi-app

    - name: Run smoke tests
      run: |
        # 本番環境の動作確認
        sleep 30
        curl -f https://your-domain.com/health || exit 1

監視とログ

Prometheus + Grafana設定

# app/monitoring.py
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
from fastapi import FastAPI, Response
import time

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

def setup_monitoring(app: FastAPI):
    @app.middleware("http")
    async def monitor_requests(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_DURATION.observe(duration)

        return response

    @app.get("/metrics")
    async def metrics():
        return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

ログ設定

# app/logging_config.py
import logging
import sys
from pythonjsonlogger import jsonlogger

def setup_logging():
    # JSON形式のログフォーマッター
    json_formatter = jsonlogger.JsonFormatter(
        '%(asctime)s %(name)s %(levelname)s %(message)s'
    )

    # ハンドラー設定
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(json_formatter)

    # ロガー設定
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    logger.addHandler(handler)

    # uvicornログの設定
    logging.getLogger("uvicorn.access").handlers = [handler]
    logging.getLogger("uvicorn.error").handlers = [handler]

セキュリティ強化

セキュリティベストプラクティス

# app/security_config.py
from fastapi import FastAPI
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
import os

def configure_security(app: FastAPI):
    # HTTPS リダイレクト(本番環境のみ)
    if os.getenv("ENVIRONMENT") == "production":
        app.add_middleware(HTTPSRedirectMiddleware)

    # 信頼できるホストの制限
    allowed_hosts = os.getenv("ALLOWED_HOSTS", "localhost").split(",")
    app.add_middleware(TrustedHostMiddleware, allowed_hosts=allowed_hosts)

    # セキュリティヘッダー
    @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"
        return response

パフォーマンス最適化

本番環境設定

# app/production_config.py
import os
import uvicorn
from multiprocessing import cpu_count

class ProductionConfig:
    # ワーカー数の設定
    workers = int(os.getenv("WORKERS", cpu_count()))

    # 接続設定
    host = os.getenv("HOST", "0.0.0.0")
    port = int(os.getenv("PORT", 8000))

    # タイムアウト設定
    keepalive_timeout = int(os.getenv("KEEPALIVE_TIMEOUT", 65))
    graceful_timeout = int(os.getenv("GRACEFUL_TIMEOUT", 30))

    # ログ設定
    log_level = os.getenv("LOG_LEVEL", "info")
    access_log = os.getenv("ACCESS_LOG", "true").lower() == "true"

def run_production_server():
    config = ProductionConfig()

    uvicorn.run(
        "main:app",
        host=config.host,
        port=config.port,
        workers=config.workers,
        log_level=config.log_level,
        access_log=config.access_log,
        proxy_headers=True,
        forwarded_allow_ips="*"
    )

if __name__ == "__main__":
    run_production_server()

障害対応とトラブルシューティング

ヘルスチェックエンドポイント

# app/health.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
import redis
import os

router = APIRouter()

@router.get("/health")
async def health_check():
    return {"status": "healthy", "timestamp": datetime.utcnow()}

@router.get("/health/detailed")
async def detailed_health_check(db: Session = Depends(get_db)):
    checks = {}

    # データベース接続チェック
    try:
        db.execute("SELECT 1")
        checks["database"] = "healthy"
    except Exception as e:
        checks["database"] = f"unhealthy: {str(e)}"

    # Redis接続チェック
    try:
        redis_client = redis.from_url(os.getenv("REDIS_URL"))
        redis_client.ping()
        checks["redis"] = "healthy"
    except Exception as e:
        checks["redis"] = f"unhealthy: {str(e)}"

    # 全体のステータス判定
    all_healthy = all(status == "healthy" for status in checks.values())

    if not all_healthy:
        raise HTTPException(status_code=503, detail={"status": "unhealthy", "checks": checks})

    return {"status": "healthy", "checks": checks}

まとめ

FastAPIの本格的なデプロイメントには以下が重要です:

  1. コンテナ化: Dockerによる一貫した実行環境
  2. オーケストレーション: Kubernetesによる自動スケーリングと管理
  3. CI/CD: 自動化されたテスト・ビルド・デプロイパイプライン
  4. 監視: メトリクス、ログ、アラートによる運用監視
  5. セキュリティ: HTTPS、セキュリティヘッダー、認証の適切な実装

これらの技術を組み合わせることで、スケーラブルで信頼性の高いFastAPIアプリケーションを運用できます。

参考リンク