Exécuter un serveur Go de manière évolutive avec dotnet Aspire

dotnet aspire ?
dotnet aspire est un outil créé pour aider les développeurs dans le développement et la configuration d'applications cloud natives, à mesure que les environnements cloud natifs se multiplient. Cet outil permet aux développeurs .NET de déployer facilement leurs projets .NET, diverses infrastructures cloud natives, ainsi que des services ou des conteneurs écrits dans d'autres langages.
Naturellement, la chaîne va de Docker jusqu'à Kubernetes (k8s) pour la mise en production et l'exploitation, et de nombreux secteurs, industries et développeurs ont migré, ou sont déjà migrés, des environnements on-premise existants vers des environnements cloud natifs. C'est désormais un domaine mature. Il n'est donc, selon moi, pas nécessaire d'expliquer les difficultés antérieures liées aux noms d'hôte, à la configuration des ports, aux pare-feux, à la gestion des métriques, etc.
Ainsi, à la seule lecture de ces explications, il est probable que vous ne compreniez absolument pas ce qu'est dotnet aspire. En effet, Microsoft ne donne pas non plus de définition précise de cet outil. Je ne proposerai donc pas non plus de définition particulière. Cependant, cet article utilisera les fonctionnalités de base de dotnet aspire telles que je les ai comprises ; vous pourrez vous y référer pour définir votre propre positionnement.
Configuration du projet
Création d'un projet dotnet aspire
Si le modèle dotnet aspire n'est pas disponible, il faut d'abord l'installer. Installez le modèle à l'aide de la commande suivante. Si .NET n'est pas installé, veuillez l'installer vous-même.
1dotnet new install Aspire.ProjectTemplates
Créez ensuite une nouvelle solution dans un dossier approprié.
1dotnet new sln
Puis, depuis le dossier de la solution, exécutez la commande suivante pour créer le projet à partir du modèle aspire-apphost.
1dotnet new aspire-apphost -o AppHost
Un projet aspire-apphost contenant uniquement un code minimal de configuration est alors créé.
Ajout de Valkey
Ajoutons maintenant brièvement Valkey.
Avant de l'ajouter, notez que dotnet aspire fournit diverses solutions tierces via le community hosting.
Valkey peut naturellement bénéficier de ce support de community hosting, et il est facilement utilisable grâce au package NuGet suivant.
1dotnet add package Aspire.Hosting.Valkey
D'autres hébergements intégrés sont également disponibles et peuvent être consultés ici.
Revenons maintenant à Valkey : ouvrez le fichier Program.cs du projet AppHost et modifiez-le comme suit.
1var builder = DistributedApplication.CreateBuilder(args);
2
3var cache = builder.AddValkey("cache")
4 .WithDataVolume(isReadOnly: false)
5 .WithPersistence(interval: TimeSpan.FromMinutes(5),
6 keysChangedThreshold: 100);
7
8builder.Build().Run();
cache est une implémentation de l'interface IResourceBuilder contenant les informations permettant de construire le service Valkey.WithDataVolume crée un volume destiné à stocker les données du cache, et WithPersistence permet de sauvegarder les données du cache de manière continue.
À ce stade, cela semble jouer un rôle similaire à volumes dans docker-compose.
Bien entendu, vous pouvez facilement le mettre en place vous-mêmes.
Toutefois, cela dépasse le cadre de cet article, je n'en parlerai donc pas maintenant.
Création d'un serveur d'écho en Go
Ajoutons maintenant un serveur Go simple.
Commencez par initialiser un workspace avec go work init dans le dossier de la solution.
Pour un développeur .NET, le workspace Go peut être considéré comme similaire à une solution.
Ensuite, créez un dossier nommé EchoServer, déplacez-vous à l'intérieur et exécutez go mod init EchoServer.
Cette commande crée un module Go. Pour un développeur .NET, un module peut être considéré comme similaire à un projet.
Créez ensuite un fichier main.go et écrivez le code suivant.
1package main
2
3import (
4 "log"
5 "net/http"
6 "os"
7)
8
9func main() {
10 addr := os.Getenv("PORT")
11 log.Printf("Server started on %s", addr)
12
13 http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
14 name := request.URL.Query().Get("name")
15 writer.Write([]byte("Hello, " + name))
16 })
17
18 http.ListenAndServe(":"+addr, nil)
19}
Ce serveur lit le port sur lequel il doit écouter à partir de la variable d'environnement PORT, que l'AppHost Aspire injecte lors de son exécution, puis démarre le serveur sur ce port.
Il s'agit d'un serveur simple qui reçoit le paramètre de requête name et renvoie Hello, {name}.
Ajoutons maintenant ce serveur à dotnet aspire.
Ajout du serveur d'écho à Aspire
Revenez au projet Aspire AppHost dans lequel vous avez ajouté Valkey, puis ajoutez le community hosting pour Go.
1dotnet add package CommunityToolkit.Aspire.Hosting.Golang
Ouvrez ensuite le fichier Program.cs et ajoutez l'instruction suivante.
1var builder = DistributedApplication.CreateBuilder(args);
2
3var cache = builder.AddValkey("cache")
4 .WithDataVolume(isReadOnly: false)
5 .WithPersistence(interval: TimeSpan.FromMinutes(5),
6 keysChangedThreshold: 100);
7
8var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
9 .WithHttpEndpoint(port: 3000, env: "PORT")
10 .WithExternalHttpEndpoints();
11
12builder.Build().Run();
Ici, echoServer est une implémentation de l'interface IResourceBuilder contenant les informations permettant de construire le serveur Go.
La méthode AddGolangApp que nous venons d'ajouter est une méthode d'extension de l'hôte personnalisé permettant d'ajouter un serveur Go.
Le serveur utilise le port 3000 de manière fixe, et l'on constate qu'il injecte la variable d'environnement PORT.
Enfin, WithExternalHttpEndpoints permet d'y accéder depuis l'extérieur.
Pour tester, accédez à http://localhost:3000/?name=world : vous devriez voir s'afficher Hello, world.
Cependant, dotnet aspire impose actuellement une lourde pénalité aux projets non-dotnet.
Il s'agit de...
Extension du projet
Comment réaliser la mise à l'échelle horizontale ?
Actuellement, dotnet aspire ne fournit l'option WithReplica que pour le builder des projets .NET ajoutés via la méthode AddProject.
En revanche, cette option n'est pas disponible pour les hôtes Go ni pour les projets externes tels que ceux ajoutés via AddContainer.
Il faut donc implémenter soi-même la solution, en utilisant un load balancer ou un reverse proxy distinct.
Toutefois, ce reverse proxy pourrait alors devenir un SPOF (point de défaillance unique) ; il est donc préférable qu'il bénéficie également de l'option WithReplica.
Dans ce cas, le reverse proxy doit nécessairement être un projet .NET.
Jusqu'ici, j'avais tenté de résoudre ce problème avec nginx, Traefik, ou une implémentation personnalisée ; mais dès lors que la contrainte du projet .NET s'imposait, je n'avais pas de solution immédiate.
J'ai donc recherché un reverse proxy implémenté en .NET et, heureusement, YARP s'est présenté comme une option.
YARP est un reverse proxy implémenté en .NET, qui peut également jouer le rôle de load balancer et offre de nombreuses fonctionnalités ; je l'ai donc jugé comme un bon choix.
Ajoutons maintenant YARP.
Configuration d'un reverse proxy avec YARP
Commencez par créer un projet pour utiliser YARP.
1dotnet new web -n ReverseProxy
Puis déplacez-vous dans le projet et installez YARP.
1dotnet add package Yarp.ReverseProxy --version 2.2.0
Une fois l'installation terminée, ouvrez le fichier Program.cs et écrivez le code suivant.
1using Yarp.ReverseProxy.Configuration;
2
3var builder = WebApplication.CreateBuilder(args);
4
5var routes = new List<RouteConfig>();
6var clusters = new List<ClusterConfig>();
7
8builder.Services.AddReverseProxy()
9 .LoadFromMemory(routes, clusters);
10
11var app = builder.Build();
12
13app.MapReverseProxy();
14app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");
Ce code constitue la configuration de base permettant d'utiliser YARP.routes contient les informations de routes que le reverse proxy utilisera, et clusters contient les informations de clusters que le reverse proxy utilisera.
Ces informations sont chargées dans le reverse proxy via la méthode LoadFromMemory.
Enfin, la méthode MapReverseProxy mappe et exécute le reverse proxy.
Ensuite, pour une utilisation réelle, ajoutez le projet reverse proxy comme référence dans le projet AppHost d'Aspire, puis ajoutez et modifiez l'instruction suivante dans le fichier Program.cs.
1dotnet add reference ../ReverseProxy
1var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
2 .WithHttpEndpoint(env: "PORT");
3
4var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
5 .WithReference(echoServer)
6 .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
7 .WithExternalHttpEndpoints();
Le reverse proxy peut désormais référencer le serveur d'écho.
La structure évolue vers une configuration dans laquelle les requêtes externes sont reçues par le reverse proxy, puis transmises au serveur d'écho.
Modification du reverse proxy
Tout d'abord, il faut modifier l'adresse d'écoute du projet attribué au reverse proxy.
Supprimez la valeur applicationUrl dans le fichier Properties/launchSettings.json.
Ensuite, ouvrez le fichier Program.cs et modifiez-le en profondeur comme indiqué ci-dessous.
1using Yarp.ReverseProxy.Configuration;
2
3var builder = WebApplication.CreateBuilder(args);
4
5var routes = new List<RouteConfig>
6{
7 new RouteConfig
8 {
9 ClusterId = "cluster-echo",
10 RouteId = "route-echo",
11 Match = new RouteMatch
12 {
13 Path = "/"
14 }
15 }
16};
17
18var echoServerAddr = Environment.GetEnvironmentVariable("services__echo-server__http__0") ?? "http://localhost:8080";
19
20var clusters = new List<ClusterConfig>
21{
22 new ClusterConfig
23 {
24 ClusterId = "cluster-echo",
25 Destinations = new Dictionary<string, DestinationConfig>
26 {
27 { "destination-echo", new DestinationConfig { Address = echoServerAddr } }
28 }
29 }
30};
31
32builder.Services.AddReverseProxy()
33 .LoadFromMemory(routes, clusters);
34
35var app = builder.Build();
36
37app.MapReverseProxy();
38app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");
Tout d'abord, modifiez les informations de routes et clusters.
Ajoutez respectivement echo-route et echo-cluster afin de configurer l'envoi des requêtes vers le serveur d'écho.
Ensuite, modifiez le code pour lire l'adresse du serveur d'écho à partir des variables d'environnement.
La règle de cette adresse est services__{service-name}__http__{index}.
Dans le cas du serveur d'écho, le nom du service est echo-server et il s'agit d'une instance unique ; on utilise donc l'index 0.
Si vous ajoutez un serveur ASP.NET Core, plusieurs instances peuvent être créées via WithReplica ; il suffit alors d'incrémenter l'index.
L'adresse http://localhost:8080, définie comme valeur de repli dans la gestion des exceptions, est une valeur sans signification.
Lancez maintenant le projet et accédez à http://localhost:3000/?name=world : vous devriez toujours voir s'afficher Hello, world.
Idées d'extension
Nous avons maintenant ajouté un serveur Go à dotnet aspire et vérifié le transfert des requêtes via le reverse proxy.
Il devient alors possible d'étendre ce processus afin de l'implémenter de manière programmatique.
Par exemple, en ajoutant une numérotation après le nom du service pour le serveur d'écho, on peut créer plusieurs instances et ajouter automatiquement la configuration correspondante au reverse proxy.
Modifiez le fichier Program.cs du projet AppHost d'Aspire, qui utilise le reverse proxy et le serveur d'écho, comme suit.
1var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
2 .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
3 .WithExternalHttpEndpoints();
4
5for (var i = 0; i < 8; i++)
6{
7 var echoServer = builder.AddGolangApp($"echo-server-{i}", "../EchoServer")
8 .WithHttpEndpoint(env: "PORT");
9 reverseProxy.WithReference(echoServer);
10}
Modifiez ensuite le fichier Program.cs du projet reverse proxy comme suit.
1var echoServerDestinations = new Dictionary<string, DestinationConfig>();
2for (var i = 0; i < 8; i++)
3{
4 echoServerDestinations[$"destination-{i}"] = new DestinationConfig
5 {
6 Address = Environment.GetEnvironmentVariable($"services__echo-server-{i}__http__0") ?? "http://localhost:8080"
7 };
8}
9
10var clusters = new List<ClusterConfig>
11{
12 new ClusterConfig
13 {
14 ClusterId = "cluster-echo",
15 Destinations = echoServerDestinations
16 }
17};
Nous ajoutons la configuration des destinations pour les 8 instances du serveur d'écho.
Le reverse proxy dispose désormais des informations de destination de chacun des serveurs d'écho et peut transmettre les requêtes en conséquence.
Accédez à nouveau à http://localhost:3000/?name=world : vous devriez toujours voir s'afficher Hello, world.
Conclusion
Dans cet article, nous avons décrit le processus consistant à ajouter un serveur Go à dotnet aspire puis à transmettre les requêtes via un reverse proxy.
Toutefois, l'extension n'a pas encore été entièrement rédigée ; un exemple plus programmatique, reposant sur des variables d'environnement, a été rédigé dans un dépôt séparé.
Pour la configuration détaillée du projet et le code, veuillez consulter snowmerak/AspireStartPack.
Personnellement, je pense que dotnet aspire peut jouer son propre rôle, à la fois comme alternative à docker compose et comme outil de déploiement cloud.
Des générateurs produisant déjà des manifestes docker compose ou Kubernetes existent, et je pense que l'accès aux outils d'infrastructure est ainsi devenu plus aisé pour les développeurs ordinaires.