1. Variables d'environnement

Ne jamais coder en dur les secrets. Utilisez des variables d'environnement pour toute configuration sensible ou variable selon l'environnement.

# .env — NE PAS committer ce fichier !
# Ajoutez .env dans .gitignore

DATABASE_URL=postgresql://user:password@localhost:5432/taskapi
SECRET_KEY=super-secret-key-generee-aleatoirement
DEBUG=false
ALLOWED_HOSTS=taskapi.com,api.taskapi.com
LOG_LEVEL=INFO
SENTRY_DSN=https://xxx@sentry.io/xxx
from __future__ import annotations

import os
import secrets

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    # Environnement
    environment: str = "development"
    debug: bool = False

    # Base de données
    database_url: str = "sqlite:///./taskapi.db"

    # Sécurité — en prod : secrets.token_urlsafe(32)
    secret_key: str = os.getenv("SECRET_KEY", secrets.token_urlsafe(32))
    access_token_expire_minutes: int = 60

    # Serveur
    host: str = "0.0.0.0"
    port: int = 8000
    workers: int = 4

    # Logging
    log_level: str = "INFO"
    log_format: str = "json"    # json | text

    model_config = SettingsConfigDict(env_file=".env", extra="ignore")

settings = Settings()

2. ASGI vs WSGI

InterfaceSynchrone/AsyncServeursFrameworks
WSGISynchrone uniquementGunicorn, uWSGIFlask, Django (sync)
ASGISync + AsyncUvicorn, Hypercorn, DaphneFastAPI, Django (async), Starlette
# ── FastAPI avec Uvicorn (développement)
uvicorn main:app --reload --host 0.0.0.0 --port 8000

# ── Production : Gunicorn + Uvicorn workers (multi-process)
gunicorn main:app \
  --workers 4 \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000 \
  --timeout 60 \
  --keepalive 5 \
  --access-logfile - \
  --error-logfile -

# Règle : workers = 2 * CPU + 1
# Sur un serveur 2 cœurs → 5 workers

3. Dockerfile multi-stage

Le build multi-stage sépare la phase de build (installation des dépendances) de la phase d'exécution, produisant une image plus petite et plus sécurisée.

# ── Stage 1 : Builder (installe les dépendances)
FROM python:3.12-slim AS builder

WORKDIR /build

# Copier uniquement requirements en premier (cache Docker optimisé)
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir --target=/build/packages -r requirements.txt

# ── Stage 2 : Runtime (image finale légère)
FROM python:3.12-slim AS runtime

# Métadonnées
LABEL maintainer="Votre Nom" \
      version="1.0.0" \
      description="TaskAPI FastAPI"

# Variables d'environnement Python
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PYTHONPATH=/app/packages

WORKDIR /app

# Copier les packages depuis le builder
COPY --from=builder /build/packages /app/packages

# !! Utilisateur non-root (sécurité)
RUN addgroup --system appgroup && \
    adduser --system --ingroup appgroup appuser

# Copier le code source
COPY --chown=appuser:appgroup . .

# Basculer vers l'utilisateur non-root
USER appuser

EXPOSE 8000

# Healthcheck intégré
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["python", "-m", "gunicorn", "main:app", \
     "--workers", "4", \
     "--worker-class", "uvicorn.workers.UvicornWorker", \
     "--bind", "0.0.0.0:8000", \
     "--access-logfile", "-"]

4. docker-compose

version: "3.9"

services:
  app:
    build:
      context: .
      target: runtime
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://taskuser:taskpass@db:5432/taskapi
      - SECRET_KEY=${SECRET_KEY}
      - DEBUG=false
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: taskuser
      POSTGRES_PASSWORD: taskpass
      POSTGRES_DB: taskapi
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U taskuser -d taskapi"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app
    restart: unless-stopped

volumes:
  postgres_data:

5. Health checks

from __future__ import annotations

import time
from contextlib import asynccontextmanager
from typing import AsyncGenerator

from fastapi import FastAPI
from sqlalchemy import text
from sqlalchemy.orm import Session

start_time = time.time()

@app.get("/health", tags=["Monitoring"])
async def health_check(db: Session = Depends(get_db)) -> dict:
    """Health check complet — vérifie l'application et la base de données."""
    checks = {}

    # ── Check base de données
    try:
        db.execute(text("SELECT 1"))
        checks["database"] = {"status": "ok"}
    except Exception as e:
        checks["database"] = {"status": "error", "detail": str(e)}

    # ── Check mémoire (psutil optionnel)
    try:
        import psutil
        mem = psutil.virtual_memory()
        checks["memory"] = {
            "status": "ok" if mem.percent < 90 else "warning",
            "used_percent": mem.percent,
        }
    except ImportError:
        checks["memory"] = {"status": "unavailable"}

    overall = "ok" if all(v["status"] == "ok" for v in checks.values()) else "degraded"

    return {
        "status": overall,
        "uptime_seconds": round(time.time() - start_time, 1),
        "checks": checks,
        "version": "1.0.0",
    }

# ── Liveness (Kubernetes)
@app.get("/healthz/live")
async def liveness() -> dict:
    return {"status": "ok"}

# ── Readiness (Kubernetes — prêt à recevoir du trafic)
@app.get("/healthz/ready")
async def readiness(db: Session = Depends(get_db)) -> dict:
    db.execute(text("SELECT 1"))
    return {"status": "ready"}

6. Logging structuré (JSON)

from __future__ import annotations

import json
import logging
import time
from datetime import datetime, timezone

class JsonFormatter(logging.Formatter):
    """Formateur JSON pour les logs en production."""
    def format(self, record: logging.LogRecord) -> str:
        log_data = {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "module": record.module,
            "function": record.funcName,
            "line": record.lineno,
        }
        if record.exc_info:
            log_data["exception"] = self.formatException(record.exc_info)
        return json.dumps(log_data, ensure_ascii=False)

def configure_logging(log_level: str = "INFO", format_type: str = "json") -> None:
    handler = logging.StreamHandler()
    if format_type == "json":
        handler.setFormatter(JsonFormatter())
    else:
        handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s — %(message)s"))

    logging.basicConfig(
        level=getattr(logging, log_level.upper()),
        handlers=[handler],
    )
    # Réduire le bruit des libs tierces
    logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
    logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)

# Appel au démarrage
configure_logging(log_level="INFO", format_type="json")

7. CI/CD avec GitHub Actions

# .github/workflows/ci.yml
name: CI/CD TaskAPI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: taskapi_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: "pip"

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

      - name: Lint (ruff)
        run: ruff check .

      - name: Type check (mypy)
        run: mypy . --ignore-missing-imports

      - name: Tests (pytest)
        env:
          DATABASE_URL: postgresql://test:test@localhost/taskapi_test
          SECRET_KEY: test-secret-key
        run: pytest --cov=. --cov-report=xml -v

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

  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker image
        run: docker build -t taskapi:${{ github.sha }} .
      - name: Run container tests
        run: |
          docker run -d -p 8000:8000 --name test-app taskapi:${{ github.sha }}
          sleep 5
          curl --fail http://localhost:8000/health
          docker stop test-app

8. Bonnes pratiques production

from __future__ import annotations

# ── Checklist déploiement production

# 1. Variables d'environnement
# ✅ DEBUG=false
# ✅ SECRET_KEY générée avec secrets.token_urlsafe(32)
# ✅ DATABASE_URL avec credentials sécurisés
# ✅ ALLOWED_HOSTS explicites

# 2. Base de données
# ✅ Migrations appliquées avant le démarrage : alembic upgrade head
# ✅ Pool de connexions configuré : pool_size=10, max_overflow=20
# ✅ Backups automatiques (ex: pg_dump quotidien)

# 3. Sécurité
# ✅ HTTPS forcé (nginx / Let's Encrypt)
# ✅ Headers de sécurité (HSTS, CSP, X-Frame-Options)
# ✅ Rate limiting sur /auth/login
# ✅ Logs d'audit pour les actions sensibles

# 4. Performance
# ✅ Gunicorn + UvicornWorker (workers = 2 * CPU + 1)
# ✅ Cache Redis pour les réponses fréquentes
# ✅ Compression gzip (nginx)
# ✅ Index DB sur les colonnes filtrées/triées

# 5. Observabilité
# ✅ Logs JSON structurés (ELK, Datadog, Loki)
# ✅ Métriques Prometheus (/metrics)
# ✅ Tracing distribué (OpenTelemetry)
# ✅ Alertes sur les erreurs 5xx

# ── Commandes utiles
"""
# Générer une SECRET_KEY sécurisée
python -c "import secrets; print(secrets.token_urlsafe(32))"

# Vérifier le Dockerfile
docker build --target runtime -t taskapi:prod .
docker run --rm -p 8000:8000 --env-file .env taskapi:prod

# Tester le health check
curl http://localhost:8000/health | python3 -m json.tool

# Monitoring des logs en temps réel
docker logs -f taskapi_app
"""
← Module 07 🏠 Retour à l'accueil