
Création d'un microservice avec gRPC
Créez un microservice gRPC robuste en Go : définition de service (protobuf), génération de code, implémentation serveur/client, tests unitaires et intégration pour des APIs performantes.
Introduction au développement d'un microservice gRPC : Performance et interopérabilité
Après avoir exploré les concepts de gRPC et Protocol Buffers (chapitre 16), il est temps de mettre en pratique ces connaissances et de se lancer dans le développement d'un microservice gRPC complet en Go. Ce chapitre vous guidera pas-à-pas à travers les étapes clés de la création d'un microservice gRPC fonctionnel, de la définition du service avec protobuf à la génération du code Go client et serveur, en passant par l'implémentation du serveur gRPC, la construction d'un client gRPC, et la mise en place de tests unitaires pour valider le microservice.
L'objectif de ce chapitre est de vous fournir un exemple concret et pédagogique de développement d'un microservice gRPC en Go de A à Z, en mettant en oeuvre les concepts et les techniques abordés dans les chapitres précédents. Nous allons construire un microservice gRPC simple mais complet, en utilisant Protocol Buffers pour définir le service et les messages, gRPC Go pour générer le code client et serveur, et le package testing de Go pour les tests unitaires. Ce projet pratique vous permettra de consolider vos acquis, de développer vos compétences en développement de microservices gRPC en Go, et de vous préparer à la construction d'APIs inter-services performantes, robustes, et modernes avec gRPC et Go.
Définition du service gRPC avec Protocol Buffers : Fichier helloworld.proto
La première étape du développement d'un microservice gRPC est la définition du service gRPC et de ses méthodes RPC (Remote Procedure Call) en utilisant le langage Protocol Buffers (protobuf). Nous allons créer un fichier helloworld.proto qui définit un service gRPC simple Greeter avec une méthode RPC SayHello, prenant une requête HelloRequest et retournant une réponse HelloReply.
Fichier proto/helloworld.proto (définition du service Greeter en Protocol Buffers) :
syntax = "proto3";
package helloworld;
option go_package = "./pb";
// Définition du message 'HelloRequest'
message HelloRequest {
string nom = 1;
}
// Définition du message 'HelloReply'
message HelloReply {
string message = 1;
}
// Définition du service 'Greeter'
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
Ce fichier helloworld.proto définit :
syntax = "proto3";: Spécifie la syntaxe protobuf version 3.package helloworld;: Définit le nom du package protobufhelloworld.option go_package = "./pb";: Option protobuf pour spécifier le chemin du package Go où le code Go généré sera placé (ici, le répertoirepbrelatif au répertoire courant).message HelloRequest { ... }: Définit un message protobufHelloRequest(message de requête pour la méthodeSayHello) avec un champnomde typestringet de numéro1.message HelloReply { ... }: Définit un message protobufHelloReply(message de réponse pour la méthodeSayHello) avec un champmessagede typestringet de numéro1.service Greeter { ... }: Définit un service gRPCGreeteravec une méthode RPCSayHello:rpc SayHello (HelloRequest) returns (HelloReply) {}: Déclare la méthode RPCSayHello, qui prend un messageHelloRequesten entrée et retourne un messageHelloReplyen sortie.
Ce fichier helloworld.proto constitue la définition de notre API gRPC pour le microservice Greeter. A partir de ce fichier .proto, nous allons générer le code Go client et serveur gRPC dans l'étape suivante.
Génération du code Go gRPC : Compilation du fichier proto avec protoc
A partir du fichier helloworld.proto, nous allons générer le code Go gRPC client et serveur en utilisant le compilateur protobuf protoc et les plugins Go protoc-gen-go et protoc-gen-go-grpc, comme expliqué au chapitre 16 (section "Génération de code Go gRPC avec protoc et protoc-gen-go-grpc").
Commande protoc pour générer le code Go gRPC (à exécuter à la racine du projet) :
protoc --go_out=pb --go-grpc_out=pb proto/helloworld.proto
Cette commande (exécutée à la racine du projet, en supposant que le fichier helloworld.proto est dans un répertoire proto) va générer deux fichiers Go dans le répertoire pb (défini par l'option option go_package = "./pb"; dans le fichier .proto) :
pb/helloworld.pb.go: Contient le code Go généré pour les messages protobuf (HelloRequest,HelloReply).pb/helloworld_grpc.pb.go: Contient le code Go généré pour le service gRPC (Greeter) et les interfaces client et serveur du service.
Après l'exécution de la commande protoc, vous aurez le code Go gRPC généré dans le répertoire pb, prêt à être utilisé pour implémenter le serveur gRPC et construire le client gRPC dans les étapes suivantes.
Implémentation du serveur gRPC en Go : Création et enregistrement du service
L'étape suivante consiste à implémenter le serveur gRPC en Go pour notre microservice Greeter, en utilisant le code Go gRPC généré à l'étape précédente. Nous allons créer un serveur gRPC Go, implémenter l'interface de service GreeterServer générée par protobuf, et enregistrer notre implémentation de service auprès du serveur gRPC.
Fichier server/server.go (implémentation du serveur gRPC Greeter) :
package server
import (
"context"
"log"
pb "application-web-go-complete/pb" // Import du package Go gRPC généré (pb)
)
// Struct 'server' qui implémente l'interface 'pb.GreeterServer' (code serveur gRPC)
type server struct {
pb.UnimplementedGreeterServer // Embed pour la compatibilité ascendante
}
// Implémentation de la méthode RPC 'SayHello' du service 'Greeter'
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Reçu la requête SayHello: %v", req.GetNom())
message := "Bonjour " + req.GetNom() + " depuis le serveur gRPC Go!"
return &pb.HelloReply{Message: message}, nil // Retourne la réponse HelloReply
}
Ce code définit un struct server Go qui implémente l'interface pb.GreeterServer (générée par protobuf dans pb/helloworld_grpc.pb.go). La méthode SayHello du struct server implémente la logique métier de notre service gRPC : elle reçoit un message HelloRequest, construit un message de réponse HelloReply, et le retourne au client gRPC.
Fichier main.go (configuration et lancement du serveur gRPC) :
package main
import (
"log"
"net"
"os"
"google.golang.org/grpc"
pb "application-web-go-complete/pb" // Import du package Go gRPC généré (pb)
"application-web-go-complete/server" // Import du package 'server' contenant l'implémentation du serveur gRPC
)
func main() {
adresseServeur := ":50051" // Adresse d'écoute du serveur gRPC
listener, err := net.Listen("tcp", adresseServeur) // Création du listener TCP
if err != nil {
log.Fatalf("Echec de l'écoute: %v", err)
os.Exit(1)
}
grpcServer := grpc.NewServer() // Création du serveur gRPC
pb.RegisterGreeterServer(grpcServer, &server.server{}) // Enregistrement du service Greeter auprès du serveur gRPC
log.Printf("Serveur gRPC en écoute sur %s", adresseServeur)
if err := grpcServer.Serve(listener); err != nil {
log.Fatalf("Echec du démarrage du serveur gRPC: %v", err)
os.Exit(1)
}
}
Ce code configure et lance le serveur gRPC Go : il crée un listener TCP sur le port 50051, crée un serveur gRPC avec grpc.NewServer(), enregistre notre implémentation de service server.server{} auprès du serveur gRPC avec pb.RegisterGreeterServer, et lance le serveur gRPC en écoute avec grpcServer.Serve(listener).
Construction du client gRPC en Go : Appeler le microservice
Pour tester et consommer notre microservice gRPC Greeter, nous allons construire un client gRPC en Go. Le client gRPC permettra d'établir une connexion avec le serveur gRPC et d'appeler la méthode RPC SayHello du service Greeter.
Fichier client/client.go (implémentation du client gRPC Greeter) :
package client
import (
"context"
"log"
"os"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "application-web-go-complete/pb" // Import du package Go gRPC généré (pb)
)
// ClientGreeter gère la communication avec le service Greeter gRPC
type ClientGreeter struct {
client pb.GreeterClient // Client stub gRPC généré par protobuf
}
// NewClientGreeter crée un nouveau client Greeter gRPC
func NewClientGreeter(adresseServeur string) (*ClientGreeter, error) {
// Etablir une connexion gRPC non sécurisée avec le serveur
conn, err := grpc.Dial(adresseServeur, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, fmt.Errorf("échec de la connexion au serveur gRPC: %w", err)
}
client := pb.NewGreeterClient(conn) // Création du client stub GreeterClient
return &ClientGreeter{client: client}, nil
}
// DireBonjour appelle la méthode RPC SayHello du service Greeter
func (c *ClientGreeter) DireBonjour(nom string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // Contexte avec timeout de 5 secondes
defer cancel()
requete := &pb.HelloRequest{Nom: nom} // Création du message de requête HelloRequest
reponse, err := c.client.SayHello(ctx, requete) // Appel de la méthode RPC SayHello via le client stub
if err != nil {
return "", fmt.Errorf("échec de l'appel RPC SayHello: %w", err)
}
return reponse.GetMessage(), nil // Retourne le message de réponse
}
Ce code définit un struct ClientGreeter Go qui encapsule le client stub gRPC (pb.GreeterClient) généré par protobuf, et fournit des méthodes Go conviviales (comme DireBonjour) pour appeler les méthodes RPC du service Greeter. La fonction NewClientGreeter crée et initialise le client gRPC, et la méthode DireBonjour appelle la méthode RPC SayHello du service gRPC via le client stub, en gérant la création du contexte, la construction du message de requête, l'appel RPC, la gestion des erreurs, et la récupération du message de réponse.
Fichier main.go (utilisation du client gRPC pour appeler le microservice) :
package main
import (
"fmt"
"log"
"os"
"application-web-go-complete/client" // Import du package 'client' contenant le client gRPC Greeter
)
func main() {
adresseServeur := "localhost:50051" // Adresse du serveur gRPC
clientGreeter, err := client.NewClientGreeter(adresseServeur) // Création du client Greeter gRPC
if err != nil {
log.Fatalf("Erreur lors de la création du client Greeter: %v", err)
os.Exit(1)
}
nomUtilisateur := "Go Client"
messageBonjour, err := clientGreeter.DireBonjour(nomUtilisateur) // Appel de la méthode DireBonjour du client Greeter gRPC
if err != nil {
log.Fatalf("Erreur lors de l'appel de DireBonjour: %v", err)
os.Exit(1)
}
fmt.Println("Réponse du serveur gRPC :", messageBonjour)
}
Ce code utilise le client gRPC ClientGreeter pour se connecter au serveur gRPC (sur localhost:50051) et appeler la méthode RPC DireBonjour, en passant un nom d'utilisateur en argument et en affichant le message de réponse reçu du serveur gRPC.
Tests unitaires du microservice gRPC : Valider le serveur et le client
Pour garantir la qualité et la robustesse de notre microservice gRPC Greeter, il est essentiel d'écrire des tests unitaires pour valider le comportement du serveur gRPC et du client gRPC.
Tests unitaires du serveur gRPC (server/server_test.go) :
package server_test
import (
"context"
"net"
"testing"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
pb "application-web-go-complete/pb"
"application-web-go-complete/server"
"github.com/stretchr/testify/assert"
)
func TestSayHello(t *testing.T) {
// Préparation de l'environnement de test : serveur gRPC en mémoire (bufconn)
listener := bufconn.Listen("bufnet")
srv := grpc.NewServer()
pb.RegisterGreeterServer(srv, &server.server{}) // Enregistrement du service serveur pour les tests
go func() {
if err := srv.Serve(listener); err != nil {
log.Fatalf("Serveur en mémoire terminé avec erreur: %%v", err)
}
}()
defer srv.GracefulStop()
// Création du client gRPC en mémoire (bufconn)
conn, err := grpc.DialContext(context.Background(), "bufnet",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return listener.Dial()
}))
if err != nil {
t.Fatalf("Echec de la connexion au serveur en mémoire: %%v", err)
}
defer conn.Close()
client := pb.NewGreeterClient(conn)
// Appel de la méthode SayHello du serveur gRPC via le client
nom := "TestUser"
requete := &pb.HelloRequest{Nom: nom}
reponse, err := client.SayHello(context.Background(), requete)
// Assertions sur la réponse du serveur
assert.NoError(t, err, "Pas d'erreur attendue lors de l'appel RPC")
assert.Equal(t, "Bonjour "+nom+" depuis le serveur gRPC Go!", reponse.GetMessage(), "Message de réponse incorrect")
}
Cet exemple illustre un test unitaire du serveur gRPC Greeter, en utilisant un serveur gRPC en mémoire (bufconn) pour isoler le test du serveur réel et le rendre plus rapide et plus déterministe. Le test crée un serveur gRPC en mémoire, enregistre notre implémentation de service server.server{} auprès de ce serveur, crée un client gRPC en mémoire connecté au serveur en mémoire, appelle la méthode RPC SayHello via le client, et effectue des assertions sur la réponse du serveur (vérification de l'absence d'erreur et du message de réponse attendu) avec la bibliothèque d'assertions testify/assert.
Tests unitaires du client gRPC (client/client_test.go) :
Les tests unitaires du client gRPC peuvent être implémentés de manière similaire, en mockant ou en stubbant le serveur gRPC (si nécessaire) pour isoler le test du client et vérifier son comportement indépendamment du serveur réel. Dans de nombreux cas, les tests unitaires du client gRPC peuvent être moins critiques que les tests unitaires du serveur, car le code client est souvent plus simple et moins sujet aux erreurs que le code serveur. Les tests d'intégration (tests entre le client et le serveur gRPC réel ou un serveur de test) et les tests end-to-end (tests du système complet client-serveur) sont souvent plus pertinents et plus importants pour valider le bon fonctionnement de l'ensemble du système gRPC.
Bonnes pratiques pour le développement de microservices gRPC en Go
Pour développer des microservices gRPC robustes, performants, scalables, et faciles à maintenir en Go, et pour tirer pleinement parti des avantages de gRPC et de Protocol Buffers, voici quelques bonnes pratiques à suivre :
- Définir des APIs gRPC claires, concises et bien conçues avec Protocol Buffers : Passez du temps à concevoir soigneusement vos fichiers
.proto, en définissant des services gRPC et des messages protobuf clairs, concis, bien structurés, et sémantiquement riches. Utilisez des noms de services, de méthodes RPC, et de messages descriptifs et significatifs. Documentez clairement vos APIs gRPC dans les fichiers.proto(avec des commentaires protobuf) pour faciliter leur compréhension et leur utilisation par les développeurs clients. Respectez les conventions de nommage et de style de Protocol Buffers et de gRPC (PascalCase pour les noms de services et de messages, camelCase pour les noms de champs, snake_case pour les noms de fichiers, etc.). - Générer le code Go gRPC à partir des fichiers proto avec
protocetprotoc-gen-go-grpc: Utilisez systématiquement le compilateurprotocet les plugins Goprotoc-gen-goetprotoc-gen-go-grpcpour générer automatiquement le code Go gRPC client et serveur à partir de vos fichiers.proto. La génération de code automatise une grande partie du code boilerplate et garantit la cohérence et la compatibilité entre la définition de l'API (fichier.proto) et le code Go client et serveur. Intégrez la génération de code gRPC dans votre workflow de build et votre pipeline CI/CD (chapitre 22) pour automatiser le processus de génération de code à chaque modification du fichier.proto. - Implémenter les serveurs gRPC de manière robuste, performante et sécurisée : Implémentez vos serveurs gRPC Go en suivant les principes de robustesse, de performance, et de sécurité. Gérez correctement les erreurs (chapitre 10), les timeouts (chapitre 12, section "Timeouts avec Select"), l'annulation (chapitre 11, section "Context et annulation de goroutines"), la concurrence (chapitre 11, section "Goroutines et concurrence de base", et chapitre 12, section "Channels et synchronisation"), la validation des données, la sécurité (chapitre 23 et 24), le monitoring et le tracing (chapitre 25). Optimisez la performance de vos serveurs gRPC en utilisant les techniques d'optimisation Go (chapitres 21 et 27) : profiling, benchmarking, gestion efficace de la mémoire, parallélisation multi-coeurs, caching, etc.
- Construire des clients gRPC idiomatiques, faciles à utiliser et robustes : Construisez des clients gRPC Go idiomatiques, faciles à utiliser, et robustes pour consommer vos APIs gRPC. Encapsulez la logique de connexion, d'appel RPC, de gestion des erreurs, et de retries dans des clients gRPC Go dédiés (comme le struct
ClientGreeterdans l'exemple précédent). Utilisez des contexte Go (context.Context) avec timeouts et annulation pour contrôler les appels RPC et éviter les blocages indéfinis. Gérez correctement les erreurs retournées par les appels RPC et mettez en place des mécanismes de retries (ré-essais) automatiques pour les erreurs transitoires. - Tester rigoureusement les microservices gRPC (tests unitaires, tests d'intégration, tests E2E, tests de performance) : Testez rigoureusement vos microservices gRPC Go à tous les niveaux de test : tests unitaires pour valider la logique interne des services et des méthodes RPC (avec des serveurs gRPC en mémoire - bufconn, comme illustré dans l'exemple), tests d'intégration pour valider l'interaction entre les microservices et les dépendances externes (bases de données, autres services gRPC, APIs externes), tests end-to-end (E2E) pour valider le fonctionnement du système complet client-serveur gRPC, et tests de charge et de performance pour évaluer la performance et la scalabilité des microservices gRPC sous charge. Des tests robustes et complets sont essentiels pour garantir la qualité, la fiabilité, et la performance de vos microservices gRPC en production.
- Monitorer et observer les microservices gRPC en production (métriques, logs, tracing distribué) : Mettez en place un système d'observabilité complet pour vos microservices gRPC Go en production, en utilisant le monitoring de métriques (Prometheus, Grafana), le logging structuré et centralisé (chapitre 25), et le tracing distribué (OpenTelemetry, Jaeger, Zipkin, chapitre 25). L'observabilité est essentielle pour comprendre le comportement, la performance, et l'état de santé de vos microservices gRPC en production, pour détecter et résoudre rapidement les problèmes, et pour optimiser la performance et la scalabilité de votre architecture microservices.
En appliquant ces bonnes pratiques, vous développerez des microservices gRPC Go robustes, performants, scalables, sécurisés, et faciles à maintenir, en tirant pleinement parti des avantages de gRPC et Protocol Buffers pour la construction d'APIs inter-services modernes et efficaces.