FastAPI DevOps Playground - Social Media API
📝 Description
API REST complète construite avec FastAPI et SQLAlchemy ORM pour gérer un système de posts de type réseau social. L'application inclut l'authentification JWT, la gestion des utilisateurs, les posts et un système de votes.
✨ Fonctionnalités
- 🔐 Authentification JWT avec tokens expirables
- 👤 Gestion des utilisateurs (inscription, connexion)
- 📄 CRUD complet pour les posts (Create, Read, Update, Delete)
- 👍 Système de votes (like/unlike)
- 🔍 Recherche et pagination des posts
- 🔒 Autorisation : seul le propriétaire peut modifier/supprimer ses posts
- 🗄️ Base de données PostgreSQL avec SQLAlchemy ORM
- 📊 Validation des données avec Pydantic V2
🏗️ Architecture
app/
├── routers/ # Routes API organisées par domaine
│ ├── auth.py # Authentification (login)
│ ├── post.py # CRUD posts
│ ├── user.py # Gestion utilisateurs
│ └── votes.py # Système de votes
├── models.py # Modèles SQLAlchemy (Post, User, Votes)
├── schemas.py # Schémas Pydantic pour validation
├── database.py # Configuration base de données
├── oauth2.py # Gestion JWT tokens
├── utils.py # Utilitaires (hash password)
└── main.py # Point d'entrée FastAPI
🗄️ Modèles de données
Post
id: Identifiant uniquetitle: Titre du postcontent: Contenupublished: Statut de publicationcreated_at: Date de créationuser_id: Référence vers l'utilisateur propriétaire
User
id: Identifiant uniqueemail: Email (unique)password: Mot de passe hashécreated_at: Date de création
Votes
post_id: Référence vers le postuser_id: Référence vers l'utilisateur
⚙️ Installation
Prérequis
- Python 3.12+
- PostgreSQL
Étapes d'installation
-
Cloner le repository et basculer sur la branche ORM
git checkout feature/fastapi-ORM -
Créer et activer l'environnement virtuel
python3 -m venv venv source venv/bin/activate -
Installer les dépendances
# Dépendances de prod (runtime) pip install -r requirements.txt # + dépendances de test (pytest, pytest-cov, httpx) pour lancer la suite pip install -r requirements-dev.txt -
Configurer PostgreSQL
Créer une base de données PostgreSQL :
CREATE DATABASE fastapi_db;
CREATE USER fastapi WITH PASSWORD 'fastapi';
GRANT ALL PRIVILEGES ON DATABASE fastapi_db TO fastapi;
- Configurer les variables d'environnement (optionnel)
Créer un fichier .env :
DB_HOSTNAME=localhost
DB_PORT=5432
DB_PASSWORD=fastapi
DB_NAME=fastapi_db
DB_USERNAME=fastapi
SECRET_KEY=your-secret-key
ACCESS_TOKEN_EXPIRE_MINUTES=30
- Lancer l'application
en local
uvicorn app.main:app --reload --port 8080
uvicorn --host 0.0.0.0 app.main:app --reload --port 8080
L'API sera accessible sur : http://localhost:8080
Documentation interactive : http://localhost:8080/docs
🚀 Utilisation de l'API
Authentification
1. Créer un compte
POST /users/
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword"
}
2. Se connecter
POST /login
Content-Type: application/x-www-form-urlencoded
username=user@example.com&password=securepassword
Réponse :
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}
3. Utiliser le token
Ajouter le header à toutes les requêtes protégées :
Authorization: Bearer <votre_token>
Posts
Créer un post
POST /posts/
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "Mon premier post",
"content": "Contenu du post",
"published": true
}
Récupérer tous les posts
GET /posts/
Authorization: Bearer <token>
Récupérer tous les posts (public, lecture seule)
GET /posts/public
app.devopsyouss.com). Même requête que GET /posts/ (posts + votes),
sans le gate JWT. Supporte les mêmes query params (limit, skip, search).
Le back autorise cette origine via CORS (ALLOWED_ORIGINS, restreint à ce seul
sous-domaine, pas de wildcard).
Récupérer un post spécifique
GET /posts/{id}
Authorization: Bearer <token>
Mettre à jour un post
PUT /posts/{id}
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "Titre modifié",
"content": "Contenu modifié",
"published": true
}
Supprimer un post
DELETE /posts/{id}
Authorization: Bearer <token>
Votes
Voter pour un post (like)
POST /votes/
Authorization: Bearer <token>
Content-Type: application/json
{
"post_id": 1,
"dir": 1
}
Retirer son vote (unlike)
POST /votes/
Authorization: Bearer <token>
Content-Type: application/json
{
"post_id": 1,
"dir": 0
}
🔍 Query Parameters
Les query parameters permettent de filtrer et paginer les résultats :
Pagination
GET /posts?limit=10&skip=0
limit : Nombre maximum de résultats (défaut: 10)
- skip : Nombre de résultats à ignorer (défaut: 0)
Recherche
GET /posts?search=FastAPI
search : Recherche dans les titres des posts
Combinaison
GET /posts?search=FastAPI&limit=5&skip=2
Note : Pour rechercher avec des espaces, utiliser %20 :
GET /posts?search=seminars%20fiqh
🧪 Tester l'API
Avec Postman
- Importer la collection (si disponible)
- Créer une variable d'environnement
{{URL}}=http://localhost:8080 - Tester les endpoints
Avec Swagger UI
Accéder à http://localhost:8080/docs pour une interface interactive.
Peupler la db de données avec Posts,Votes,Users
python3 -m example_data.seed_db
🩺 Health checks
Deux endpoints dédiés (sans logique métier ni auth) pour les probes Kubernetes :
| Endpoint | Rôle | Probe K8s | Réponse |
|---|---|---|---|
GET /healthz/live |
le process répond (liveness) | livenessProbe |
200 tant que le process tourne |
GET /healthz/ready |
dépendances prêtes (readiness) | readinessProbe |
200 si SELECT 1 DB OK, sinon 503 |
Les probes du Deployment pointent sur ces endpoints (et non plus sur /), pour ne pas router de trafic vers un pod dont la base n'est pas joignable (#47).
📊 Métriques (observabilité)
L'application expose ses métriques au format Prometheus via l'endpoint /metrics, branché par prometheus-fastapi-instrumentator (#73).
| Endpoint | Rôle | Format |
|---|---|---|
GET /metrics |
métriques runtime (process, GC) + HTTP (compteurs de requêtes, latences, tailles par route et code) | exposition Prometheus (text/plain; version=0.0.4) |
- Les probes (
/healthz/live,/healthz/ready) et/metricslui-même sont exclus du comptage HTTP pour ne pas polluer les séries. /metricsest scrapé en interne par Prometheus (Service ClusterIP, kube-prometheus-stack #74), pas exposé publiquement. Le Gateway public renvoie 404 sur/metricsvia une rule HTTPRoute dédiée +HTTPRouteFilter directResponse(#81, ADR 018) : la requête n'atteint jamais le backend, le scrape interne n'est pas affecté.- La consultation humaine du monitoring se fait via Grafana (#76), pas via
/metricsbrut.
🔭 Tracing distribué (OpenTelemetry)
L'application émet des traces OpenTelemetry (#107/#108, ADR 027), 3e pilier d'observabilité après les métriques et les logs. L'instrumentation vit dans app/tracing.py, branchée depuis main.py :
- Auto-instrumentation FastAPI : un span par requête HTTP, avec propagation du contexte W3C
traceparent. - Auto-instrumentation SQLAlchemy : un span par requête SQL, pour visualiser le temps passé dans la base (hors HTTP).
- Les spans partent en OTLP/HTTP vers le OTel Collector (
otel-collector.tracing.svc:4318), qui les relaie à Tempo ; consultation et corrélation trace ↔ logs dans Grafana.
Le tracing est conditionnel :
app/tracing.pyne s'active que si la variableOTEL_EXPORTER_OTLP_ENDPOINTest présente (injectée par le Deployment en prod EKS). En local et dans les tests, c'est un no-op, l'app démarre sans tracing ni erreur d'export. Il coexiste avecprometheus-fastapi-instrumentator(#73) sans bump de starlette (dette #95 préservée).
Service graph + fix OOM boot (#117/#125) : le metrics-generator de Tempo (service_graphs + span_metrics) dérive des métriques Prometheus depuis les traces déjà ingérées, sans nouvelle instrumentation. OTEL_PYTHON_EXCLUDED_URLS=healthz,metrics sur le Deployment réduit le volume de spans des probes/scrape à la source (lu nativement par le SDK OTel, zéro changement dans app/tracing.py), mitigation du pic mémoire observé au boot de Tempo.
Le détail bout-en-bout (pipeline, Tempo, service graph, corrélation des 3 piliers) est dans le guide comprendre/tracing.md.
🎨 Frontend (SPA)
Un 2e workload vit dans frontend/ (#109, ADR 028) : une SPA React + Vite volontairement minimale. L'intérêt est l'angle DevOps (image durcie, scan, GitOps), pas l'applicatif. Le contenu (une page qui liste des posts) est jetable et destiné à évoluer sans toucher la chaîne DevSecOps (stratégie V1/V2).
Image multi-stage durcie (frontend/Dockerfile, bases pinnées par digest) :
- Build :
node:22-slimcompile les assets Vite (npm cidepuis le lock, puisnpm run build). - Runtime :
nginxinc/nginx-unprivileged:1.27-alpinesert les assets statiques. nginx tourne déjà en uid 101 non-root sur le port 8080 (non privilégié), compatiblerunAsNonRoot/readOnlyRootFilesystem. Seuldist/passe en production (pas de toolchain Node). apk upgradeau runtime : le tag1.27-alpineembarque un alpine figé (openssl/musl avec des CVE HIGH/CRITICAL déjà corrigées en amont) ; l'upgrade ramène les versions patchées → 0 CVE fixable au gate Trivy, même philosophie que le back (patcher les fixables).nginx.conf: fallback SPAtry_files $uri $uri/ /index.html(le routing côté client gère les chemins inconnus).
Intégration front/back (#110) : la SPA fait un vrai fetch vers ${VITE_API_BASE_URL}/posts/public (endpoint public lecture seule, CORS restreint à app.devopsyouss.com côté FastAPI). VITE_API_BASE_URL est figée au build par Vite (défaut prod dans le Dockerfile ; le docker-compose-dev l'override en http://localhost:8080 pour exercer CORS en local avant push). Build + CI + scan de l'image = #109 ; déploiement GitOps + routing + intégration = #110.
Rendu (#124) : la page est présentée en feed vertical façon LinkedIn (cartes à ombre douce, avatar + auteur + date en en-tête, compteur de votes en pied, accent bleu). Changement purement cosmétique (frontend/src/index.css + markup App.jsx) : il réutilise le payload /posts/public existant (owner, created_at, votes) sans toucher ni au back, ni au CORS, ni au routing, ni à la NetworkPolicy. Reste dans la promesse V1/V2.
Chaîne CI (#109) — deux jobs jumeaux du backend, sur le repo ECR dédié fastapi-eks/frontend :
build-frontend-candidate: kaniko build l'image (contextefrontend/) et pousse le tag candidate dans ECR.trivy-frontend-image-scan: double-gate Trivy (CRITICAL/HIGH,--ignore-unfixed) + SBOM CycloneDX. Scan sans.trivyignore/VEX : les exceptions du repo ne visent que des paquets OS Debian de la base backend, sans objet pour une image alpine.
Déploiement GitOps + routing (#110) — le front est déployé comme le back (pull-based ArgoCD), dans son propre namespace :
- Manifests
k8s/frontend/: Deployment nginx (2 replicas, non-root uid 101,readOnlyRootFilesystem+emptyDirpour le cache/pid nginx,drop: ALL,seccompProfile: RuntimeDefault), Service ClusterIP80 → 8080, SA dédié sans token monté. - Namespace
frontend: pré-créé par le bootstrap Ansible (labels PSAenforce: restricted+ LimitRange avant tout pod, leçon #112), pas par ArgoCD (CreateNamespace=false). - Routing : HTTPRoute
app.devopsyouss.comsur le Gateway partagéfastapi-gateway(cross-namespace, autorisé parallowedRoutes:from:All). Le cert wildcard*.devopsyouss.comcouvreapp., ExternalDNS crée le CNAME automatiquement (pattern grafana/argocd). - NetworkPolicy :
default-deny(ingress + egress) + une règle qui n'ouvre que l'ingress 8080 depuisenvoy-gateway-system. Egress totalement fermé : nginx sert du statique sans upstream, lefetch /posts/publicpart du navigateur vers l'API, jamais du pod front. - Application ArgoCD
frontend(k8s/platform/argocd-apps/) : le champimages:dukustomization.yamlest la source de vérité du tag. Bootstrap sur une imagecandidate-(#110 MR2), basculé surdeployed-<sha>par le write-back CI (#110 MR3, blindage expiration ECR #104).
Supply chain de déploiement (#110) — même mécanique promote-by-digest que le back :
promote-frontend-image: retag par digest de la candidate scannée versdeployed-<sha>sur le repo ECR front (image déployée == image scannée). Job promote distinct du back (il écrit dans ECR, pas de course).- Un seul job write-back (
update-image-tag) bumpe les deux kustomizations (k8s/base+k8s/frontend) et pousse un seul commit[skip ci]sur develop. Deux write-back concurrents se rejetteraient (non-fast-forward) : un seul écrivain Git pour les deux workloads. - Re-scan continu de l'image front déployée (
registry-scan) : suivi côté vuln-mgmt (#118), pas dans #110.
V2 — authentification (#128, MR1) — première des 3 MR de contenu (auth → écriture+vote → lecture enrichie), toutes hors chaîne DevSecOps (cf. addendum V2 de l'ADR 028) :
frontend/src/api.js:login()(form-urlencoded,POST /login) etregister()(JSON,POST /users/), fetch purs sans dépendance ajoutée.frontend/src/auth.js: lecture/écriture du JWT ensessionStorage, rehydraté au montage de l'app.frontend/src/AuthForms.jsx: panneau connexion/inscription (bascule d'onglet), affiché tant qu'aucune session n'est active.- L'en-tête affiche l'email connecté + un bouton déconnexion (efface l'état +
sessionStorage, pas d'endpoint/logout: JWT stateless). - Aucun appel authentifié n'est encore fait (pas de création de post ni de vote) : le token posé ici sera consommé en MR2. Détail du flux et des choix (stockage, CORS) dans le guide
comprendre/auth-frontend.md.
V2 — écriture (#128, MR2) — consomme le JWT posé en MR1 :
frontend/src/api.jsétendu :createPost()(POST /posts/, Bearer) etcastVote()(POST /votes/, Bearer), via un helperauthFetch()interne qui attache le headerAuthorizationet propage le status HTTP de l'erreur.frontend/src/PostForm.jsx: formulaire titre/contenu, visible uniquement connecté. Le post créé (déjà avecownerrésolu par l'API) est ajouté en tête du feed sans re-fetch.- Bouton vote sur chaque carte (visible uniquement connecté) : l'API ne renvoyant pas de flag « j'ai voté », l'état est déduit des réponses
409/404dePOST /votes/. Détail danscomprendre/auth-frontend.md.
V2 — lecture enrichie (#128, MR3) — dernière des 3 MR de contenu, referme #128 :
frontend/src/api.jsétendu :fetchPublicPosts({ search, skip, limit })(réutilise la pagination/recherche déjà servies par/posts/public) etgetPostDetail()(GET /posts/{id}, Bearer — réservé aux connectés côté API, contrat inchangé).- Barre de recherche + bouton « Charger plus » (
skipavance par pages de 5) sur le feed public. - Panneau détail (connecté uniquement) affichant le post via
GET /posts/{id}. Ce endpoint renvoie le même formatPostOut({ Post, votes }) que/posts/public, pas unPostplat — piège repéré en testant, corrigé avant merge.
Les 3 MR de #128 sont closes : promesse V1/V2 tenue, zéro changement back/infra pour tout le contenu React.
V3 — CRUD complet (#130, MR1) — modification et suppression de ses propres posts, détail repositionné :
frontend/src/api.jsétendu :updatePost()(PUT /posts/{id}, Bearer, body identique à la création) etdeletePost()(DELETE /posts/{id}, Bearer, 204). Les deux endpoints existaient côté API depuis l'origine (owner-only, 403 sinon) : zéro changement back.- Boutons Modifier / Supprimer affichés uniquement sur ses propres posts (comparaison
owner.email, l'API restant l'autorité via le 403). Édition inline dans la carte (formulaire pré-rempli,EditPostForm), suppression avec confirmation. - Le panneau détail s'ouvre désormais sous le post cliqué (re-clic pour replier), au lieu d'un bloc fixe en haut de page.
V3 — habillage DevOps (#130, MR2) — footer « stack » et logos :
frontend/src/StackFooter.jsx: footer avec les logos de la stack (React, FastAPI, GitLab, Argo CD, Kubernetes, Terraform, Grafana) embarqués en SVG inline (source simple-icons, CC0) — aucun chargement externe au runtime, surface supply chain inchangée. Couleurs de marque permanentes.- Logos cliquables vers les livrables : repo GitLab, Argo CD (
argocd.), Grafana (grafana.), Swagger de l'API (api./docs), plus un lien vers la doc MkDocs (doc.). - AWS EKS en badge texte : le logo AWS a été retiré de simple-icons à la demande d'AWS, on n'embarque pas une marque retirée volontairement.
🔒 Sécurité
- Mots de passe hashés avec bcrypt
- Authentification par JWT tokens
- Tokens expirables (configurable)
- Protection CSRF
- Validation des données avec Pydantic
Durcissement au niveau cluster (Sprint 3) — voir Infrastructure EKS et l'ADR 008 :
- Conteneur non-root,
readOnlyRootFilesystem,capabilities: drop ALL,seccompProfile: RuntimeDefault(#43) - ServiceAccount dédié,
automountServiceAccountToken: false(#45) - Pod Security Admission
enforce: restrictedsur le namespace (#48, effectif depuis INC-045) - NetworkPolicy default-deny + allowlists egress (#48), non enforced tant que le VPC CNI n'est pas en addon managé (#55)
📦 Technologies utilisées
- FastAPI : Framework web moderne et rapide
- SQLAlchemy : ORM Python
- PostgreSQL : Base de données relationnelle
- Pydantic V2 : Validation des données
- JWT : Authentification par tokens
- Bcrypt : Hashage des mots de passe
- Uvicorn : Serveur ASGI
📝 Notes de migration Pydantic V2
Ce projet utilise Pydantic V2. Changements importants :
- orm_mode = True → from_attributes = True
- BaseSettings déplacé vers pydantic-settings
- Installation requise : pip install pydantic-settings
🤝 Contribution
Les contributions sont les bienvenues ! N'hésitez pas à ouvrir une issue ou une pull request.
📄 Licence
Ce projet est un playground éducatif pour apprendre FastAPI et les pratiques DevOps.