GoSuda

Exploración de la ejecución escalable de un servidor Go en dotnet aspire

By snowmerak
views ...

¿dotnet aspire?

dotnet aspire es una herramienta creada para asistir a los desarrolladores en el desarrollo y la configuración de aplicaciones nativas de la nube, en un entorno donde estas se encuentran en constante crecimiento. Esta herramienta permite a los desarrolladores de .NET implementar fácilmente proyectos .NET, diversas infraestructuras nativas de la nube y servicios o contenedores en otros lenguajes.

Naturalmente, se lanza y opera desde docker hasta k8s, y un número considerable de áreas, industrias y desarrolladores en el entorno on-premise tradicional se están trasladando al entorno nativo de la nube, o ya se han trasladado. Ahora es un campo maduro. Por lo tanto, considero que no es necesario explicar las molestias existentes con respecto a los nombres de host, la configuración de puertos, los firewalls o la gestión de métricas.

Por eso, es probable que basándose en las explicaciones anteriores, aún no se entienda bien qué es dotnet aspire. Esto se debe a que Microsoft tampoco ha proporcionado una definición precisa. Por lo tanto, yo tampoco ofreceré una definición específica. Sin embargo, dado que en este artículo utilizaré las funciones básicas de dotnet aspire que he comprendido, sería útil que lo tomen como referencia para definir su propia posición al respecto.

Configuración del proyecto

Creación del proyecto dotnet aspire

Si no tiene una plantilla de dotnet aspire, primero debe instalarla. Instale la plantilla con el siguiente comando. Si no tiene .net, instálelo usted mismo.

1dotnet new install Aspire.ProjectTemplates

Luego, cree una nueva solución en una carpeta adecuada.

1dotnet new sln

Posteriormente, ejecute el siguiente comando en la carpeta de la solución para crear un proyecto con la plantilla aspire-apphost.

1dotnet new aspire-apphost -o AppHost

Esto creará un proyecto aspire-apphost con solo un código básico para la configuración.

Adición de Valkey

Ahora, agreguemos Valkey de manera sencilla.

Antes de agregar directamente, dotnet aspire ofrece diversas soluciones de terceros a través de community hosting. Por supuesto, valkey también puede recibir soporte de este community hosting, y se puede utilizar fácilmente a través del siguiente paquete nuget.

1dotnet add package Aspire.Hosting.Valkey

También ofrece una variedad de hosting integrado, que puede consultar aquí. Volviendo a valkey, abra el archivo Program.cs en el proyecto AppHost y modifíquelo de la siguiente manera.

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 es una implementación de la interfaz IResourceBuilder que contiene información para construir el servicio valkey.WithDataVolume crea un volumen para almacenar datos de caché, y WithPersistence permite el almacenamiento persistente de datos de caché. Hasta ahora, parece tener un rol similar a los volumes de docker-compose. Naturalmente, ustedes también pueden crear algo así sin dificultad. Sin embargo, no lo abordaremos en este momento, ya que está fuera del alcance de este artículo.

Creación de un servidor eco en Go

Ahora, agreguemos un simple servidor en lenguaje Go. Primero, cree un espacio de trabajo mediante go work init en la carpeta de la solución. Para un desarrollador de .NET, el espacio de trabajo de Go es similar a una solución.

Luego, cree una carpeta llamada EchoServer y después de acceder a ella, ejecute go mod init EchoServer. Este comando crea un módulo de Go. Para un desarrollador de .NET, un módulo se puede considerar similar a un proyecto. Luego, cree un archivo main.go y escriba lo siguiente en él.

 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}

Este servidor, cuando Aspire AppHost se ejecuta, lee la variable de entorno PORT que se le inyecta y ejecuta el servidor en dicho puerto. Es un servidor que simplemente recibe una consulta name y devuelve Hello, {name}.

Ahora, agreguemos este servidor a dotnet aspire.

Adición del servidor eco a aspire

Vuelva al proyecto Aspire AppHost donde agregó Valkey, y agregue el community hosting para el lenguaje Go.

1dotnet add package CommunityToolkit.Aspire.Hosting.Golang

Luego, abra el archivo Program.cs y agregue las siguientes sentencias.

 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();

Aquí, echoServer es una implementación de la interfaz IResourceBuilder que contiene información para construir el servidor en lenguaje Go. El método AddGolangApp que acaba de agregar es un método de extensión de un host personalizado para añadir un servidor en lenguaje Go. Se puede observar que usa el puerto 3000 de forma fija, y que inyecta la variable de entorno PORT. Finalmente, WithExternalHttpEndpoints permite el acceso desde el exterior.

Para probarlo, si accede a http://localhost:3000/?name=world, podrá comprobar que se imprime Hello, world.

Sin embargo, actualmente dotnet aspire tiene una penalización considerable para los proyectos que no son .NET. Eso es precisamente...

Extensión del proyecto

Entonces, ¿cómo se realiza la escalabilidad horizontal?

Actualmente, dotnet aspire solo proporciona la opción WithReplica a los constructores de proyectos .NET que se añaden mediante el método AddProject. Sin embargo, no ofrece esta opción para hosts de lenguaje Go ni para proyectos externos como AddContainer.

Por lo tanto, es necesario implementarlo directamente utilizando un balanceador de carga o un proxy inverso independiente. Sin embargo, dado que esto puede convertir dicho proxy inverso en un SPOF (Single Point of Failure), es recomendable que el proxy inverso proporcione la opción WithReplica. En consecuencia, es inevitable que el proxy inverso sea un proyecto .NET.

Hasta ahora, he usado métodos como nginx, trafik o la implementación directa para estos problemas, pero dado que existe la restricción de que sea un proyecto .NET, no tenía una solución a mano. Por eso, busqué un proxy inverso implementado en .NET y, afortunadamente, encontré la opción YARP. YARP es un proxy inverso implementado en .NET, que también puede actuar como balanceador de carga y ofrece diversas funciones, por lo que consideré que era una buena opción.

Ahora, agreguemos YARP.

Configuración del proxy inverso con YARP

Primero, cree un proyecto para usar YARP.

1dotnet new web -n ReverseProxy

Luego, ingrese al proyecto e instale YARP.

1dotnet add package Yarp.ReverseProxy --version 2.2.0

Una vez finalizada la instalación, abra el archivo Program.cs y escriba lo siguiente.

 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"}");

Este código es un código básico para usar YARP.routes contiene la información de ruta que utilizará el proxy inverso y clusters contiene la información de clúster que utilizará el proxy inverso. Esta información se carga en el proxy inverso mediante el método LoadFromMemory. Finalmente, se mapea y ejecuta el proxy inverso mediante el método MapReverseProxy.

Para el uso real, agregue el proyecto del proxy inverso como referencia en el proyecto aspire apphost, y agregue y modifique las siguientes sentencias en el archivo 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();

Ahora, el proxy inverso puede hacer referencia al servidor echo. La estructura está cambiando para que las solicitudes entrantes desde el exterior se reciban en el proxy inverso y se pasen al servidor echo.

Modificación del proxy inverso

Primero, debe cambiar la dirección de escucha del proyecto asignado al proxy inverso. Elimine applicationUrl dentro del archivo Properties/launchSettings.json. Luego, abra el archivo Program.cs y modifíquelo ampliamente de la siguiente manera.

 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"}");

Primero, se modifica la información de routes y clusters. Se agrega echo-route y echo-cluster respectivamente para configurar el envío de solicitudes al servidor echo. Además, se modifica para que la dirección del servidor echo se lea y se use desde una variable de entorno.

La regla para esta dirección es services__{nombre-servicio}__http__{índice}. En el caso del servidor echo, el nombre del servicio es echo-server y, dado que es una instancia única, se usa 0 como índice. Si agrega un servidor asp .net core, se pueden crear varias instancias mediante WithReplica, por lo que puede usar índices incrementales. El valor http://localhost:8080 que se procesa como excepción es un valor basura sin ningún significado.

Ahora, si ejecuta el proyecto y accede a http://localhost:3000/?name=world, podrá verificar que se sigue imprimiendo Hello, world.

Ideas para la expansión

Ahora, ha comprobado que es posible añadir un servidor Go a dotnet aspire y transmitir solicitudes a través de un proxy inverso. Entonces, ahora podría ser posible extender este proceso para que pueda ser implementado programáticamente. Por ejemplo, se pueden crear varias instancias añadiendo un número después del nombre del servicio para el servidor echo, y se puede añadir automáticamente la configuración para el proxy inverso.

Modifique el código para usar el proxy inverso y el servidor echo en el archivo Program.cs del proyecto aspire apphost de la siguiente manera.

 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}

Y modifique el archivo Program.cs del proyecto proxy inverso de la siguiente manera.

 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};

Se añade la configuración de destino para las 8 instancias del servidor echo que se han incrementado. Ahora, el proxy inverso tiene información de destino para los servidores echo incrementados y puede transmitir solicitudes. Si accede a la dirección http://localhost:3000/?name=world, podrá comprobar que se sigue imprimiendo Hello, world.

Conclusión

En este artículo, se ha explicado el proceso de agregar un servidor Go a dotnet aspire y transmitir solicitudes a través de un proxy inverso. Sin embargo, en lo que respecta a la expansión, aún no se ha escrito todo, y se ha creado un ejemplo que permite la implementación programática mediante variables de entorno en un repositorio separado. Para una configuración de proyecto más detallada y código, consulte snowmerak/AspireStartPack.

Personalmente, espero que dotnet aspire pueda desempeñar su propio papel como una alternativa a docker compose y como herramienta de implementación en la nube. Ya existen generadores que crean docker compose o manifiestos k8s, y creo que la accesibilidad a las herramientas de infraestructura para los desarrolladores generales ha mejorado.