Skip to content

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 unique
  • title: Titre du post
  • content: Contenu
  • published: Statut de publication
  • created_at: Date de création
  • user_id: Référence vers l'utilisateur propriétaire

User

  • id: Identifiant unique
  • email: Email (unique)
  • password: Mot de passe hashé
  • created_at: Date de création

Votes

  • post_id: Référence vers le post
  • user_id: Référence vers l'utilisateur

⚙️ Installation

Prérequis

  • Python 3.12+
  • PostgreSQL

Étapes d'installation

  1. Cloner le repository et basculer sur la branche ORM

    git checkout feature/fastapi-ORM
    

  2. Créer et activer l'environnement virtuel

    python3 -m venv venv
    source venv/bin/activate
    

  3. 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
    

  4. 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;

  1. 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

  1. Lancer l'application

en local

uvicorn app.main:app --reload --port 8080
en production
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
Endpoint sans authentification (#110) : c'est le contrat consommé par la SPA frontend (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

  1. Importer la collection (si disponible)
  2. Créer une variable d'environnement {{URL}} = http://localhost:8080
  3. 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 /metrics lui-même sont exclus du comptage HTTP pour ne pas polluer les séries.
  • /metrics est scrapé en interne par Prometheus (Service ClusterIP, kube-prometheus-stack #74), pas exposé publiquement. Le Gateway public renvoie 404 sur /metrics via 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 /metrics brut.

🔭 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.py ne s'active que si la variable OTEL_EXPORTER_OTLP_ENDPOINT est 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 avec prometheus-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-slim compile les assets Vite (npm ci depuis le lock, puis npm run build).
  • Runtime : nginxinc/nginx-unprivileged:1.27-alpine sert les assets statiques. nginx tourne déjà en uid 101 non-root sur le port 8080 (non privilégié), compatible runAsNonRoot / readOnlyRootFilesystem. Seul dist/ passe en production (pas de toolchain Node).
  • apk upgrade au runtime : le tag 1.27-alpine embarque 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 SPA try_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 (contexte frontend/) 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 + emptyDir pour le cache/pid nginx, drop: ALL, seccompProfile: RuntimeDefault), Service ClusterIP 80 → 8080, SA dédié sans token monté.
  • Namespace frontend : pré-créé par le bootstrap Ansible (labels PSA enforce: restricted + LimitRange avant tout pod, leçon #112), pas par ArgoCD (CreateNamespace=false).
  • Routing : HTTPRoute app.devopsyouss.com sur le Gateway partagé fastapi-gateway (cross-namespace, autorisé par allowedRoutes:from:All). Le cert wildcard *.devopsyouss.com couvre app., ExternalDNS crée le CNAME automatiquement (pattern grafana/argocd).
  • NetworkPolicy : default-deny (ingress + egress) + une règle qui n'ouvre que l'ingress 8080 depuis envoy-gateway-system. Egress totalement fermé : nginx sert du statique sans upstream, le fetch /posts/public part du navigateur vers l'API, jamais du pod front.
  • Application ArgoCD frontend (k8s/platform/argocd-apps/) : le champ images: du kustomization.yaml est la source de vérité du tag. Bootstrap sur une image candidate- (#110 MR2), basculé sur deployed-<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 vers deployed-<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) et register() (JSON, POST /users/), fetch purs sans dépendance ajoutée.
  • frontend/src/auth.js : lecture/écriture du JWT en sessionStorage, 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) et castVote() (POST /votes/, Bearer), via un helper authFetch() interne qui attache le header Authorization et propage le status HTTP de l'erreur.
  • frontend/src/PostForm.jsx : formulaire titre/contenu, visible uniquement connecté. Le post créé (déjà avec owner ré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/404 de POST /votes/. Détail dans comprendre/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) et getPostDetail() (GET /posts/{id}, Bearer — réservé aux connectés côté API, contrat inchangé).
  • Barre de recherche + bouton « Charger plus » (skip avance 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 format PostOut ({ Post, votes }) que /posts/public, pas un Post plat — 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) et deletePost() (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: restricted sur 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 = Truefrom_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.