Architecture microservices : Guide pratique et patterns 2024
- Les microservices représentent l'évolution naturelle des architectures logicielles modernes. Après avoir accompagné plus de 50+ entreprises dans leur transformation architecturale, nous partageons notre expertise complète
- quand migrer, comment faire, et surtout comment éviter les pièges classiques.
- Cette transformation n'est pas qu'technique
- elle impacte l'organisation, les processus, et la culture d'entreprise. Notre approche éprouvée vous guide pas-à-pas vers une architecture microservices résiliente et scalable.
Fondamentaux de l'architecture microservices
Définition et principes clés
Les microservices décomposent une application monolithique en services indépendants, chacun responsable d'une fonction métier spécifique. Cette approche suit des principes fondamentaux :
1. Single Responsibility Principle (SRP)
Chaque service gère un domaine métier unique et bien défini.
2. Décentralisation
Services autonomes avec leurs propres données et logique métier.
3. Communication via APIs
Interactions uniquement par interfaces bien définies (REST, GraphQL, gRPC).
4. Déploiement indépendant
Chaque service suit son propre cycle de déploiement.
Architecture de référence
// Service de base avec patterns modernes
import express from 'express';
import { z } from 'zod';
import { PrismaClient } from '@prisma/client';
import { Redis } from 'ioredis';
import { Logger } from 'winston';
// Domain model strict avec validation
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(2).max(100),
createdAt: z.date(),
updatedAt: z.date(),
});
type User = z.infer<typeof UserSchema>;
// Repository pattern pour abstraire la persistance
interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
create(user: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User>;
update(id: string, data: Partial<User>): Promise<User>;
delete(id: string): Promise<void>;
}
class PrismaUserRepository implements UserRepository {
constructor(private prisma: PrismaClient) {}
async findById(id: string): Promise<User | null> {
const user = await this.prisma.user.findUnique({ where: { id } });
return user ? UserSchema.parse(user) : null;
}
async findByEmail(email: string): Promise<User | null> {
const user = await this.prisma.user.findUnique({ where: { email } });
return user ? UserSchema.parse(user) : null;
}
async create(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
const user = await this.prisma.user.create({
data: {
...userData,
id: crypto.randomUUID(),
createdAt: new Date(),
updatedAt: new Date(),
}
});
return UserSchema.parse(user);
}
async update(id: string, data: Partial<User>): Promise<User> {
const user = await this.prisma.user.update({
where: { id },
data: { ...data, updatedAt: new Date() }
});
return UserSchema.parse(user);
}
async delete(id: string): Promise<void> {
await this.prisma.user.delete({ where: { id } });
}
}
// Service métier avec cache et events
class UserService {
constructor(
private userRepository: UserRepository,
private cache: Redis,
private logger: Logger,
private eventBus: EventBus
) {}
async getUserById(id: string): Promise<User | null> {
// Check cache first
const cacheKey = `user:${id}`;
const cached = await this.cache.get(cacheKey);
if (cached) {
this.logger.info('User cache hit', { userId: id });
return JSON.parse(cached);
}
const user = await this.userRepository.findById(id);
if (user) {
// Cache for 1 hour
await this.cache.setex(cacheKey, 3600, JSON.stringify(user));
}
return user;
}
async createUser(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
// Check if user already exists
const existingUser = await this.userRepository.findByEmail(userData.email);
if (existingUser) {
throw new Error('User already exists with this email');
}
const user = await this.userRepository.create(userData);
// Emit domain event
await this.eventBus.publish('user.created', {
userId: user.id,
email: user.email,
timestamp: new Date().toISOString()
});
this.logger.info('User created', { userId: user.id, email: user.email });
return user;
}
async updateUser(id: string, data: Partial<User>): Promise<User> {
const user = await this.userRepository.update(id, data);
// Invalidate cache
await this.cache.del(`user:${id}`);
// Emit domain event
await this.eventBus.publish('user.updated', {
userId: user.id,
updatedFields: Object.keys(data),
timestamp: new Date().toISOString()
});
return user;
}
}
// API Controller avec validation stricte
class UserController {
constructor(private userService: UserService, private logger: Logger) {}
async getUser(req: express.Request, res: express.Response) {
try {
const { id } = req.params;
// Validate UUID format
const validation = z.string().uuid().safeParse(id);
if (!validation.success) {
return res.status(400).json({
error: 'Invalid user ID format',
details: validation.error.errors
});
}
const user = await this.userService.getUserById(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ data: user });
} catch (error) {
this.logger.error('Error getting user', { error: error.message, userId: req.params.id });
res.status(500).json({ error: 'Internal server error' });
}
}
async createUser(req: express.Request, res: express.Response) {
try {
const CreateUserSchema = UserSchema.omit({
id: true,
createdAt: true,
updatedAt: true
});
const validation = CreateUserSchema.safeParse(req.body);
if (!validation.success) {
return res.status(400).json({
error: 'Validation failed',
details: validation.error.errors
});
}
const user = await this.userService.createUser(validation.data);
res.status(201).json({ data: user });
} catch (error) {
if (error.message === 'User already exists with this email') {
return res.status(409).json({ error: error.message });
}
this.logger.error('Error creating user', { error: error.message, body: req.body });
res.status(500).json({ error: 'Internal server error' });
}
}
}Patterns de communication
1. Communication synchrone
// Service client avec retry et circuit breaker
import axios, { AxiosInstance } from 'axios';
class ServiceClient {
private client: AxiosInstance;
private circuitBreaker: CircuitBreaker;
constructor(baseURL: string) {
this.client = axios.create({
baseURL,
timeout: 5000,
headers: {
'Content-Type': 'application/json',
}
});
this.circuitBreaker = new CircuitBreaker(
this.makeRequest.bind(this),
{
timeout: 5000,
errorThresholdPercentage: 50,
resetTimeout: 30000
}
);
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor pour l'auth
this.client.interceptors.request.use((config) => {
const token = getAuthToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor pour retry automatique
this.client.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
await refreshAuthToken();
return this.client.request(error.config);
}
throw error;
}
);
}
private async makeRequest(config: any) {
return this.client.request(config);
}
async get<T>(url: string, config?: any): Promise<T> {
const response = await this.circuitBreaker.fire({
method: 'GET',
url,
...config
});
return response.data;
}
async post<T>(url: string, data?: any, config?: any): Promise<T> {
const response = await this.circuitBreaker.fire({
method: 'POST',
url,
data,
...config
});
return response.data;
}
}
// Service de produits utilisant d'autres microservices
class ProductService {
private userServiceClient: ServiceClient;
private inventoryServiceClient: ServiceClient;
private priceServiceClient: ServiceClient;
constructor() {
this.userServiceClient = new ServiceClient(process.env.USER_SERVICE_URL!);
this.inventoryServiceClient = new ServiceClient(process.env.INVENTORY_SERVICE_URL!);
this.priceServiceClient = new ServiceClient(process.env.PRICE_SERVICE_URL!);
}
async getProductDetails(productId: string, userId: string): Promise<ProductDetails> {
// Appels parallèles pour optimiser les performances
const [product, inventory, pricing, userPreferences] = await Promise.allSettled([
this.getProduct(productId),
this.inventoryServiceClient.get(`/inventory/${productId}`),
this.priceServiceClient.get(`/pricing/${productId}`),
this.userServiceClient.get(`/users/${userId}/preferences`)
]);
return this.combineProductData(product, inventory, pricing, userPreferences);
}
private combineProductData(...results: any[]): ProductDetails {
// Logique de combinaison avec gestion des erreurs
const [productResult, inventoryResult, pricingResult, preferencesResult] = results;
if (productResult.status === 'rejected') {
throw new Error('Product not found');
}
return {
...productResult.value,
stock: inventoryResult.status === 'fulfilled' ? inventoryResult.value.stock : 0,
price: pricingResult.status === 'fulfilled' ? pricingResult.value.price : null,
personalizedPrice: preferencesResult.status === 'fulfilled'
? this.calculatePersonalizedPrice(pricingResult.value, preferencesResult.value)
: null
};
}
}2. Communication asynchrone avec événements
// Event-driven architecture avec message queues
import { EventEmitter } from 'events';
import { RedisClientType } from 'redis';
interface DomainEvent {
id: string;
type: string;
aggregateId: string;
aggregateType: string;
data: Record<string, any>;
metadata: {
timestamp: string;
version: number;
correlationId?: string;
causationId?: string;
};
}
class EventBus {
private redis: RedisClientType;
private localEmitter: EventEmitter;
constructor(redis: RedisClientType) {
this.redis = redis;
this.localEmitter = new EventEmitter();
}
async publish(eventType: string, data: Record<string, any>, metadata?: Partial<DomainEvent['metadata']>): Promise<void> {
const event: DomainEvent = {
id: crypto.randomUUID(),
type: eventType,
aggregateId: data.id || crypto.randomUUID(),
aggregateType: eventType.split('.')[0],
data,
metadata: {
timestamp: new Date().toISOString(),
version: 1,
...metadata
}
};
// Publier en local d'abord
this.localEmitter.emit(eventType, event);
// Puis sur Redis pour les autres services
await this.redis.publish(`events:${eventType}`, JSON.stringify(event));
// Store dans event store pour replay/debug
await this.redis.zadd(
`eventstore:${event.aggregateType}:${event.aggregateId}`,
Date.now(),
JSON.stringify(event)
);
}
subscribe(eventType: string, handler: (event: DomainEvent) => Promise<void>): void {
// Handler local
this.localEmitter.on(eventType, handler);
// Handler Redis
this.redis.subscribe(`events:${eventType}`, (message) => {
const event = JSON.parse(message) as DomainEvent;
handler(event).catch(error => {
console.error(`Error handling event ${eventType}:`, error);
// Dead letter queue
this.redis.lpush(`dlq:${eventType}`, JSON.stringify({ event, error: error.message }));
});
});
}
}
// Event handlers avec idempotence
class OrderEventHandler {
constructor(
private inventoryService: InventoryService,
private notificationService: NotificationService,
private eventBus: EventBus
) {
this.setupEventHandlers();
}
private setupEventHandlers() {
this.eventBus.subscribe('order.created', this.handleOrderCreated.bind(this));
this.eventBus.subscribe('order.cancelled', this.handleOrderCancelled.bind(this));
this.eventBus.subscribe('payment.completed', this.handlePaymentCompleted.bind(this));
}
private async handleOrderCreated(event: DomainEvent): Promise<void> {
const { orderId, items, userId } = event.data;
// Idempotence check
const processed = await this.redis.get(`processed:order.created:${event.id}`);
if (processed) {
console.log(`Event ${event.id} already processed`);
return;
}
try {
// Reserve inventory
const reservationId = await this.inventoryService.reserveItems(items);
// Send confirmation email
await this.notificationService.sendOrderConfirmation(userId, orderId);
// Emit follow-up event
await this.eventBus.publish('inventory.reserved', {
orderId,
reservationId,
items
}, {
correlationId: event.metadata.correlationId,
causationId: event.id
});
// Mark as processed
await this.redis.setex(`processed:order.created:${event.id}`, 86400, 'true');
} catch (error) {
console.error('Failed to handle order.created:', error);
// Emit compensation event
await this.eventBus.publish('order.processing.failed', {
orderId,
error: error.message,
originalEvent: event
});
throw error; // Will go to DLQ
}
}
private async handlePaymentCompleted(event: DomainEvent): Promise<void> {
const { orderId, paymentId, amount } = event.data;
// Confirm inventory reservation
await this.inventoryService.confirmReservation(orderId);
// Update order status
await this.eventBus.publish('order.confirmed', {
orderId,
paymentId,
amount,
confirmedAt: new Date().toISOString()
});
}
}Migration du monolithe vers microservices
Stratégie Strangler Fig Pattern
La migration progressive est la clé du succès. Nous utilisons le pattern Strangler Fig pour remplacer graduellement les fonctionnalités du monolithe.
// API Gateway avec routing dynamique
import express from 'express';
import httpProxy from 'http-proxy-middleware';
class MigrationRouter {
private app: express.Application;
private routingConfig: Map<string, ServiceRoute>;
constructor() {
this.app = express();
this.routingConfig = new Map();
this.loadRoutingConfiguration();
this.setupMiddleware();
}
private loadRoutingConfiguration() {
// Configuration centralisée des routes
const routes: ServiceRoute[] = [
{
path: '/api/users',
target: process.env.USER_SERVICE_URL!,
migrated: true,
fallback: process.env.MONOLITH_URL + '/api/users'
},
{
path: '/api/orders',
target: process.env.ORDER_SERVICE_URL!,
migrated: true,
fallback: process.env.MONOLITH_URL + '/api/orders',
// Feature toggle : certain% vers microservice
migrationPercentage: 80
},
{
path: '/api/products',
target: process.env.MONOLITH_URL + '/api/products',
migrated: false,
// Prêt pour migration
futureTarget: process.env.PRODUCT_SERVICE_URL
}
];
routes.forEach(route => {
this.routingConfig.set(route.path, route);
});
}
private setupMiddleware() {
this.app.use((req, res, next) => {
const route = this.findMatchingRoute(req.path);
if (!route) {
// Route vers monolithe par défaut
return this.proxyToMonolith(req, res, next);
}
if (!route.migrated) {
return this.proxyToTarget(route.target)(req, res, next);
}
// Logique de migration progressive
if (route.migrationPercentage && route.migrationPercentage < 100) {
const shouldUseMicroservice = this.shouldRouteTo Microservice(req, route.migrationPercentage);
const target = shouldUseMicroservice ? route.target : route.fallback!;
// Logging pour monitoring de la migration
this.logMigrationDecision(req, route, shouldUseMicroservice);
return this.proxyToTarget(target)(req, res, next);
}
// Route complètement migrée
return this.proxyToTargetWithFallback(route)(req, res, next);
});
}
private shouldRouteToMicroservice(req: express.Request, percentage: number): boolean {
// Basé sur user ID pour consistance
if (req.headers['user-id']) {
const userId = req.headers['user-id'] as string;
const hash = this.simpleHash(userId);
return (hash % 100) < percentage;
}
// Fallback random
return Math.random() * 100 < percentage;
}
private proxyToTargetWithFallback(route: ServiceRoute) {
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const primaryProxy = httpProxy({
target: route.target,
changeOrigin: true,
timeout: 5000,
onError: (err, req, res) => {
console.error(`Primary service failed, falling back:`, err.message);
if (route.fallback) {
// Fallback vers monolithe
const fallbackProxy = httpProxy({
target: route.fallback,
changeOrigin: true
});
fallbackProxy(req, res, next);
} else {
res.status(503).json({ error: 'Service temporarily unavailable' });
}
}
});
primaryProxy(req, res, next);
};
}
private logMigrationDecision(req: express.Request, route: ServiceRoute, useMicroservice: boolean) {
const logData = {
path: req.path,
method: req.method,
userId: req.headers['user-id'],
routedTo: useMicroservice ? 'microservice' : 'monolith',
migrationPercentage: route.migrationPercentage,
timestamp: new Date().toISOString()
};
// Envoyer vers système de monitoring
this.sendToMonitoringSystem(logData);
}
}
interface ServiceRoute {
path: string;
target: string;
migrated: boolean;
fallback?: string;
futureTarget?: string;
migrationPercentage?: number;
}Domain-Driven Design pour découper les services
// Identification des bounded contexts
interface BoundedContext {
name: string;
description: string;
entities: string[];
valueObjects: string[];
aggregates: string[];
services: string[];
repositories: string[];
events: string[];
}
// Exemple : E-commerce bounded contexts
const ecommerceBoundedContexts: BoundedContext[] = [
{
name: 'User Management',
description: 'Gestion des utilisateurs, authentification, profils',
entities: ['User', 'Profile', 'Preference'],
valueObjects: ['Email', 'Address', 'PhoneNumber'],
aggregates: ['UserAggregate'],
services: ['AuthenticationService', 'ProfileService'],
repositories: ['UserRepository', 'ProfileRepository'],
events: ['UserCreated', 'UserUpdated', 'UserDeleted']
},
{
name: 'Product Catalog',
description: 'Catalogue produits, catégories, recherche',
entities: ['Product', 'Category', 'Brand'],
valueObjects: ['Price', 'SKU', 'Weight'],
aggregates: ['ProductAggregate', 'CategoryAggregate'],
services: ['ProductSearchService', 'CategoryService'],
repositories: ['ProductRepository', 'CategoryRepository'],
events: ['ProductCreated', 'ProductUpdated', 'PriceChanged']
},
{
name: 'Order Management',
description: 'Commandes, workflow de traitement',
entities: ['Order', 'OrderItem', 'ShippingAddress'],
valueObjects: ['OrderStatus', 'TrackingNumber'],
aggregates: ['OrderAggregate'],
services: ['OrderProcessingService', 'ShippingService'],
repositories: ['OrderRepository'],
events: ['OrderCreated', 'OrderCancelled', 'OrderShipped']
},
{
name: 'Inventory',
description: 'Stock, réservations, approvisionnement',
entities: ['InventoryItem', 'Reservation', 'Supplier'],
valueObjects: ['Quantity', 'Location'],
aggregates: ['InventoryAggregate'],
services: ['StockService', 'ReservationService'],
repositories: ['InventoryRepository'],
events: ['StockUpdated', 'ItemReserved', 'ItemRestocked']
}
];
// Service generator basé sur DDD
class MicroserviceGenerator {
generateService(context: BoundedContext): ServiceDefinition {
return {
name: this.toKebabCase(context.name),
description: context.description,
database: {
type: 'postgresql',
schema: this.generateDatabaseSchema(context)
},
api: {
endpoints: this.generateAPIEndpoints(context),
events: context.events
},
dockerfile: this.generateDockerfile(context),
kubernetesManifests: this.generateK8sManifests(context)
};
}
private generateDatabaseSchema(context: BoundedContext): DatabaseSchema {
const tables = context.entities.map(entity => ({
name: this.toSnakeCase(entity),
columns: this.inferColumns(entity, context),
indexes: this.generateIndexes(entity),
relationships: this.generateRelationships(entity, context)
}));
return { tables };
}
private generateAPIEndpoints(context: BoundedContext): APIEndpoint[] {
const endpoints: APIEndpoint[] = [];
context.entities.forEach(entity => {
const resource = this.toKebabCase(entity);
endpoints.push(
{ method: 'GET', path: `/${resource}`, operation: `list${entity}` },
{ method: 'GET', path: `/${resource}/:id`, operation: `get${entity}` },
{ method: 'POST', path: `/${resource}`, operation: `create${entity}` },
{ method: 'PUT', path: `/${resource}/:id`, operation: `update${entity}` },
{ method: 'DELETE', path: `/${resource}/:id`, operation: `delete${entity}` }
);
});
return endpoints;
}
}Migration data pattern
// Data synchronization during migration
class DataMigrationService {
private eventBus: EventBus;
private monolithDB: MonolithDatabase;
private microserviceDB: MicroserviceDatabase;
constructor(eventBus: EventBus, monolithDB: MonolithDatabase, microserviceDB: MicroserviceDatabase) {
this.eventBus = eventBus;
this.monolithDB = monolithDB;
this.microserviceDB = microserviceDB;
}
async migrateUsers(): Promise<MigrationResult> {
const migrationId = crypto.randomUUID();
let migratedCount = 0;
let errorCount = 0;
const errors: string[] = [];
try {
// Phase 1: Snapshot des données existantes
const users = await this.monolithDB.getAllUsers();
console.log(`Starting migration of ${users.length} users`);
// Phase 2: Migration par batch pour éviter la surcharge
const batchSize = 100;
for (let i = 0; i < users.length; i += batchSize) {
const batch = users.slice(i, i + batchSize);
await this.migrateBatch(batch, migrationId);
migratedCount += batch.length;
// Log progress
console.log(`Migrated ${migratedCount}/${users.length} users`);
}
// Phase 3: Setup dual-write pour synchronisation continue
await this.setupDualWrite();
// Phase 4: Validation de consistance
const validationResult = await this.validateDataConsistency();
return {
migrationId,
totalRecords: users.length,
migratedCount,
errorCount,
errors,
validationResult
};
} catch (error) {
console.error('Migration failed:', error);
await this.rollbackMigration(migrationId);
throw error;
}
}
private async migrateBatch(users: MonolithUser[], migrationId: string): Promise<void> {
const transaction = await this.microserviceDB.beginTransaction();
try {
for (const user of users) {
const microserviceUser = this.transformUser(user);
await this.microserviceDB.insertUser(microserviceUser, transaction);
// Emit event pour notification aux autres services
await this.eventBus.publish('user.migrated', {
userId: user.id,
migrationId,
originalData: user,
migratedData: microserviceUser
});
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
private async setupDualWrite(): Promise<void> {
// Intercepter les écritures sur le monolithe
this.eventBus.subscribe('monolith.user.created', async (event) => {
const user = this.transformUser(event.data.user);
await this.microserviceDB.insertUser(user);
});
this.eventBus.subscribe('monolith.user.updated', async (event) => {
const user = this.transformUser(event.data.user);
await this.microserviceDB.updateUser(user.id, user);
});
this.eventBus.subscribe('monolith.user.deleted', async (event) => {
await this.microserviceDB.deleteUser(event.data.userId);
});
}
private transformUser(monolithUser: MonolithUser): MicroserviceUser {
return {
id: monolithUser.id,
email: monolithUser.email,
name: `${monolithUser.firstName} ${monolithUser.lastName}`,
profile: {
firstName: monolithUser.firstName,
lastName: monolithUser.lastName,
birthDate: monolithUser.birthDate,
preferences: JSON.parse(monolithUser.preferences || '{}')
},
createdAt: monolithUser.createdAt,
updatedAt: monolithUser.updatedAt
};
}
private async validateDataConsistency(): Promise<ValidationResult> {
const monolithCount = await this.monolithDB.getUserCount();
const microserviceCount = await this.microserviceDB.getUserCount();
const sampleUsers = await this.monolithDB.getSampleUsers(100);
let consistentRecords = 0;
for (const monolithUser of sampleUsers) {
const microserviceUser = await this.microserviceDB.getUserById(monolithUser.id);
if (microserviceUser && this.areUsersEquivalent(monolithUser, microserviceUser)) {
consistentRecords++;
}
}
return {
monolithCount,
microserviceCount,
countConsistent: monolithCount === microserviceCount,
dataConsistencyPercentage: (consistentRecords / sampleUsers.length) * 100,
validatedSamples: sampleUsers.length
};
}
}Patterns de résilience et monitoring
Circuit Breaker et Retry Patterns
// Circuit breaker sophistiqué avec métriques
class AdvancedCircuitBreaker {
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private failureCount = 0;
private successCount = 0;
private lastFailureTime?: Date;
private metrics: CircuitBreakerMetrics;
constructor(private options: CircuitBreakerOptions) {
this.metrics = new CircuitBreakerMetrics();
}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (this.shouldAttemptReset()) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const startTime = Date.now();
const result = await this.executeWithTimeout(operation);
const duration = Date.now() - startTime;
this.onSuccess(duration);
return result;
} catch (error) {
this.onFailure(error);
throw error;
}
}
private async executeWithTimeout<T>(operation: () => Promise<T>): Promise<T> {
return Promise.race([
operation(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Operation timeout')), this.options.timeout)
)
]);
}
private onSuccess(duration: number): void {
this.metrics.recordSuccess(duration);
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= this.options.halfOpenSuccessThreshold) {
this.reset();
}
} else {
this.failureCount = 0;
}
}
private onFailure(error: Error): void {
this.metrics.recordFailure(error);
this.failureCount++;
this.lastFailureTime = new Date();
if (this.state === 'HALF_OPEN') {
this.state = 'OPEN';
} else if (this.failureCount >= this.options.failureThreshold) {
this.state = 'OPEN';
}
}
private shouldAttemptReset(): boolean {
if (!this.lastFailureTime) return false;
const timeSinceLastFailure = Date.now() - this.lastFailureTime.getTime();
return timeSinceLastFailure >= this.options.resetTimeout;
}
private reset(): void {
this.state = 'CLOSED';
this.failureCount = 0;
this.successCount = 0;
this.lastFailureTime = undefined;
}
getMetrics(): CircuitBreakerMetrics {
return this.metrics;
}
}
// Metrics collection avec export Prometheus
class CircuitBreakerMetrics {
private requestCount = 0;
private failureCount = 0;
private totalDuration = 0;
private requestDurations: number[] = [];
recordSuccess(duration: number): void {
this.requestCount++;
this.totalDuration += duration;
this.requestDurations.push(duration);
// Garder seulement les 1000 dernières mesures
if (this.requestDurations.length > 1000) {
this.requestDurations = this.requestDurations.slice(-1000);
}
}
recordFailure(error: Error): void {
this.requestCount++;
this.failureCount++;
}
getSuccessRate(): number {
if (this.requestCount === 0) return 100;
return ((this.requestCount - this.failureCount) / this.requestCount) * 100;
}
getAverageResponseTime(): number {
if (this.requestCount === 0) return 0;
return this.totalDuration / this.requestCount;
}
getP95ResponseTime(): number {
if (this.requestDurations.length === 0) return 0;
const sorted = [...this.requestDurations].sort((a, b) => a - b);
const index = Math.ceil(sorted.length * 0.95) - 1;
return sorted[index];
}
toPrometheusMetrics(serviceName: string): string {
return `
# HELP circuit_breaker_requests_total Total number of requests
# TYPE circuit_breaker_requests_total counter
circuit_breaker_requests_total{service="${serviceName}"} ${this.requestCount}
# HELP circuit_breaker_failures_total Total number of failures
# TYPE circuit_breaker_failures_total counter
circuit_breaker_failures_total{service="${serviceName}"} ${this.failureCount}
# HELP circuit_breaker_success_rate Success rate percentage
# TYPE circuit_breaker_success_rate gauge
circuit_breaker_success_rate{service="${serviceName}"} ${this.getSuccessRate()}
# HELP circuit_breaker_response_time_avg Average response time in ms
# TYPE circuit_breaker_response_time_avg gauge
circuit_breaker_response_time_avg{service="${serviceName}"} ${this.getAverageResponseTime()}
# HELP circuit_breaker_response_time_p95 95th percentile response time in ms
# TYPE circuit_breaker_response_time_p95 gauge
circuit_breaker_response_time_p95{service="${serviceName}"} ${this.getP95ResponseTime()}
`.trim();
}
}Distributed Tracing et Observability
// OpenTelemetry integration pour tracing distribué
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
class DistributedTracingSetup {
static initialize(serviceName: string): NodeSDK {
const jaegerExporter = new JaegerExporter({
endpoint: process.env.JAEGER_ENDPOINT || 'http://localhost:14268/api/traces',
});
const sdk = new NodeSDK({
serviceName,
traceExporter: jaegerExporter,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
return sdk;
}
}
// Service avec tracing intégré
class TracedUserService {
private tracer = trace.getTracer('user-service');
async getUserWithRelatedData(userId: string): Promise<UserWithRelatedData> {
// Créer span parent
return await this.tracer.startActiveSpan('getUserWithRelatedData', async (span) => {
try {
span.setAttributes({
'user.id': userId,
'operation': 'getUserWithRelatedData'
});
// Récupération user avec span enfant
const user = await this.tracer.startActiveSpan('getUser', async (childSpan) => {
childSpan.setAttributes({ 'db.operation': 'select', 'db.table': 'users' });
const result = await this.userRepository.findById(userId);
if (!result) {
childSpan.recordException(new Error('User not found'));
childSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'User not found' });
throw new Error('User not found');
}
childSpan.setAttributes({ 'user.found': true });
childSpan.end();
return result;
});
// Appels parallèles avec tracing
const [orders, preferences, notifications] = await Promise.all([
this.getOrdersWithTracing(userId),
this.getPreferencesWithTracing(userId),
this.getNotificationsWithTracing(userId)
]);
const result = {
user,
orders,
preferences,
notifications
};
span.setAttributes({
'result.orders_count': orders.length,
'result.has_preferences': !!preferences,
'result.notifications_count': notifications.length
});
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: (error as Error).message
});
throw error;
} finally {
span.end();
}
});
}
private async getOrdersWithTracing(userId: string): Promise<Order[]> {
return await this.tracer.startActiveSpan('getOrders', async (span) => {
span.setAttributes({
'service.call': 'order-service',
'user.id': userId
});
try {
const orders = await this.orderServiceClient.get(`/users/${userId}/orders`);
span.setAttributes({ 'orders.count': orders.length });
return orders;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
// Fallback graceful
console.warn('Failed to get orders, returning empty array:', error);
return [];
} finally {
span.end();
}
});
}
// Custom metrics pour business logic
private async recordUserMetrics(user: User): Promise<void> {
const span = this.tracer.startSpan('recordUserMetrics');
try {
// Business metrics
span.setAttributes({
'user.tier': user.tier,
'user.registration_days': this.daysSinceRegistration(user.createdAt),
'user.last_activity_days': this.daysSinceLastActivity(user.lastActiveAt)
});
// Custom events
span.addEvent('user_metrics_recorded', {
'user.id': user.id,
'timestamp': new Date().toISOString()
});
// Export vers système de métriques métier
await this.businessMetrics.record('user_activity', {
userId: user.id,
tier: user.tier,
registrationAge: this.daysSinceRegistration(user.createdAt)
});
} finally {
span.end();
}
}
}
// Health checks avec métriques
class HealthCheckService {
private checks: Map<string, HealthCheck> = new Map();
constructor() {
this.registerDefaultChecks();
}
private registerDefaultChecks(): void {
// Database health
this.checks.set('database', {
name: 'database',
check: async () => {
const start = Date.now();
await this.database.query('SELECT 1');
const duration = Date.now() - start;
return {
status: 'healthy',
duration,
details: { query: 'SELECT 1' }
};
},
timeout: 5000,
critical: true
});
// External service health
this.checks.set('user-service', {
name: 'user-service',
check: async () => {
const response = await fetch(`${process.env.USER_SERVICE_URL}/health`);
return {
status: response.ok ? 'healthy' : 'unhealthy',
details: {
status: response.status,
url: process.env.USER_SERVICE_URL
}
};
},
timeout: 3000,
critical: false
});
// Memory usage
this.checks.set('memory', {
name: 'memory',
check: async () => {
const usage = process.memoryUsage();
const heapUsedMB = usage.heapUsed / 1024 / 1024;
const heapTotalMB = usage.heapTotal / 1024 / 1024;
const usagePercentage = (heapUsedMB / heapTotalMB) * 100;
return {
status: usagePercentage > 90 ? 'unhealthy' : 'healthy',
details: {
heapUsedMB: Math.round(heapUsedMB),
heapTotalMB: Math.round(heapTotalMB),
usagePercentage: Math.round(usagePercentage)
}
};
},
timeout: 1000,
critical: false
});
}
async runHealthChecks(): Promise<HealthCheckResult> {
const results = new Map<string, CheckResult>();
let overallStatus: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';
await Promise.allSettled([...this.checks.entries()].map(async ([name, check]) => {
try {
const result = await this.runSingleCheck(check);
results.set(name, result);
if (result.status === 'unhealthy') {
if (check.critical) {
overallStatus = 'unhealthy';
} else if (overallStatus === 'healthy') {
overallStatus = 'degraded';
}
}
} catch (error) {
results.set(name, {
status: 'unhealthy',
error: (error as Error).message,
duration: 0
});
if (check.critical) {
overallStatus = 'unhealthy';
}
}
}));
return {
status: overallStatus,
timestamp: new Date().toISOString(),
checks: Object.fromEntries(results),
version: process.env.APP_VERSION || 'unknown'
};
}
private async runSingleCheck(check: HealthCheck): Promise<CheckResult> {
const start = Date.now();
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Health check timeout')), check.timeout);
});
try {
const result = await Promise.race([check.check(), timeoutPromise]);
const duration = Date.now() - start;
return {
...result,
duration
};
} catch (error) {
return {
status: 'unhealthy',
error: (error as Error).message,
duration: Date.now() - start
};
}
}
}Patterns de sécurité dans les microservices
JWT et service-to-service authentication
// Sécurité inter-services avec JWT et mTLS
import jwt from 'jsonwebtoken';
import fs from 'fs';
import https from 'https';
class ServiceAuthManager {
private privateKey: string;
private publicKeys: Map<string, string> = new Map();
private serviceCertificates: Map<string, { cert: string; key: string }> = new Map();
constructor() {
this.loadKeys();
this.loadServiceCertificates();
}
private loadKeys(): void {
// Clés asymétriques pour JWT
this.privateKey = fs.readFileSync(process.env.JWT_PRIVATE_KEY_PATH!, 'utf8');
// Clés publiques des autres services
const publicKeysConfig = JSON.parse(process.env.SERVICE_PUBLIC_KEYS || '{}');
Object.entries(publicKeysConfig).forEach(([service, keyPath]) => {
this.publicKeys.set(service, fs.readFileSync(keyPath as string, 'utf8'));
});
}
private loadServiceCertificates(): void {
// Certificats mTLS pour chaque service
const certConfig = JSON.parse(process.env.SERVICE_CERTIFICATES || '{}');
Object.entries(certConfig).forEach(([service, config]: [string, any]) => {
this.serviceCertificates.set(service, {
cert: fs.readFileSync(config.cert, 'utf8'),
key: fs.readFileSync(config.key, 'utf8')
});
});
}
generateServiceToken(targetService: string, claims: Record<string, any> = {}): string {
const payload = {
iss: process.env.SERVICE_NAME, // issuer
aud: targetService, // audience
sub: 'service-to-service', // subject
iat: Math.floor(Date.now() / 1000), // issued at
exp: Math.floor(Date.now() / 1000) + (15 * 60), // expire in 15 minutes
...claims
};
return jwt.sign(payload, this.privateKey, { algorithm: 'RS256' });
}
verifyServiceToken(token: string, fromService: string): any {
const publicKey = this.publicKeys.get(fromService);
if (!publicKey) {
throw new Error(`No public key found for service: ${fromService}`);
}
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
audience: process.env.SERVICE_NAME,
issuer: fromService
});
return decoded;
} catch (error) {
throw new Error(`Token verification failed: ${(error as Error).message}`);
}
}
createSecureHttpsAgent(targetService: string): https.Agent {
const certs = this.serviceCertificates.get(targetService);
if (!certs) {
throw new Error(`No certificates found for service: ${targetService}`);
}
return new https.Agent({
cert: certs.cert,
key: certs.key,
ca: fs.readFileSync(process.env.CA_CERT_PATH!, 'utf8'),
rejectUnauthorized: true
});
}
}
// Middleware d'authentification inter-services
const serviceAuthMiddleware = (authManager: ServiceAuthManager) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid authorization header' });
}
const token = authHeader.substring(7);
const callingService = req.headers['x-calling-service'] as string;
if (!callingService) {
return res.status(401).json({ error: 'Missing x-calling-service header' });
}
try {
const decoded = authManager.verifyServiceToken(token, callingService);
(req as any).serviceAuth = decoded;
next();
} catch (error) {
return res.status(401).json({
error: 'Token verification failed',
details: (error as Error).message
});
}
};
};
// Client sécurisé pour appels inter-services
class SecureServiceClient {
private authManager: ServiceAuthManager;
private httpAgent: https.Agent;
constructor(private targetService: string) {
this.authManager = new ServiceAuthManager();
this.httpAgent = this.authManager.createSecureHttpsAgent(targetService);
}
async request<T>(options: ServiceRequestOptions): Promise<T> {
// Générer token pour ce service
const token = this.authManager.generateServiceToken(this.targetService, {
operation: options.operation,
permissions: options.requiredPermissions || []
});
const requestConfig = {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'X-Calling-Service': process.env.SERVICE_NAME!,
'Content-Type': 'application/json',
...options.headers
},
httpsAgent: this.httpAgent
};
try {
const response = await axios.request<T>(requestConfig);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
throw new Error('Service authentication failed');
}
if (error.response?.status === 403) {
throw new Error('Service authorization denied');
}
}
throw error;
}
}
}API Gateway avec rate limiting et authorization
// API Gateway avec sécurité avancée
import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
class APIGateway {
private app: express.Application;
private rateLimiters: Map<string, any> = new Map();
private authManager: ServiceAuthManager;
constructor() {
this.app = express();
this.authManager = new ServiceAuthManager();
this.setupSecurity();
this.setupRateLimiting();
this.setupRouting();
}
private setupSecurity(): void {
// Helmet pour headers de sécurité
this.app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
crossOriginEmbedderPolicy: false
}));
// CORS configuré pour microservices
this.app.use((req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
if (allowedOrigins.includes(origin || '')) {
res.header('Access-Control-Allow-Origin', origin);
}
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
res.header('Access-Control-Max-Age', '86400');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
}
private setupRateLimiting(): void {
// Rate limiting par endpoint
const endpoints = [
{
path: '/api/auth/login',
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 tentatives par IP
message: 'Too many login attempts'
},
{
path: '/api/users',
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requêtes par minute
message: 'Rate limit exceeded'
}
];
endpoints.forEach(config => {
const limiter = rateLimit({
windowMs: config.windowMs,
max: config.max,
message: { error: config.message },
standardHeaders: true,
legacyHeaders: false,
// Store in Redis pour cluster
store: new RedisStore({
client: this.redisClient,
prefix: `rl:${config.path}:`
})
});
this.rateLimiters.set(config.path, limiter);
});
}
private setupRouting(): void {
// Middleware global d'authentification
this.app.use('/api', this.authenticationMiddleware.bind(this));
// Routes avec authorization granulaire
this.app.use('/api/users',
this.getRateLimiter('/api/users'),
this.authorizationMiddleware(['users:read', 'users:write']),
this.proxyToService('user-service')
);
this.app.use('/api/orders',
this.getRateLimiter('/api/orders'),
this.authorizationMiddleware(['orders:read', 'orders:write']),
this.proxyToService('order-service')
);
}
private authenticationMiddleware(req: express.Request, res: express.Response, next: express.NextFunction): void {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid authorization header' });
}
const token = authHeader.substring(7);
try {
// Vérifier JWT utilisateur
const decoded = jwt.verify(token, process.env.JWT_PUBLIC_KEY!, { algorithms: ['RS256'] });
(req as any).user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}
private authorizationMiddleware(requiredPermissions: string[]) {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
const user = (req as any).user;
if (!user || !user.permissions) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
const hasPermission = requiredPermissions.every(permission =>
user.permissions.includes(permission)
);
if (!hasPermission) {
return res.status(403).json({
error: 'Missing required permissions',
required: requiredPermissions,
current: user.permissions
});
}
next();
};
}
private getRateLimiter(path: string): any {
return this.rateLimiters.get(path) || this.rateLimiters.get('default');
}
private proxyToService(serviceName: string) {
const serviceURL = process.env[`${serviceName.toUpperCase().replace('-', '_')}_URL`];
return httpProxy({
target: serviceURL,
changeOrigin: true,
pathRewrite: {
'^/api': '' // Remove /api prefix when forwarding
},
onProxyReq: (proxyReq, req) => {
// Ajouter headers pour le service
proxyReq.setHeader('X-User-ID', (req as any).user?.sub);
proxyReq.setHeader('X-User-Permissions', JSON.stringify((req as any).user?.permissions || []));
proxyReq.setHeader('X-Gateway-Request-ID', crypto.randomUUID());
},
onError: (err, req, res) => {
console.error(`Proxy error for ${serviceName}:`, err);
(res as express.Response).status(503).json({
error: 'Service temporarily unavailable'
});
}
});
}
}Déploiement et orchestration
Kubernetes avec Helm charts
# Chart.yaml
apiVersion: v2
name: microservice-template
description: Template Helm chart for microservices
type: application
version: 0.1.0
appVersion: "1.0.0"
dependencies:
- name: postgresql
version: 11.9.13
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
- name: redis
version: 17.3.7
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled# values.yaml
replicaCount: 3
image:
repository: myregistry/microservice
pullPolicy: IfNotPresent
tag: ""
service:
type: ClusterIP
port: 3000
targetPort: 3000
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
hosts:
- host: api.example.com
paths:
- path: /users
pathType: Prefix
tls:
- secretName: api-tls
hosts:
- api.example.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
# Base de données
postgresql:
enabled: true
auth:
postgresPassword: "secure-password"
database: "microservice_db"
primary:
persistence:
enabled: true
size: 8Gi
# Cache
redis:
enabled: true
auth:
enabled: true
password: "redis-password"
master:
persistence:
enabled: true
size: 8Gi
# Monitoring
monitoring:
enabled: true
serviceMonitor:
enabled: true
interval: 30s
path: /metrics
# Configuration
config:
nodeEnv: production
logLevel: info
secrets:
jwtSecret: "jwt-secret-key"
databaseUrl: "postgresql://user:pass@localhost:5432/db"# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "microservice.fullname" . }}
labels:
{{- include "microservice.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "microservice.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
labels:
{{- include "microservice.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "microservice.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
- name: NODE_ENV
value: {{ .Values.config.nodeEnv }}
- name: LOG_LEVEL
value: {{ .Values.config.logLevel }}
- name: PORT
value: "{{ .Values.service.targetPort }}"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "microservice.fullname" . }}-secret
key: database-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "microservice.fullname" . }}-secret
key: jwt-secret
- name: REDIS_URL
value: "redis://{{ include "microservice.fullname" . }}-redis-master:6379"
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
volumes:
- name: config
configMap:
name: {{ include "microservice.fullname" . }}-config
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}CI/CD Pipeline avec GitLab
# .gitlab-ci.yml
stages:
- test
- build
- security-scan
- deploy-staging
- integration-tests
- deploy-production
variables:
DOCKER_REGISTRY: registry.gitlab.com
DOCKER_IMAGE: $DOCKER_REGISTRY/$CI_PROJECT_PATH
KUBECONFIG: /etc/kubeconfig
# Tests unitaires et intégration
test:unit:
stage: test
image: node:18-alpine
cache:
paths:
- node_modules/
script:
- npm ci
- npm run test:unit
- npm run test:integration
coverage: '/Statements\s*:\s*([^%]+)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
junit: junit.xml
only:
- merge_requests
- main
- develop
# Tests de sécurité statique
security:sast:
stage: test
image: node:18-alpine
script:
- npm ci
- npm audit --audit-level high
- npx semgrep --config=auto .
only:
- merge_requests
- main
# Build et push Docker
build:docker:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
- |
if [ "$CI_COMMIT_REF_NAME" = "main" ]; then
export IMAGE_TAG="latest"
else
export IMAGE_TAG="$CI_COMMIT_SHORT_SHA"
fi
- docker build -t $DOCKER_IMAGE:$IMAGE_TAG .
- docker push $DOCKER_IMAGE:$IMAGE_TAG
# Multi-arch build pour production
- |
if [ "$CI_COMMIT_REF_NAME" = "main" ]; then
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t $DOCKER_IMAGE:$IMAGE_TAG --push .
fi
only:
- main
- develop
# Scan de sécurité des images
security:container:
stage: security-scan
image: aquasec/trivy:latest
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL $DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA
only:
- main
- develop
# Déploiement staging
deploy:staging:
stage: deploy-staging
image: bitnami/kubectl:latest
environment:
name: staging
url: https://staging-api.example.com
before_script:
- kubectl config use-context staging
script:
- helm upgrade --install microservice-staging ./helm-chart
--namespace staging
--create-namespace
--set image.tag=$CI_COMMIT_SHORT_SHA
--set ingress.hosts[0].host=staging-api.example.com
--set config.nodeEnv=staging
--set replicaCount=2
--wait --timeout=300s
only:
- develop
# Tests d'intégration end-to-end
integration:tests:
stage: integration-tests
image: node:18-alpine
dependencies:
- deploy:staging
script:
- npm ci
- export API_BASE_URL=https://staging-api.example.com
- npm run test:e2e
artifacts:
reports:
junit: e2e-results.xml
paths:
- e2e-screenshots/
when: always
only:
- develop
# Déploiement production avec approbation manuelle
deploy:production:
stage: deploy-production
image: bitnami/kubectl:latest
environment:
name: production
url: https://api.example.com
before_script:
- kubectl config use-context production
script:
# Blue-Green deployment
- |
CURRENT_COLOR=$(kubectl get service microservice-active -o jsonpath='{.spec.selector.color}' || echo "blue")
if [ "$CURRENT_COLOR" = "blue" ]; then
NEW_COLOR="green"
else
NEW_COLOR="blue"
fi
echo "Deploying to $NEW_COLOR environment"
# Deploy nouvelle version
- helm upgrade --install microservice-$NEW_COLOR ./helm-chart
--namespace production
--set image.tag=$CI_COMMIT_SHORT_SHA
--set ingress.hosts[0].host=api.example.com
--set config.nodeEnv=production
--set replicaCount=5
--set service.name=microservice-$NEW_COLOR
--wait --timeout=600s
# Health check de la nouvelle version
- kubectl wait --for=condition=ready pod -l app=microservice,color=$NEW_COLOR --timeout=300s
# Switch traffic vers nouvelle version
- kubectl patch service microservice-active -p '{"spec":{"selector":{"color":"'$NEW_COLOR'"}}}'
# Attendre stabilisation
- sleep 60
# Cleanup ancienne version après 10 minutes
- |
(sleep 600 && helm uninstall microservice-$CURRENT_COLOR --namespace production) &
when: manual
only:
- main
# Rollback production en cas de problème
rollback:production:
stage: deploy-production
image: bitnami/kubectl:latest
environment:
name: production
url: https://api.example.com
script:
- kubectl config use-context production
- helm rollback microservice --namespace production
when: manual
only:
- mainMonitoring et observabilité avancés
Stack complète de monitoring
# monitoring/prometheus-config.yaml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "rules/*.yml"
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
scrape_configs:
# Microservices discovery via Kubernetes
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
# Business metrics custom
- job_name: 'business-metrics'
static_configs:
- targets: ['business-metrics-service:8080']
scrape_interval: 30s
metrics_path: /business-metrics
# Infrastructure
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'postgres-exporter'
static_configs:
- targets: ['postgres-exporter:9187']# monitoring/alerts.yml
groups:
- name: microservices.rules
rules:
# SLA: Availability > 99.9%
- alert: ServiceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Service {{ $labels.instance }} is down"
description: "{{ $labels.instance }} has been down for more than 1 minute"
# SLA: Response time < 500ms for 95% of requests
- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 0.5
for: 2m
labels:
severity: warning
annotations:
summary: "High response time for {{ $labels.service }}"
description: "95th percentile response time is {{ $value }}s"
# SLA: Error rate < 1%
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.01
for: 2m
labels:
severity: critical
annotations:
summary: "High error rate for {{ $labels.service }}"
description: "Error rate is {{ $value | humanizePercentage }}"
# Business metrics
- alert: LowOrderConversionRate
expr: rate(orders_completed_total[1h]) / rate(orders_started_total[1h]) < 0.7
for: 5m
labels:
severity: warning
annotations:
summary: "Order conversion rate is low"
description: "Conversion rate is {{ $value | humanizePercentage }} (threshold: 70%)"
# Infrastructure
- alert: HighMemoryUsage
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) > 0.85
for: 2m
labels:
severity: warning
annotations:
summary: "High memory usage on {{ $labels.instance }}"
description: "Memory usage is {{ $value | humanizePercentage }}"
- alert: DatabaseConnectionsHigh
expr: postgres_stat_activity_count > 80
for: 1m
labels:
severity: warning
annotations:
summary: "High database connections"
description: "{{ $value }} active connections (threshold: 80)"Dashboard Grafana personnalisé
{
"dashboard": {
"title": "Microservices Overview",
"panels": [
{
"title": "Service Health Status",
"type": "stat",
"targets": [
{
"expr": "up",
"legendFormat": "{{ instance }}"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"steps": [
{ "value": 0, "color": "red" },
{ "value": 1, "color": "green" }
]
}
}
}
},
{
"title": "Request Rate (req/s)",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total[1m])",
"legendFormat": "{{ service }} - {{ method }}"
}
]
},
{
"title": "Response Time Distribution",
"type": "heatmap",
"targets": [
{
"expr": "rate(http_request_duration_seconds_bucket[1m])",
"legendFormat": "{{ le }}"
}
]
},
{
"title": "Error Rate by Service",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total{status=~\"5..\"}[1m]) / rate(http_requests_total[1m])",
"legendFormat": "{{ service }}"
}
],
"yAxes": [
{
"unit": "percentunit",
"max": 0.1
}
]
},
{
"title": "Business Metrics",
"type": "row",
"panels": [
{
"title": "Orders per Hour",
"type": "stat",
"targets": [
{
"expr": "rate(orders_created_total[1h]) * 3600",
"legendFormat": "Orders/hour"
}
]
},
{
"title": "Revenue Trend",
"type": "graph",
"targets": [
{
"expr": "rate(revenue_total[1h]) * 3600",
"legendFormat": "Revenue/hour"
}
]
},
{
"title": "User Signups",
"type": "graph",
"targets": [
{
"expr": "rate(user_registrations_total[1h]) * 3600",
"legendFormat": "Signups/hour"
}
]
}
]
}
]
}
}Distributed logging avec ELK Stack
// Structured logging pour microservices
import winston from 'winston';
import { ElasticsearchTransport } from 'winston-elasticsearch';
class StructuredLogger {
private logger: winston.Logger;
constructor(serviceName: string) {
const esTransport = new ElasticsearchTransport({
level: 'info',
clientOpts: {
node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200'
},
index: `microservices-${serviceName}-${new Date().getFullYear()}.${new Date().getMonth() + 1}`
});
this.logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json(),
winston.format.printf(({ timestamp, level, message, service, traceId, ...meta }) => {
return JSON.stringify({
'@timestamp': timestamp,
level,
message,
service: serviceName,
traceId,
...meta
});
})
),
defaultMeta: {
service: serviceName,
environment: process.env.NODE_ENV,
version: process.env.APP_VERSION
},
transports: [
new winston.transports.Console(),
esTransport
]
});
}
info(message: string, meta?: Record<string, any>): void {
this.logger.info(message, this.enrichMeta(meta));
}
error(message: string, error?: Error, meta?: Record<string, any>): void {
this.logger.error(message, this.enrichMeta({
error: error ? {
message: error.message,
stack: error.stack,
name: error.name
} : undefined,
...meta
}));
}
warn(message: string, meta?: Record<string, any>): void {
this.logger.warn(message, this.enrichMeta(meta));
}
debug(message: string, meta?: Record<string, any>): void {
this.logger.debug(message, this.enrichMeta(meta));
}
// Business events logging
logBusinessEvent(event: BusinessEvent): void {
this.logger.info('Business event', {
eventType: 'business_event',
eventName: event.name,
entityId: event.entityId,
entityType: event.entityType,
userId: event.userId,
data: event.data,
timestamp: event.timestamp
});
}
// Performance logging
logPerformance(operation: string, duration: number, meta?: Record<string, any>): void {
this.logger.info('Performance metric', {
eventType: 'performance',
operation,
duration,
...this.enrichMeta(meta)
});
}
private enrichMeta(meta?: Record<string, any>): Record<string, any> {
const traceId = this.getCurrentTraceId();
const userId = this.getCurrentUserId();
return {
traceId,
userId,
...meta
};
}
private getCurrentTraceId(): string | undefined {
// Récupérer depuis OpenTelemetry context
return trace.getActiveSpan()?.spanContext().traceId;
}
private getCurrentUserId(): string | undefined {
// Récupérer depuis context utilisateur
return (global as any).currentUserId;
}
}
interface BusinessEvent {
name: string;
entityId: string;
entityType: string;
userId?: string;
data: Record<string, any>;
timestamp: string;
}Conclusion et recommandations IT INNOVE
Notre expertise en chiffres
Après 5 ans d'accompagnement en architecture microservices :
- 50+ migrations réussies du monolithe vers microservices
- 95% de taux de satisfaction client
- 40% d'amélioration moyenne des performances
- 60% de réduction du time-to-market pour les nouvelles features
Recommandations clés pour 2024
1. Commencez petit, pensez grand
Ne migrez pas tout d'un coup. Identifiez 1-2 domaines métier critiques et commencez par là.
2. Investissez dans l'observabilité dès le début
- Monitoring, logging, tracing
- ces outils sont critiques pour débugger des systèmes distribués.
3. Automatisez tout
- CI/CD, tests, déploiements, monitoring
- l'automatisation est la clé de la vélocité en microservices.
4. Formez vos équipes
La transformation microservices est autant organisationnelle que technique. Investissez dans la formation.
Notre accompagnement sur-mesure
Audit architecture (2 semaines)
- Analyse de votre monolithe existant
- Identification des bounded contexts
- Stratégie de migration personnalisée
- Estimation des coûts et délais
POC technique (4-6 semaines)
- Migration d'un premier service pilote
- Setup de l'infrastructure (K8s, monitoring)
- Formation de vos équipes
- Validation des patterns techniques
Accompagnement migration (3-12 mois)
- Migration progressive service par service
- Coaching technique continu
- Support 24/7 pendant les phases critiques
- Optimisations performance et coûts
---
Prêt pour votre transformation microservices ?
Nos architectes seniors vous accompagnent de l'audit initial jusqu'à la mise en production complète.
Planifiez votre audit gratuit ou découvrez nos architectures de référence.
- *Prochain article
- "Sécurité applications web : OWASP Top 10 2024 et contre-mesures pratiques"*

