GoSuda

Cómo ejecutar un servidor Go de forma extensible en dotnet aspire

By snowmerak
views ...

¿Qué es dotnet aspire?

dotnet aspire es una herramienta creada para ayudar a los desarrolladores en el desarrollo y configuración de aplicaciones nativas de la nube, a medida que el entorno "cloud-native" se expande. Esta herramienta permite a los desarrolladores de .NET implementar fácilmente proyectos .NET, diversas infraestructuras "cloud-native", y servicios o contenedores escritos en otros lenguajes.

Naturalmente, desde el lanzamiento y operación de Docker hasta Kubernetes, un número considerable de sectores, industrias y desarrolladores están migrando o ya han migrado del entorno "on-premise" al entorno "cloud-native". Es un campo ya maduro. Por lo tanto, considero que no es necesario explicar las molestias previas relacionadas con la gestión de nombres de host, configuraciones de puertos, firewalls y métricas.

Por lo tanto, incluso con las explicaciones anteriores, es probable que no logre comprender qué es dotnet aspire. Esto se debe a que ni siquiera Microsoft ha proporcionado una definición precisa. Por consiguiente, tampoco ofreceré una definición particular. Sin embargo, en este artículo, emplearé las funcionalidades básicas de dotnet aspire tal como las he comprendido, por lo que puede utilizarlas como referencia para establecer su propia perspectiva.

Configuración del proyecto

Creación de un proyecto dotnet aspire

Si no dispone de la 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 apropiada.

1dotnet new sln

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

1dotnet new aspire-apphost -o AppHost

Esto generará un proyecto aspire-apphost que contiene únicamente un código sencillo para la configuración.

Adición de Valkey

Ahora, intentemos añadir Valkey de forma sencilla.

Antes de añadirlo directamente, dotnet aspire ofrece diversas soluciones de terceros a través de "community hosting". Naturalmente, Valkey también puede beneficiarse de este soporte de "community hosting", y se puede utilizar fácilmente a través del siguiente paquete NuGet.

1dotnet add package Aspire.Hosting.Valkey

Además, ofrece una variedad de "hostings" integrados, los cuales se pueden verificar 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 la información necesaria para construir el servicio Valkey. WithDataVolume crea un volumen para almacenar los datos de la caché, y WithPersistence permite que los datos de la caché se almacenen de forma persistente. Hasta este punto, parece que desempeña un rol similar al de los volumes en docker-compose. Naturalmente, ustedes también pueden crear esto sin dificultad. Sin embargo, dado que esto excede el alcance de este artículo, no lo abordaremos ahora.

Creación de un servidor Echo en lenguaje Go

Ahora añadamos un servidor Go simple. Primero, en la carpeta de la solución, inicializamos un espacio de trabajo con go work init. Para un desarrollador .NET, un espacio de trabajo Go es similar a una solución.

Luego, cree una carpeta llamada EchoServer, acceda a ella y ejecute go mod init EchoServer. Este comando crea un módulo Go. Para un desarrollador .NET, un módulo se percibe de forma similar a un proyecto. Después, cree un archivo main.go y escriba el siguiente código.

 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 se ejecuta Aspire AppHost, lee la variable de entorno PORT que se le inyecta para la escucha, y luego inicia el servidor en ese puerto. Es un servidor simple que toma un parámetro de consulta name y devuelve Hello, {name}.

Ahora, intentemos añadir este servidor a dotnet aspire.

Adición del servidor Echo a Aspire

Volviendo al proyecto Aspire AppHost, donde se añadió Valkey, se procede a incorporar el "community hosting" para el lenguaje Go.

1dotnet add package CommunityToolkit.Aspire.Hosting.Golang

Luego, abra el archivo Program.cs y añada las siguientes líneas de código.

 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 la información necesaria para construir el servidor Go. El método AddGolangApp que acabamos de añadir es un método de extensión del host personalizado para añadir un servidor Go. Se puede observar que utiliza el puerto 3000 de forma fija y que inyecta la variable de entorno PORT. Finalmente, WithExternalHttpEndpoints permite el acceso desde el exterior.

Para realizar una prueba, si accede a http://localhost:3000/?name=world, podrá verificar que se muestra Hello, world.

Sin embargo, actualmente dotnet aspire impone una penalización significativa a los proyectos que no son .NET. Esta es precisamente...

Expansión del proyecto

¿Cómo se escala horizontalmente entonces?

Actualmente, dotnet aspire solo proporciona la opción WithReplica al constructor para proyectos .NET añadidos con el método AddProject. Sin embargo, esta opción no está disponible para hosts de lenguaje Go o proyectos externos como AddContainer.

Por lo tanto, se debe implementar directamente utilizando un "load balancer" o un "reverse proxy" independiente. Sin embargo, esto podría convertir el "reverse proxy" en un SPOF, por lo que es preferible que el "reverse proxy" ofrezca la opción WithReplica. En consecuencia, el "reverse proxy" debe ser necesariamente un proyecto .NET.

Hasta ahora, para este problema, se han utilizado métodos como Nginx, Traefik o implementaciones personalizadas, pero cuando se impone la restricción de que debe ser un proyecto .NET, en mi experiencia actual, no había una solución inmediata. Por lo tanto, busqué un "reverse proxy" implementado en .NET y, afortunadamente, encontré una opción en YARP. YARP es un "reverse proxy" implementado en .NET que puede actuar como "load balancer" y ofrece diversas funcionalidades, por lo que se consideró una buena elección.

Ahora, procederemos a añadir YARP.

Configuración del Reverse Proxy con YARP

Primero, cree un proyecto para utilizar YARP.

1dotnet new web -n ReverseProxy

Luego, acceda 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 el código básico para utilizar YARP. routes contiene la información de las rutas que utilizará el "reverse proxy", y clusters contiene la información de los clústeres que utilizará el "reverse proxy". Esta información se carga en el "reverse proxy" mediante el método LoadFromMemory. Finalmente, el "reverse proxy" se mapea y ejecuta utilizando el método MapReverseProxy.

Para su uso práctico, añada una referencia al proyecto "reverse proxy" en el proyecto "aspire apphost", y luego añada 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 "reverse proxy" puede hacer referencia al servidor Echo. La estructura está cambiando para que las solicitudes entrantes desde el exterior sean recibidas por el "reverse proxy" y luego reenviadas al servidor Echo.

Modificación del Reverse Proxy

En primer lugar, es necesario modificar la dirección de escucha del proyecto asignado al "reverse proxy". Elimine applicationUrl dentro del archivo Properties/launchSettings.json. Luego, abra el archivo Program.cs y realice las siguientes modificaciones extensas.

 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, modifique la información de routes y clusters. Se añaden echo-route y echo-cluster respectivamente para configurar el envío de solicitudes al servidor Echo. Además, la dirección del servidor Echo se modifica para que se lea de la variable de entorno.

La regla para esta dirección es services__{service-name}__http__{index}. En el caso del servidor Echo, el nombre del servicio es echo-server, y como es una única instancia, se utiliza el índice 0. Si se añade un servidor ASP.NET Core, se pueden generar varias instancias mediante WithReplica, por lo que se incrementaría el índice. El valor de excepción http://localhost:8080 es un valor de desecho sin ningún significado.

Ahora, ejecute el proyecto y acceda a http://localhost:3000/?name=world. Debería seguir viendo Hello, world impreso.

Ideas de expansión

Ahora hemos verificado cómo añadir un servidor Go a dotnet aspire y cómo las solicitudes se transmiten a través de un "reverse proxy". Por lo tanto, ahora podemos extender este proceso para implementarlo de forma programática. Por ejemplo, se pueden generar múltiples instancias del servidor Echo añadiendo numeración después del nombre del servicio, y se puede añadir automáticamente la configuración para el "reverse proxy".

Modifique el código que utiliza el "reverse proxy" 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 "reverse proxy" 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 destinos para las 8 instancias del servidor Echo que se han incrementado. Ahora, el "reverse proxy" posee la información de destinos para los servidores Echo expandidos y puede reenviar las solicitudes. Si accede a la URL original http://localhost:3000/?name=world, podrá verificar que sigue mostrando Hello, world.

Conclusión

En este artículo, se ha explicado el proceso de añadir un servidor Go a dotnet aspire y de reenviar solicitudes a través de un "reverse proxy". Sin embargo, no se ha desarrollado completamente la sección de expansión, y se ha creado un ejemplo programático más detallado utilizando variables de entorno en un repositorio separado. Para obtener la configuración completa del proyecto y el código, consulte snowmerak/AspireStartPack.

Personalmente, tengo la expectativa de que dotnet aspire pueda desempeñar su propio rol como alternativa a docker compose y como herramienta de implementación en la nube. Ya existen generadores que crean manifiestos de docker compose o k8s, lo que, en mi opinión, ha mejorado la accesibilidad a las herramientas de infraestructura para los desarrolladores comunes.