YARP y Dapr Creando un ApiGateway para nuestros microservicios.

Escrito por  Sergio Parra

A todos nos molan los microservicios, ¿verdad? Has escuchado lo que es Dapr y alucinas con lo que ofrece para que tus desarrollos suban a un nivel superior. Si te cuento que podemos implementar un ApiGateway con YARP (Yet Another Reverse Proxy) y configurar las rutas para que sea Dapr el encargado de llamar a tus microservicios empleando el Service Invocation API, ¿te interesaría? Sigue leyendo.

¿Qué es un Proxy Inverso?

Es necesario conocer el concepto de Proxy Inverso. Un proxy inverso es una pieza de software que se encuentra frente a los servidores de backend y se asegura de que ningún cliente se comunique directamente con ellos. Generalmente se implementan para ayudar a aumentar la seguridad, el rendimiento y la confiabilidad ya que proveen de diferentes beneficios como equilibrio de carga, almacenamiento en caché, cifrado SSL y protección contra ataques DoS y DDoS.

¿Qué es YARP?

Lo primero es dejarte por aquí una estupenda charla de mi amigo Diego Zapico en la .NET Conf 2022 de Madrid que habla sobre YARP para que tengas más información (https://www.youtube.com/watch?v=a-Fm-aSHgrY) y la dirección de la documentación oficial del producto (https://microsoft.github.io/reverse-proxy/articles/getting-started.html).

"YARP es una biblioteca de código abierto con la que puede implementar funcionalidades básicas relacionadas con el proxy agregando o reemplazando módulos. Actualmente, YARP se puede utilizar en forma de paquetes NuGet y fragmentos de código. Microsoft planea proporcionar una plantilla de proyecto y un exe precompilado para YARP para que los usuarios no pierdan tiempo manejando fragmentos de código redundantes y configurando un proyecto.

YARP se implementa sobre el marco .NET y está disponible para usuarios de Windows, Mac y Linux"

¿Qué es Dapr?

Según la documentación oficial (https://docs.dapr.io), Dapr es un tiempo de ejecución portable a varios sistemas operativos basado en eventos que facilita a cualquier desarrollador la creación de aplicaciones resilientes, con y sin estado que se ejecutan en la nube y en el perímetro, y abarca la diversidad de lenguajes y marcos de desarrollo. Al aprovechar los beneficios de una arquitectura sidecar, Dapr lo ayuda a enfrentar los desafíos que surgen con la creación de microservicios y mantiene su plataforma de código independiente.

Para este artículo que nos ocupa, emplearemos un "building block", que es un API HTTP o gRPC que puede ser llamado desde nuestro código para usar uno o más componentes Dapr.

Emplearemos el bloque de invocación de servicio que tiene el siguiente esquema

Diagram showing the steps of service
invocation
  1. El servicio A hace una llamada HTTP o gRPC indicando como destino el servicio B. Esta llamada se hace al sidecar de Dapr.

  2. Dapr descubre la localización del servicio B usando name resolution component.

  3. Dapr reenvía el mensaje al sidecar Dapr del servicio B (usando gRPC siempre).

  4. El sidecar Dapr del servicio B reenvía la solicitud al endpoint concreto de Service B.

  5. El servicio B ejecuta su código y envía su respuesta al servicio A por medio del su sidecar Dapr.

  6. Dapr reenvía la respuesta al sidecar Dapr del servicio A.

  7. El servicio A recibe dicha respuesta.

Al lío...

Una vez ya tenemos algunos conceptos claros, vamos a empezar a ver algo de código. Para la demo se ha generado una solución con tres proyectos, de los cuales dos, serán nuestras Minimal Api de backend. La parte de autenticación la delegaremos en nuestro Api Gateway por lo que se descarga el backend de realizar estas comprobaciones.

image2

Nos centraremos en la aplicación que implementa YARP. Lo primero que hay que realizar es instalar el paquete Yarp.ReverseProxy para tener disponible el servicio de proxy inverso.

Una vez hecho esto y si nos fijamos en la documentación de YARP sobre la configuración, las settings deberían tener el siguiente esquema:

1{
2    "ReverseProxy": {
3      "Routes": {
4        "route1": {
5          "ClusterId": "cluster1",
6          "Match": {
7            "Path": "{**catch-all}",
8            "Hosts": [ "www.aaaaa.com", "www.bbbbb.com"],
9          },
10        }
11      },
12      "Clusters": {
13        "cluster1": {
14          "Destinations": {
15            "cluster1/destination1": {
16              "Address": "https://example.com/"
17            }
18          }
19        }
20      }
21    }
22  }
23

En la sección "Routes" se definen aquellas rutas que vamos a tener en cuenta en nuestro ApiGateway y su configuración. Es importante saber que podemos incluir Metadatos en nuestras rutas para su posterior uso como ya veremos. Fijaos en que es obligatorio establecer un ClusterId y definir una sección "Clusters" que contiene principalmente una colección de destinos con nombre y sus direcciones capaces de manejar solicitudes para una ruta determinada.

Hemos comentado de usar Dapr para realizar la comunicación entre servicios, ¿cierto? Pues continúa leyendo.

Nuestra configuración por defecto tendrá este formato

1"ReverseProxy": {
2    "Routes": {
3      "customersRoute": {
4        "ClusterId": "dapr-sidecar",
5        "CorsPolicy": "CorsPolicy",
6        "AuthorizationPolicy": "default",
7        "Match": {
8          "Path": "/api/customers/{**catch-all}"
9        },
10        "Metadata": {
11          "DaprEnabled": "true",
12          "DaprAppId": "demo-customers-api"
13        }
14      },
15      "productsRoute": {
16        "ClusterId": "dapr-sidecar",
17        "CorsPolicy": "CorsPolicy",
18        "AuthorizationPolicy": "default",
19        "Match": {
20          "Path": "/api/products/{**catch-all}"
21        },
22        "Metadata": {
23          "DaprEnabled": "true",
24          "DaprAppId": "demo-products-api"
25        }
26      }
27    }
28  }
29

El ClusterId será siempre fijo a "dapr-sidecar". Definimos un objeto Metadata en el que le indicamos que se ha habilitado Dapr y le indicamos qué Application Id de Dapr está asociado a esa ruta. Esto es esencial ya que como veremos, necesitamos este dato para realizar la invocación de servicio a servicio. Sabemos que Dapr es un sidecar que se estará ejecutando "pegadito" a nuestra Api contenerizada, pero no sabemos cuál es su puerto (por defecto en HTTP es el 3500 y siempre vamos a acceder a él con la uri http://localhost:PUERTO) por lo que usaremos un método de extensión para construir dinámicamente la sección "Clusters" de la configuración. Aquí el truquito.

1public static IConfigurationBuilder AddDaprConfiguration(this IConfigurationBuilder configuration)
2{
3    var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint();
4    return configuration.AddInMemoryCollection(new[]
5    {
6        new KeyValuePair<string, string>("ReverseProxy:Clusters:dapr-sidecar:Destinations:d1:Address", httpEndpoint!),
7    });
8}
9public static string? GetDefaultHttpEndpoint()
10{
11    if (httpEndpoint is null)
12    {
13        var port = Environment.GetEnvironmentVariable("DAPR_HTTP_PORT");
14        port = string.IsNullOrEmpty(port) ? "3500" : port;
15        httpEndpoint = $"http://127.0.0.1:{port}";
16    }
17    return httpEndpoint;
18}
19

Con esto es como si tuviéramos en un archivo de configuración las settings completas de la sección "Clusters". Bien, ya tenemos la configuración de las rutas y de los clusters. Vamos ahora a incluir un proveedor de transformación de rutas para una vez capturada la ruta de la petición, generaremos una petición al sidecar de Dapr y poder así llamar al servicio de backend que queramos.

1using Demo.ApiGateway.Configuration;
2using Microsoft.AspNetCore.Http;
3using Yarp.ReverseProxy.Transforms;
4using Yarp.ReverseProxy.Transforms.Builder;
5namespace Demo.ApiGateway.Providers;
6public class DaprTransformProvider : ITransformProvider
7{
8    public void ValidateRoute(TransformRouteValidationContext context)
9    {
10    }
11    public void ValidateCluster(TransformClusterValidationContext context)
12    {
13    }
14    public void Apply(TransformBuilderContext context)
15    {
16        if (context.Route.Metadata?.TryGetValue(DaprYarpConstants.MetaKeys.DaprEnabled, out string? daprEnabled) ?? false)
17        {
18            if (string.IsNullOrWhiteSpace(daprEnabled))
19            {
20                throw new ArgumentException("A non empty DaprEnabled value is required");
21            }
22            if (!bool.TryParse(daprEnabled, out bool enabled))
23            {
24                throw new ArgumentException("A valid DaprEnabled value is required");
25            }
26            if (enabled)
27            {
28                if (context.Route.Metadata?.TryGetValue(DaprYarpConstants.MetaKeys.DaprAppId, out string? appId) ?? false)
29                {
30                    if (string.IsNullOrWhiteSpace(appId))
31                    {
32                        throw new ArgumentException("A valid Dapr AppId value is required");
33                    }
34                    context.AddRequestTransform(transformContext =>
35                    {
36 transformContext.ProxyRequest.Headers.Add("dapr-app-id", appId);
37                        transformContext.ProxyRequest.RequestUri =
38                                        new Uri($"{transformContext.DestinationPrefix}{transformContext.Path.Value!}{transformContext.Query.QueryString.Value}");
39                        return ValueTask.CompletedTask;
40                    });
41                }
42            }
43        }
44    }
45}
46

Verificamos los datos incluidos en el objeto Metadata de la ruta y transformamos la petición original en otra que llama a Dapr. El código es muy simple y descriptivo. Bien, ahora toca inicializar YARP y Dapr en el arranque de nuestro ApiGateway empleando diversos métodos de extensión.

1public static void AddReverseProxy(this WebApplicationBuilder builder, IConfiguration configuration)
2{
3    builder.Services.AddReverseProxy()
4 .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
5                    .AddTransforms<DaprTransformProvider>(); //agregar proveedor de transformación
6    builder.Services.AddDaprClient();
7    builder.Services.AddAuthenticationDefault(configuration);
8    builder.Services.AddAuthorization()
9                    .AddCorsPolicy(configuration)
10                    .AddHttpContextAccessor();
11    builder.Services.AddSingleton(configuration);
12    builder.Services.AddHealthChecks();                        
13}
14

Y establecemos los middlewares

1private static void UseYarp(this WebApplication app)
2{
3    app.UseEndpoints(endpoints =>
4    {
5        endpoints.MapHealthChecks("/hc",
6                new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { Predicate = _ => true });
7        endpoints.MapHealthChecks("/liveness",
8            new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
9            {
10                Predicate = r => r.Name.Contains("self")
11            });
12        endpoints.MapReverseProxy(pipeline =>
13        {
14            pipeline.UseReverseProxyPipeline();
15        });
16    });
17}
18
19public static void UseReverseProxy(this WebApplication app)
20{
21    app.UseRouting();
22    app.UseCors("CorsPolicy");
23    app.UseAuthentication();
24    app.UseAuthorization();
25    app.UseYarp();
26}
27

Veamos el ejemplo

Ejecutamos docker-compose para iniciar nuestras Api y los sidecars de Dapr asociados. Una vez ejecutado comprobamos que los contenedores están en ejecución.

image3

Bien, buscamos nuestro ApiGateway (que en la imagen está en el puerto http 55941). Ejecutamos desde Postman por ejemplo una llamada para crear un "customer"

image4

Se observa que nos devuelve un 401 porque no enviamos un token válido. Realizamos una petición de Token y establecemos la cabecera Authorization y...

image5

¡Ya hemos creado un customer!

Ok, ahora a crear un "product" (fijaos en la url, mismo host y puerto que la solicitud anterior de customers).

image6

¿Dónde puedo encontrar el código de ejemplo?

No os preocupéis si no lo tenéis claro, lo veréis mejor al descargar el código fuente en https://github.com/sparraguerra/compartimoss/tree/master/ContainerAppsYarpProxy

Conclusiones

Hemos visto lo sencillo que es implementar con YARP nuestro amado ApiGateway. Con esto ganamos seguridad ya que permitimos que nuestras Api de backend estén "ocultas" al exterior. Y con Dapr hemos confirmado lo fácil que es llamar a cualquier Api de backend.

Happy coding!

Sergio Parra Guerra
Software & Cloud Architect at Encamina
https://twitter.com/sparraguerra

Siguemos en LinkedInSiguemos en Twitter
Powered by  ENCAMINA