Nouveau : Datasets open source gratuits disponibles !Decouvrir →
🦀
Avance 30 min Rust

Microservices avec Rust

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

Besoin d'aide sur Rust ?

Besoin d'aide sur un projet technique ? Decrivez-le pour des conseils personnalises.

Recevoir des conseils

Questions frequentes

Quelle est la particularité de Rust dans le développement de microservices?
Rust offre des avantages tels que sa gestion efficace de la mémoire et son système de type statique, ce qui contribue à une meilleure sécurité et performance en microservices.
Comment gérer les dépendances entre différents microservices en Rust?
En utilisant des outils comme Cargo pour gérer les dépendances, vous pouvez définir et installer facilement les bibliothèques nécessaires pour chaque microservice.
Quelle est l'importance de la conception modulaire en Rust pour le développement de microservices?
La conception modulaire permet de développer des microservices indépendants et faciles à maintenir. Rust, avec sa syntaxe intuitive et son système de type robuste, facilite ce processus.

Pages liees

Chaque semaine, le meilleur de la tech francaise

Tendances, salaires, outils et opportunites — directement dans votre boite mail.

Gratuit. Desabonnement en un clic. Pas de spam.