Pourquoi Microservices avec Rust ?
Dans un monde où les systèmes d'informations deviennent de plus en plus complexes, la modularité et la scalabilité sont des priorités cruciales. Les microservices offrent une solution adéquate pour ces défis. Ils permettent de diviser un système grand et complexe en composants indépendants et autonomes, chacun répondant à une fonction spécifique. Rust, avec sa sécurité accrue, sa performance optimale et son système d'ownership robuste, est idéal pour développer des microservices performants et fiables.
Un cas concret : imaginez une application de e-commerce où chaque service a une responsabilité distincte (authentification, paiement, inventaire). Chaque service peut être développé, testé et déployé indépendamment, améliorant ainsi la vitesse de développement et la facilité de maintenance.
Prerequis
- Connaissances en Rust : Vous devriez être familier avec les concepts de base de Rust (types de données, fonctions, structures, traits).
- Système d'exploitation : Linux ou macOS (Windows peut fonctionner mais nécessite des outils supplémentaires). Windows n'est pas recommandé pour le développement Rust.
- Rustup : Utilisé pour installer et gérer les versions de Rust.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - Cargo : Le gestionnaire de paquets et le compilateur Rust.
rustup update - Docker (optionnel mais recommandé) : Pour faciliter le déploiement et la mise en production.
sudo apt-get install docker.io - Docker Compose (optionnel) : Facilite la gestion de plusieurs conteneurs Docker.
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose
Concepts fondamentaux
1. Service Rustique
Un service Rustique est un projet basé sur les bibliothèques de Rust qui expose une API HTTP via le framework actix-web.
// src/main.rs
use actix_web::{web, App, HttpResponse, HttpServer};
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello world!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(hello))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
2. Communication entre Microservices
Les microservices communcient généralement via HTTP ou gRPC. Pour cet exemple, nous utiliserons HTTP avec le format JSON.
// src/services/user_service.rs
use actix_web::{web, HttpResponse};
use serde_json::json;
async fn get_user(id: web::Path<i32>) -> impl Responder {
let user = json!({
"id": id.into_inner(),
"name": "John Doe"
});
HttpResponse::Ok().json(user)
}
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::resource("/users/{id}")
.route(web::get().to(get_user)),
);
}
3. Déploiement avec Docker
Pour déployer un microservice Rustique avec Docker, nous devons créer un Dockerfile.
## Dockerfile
FROM rust:1.56 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/target/release/my_microservice .
CMD ["./my_microservice"]
4. Orchestration avec Docker Compose
Si nous avons plusieurs services, docker-compose peut être utilisé pour les orchestrer.
## docker-compose.yml
version: '3'
services:
user_service:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
Mise en pratique : Projet fil rouge
Nous allons créer un simple microservice RESTful pour gérer des tâches. Ce service permettra d'ajouter, de récupérer et de supprimer des tâches.
Étape 1 : Structure du projet
mkdir task_manager
cd task_manager
cargo new --bin task_manager
Étape 2 : Ajout des dépendances
Modifier Cargo.toml pour inclure les dépendances nécessaires :
[dependencies]
actix-web = "4.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Étape 3 : Création du serveur
Modifier src/main.rs pour créer un serveur simple :
// src/main.rs
use actix_web::{web, App, HttpResponse, HttpServer};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Task {
id: u64,
title: String,
completed: bool,
}
async fn get_tasks() -> impl Responder {
let tasks = vec![
Task {
id: 1,
title: "Task 1".to_string(),
completed: false,
},
Task {
id: 2,
title: "Task 2".to_string(),
completed: true,
},
];
HttpResponse::Ok().json(tasks)
}
async fn create_task(task: web::Json<Task>) -> impl Responder {
let new_task = Task {
id: task.id + 1, // Simplifié pour l'exemple
title: task.title.clone(),
completed: false,
};
HttpResponse::Created().json(new_task)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/tasks", web::get().to(get_tasks))
.route("/tasks", web::post().to(create_task))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Étape 4 : Ajout de Dockerfile
Créer un Dockerfile pour le service :
## Dockerfile
FROM rust:1.56 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/target/release/task_manager .
CMD ["./task_manager"]
Étape 5 : Création de docker-compose.yml
Ajouter un docker-compose.yml pour orchestrer le service :
## docker-compose.yml
version: '3'
services:
task_manager:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
Étape 6 : Exécution du service
Exécuter le service avec Docker Compose :
docker-compose up --build
Erreurs frequentes et debugging
Erreur 1 : error[E0277]: the trait bound () -> _: Future is not satisfied
Cette erreur signifie que vous essayez de retourner un type qui n'est pas une future. Assurez-vous de retourner une future avec le mot-clé async.
## ❌ Mauvais
async fn get_tasks() -> impl Responder {
let tasks = vec![
Task {
id: 1,
title: "Task 1".to_string(),
completed: false,
},
Task {
id: 2,
title: "Task 2".to_string(),
completed: true,
},
];
HttpResponse::Ok().json(tasks)
}
## ✅ Correct
async fn get_tasks() -> impl Responder {
let tasks = vec![
Task {
id: 1,
title: "Task 1".to_string(),
completed: false,
},
Task {
id: 2,
title: "Task 2".to_string(),
completed: true,
},
];
HttpResponse::Ok().json(tasks)
}
Erreur 2 : error[E0596]: cannot borrow data in a captured outer variable as mutable more than once at a time
Cette erreur signifie que vous essayez de modifier une donnée capturée plus d'une fois en même temps. Assurez-vous de gérer les emprunts correctement.
## ❌ Mauvais
let mut tasks = vec![
Task {
id: 1,
title: "Task 1".to_string(),
completed: false,
},
];
async fn update_task(id: u64, new_title: String) -> impl Responder {
let task_index = tasks.iter().position(|t| t.id == id).unwrap();
tasks[task_index].title = new_title;
HttpResponse::Ok().json(tasks)
}
## ✅ Correct
let mut tasks = vec![
Task {
id: 1,
title: "Task 1".to_string(),
completed: false,
},
];
async fn update_task(id: u64, new_title: String) -> impl Responder {
let task_index = tasks.iter().position(|t| t.id == id).unwrap();
let mut task = &mut tasks[task_index];
task.title = new_title;
HttpResponse::Ok().json(tasks)
}
Erreur 3 : error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting lifetimes
Cette erreur signifie que le compilateur ne peut pas déterminer la durée de vie appropriée pour une référence automatique. Assurez-vous d'ajouter les durées de vie nécessaires.
## ❌ Mauvais
async fn get_task(id: u64) -> impl Responder {
let tasks = vec![
Task {
id: 1,
title: "Task 1".to_string(),
completed: false,
},
Task {
id: 2,
title: "Task 2".to_string(),
completed: true,
},
];
tasks.into_iter().find(|t| t.id == id).unwrap()
}
## ✅ Correct
async fn get_task(id: u64) -> impl Responder {
let tasks = vec![
Task {
id: 1,
title: "Task 1".to_string(),
completed: false,
},
Task {
id: 2,
title: "Task 2".to_string(),
completed: true,
},
];
tasks.into_iter().find(|t| t.id == id).unwrap()
}
Pour aller plus loin
1. Sécurité avec OAuth2
Ajoutez une couche de sécurité à votre microservice en utilisant OAuth2.
2. Logging et Monitoring
Intégrez des outils de logging et de monitoring pour surveiller la santé de vos services.
3. Tests unitaires et d'intégration
Ajoutez des tests unitaires et d'intégration pour vous assurer que votre microservice fonctionne comme prévu.
// src/tests/task_manager.rs
use super::*;
use actix_web::test;
#[actix_rt::test]
async fn test_get_tasks() {
let app = test::init_service(App::new().service(get_tasks)).await;
let req = test::TestRequest::get()
.uri("/tasks")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
Défi pratique
Développez un microservice pour gérer des utilisateurs. L'API devrait permettre d'ajouter des utilisateurs, de récupérer tous les utilisateurs et de mettre à jour l'état d'un utilisateur (actif/inactif).