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
| Interface | Synchrone/Async | Serveurs | Frameworks |
|---|---|---|---|
| WSGI | Synchrone uniquement | Gunicorn, uWSGI | Flask, Django (sync) |
| ASGI | Sync + Async | Uvicorn, Hypercorn, Daphne | FastAPI, 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
"""