🧩 Architecture Microservices
📖 Définition
L'architecture microservices (MSA, Microservices Architecture) est un modèle architectural qui divise une grande application en plusieurs services petits et indépendants pour le développement et le déploiement. Chaque service est responsable d'une fonction métier spécifique et peut être déployé et mis à l'échelle de manière indépendante. Contrairement à l'architecture monolithique, elle offre flexibilité et évolutivité grâce à un couplage faible entre les services.
🎯 Comprendre par analogies
Grande entreprise vs Startups
Monolithique = Grande entreprise
├─ Tous les départements dans un bâtiment
├─ Gestion centralisée
├─ Problème dans un département → Affecte tout
├─ Changements difficiles
└─ Décisions lentes
Microservices = Fédération de startups
├─ Chaque équipe a un bureau indépendant
├─ Décisions autonomes
├─ Problème dans une équipe → Les autres fonctionnent normalement
├─ Changements rapides
└─ Mise à l'échelle flexible
LEGO vs Argile
Monolithique = Bloc d'argile
┌──────────────────────────────┐
│ Utilisateur │ Produit │ Commande │
│ Gestion │ Gestion │ Gestion │
│ Tout en un │
└──────────────────────────────┘
- Doit être refait complètement
- Modifier une partie → Affecte le tout
- Difficile à mettre à l'échelle
Microservices = Briques LEGO
┌─────┐ ┌─────┐ ┌─────┐
│Utilisateur│ │Produit│ │Commande│
│Service│ │Service│ │Service│
└─────┘ └─────┘ └─────┘
- Facile de remplacer les briques
- Modification indépendante
- Mettre à l'échelle uniquement ce qui est nécessaire
⚙️ Fonctionnement
1. Monolithique vs Microservices
========== Monolithique ==========
┌─────────────────────────────────────┐
│ Une application │
│ │
│ ┌─────────────────────────────┐ │
│ │ Module gestion utilisateurs│ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ Module gestion produits │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ Module gestion commandes│ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ Module de paiements │ │
│ └─────────────────────────────┘ │
│ │
│ Une base de données │
│ Une base de code │
│ Une unité de déploiement │
└─────────────────────────────────────┘
Avantages :
✅ Rapide au début du développement
✅ Tests simples
✅ Déploiement simple (un seul)
✅ Débogage facile
Inconvénients :
❌ Complexe en grandissant
❌ Interruption totale lors du déploiement
❌ Impossible de mettre à l'échelle partiellement
❌ Difficile de changer la stack technologique
========== Microservices ==========
┌──────────┐ ┌──────────┐ ┌──────────┐
│Utilisateur│ │Produit │ │Commande │
│Service │ │Service │ │Service │
│ │ │ │ │ │
│Node.js │ │Java │ │Go │
│MongoDB │ │MySQL │ │PostgreSQL │
└──────────┘ └──────────┘ └──────────┘
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│Paiement │ │Notification│ │Avis │
│Service │ │Service │ │Service │
│ │ │ │ │ │
│Python │ │Node.js │ │Ruby │
│Redis │ │Kafka │ │Cassandra │
└──────────┘ └──────────┘ └──────────┘
Avantages :
✅ Déploiement indépendant
✅ Liberté de stack technologique
✅ Mise à l'échelle partielle possible
✅ Indépendance des équipes
✅ Isolation des pannes
Inconvénients :
❌ Complexité initiale élevée
❌ Surcharge de communication réseau
❌ Transactions distribuées difficiles
❌ Tests complexes
❌ Coûts opérationnels augmentés
2. Communication entre services
========== Communication synchrone (HTTP/REST) ==========
Service de Commandes → Service de Produits
"Y a-t-il du stock pour le produit 123 ?"
↓
"Oui, 5 unités disponibles"
↓
Service de Commandes → Service de Paiements
"Veuillez traiter un paiement de 10 000"
↓
"Paiement effectué"
↓
Commande terminée
Avantages : Simple, intuitif
Inconvénients : Échec total si un service échoue
========== Communication asynchrone (File de messages) ==========
Service de Commandes → File de Messages
Publier message "Commande créée"
↓
┌──────────────────┐
│ File de Messages│
│ (RabbitMQ, │
│ Kafka, etc.) │
└──────────────────┘
↓
┌────┴────┬────────┐
↓ ↓ ↓
Paiement Notification Inventaire
Service Service Service
Chacun traite indépendamment
Avantages : Couplage faible, isolation des pannes
Inconvénients : Complexité accrue, débogage difficile
========== API Gateway ==========
Client (Mobile/Web)
↓
┌──────────────────┐
│ API Gateway │
│ - Routage │
│ - Authentification│
│ - Équilibrage de charge│
│ - Journalisation│
└──────────────────┘
┌────┴────┬────────┐
↓ ↓ ↓
ServiceA ServiceB ServiceC
Rôles :
- Point d'entrée unique
- Simplification du client
- Gestion des fonctions communes
3. Gestion des données
========== Monolithique : Base de données partagée ==========
┌─────────────────────────────────────┐
│ Application │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ModuleA│ │ModuleB│ │ModuleC│ │
│ └──┬──┘ └──┬──┘ └──┬──┘ │
└─────┼────────┼───────┼──────────────┘
└────────┼───────┘
↓
┌──────────────────┐
│ Une base de données│
│ ┌────┬────┬────┐│
│ │TA│TB│TC││
│ └────┴────┴────┘│
└──────────────────┘
Avantages : JOINs faciles, cohérence garantie
Inconvénients : Couplage élevé, difficile à mettre à l'échelle
========== Microservices : BDD par service ==========
┌──────────┐ ┌──────────┐ ┌──────────┐
│Service A │ │Service B │ │Service C │
└────┬─────┘ └────┬─────┘ └────┬─────┘
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ BDD A │ │ BDD B │ │ BDD C │
│ (MySQL) │ │(MongoDB)│ │(PostgreSQL)│
└─────────┘ └─────────┘ └─────────┘
Avantages : Indépendance, liberté de choix technologique
Inconvénients : Pas de JOINs, cohérence difficile
========== Cohérence des données ==========
// Pattern Saga
1. Service de Commandes : Créer commande
2. Service de Paiements : Traiter paiement
3. Service d'Inventaire : Réduire stock
4. Service d'Expédition : Démarrer expédition
Que se passe-t-il en cas d'échec à l'étape 3 ?
→ Transaction compensatoire
4. Échec de réduction du stock
3. Annuler paiement ← Compensation
2. Annuler commande ← Compensation
💡 Exemples pratiques
Exemple monolithique (Express.js)
// ========== Application monolithique ==========
// server.js - Un fichier avec toutes les fonctionnalités
const express = require('express');
const app = express();
app.use(express.json());
// Une base de données
const db = require('./database');
// ========== Gestion des utilisateurs ==========
app.post('/api/users', async (req, res) => {
const { username, email, password } = req.body;
const user = await db.users.create({ username, email, password });
res.json(user);
});
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user);
});
// ========== Gestion des produits ==========
app.post('/api/products', async (req, res) => {
const { name, price, stock } = req.body;
const product = await db.products.create({ name, price, stock });
res.json(product);
});
app.get('/api/products', async (req, res) => {
const products = await db.products.findAll();
res.json(products);
});
// ========== Gestion des commandes ==========
app.post('/api/orders', async (req, res) => {
const { userId, productId, quantity } = req.body;
// Transaction pour garantir la cohérence
const transaction = await db.sequelize.transaction();
try {
// 1. Vérifier le stock
const product = await db.products.findById(productId, { transaction });
if (product.stock < quantity) {
throw new Error('Stock insuffisant');
}
// 2. Réduire le stock
await product.update(
{ stock: product.stock - quantity },
{ transaction }
);
// 3. Créer la commande
const order = await db.orders.create(
{ userId, productId, quantity, total: product.price * quantity },
{ transaction }
);
// 4. Traiter le paiement
await processPayment(order.total);
await transaction.commit();
res.json(order);
} catch (error) {
await transaction.rollback();
res.status(400).json({ error: error.message });
}
});
// ========== Traitement des paiements ==========
app.post('/api/payments', async (req, res) => {
const { orderId, amount } = req.body;
const payment = await db.payments.create({ orderId, amount });
res.json(payment);
});
// Exécuter avec un seul serveur
app.listen(3000, () => {
console.log('Serveur monolithique : http://localhost:3000');
});
/*
Avantages :
- Code en un seul endroit
- Développement rapide
- Débogage facile
- Transactions simples
Inconvénients :
- Complexe en grandissant
- Redémarrage complet lors du déploiement
- Impossible de mettre à l'échelle partiellement
- Un dysfonctionnement → Affecte tout
*/
Exemple de microservices
// ========== 1. Service Utilisateurs (user-service.js) ==========
// Port : 3001
const express = require('express');
const app = express();
const mongoose = require('mongoose');
app.use(express.json());
// Base de données indépendante
mongoose.connect('mongodb://localhost/users-db');
const User = mongoose.model('User', {
username: String,
email: String,
password: String
});
// Créer un utilisateur
app.post('/users', async (req, res) => {
const { username, email, password } = req.body;
try {
const user = new User({ username, email, password });
await user.save();
// Publier un événement (notifier les autres services)
await publishEvent('user.created', { userId: user._id, email });
res.json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Consulter un utilisateur
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
});
app.listen(3001, () => {
console.log('Service utilisateurs : http://localhost:3001');
});
// ========== 2. Service Produits (product-service.js) ==========
// Port : 3002
const express = require('express');
const app = express();
const { Pool } = require('pg');
app.use(express.json());
// Utiliser PostgreSQL (différente BDD !)
const pool = new Pool({
host: 'localhost',
database: 'products-db',
port: 5432
});
// Liste des produits
app.get('/products', async (req, res) => {
const result = await pool.query('SELECT * FROM products');
res.json(result.rows);
});
// Détails du produit
app.get('/products/:id', async (req, res) => {
const result = await pool.query(
'SELECT * FROM products WHERE id = $1',
[req.params.id]
);
res.json(result.rows[0]);
});
// Vérifier le stock
app.get('/products/:id/stock', async (req, res) => {
const result = await pool.query(
'SELECT stock FROM products WHERE id = $1',
[req.params.id]
);
res.json({ stock: result.rows[0].stock });
});
// Réduire le stock
app.post('/products/:id/decrease-stock', async (req, res) => {
const { quantity } = req.body;
const client = await pool.connect();
try {
await client.query('BEGIN');
// Vérifier le stock actuel
const result = await client.query(
'SELECT stock FROM products WHERE id = $1 FOR UPDATE',
[req.params.id]
);
const currentStock = result.rows[0].stock;
if (currentStock < quantity) {
throw new Error('Stock insuffisant');
}
// Réduire le stock
await client.query(
'UPDATE products SET stock = stock - $1 WHERE id = $2',
[quantity, req.params.id]
);
await client.query('COMMIT');
res.json({ success: true });
} catch (error) {
await client.query('ROLLBACK');
res.status(400).json({ error: error.message });
} finally {
client.release();
}
});
app.listen(3002, () => {
console.log('Service produits : http://localhost:3002');
});
// ========== 3. Service Commandes (order-service.js) ==========
// Port : 3003
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
const orders = []; // Devrait utiliser une base de données
// Créer une commande
app.post('/orders', async (req, res) => {
const { userId, productId, quantity } = req.body;
try {
// 1. Vérifier l'utilisateur (appeler service utilisateurs)
const userResponse = await axios.get(
`http://localhost:3001/users/${userId}`
);
const user = userResponse.data;
if (!user) {
return res.status(404).json({ error: 'Utilisateur non trouvé' });
}
// 2. Consulter les infos produit (appeler service produits)
const productResponse = await axios.get(
`http://localhost:3002/products/${productId}`
);
const product = productResponse.data;
// 3. Vérifier le stock
const stockResponse = await axios.get(
`http://localhost:3002/products/${productId}/stock`
);
const { stock } = stockResponse.data;
if (stock < quantity) {
return res.status(400).json({ error: 'Stock insuffisant' });
}
// 4. Réduire le stock
await axios.post(
`http://localhost:3002/products/${productId}/decrease-stock`,
{ quantity }
);
// 5. Traiter le paiement (appeler service paiements)
const total = product.price * quantity;
const paymentResponse = await axios.post(
'http://localhost:3004/payments',
{ userId, amount: total }
);
// 6. Créer la commande
const order = {
id: orders.length + 1,
userId,
productId,
quantity,
total,
status: 'completed',
createdAt: new Date()
};
orders.push(order);
// 7. Publier un événement
await publishEvent('order.created', order);
res.json(order);
} catch (error) {
// Pattern Saga : Transaction compensatoire
console.error('Échec de la commande :', error.message);
// Restaurer le stock
try {
await axios.post(
`http://localhost:3002/products/${productId}/increase-stock`,
{ quantity }
);
} catch (rollbackError) {
console.error('Échec de restauration du stock :', rollbackError.message);
}
res.status(500).json({ error: 'Échec du traitement de la commande' });
}
});
// Consulter une commande
app.get('/orders/:id', (req, res) => {
const order = orders.find(o => o.id === parseInt(req.params.id));
res.json(order);
});
app.listen(3003, () => {
console.log('Service commandes : http://localhost:3003');
});
// ========== 4. Service Paiements (payment-service.js) ==========
// Port : 3004
const express = require('express');
const app = express();
app.use(express.json());
const payments = [];
app.post('/payments', async (req, res) => {
const { userId, amount } = req.body;
// Appeler API de paiement externe (ex : Stripe, Toss Payments)
try {
// Traitement de paiement réel
const payment = {
id: payments.length + 1,
userId,
amount,
status: 'success',
createdAt: new Date()
};
payments.push(payment);
// Publier un événement
await publishEvent('payment.completed', payment);
res.json(payment);
} catch (error) {
res.status(400).json({ error: 'Échec du paiement' });
}
});
app.listen(3004, () => {
console.log('Service paiements : http://localhost:3004');
});
// ========== 5. API Gateway (gateway.js) ==========
// Port : 3000
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// Middleware d'authentification
function authenticate(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Authentification requise' });
}
// Vérification JWT, etc.
next();
}
// Middleware de journalisation
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
// Proxy service utilisateurs
app.use('/api/users', authenticate, createProxyMiddleware({
target: 'http://localhost:3001',
pathRewrite: { '^/api/users': '/users' },
changeOrigin: true
}));
// Proxy service produits
app.use('/api/products', createProxyMiddleware({
target: 'http://localhost:3002',
pathRewrite: { '^/api/products': '/products' },
changeOrigin: true
}));
// Proxy service commandes
app.use('/api/orders', authenticate, createProxyMiddleware({
target: 'http://localhost:3003',
pathRewrite: { '^/api/orders': '/orders' },
changeOrigin: true
}));
// Proxy service paiements
app.use('/api/payments', authenticate, createProxyMiddleware({
target: 'http://localhost:3004',
pathRewrite: { '^/api/payments': '/payments' },
changeOrigin: true
}));
app.listen(3000, () => {
console.log('API Gateway : http://localhost:3000');
});
// ========== 6. Bus d'événements (event-bus.js) ==========
const amqp = require('amqplib');
let connection, channel;
// Connexion RabbitMQ
async function connect() {
connection = await amqp.connect('amqp://localhost');
channel = await connection.createChannel();
}
// Publier un événement
async function publishEvent(eventType, data) {
await channel.assertQueue(eventType);
channel.sendToQueue(
eventType,
Buffer.from(JSON.stringify(data))
);
console.log(`Événement publié : ${eventType}`, data);
}
// S'abonner à un événement
async function subscribeEvent(eventType, callback) {
await channel.assertQueue(eventType);
channel.consume(eventType, (msg) => {
const data = JSON.parse(msg.content.toString());
console.log(`Événement reçu : ${eventType}`, data);
callback(data);
channel.ack(msg);
});
}
connect();
module.exports = { publishEvent, subscribeEvent };
Exécuter des microservices avec Docker Compose
# docker-compose.yml
version: '3.8'
services:
# API Gateway
gateway:
build: ./gateway
ports:
- "3000:3000"
depends_on:
- user-service
- product-service
- order-service
- payment-service
# Service utilisateurs
user-service:
build: ./user-service
ports:
- "3001:3001"
environment:
- MONGO_URL=mongodb://mongo:27017/users
depends_on:
- mongo
- rabbitmq
# Service produits
product-service:
build: ./product-service
ports:
- "3002:3002"
environment:
- POSTGRES_URL=postgres://postgres:password@postgres:5432/products
depends_on:
- postgres
- rabbitmq
# Service commandes
order-service:
build: ./order-service
ports:
- "3003:3003"
environment:
- MYSQL_URL=mysql://root:password@mysql:3306/orders
depends_on:
- mysql
- rabbitmq
# Service paiements
payment-service:
build: ./payment-service
ports:
- "3004:3004"
depends_on:
- rabbitmq
# Bases de données
mongo:
image: mongo:6
volumes:
- mongo-data:/data/db
postgres:
image: postgres:15
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=products
volumes:
- postgres-data:/var/lib/postgresql/data
mysql:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=orders
volumes:
- mysql-data:/var/lib/mysql
# File de messages
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672" # UI de gestion
volumes:
mongo-data:
postgres-data:
mysql-data:
Service Mesh (Exemple Istio)
# istio-config.yaml
# Service mesh - Gestion de la communication entre services
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
# Distribution du trafic (déploiement canary)
- match:
- headers:
user-type:
exact: beta
route:
- destination:
host: order-service
subset: v2 # Nouvelle version
weight: 20
- destination:
host: order-service
subset: v1 # Version existante
weight: 80
# Politique de réessai
- route:
- destination:
host: order-service
retries:
attempts: 3
perTryTimeout: 2s
# Timeout
timeout: 10s
---
# Circuit Breaker
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: payment-service
spec:
host: payment-service
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
http:
http1MaxPendingRequests: 50
maxRequestsPerConnection: 2
outlierDetection:
consecutiveErrors: 5
interval: 30s
baseEjectionTime: 30s
maxEjectionPercent: 50
🤔 Questions fréquentes
Q1. Quand dois-je utiliser les microservices ?
R :
✅ Approprié pour les microservices :
1. Application de grande échelle
- Équipe : Plus de 10 personnes
- Code : Plus de 100 000 lignes
- Utilisateurs : Des centaines de milliers ou plus
2. Besoin de déploiement rapide
- Déploiements multiples par jour
- Lancement de fonctionnalités indépendantes
- Tests A/B fréquents
3. Besoin de stack technologique diversifié
- Choisir la meilleure technologie par service
- Intégration avec systèmes legacy
4. Besoin de mise à l'échelle indépendante
- Trafic élevé sur fonctions spécifiques
- Exigences de ressources différentes par service
5. Indépendance des équipes importante
- Développement simultané par plusieurs équipes
- Minimiser les dépendances entre équipes
Exemples :
- Netflix : Des centaines de microservices
- Amazon : 2-pizza team (équipe par service)
- Uber : Services séparés par région et fonction
❌ Approprié pour monolithique :
1. Petite application
- Équipe : 5 personnes ou moins
- Fonctions : Claires et simples
- Trafic : Faible
2. Startup en début de parcours
- Besoin de développement rapide d'un MVP
- Les exigences changent fréquemment
- Ressources limitées
3. CRUD simple
- Pas de logique métier complexe
- Limites de service peu claires
4. Manque d'expérience opérationnelle
- Pas d'équipe DevOps
- Pas d'expérience des systèmes distribués
Exemples :
- Blog, portfolio
- Petit commerce électronique
- Outils internes
📊 Liste de vérification pour la décision :
□ Équipe de plus de 10 personnes ?
□ Base de code de plus de 100 000 lignes ?
□ Besoin fréquent de déploiement indépendant ?
□ Besoin de mise à l'échelle partielle ?
□ Équipe DevOps présente ?
□ Expérience des systèmes distribués ?
3 ou plus cochés → Considérer les microservices
2 ou moins → Maintenir le monolithique
Q2. Quel est le plus grand défi des microservices ?
R :
// ========== 1. Transactions distribuées ==========
// Monolithique : Transaction simple
await db.transaction(async (t) => {
await createOrder(data, t);
await decreaseStock(productId, t);
await processPayment(amount, t);
// En cas d'échec, rollback complet
});
// Microservices : Pattern Saga complexe
async function createOrderSaga(data) {
try {
// Étape 1
const order = await orderService.create(data);
// Étape 2
await productService.decreaseStock(data.productId);
// Étape 3
await paymentService.process(order.total);
return order;
} catch (error) {
// Transactions compensatoires (en sens inverse)
await paymentService.refund(order.total);
await productService.increaseStock(data.productId);
await orderService.cancel(order.id);
throw error;
}
}
// ========== 2. Cohérence des données ==========
// Problème : Données dispersées dans plusieurs services
// Service utilisateurs : userId, name
// Service commandes : userId, orders
// Service paiements : userId, payments
// Solution 1 : Event Sourcing
eventBus.on('user.updated', async (event) => {
// Lors du changement d'infos utilisateur, mettre à jour les autres services
await orderService.updateUserInfo(event.userId, event.name);
await paymentService.updateUserInfo(event.userId, event.name);
});
// Solution 2 : CQRS (Command Query Responsibility Segregation)
// Séparer écriture et lecture
// Écriture : Chaque service indépendant
// Lecture : Vue intégrée (Read Model)
// ========== 3. Latence réseau ==========
// Monolithique : Appel de fonction (rapide)
const user = getUser(userId); // 1ms
// Microservices : Requête HTTP (lente)
const user = await axios.get(`http://user-service/users/${userId}`); // 50ms
// Solution : Mise en cache
const redis = require('redis');
const cache = redis.createClient();
async function getUser(userId) {
// 1. Vérifier le cache
const cached = await cache.get(`user:${userId}`);
if (cached) return JSON.parse(cached);
// 2. Appeler le service
const response = await axios.get(`http://user-service/users/${userId}`);
const user = response.data;
// 3. Enregistrer dans le cache
await cache.setex(`user:${userId}`, 3600, JSON.stringify(user));
return user;
}
// ========== 4. Gestion des pannes de service ==========
// Pattern Circuit Breaker
const CircuitBreaker = require('opossum');
const options = {
timeout: 3000, // Timeout de 3 secondes
errorThresholdPercentage: 50, // À 50% d'échec
resetTimeout: 30000 // Réessayer après 30 secondes
};
const breaker = new CircuitBreaker(async (userId) => {
return await axios.get(`http://user-service/users/${userId}`);
}, options);
breaker.fallback(() => ({
id: userId,
name: 'Inconnu', // Données de secours
cached: true
}));
// Utiliser
breaker.fire(userId)
.then(console.log)
.catch(console.error);
// ========== 5. Surveillance et débogage ==========
// Traçage distribué (Distributed Tracing)
// Utiliser Jaeger, Zipkin
const tracer = require('jaeger-client').initTracer(config);
app.use((req, res, next) => {
const span = tracer.startSpan('http_request');
span.setTag('http.method', req.method);
span.setTag('http.url', req.url);
req.span = span;
next();
});
// Passer l'ID de trace lors des appels entre services
await axios.get('http://order-service/orders', {
headers: {
'x-trace-id': req.span.context().toTraceId()
}
});
Q3. Quel est le rôle de l'API Gateway ?
R :
// ========== Fonctions principales de l'API Gateway ==========
const express = require('express');
const rateLimit = require('express-rate-limit');
const jwt = require('jsonwebtoken');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// ========== 1. Routage ==========
// Le client n'a besoin de connaître qu'un seul point d'accès
app.use('/api/users', createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true
}));
app.use('/api/products', createProxyMiddleware({
target: 'http://product-service:3002',
changeOrigin: true
}));
// ========== 2. Authentification et autorisation ==========
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Token requis' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Token invalide' });
}
}
app.use('/api/orders', authenticate, createProxyMiddleware({
target: 'http://order-service:3003'
}));
// ========== 3. Limitation du taux ==========
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // Maximum 100 requêtes
});
app.use('/api/', limiter);
// ========== 4. Équilibrage de charge ==========
const productServiceInstances = [
'http://product-service-1:3002',
'http://product-service-2:3002',
'http://product-service-3:3002'
];
let currentIndex = 0;
app.use('/api/products', createProxyMiddleware({
target: productServiceInstances[currentIndex],
router: () => {
// Round robin
const target = productServiceInstances[currentIndex];
currentIndex = (currentIndex + 1) % productServiceInstances.length;
return target;
}
}));
// ========== 5. Transformation requête/réponse ==========
app.use('/api/legacy', createProxyMiddleware({
target: 'http://legacy-service:8080',
onProxyReq: (proxyReq, req) => {
// Transformer la requête
proxyReq.setHeader('X-API-Version', '2.0');
},
onProxyRes: (proxyRes, req, res) => {
// Transformer la réponse
proxyRes.headers['X-Custom-Header'] = 'Gateway';
}
}));
// ========== 6. Mise en cache ==========
const redis = require('redis');
const cache = redis.createClient();
app.get('/api/products/:id', async (req, res) => {
const cacheKey = `product:${req.params.id}`;
// Vérifier le cache
const cached = await cache.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
// Appeler le service
const response = await axios.get(
`http://product-service:3002/products/${req.params.id}`
);
// Enregistrer dans le cache
await cache.setex(cacheKey, 3600, JSON.stringify(response.data));
res.json(response.data);
});
// ========== 7. Journalisation et surveillance ==========
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log({
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
user: req.user?.id
});
});
next();
});
// ========== 8. Gestion des erreurs ==========
app.use((err, req, res, next) => {
console.error('Gateway Error:', err);
if (err.code === 'ECONNREFUSED') {
return res.status(503).json({
error: 'Service non disponible'
});
}
res.status(500).json({
error: 'Une erreur serveur s\'est produite'
});
});
// ========== 9. Découverte de services ==========
const consul = require('consul')();
async function getServiceUrl(serviceName) {
const result = await consul.health.service({
service: serviceName,
passing: true // Seulement les instances qui passent le health check
});
if (result.length === 0) {
throw new Error(`Service ${serviceName} non trouvé`);
}
// Sélection aléatoire
const instance = result[Math.floor(Math.random() * result.length)];
return `http://${instance.Service.Address}:${instance.Service.Port}`;
}
app.listen(3000);
Q4. Quelles sont les stratégies de déploiement des microservices ?
R :
# ========== 1. Déploiement Blue-Green ==========
# Déployer la nouvelle version (Green) et basculer le trafic en une fois
# Blue (version actuelle)
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
version: blue # Trafic actuel
ports:
- port: 80
---
# Déployer Green (nouvelle version)
kubectl apply -f order-service-green.yaml
# Basculer le trafic après test
kubectl patch service order-service -p '{"spec":{"selector":{"version":"green"}}}'
# Rollback immédiat en cas de problème
kubectl patch service order-service -p '{"spec":{"selector":{"version":"blue"}}}'
# ========== 2. Déploiement Canary ==========
# Envoyer seulement une partie du trafic vers la nouvelle version, étendre progressivement
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- route:
- destination:
host: order-service
subset: v1 # Version existante
weight: 90 # 90% trafic
- destination:
host: order-service
subset: v2 # Nouvelle version
weight: 10 # 10% trafic
# Augmenter progressivement
# 10% → 25% → 50% → 75% → 100%
# ========== 3. Mise à jour continue ==========
# Stratégie par défaut de Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Maximum 1 supplémentaire
maxUnavailable: 1 # Maximum 1 indisponible
template:
spec:
containers:
- name: order-service
image: order-service:v2
# Ordre :
# 1. Démarrer 1 nouveau Pod
# 2. Terminer 1 Pod existant quand health check réussit
# 3. Répéter (jusqu'à ce que les 5 soient remplacés)
# ========== 4. Déploiement avec Docker Compose ==========
# docker-compose.yml
version: '3.8'
services:
order-service:
image: order-service:latest
deploy:
replicas: 3
update_config:
parallelism: 1 # Un à la fois
delay: 10s # Intervalle de 10 secondes
failure_action: rollback # Rollback en cas d'échec
restart_policy:
condition: on-failure
# Déployer
docker stack deploy -c docker-compose.yml myapp
# ========== 5. Pipeline CI/CD ==========
# .github/workflows/deploy.yml
name: Deploy Microservices
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# Déployer chaque service indépendamment
- name: Détecter les services modifiés
uses: dorny/paths-filter@v2
id: changes
with:
filters: |
user-service:
- 'services/user/**'
product-service:
- 'services/product/**'
- name: Déployer le service utilisateurs
if: steps.changes.outputs.user-service == 'true'
run: |
docker build -t user-service:${{ github.sha }} services/user
docker push user-service:${{ github.sha }}
kubectl set image deployment/user-service user-service=user-service:${{ github.sha }}
- name: Déployer le service produits
if: steps.changes.outputs.product-service == 'true'
run: |
docker build -t product-service:${{ github.sha }} services/product
docker push product-service:${{ github.sha }}
kubectl set image deployment/product-service product-service=product-service:${{ github.sha }}
Q5. Comment migrer d'un monolithe vers les microservices ?
R :
// ========== Stratégie de migration par étapes ==========
// ========== Phase 1 : Pattern Strangler Fig ==========
// Nouvelles fonctionnalités en microservices, conserver l'existant
┌─────────────────────────────────┐
│ Application monolithique │
│ ┌──────────┬──────────┬──────┐ │
│ │Gestion │Gestion │Gestion│ │
│ │utilisateurs│produits │commandes│ │
│ └──────────┴──────────┴──────┘ │
└─────────────────────────────────┘
// Étape 1 : Nouvelle fonctionnalité (notifications) en microservice
┌─────────────────────┐ ┌──────────────┐
│ Monolithe │ │Service │
│ utilisateurs│produits│commandes│ notifications│
└─────────────────────┘ └──────────────┘
// Étape 2 : Séparer la fonction commandes
┌─────────────────────┐ ┌─── ───────────┐
│ Monolithe │ │Service │
│ utilisateurs│produits│ commandes │
└─────────────────────┘ └──────────────┘
┌──────────────┐
│Service │
│notifications │
└──────────────┘
// Étape 3 : Séparer toutes les fonctions
┌──────────┐ ┌──────────┐ ┌──────────┐
│Service │ │Service │ │Service │
│utilisateurs│ produits │ commandes │
└──────────┘ └──────────┘ └──────────┘
┌──────────┐
│Service │
│notifications│
└──────────┘
// ========== Phase 2 : Introduire API Gateway ==========
// Avant : Le client appelle directement le monolithe
const response = await fetch('http://monolith/api/orders');
// Après : Appeler via l'API Gateway
const response = await fetch('http://api-gateway/api/orders');
// L'API Gateway effectue le routage
if (route === '/api/orders') {
// Router vers le nouveau service
proxy('http://order-service/orders');
} else {
// Encore vers le monolithe
proxy('http://monolith/api');
}
// ========== Phase 3 : Séparer la base de données ==========
// Problème : Base de données partagée
┌────────────────┐
│ BDD monolithe │
│ ┌──────────┐ │
│ │Users │ │
│ │Products │ │
│ │Orders │ │
│ └──────────┘ │
└────────────────┘
// Solution : Base de données par service
// 1) Écriture double (Dual Write)
async function createOrder(data) {
// Écrire dans la BDD monolithe
await monolithDB.orders.create(data);
// Écrire aussi dans la nouvelle BDD du service
await orderServiceDB.orders.create(data);
}
// 2) Capture des changements de données (CDC)
// Synchroniser automatiquement les changements de la BDD monolithe
const debezium = require('debezium');
debezium.on('orders.insert', async (change) => {
// Répercuter dans la nouvelle BDD du service
await orderServiceDB.orders.create(change.data);
});
// 3) Séparation complète
┌──────────┐ ┌──────────┐ ┌──────────┐
│BDD Users │ │BDD │ │BDD Orders│
│(MongoDB) │ │Products │ │(Postgres)│
│ │ │(MySQL) │ │ │
└──────────┘ └──────────┘ └──────────┘
// ========== Phase 4 : Basculement progressif du trafic ==========
// Ajuster la proportion dans l'API Gateway
const MIGRATION_PERCENTAGE = 10; // Seulement 10% vers le nouveau service
app.use('/api/orders', (req, res, next) => {
if (Math.random() * 100 < MIGRATION_PERCENTAGE) {
// Vers le nouveau service
proxy('http://order-service/orders')(req, res, next);
} else {
// Vers le monolithe
proxy('http://monolith/api/orders')(req, res, next);
}
});
// Augmenter progressivement
// 10% → 25% → 50% → 75% → 100%
// ========== Phase 5 : Surveillance et préparation du rollback ==========
const NEW_SERVICE_ERROR_THRESHOLD = 0.05; // 5% de taux d'erreur
async function monitorNewService() {
const errorRate = await getErrorRate('order-service');
if (errorRate > NEW_SERVICE_ERROR_THRESHOLD) {
// Rollback si le taux d'erreur est élevé
console.error('Taux d\'erreur élevé ! Rollback vers le monolithe');
MIGRATION_PERCENTAGE = 0;
// Notifier
await sendAlert('Rollback de migration effectué');
}
}
// ========== Liste de vérification pratique ==========
Liste de vérification pour la migration :
□ 1. Identifier les limites
- Conception orientée domaine (DDD)
- Diviser par fonction métier
□ 2. Commencer par la fonction la plus indépendante
- Peu de dépendances
- Faible impact métier
- Ex : notifications, journalisation, recherche
□ 3. Introduire l'API Gateway
- Basculement progressif du trafic
□ 4. Stratégie de séparation de BDD
- Écriture double → CDC → Séparation complète
□ 5. Renforcer la surveillance
- Taux d'erreur, temps de réponse, trafic
- Préparer le rollback
□ 6. Formation de l'équipe
- Architecture microservices
- Outils DevOps (Docker, Kubernetes)
□ 7. Documentation
- Catalogue de services
- Documentation API
- Guide de déploiement
🎓 Prochaines étapes
Après avoir compris l'architecture microservices, apprenez :
- Qu'est-ce que Docker ? (document en développement) - Conteneurisation
- Qu'est-ce que CI/CD ? - Déploiement automatisé
- REST API vs GraphQL - Conception d'API
Pratique
# ========== 1. Pratique simple des microservices ==========
# Structure du projet
mkdir microservices-demo
cd microservices-demo
# Créer les services
mkdir -p services/{user,product,order}
mkdir gateway
# Écrire Docker Compose
cat > docker-compose.yml
# Exécuter
docker-compose up -d
# Vérifier les logs
docker-compose logs -f
# ========== 2. Déploiement Kubernetes ==========
# Installer minikube (K8s local)
brew install minikube
minikube start
# Déployer
kubectl apply -f kubernetes/
# Vérifier les services
kubectl get pods
kubectl get services
# Vérifier les logs
kubectl logs <pod-name>
# ========== 3. Installer Istio (Service Mesh) ==========
# Installer Istio
istioctl install --set profile=demo -y
# Injecter sidecar dans les services
kubectl label namespace default istio-injection=enabled
# Tableau de bord Istio
istioctl dashboard kiali
🎬 Conclusion
L'architecture microservices offre évolutivité et flexibilité :
- Indépendance : Développement, déploiement et mise à l'échelle indépendants par service
- Diversité technologique : Choisir la meilleure technologie pour chaque service
- Isolation des pannes : Une panne de service n'affecte pas l'ensemble
- Autonomie des équipes : Équipes responsables des services
Mais la complexité augmente, choisissez donc en fonction de l'échelle du projet et des capacités de l'équipe ! 🧩