Registros de servicios con Consul

Al trabajar con microservicios llegamos a un punto en el que trabajamos con decenas, cientos o incluso miles, en algunas empresas, de servicios. Administrar dichos servicios se hace exponencialmente más complicado a medida que vas añadiendo más y más servicios.

Y uno de los problemas principales viene en saber donde están ubicados dichos servicios, y por lo tanto la url a la cual debemos acceder para contactar con ellos.  

 

1 - Registros de servicios en microservicios

Cuando tenemos un sistema muy grande, tenemos muchos microservicios y estos se comunican unos con otros. 

Administrar toda la configuración puede ser bastante complejo y costoso, además si seguimos un enfoque clásico tendremos toda la información de los servicios que vamos a consumir en el fichero de configuración de dicho servicio, y esto lo tendremos que hacer para cada uno de los servicios.

 

enfoque clasico comunicacion servicios

Cuando digo toda la configuración me refiero principalmente a las URL, de un microservicio o de un servicio de la arquitectura como puede ser el host de la base de datos o del sistema de mensajes para la comunicación asíncrona.

Y no solo eso, sino que por cada entorno que tengamos tendremos que tener dicha información almacenada, lo cual va a terminar siendo muy tedioso.

 

2 - El servicio de registro

Para administrar toda esta información utilizaremos un servicio de registro (service registry o service discovery en inglés) el cual nos permite registrar toda esta información, al final del día, dicho servicio actuará como base de datos que contiene la información de todos los servicios, como puede ser url, puerto, protocolo, etc.

 

Por lo tanto, cuando queremos realizar una llamada del microservicio A al Microservicio B debemos consultar primero dónde está ubicado a través del servicio de registro.

 

ms communication with discovery

Tener un servicio que actúe como service registry nos permite delegar toda la complejidad de la administración de URLs/hosts a dicho servicio.

 

Y en nuestro servicio únicamente tendremos una referencia, similar a una clave, la cual referencia el registro en el service discovery.

 

Como vimos en el post de RabbitMQ o en el de Vault la creación de la arquitectura será independiente a la del código, pero puedes hacer, si quieres, que un servicio se registre automáticamente cuando se despliega. 

Una comparativa sería muy similar a un servicio DNS pero que actúa únicamente dentro de nuestra arquitectura. 

 

 

3 - Qué es Consul de hashicorp?

Igual que vimos en el post anterior sobre la gestión de credenciales voy a utilizar otro software de hashicorp, en este caso Consul.

 

El motivo principal es porque Consul no solo es un service discovery sino que además actúa como app mesh en caso de que lo necesitáramos. Esto quiere decir que incluye elementos como observabilidad, seguridad entre componentes, health checks, etc. Todo eso siendo 100% compatible con servicios como AWS, Azure, Google Cloud, etc.

compatibilidad consulPero para este post, utilizaremos únicamente la parte de registrar y descubrir los servicios.

 

3.1 - Añadir Consul con docker

Como en los post anteriores lo primero que vamos a hacer es añadir Consul a nuestra infraestructura. Para ello únicamente lo incluimos en nuestro fichero docker, donde ya tenemos Vault y RabbitMQ:

consul:
container_name: consul
image: consul
ports:
    - 8500:8500 # this is the UI/API calls
    - 8400:8400
    - 8600:8600
    - 8600:8600/udp

Con esta configuración podremos acceder a la UI si lo necesitamos, a través de localhost:8500; Ahora solo nos queda registrar los servicios dentro de Consul. Lo podemos hacer a través de la UI, de llamadas a la api o de la línea de comandos. 

 

En nuestro caso igual que con Vault, vamos a crear los elementos a través de la línea de comandos, con un fichero bash, el cual ejecutaremos en nuestro script de inicialización de la arquitectura:

#!/bin/bash
docker exec -it consul consul services register -name=RabbitMQ -address=localhost
docker exec -it consul consul services register -name=SecretManager -address=http://localhost -port=8200
docker exec -it consul consul services register -name=EmailsApi -address=http://localhost -port=50120
docker exec -it consul consul services register -name=ProductsApi -address=http://localhost -port=50220
docker exec -it consul consul services register -name=OrdersApi -address=http://localhost -port=50320
docker exec -it consul consul services register -name=SubscriptionsApi -address=http://localhost -port=50420

Consul tiene una gran documentación pero para este ejemplo únicamente necesitamos el comando register con las propiedades 

  • name para el nombre
  • address para la ubicación, en producción serán diferentes
  • port para el puerto, si no lo indicas se especificará el puerto 0 de forma automática

Si vamos a la interfaz de Consul podemos ver como todos los servicios se han registrado correctamente.

servicios registrados en consul

Por supuesto podemos recoger  toda esta información a través de la api, de la línea de comandos o en nuestro caso, con el SDK.

 

3.2 - Implementación de Consul en .NET

Para nuestra implementación vamos a seguir la lógica de abstraer la configuración para así si en el futuro queremos cambiar de Consul a otro servicio únicamente tener que cambiar la implementación en un único lugar.

 

Y en nuestro caso en particular, ya que registramos los servicios a través de la infraestructura únicamente deberemos leer dichos registros.

 

Para ello creamos un proyecto llamado Distribt.Shared.Discovery donde crearemos nuestra abstracción.

Lo primero que debemos hacer es añadir la referencia, o incluir el paquete NuGet de Consul, personalmente voy a añadir también caché en memoria, ya que en la arquitectura actual, las direcciones no van a cambiar. Pero esto depende 100% de tu arquitectura.

Si van a cambiar mes a mes, lo que puedes hacer es un wrapper para reintentar la llamada si falla. 

Por ejemplo: utilizas las de la caché, si falla, vuelves a consultar los registros y actualizar la caché.

 

Una vez tenemos la referencia creamos la interfaz a utilizar para consultar la información:

public interface IServiceDiscovery
{
    Task<string> GetFullAddress(string serviceKey, CancellationToken cancellationToken = default);
}

Y ahora creamos la implementación de Consul en c# para leer la información del servicio; Donde únicamente tenemos que inyectar la interfaz IConsulClient para poder utilizar toda la API. En nuestro caso, únicamente debemos leer un servicio dentro del catálogo:

public class ConsulServiceDiscovery : IServiceDiscovery
{
    private readonly IConsulClient _client;
    private readonly MemoryCache _cache;

    public ConsulServiceDiscovery(IConsulClient client)
    {
        _client = client;
        _cache = new MemoryCache(new MemoryCacheOptions());
    }


    public async Task<string> GetFullAddress(string serviceKey, CancellationToken cancellationToken = default)
    {
        if (_cache.TryGetValue(serviceKey, out string serviceAddress))
        {
            return serviceAddress;
        }

        return await GetAddressFromService(serviceKey, cancellationToken);
    }


    private async Task<string> GetAddressFromService(string serviceKey, CancellationToken cancellationToken = default)
    {
        
        var services = await _client.Catalog.Service(serviceKey, cancellationToken);
        if (services.Response != null && services.Response.Any())
        {
            var service = services.Response.First();
            StringBuilder serviceAddress = new StringBuilder();
            serviceAddress.Append(service.ServiceAddress);
            if (service.ServicePort != 0)
            {
                serviceAddress.Append($":{service.ServicePort}");
            }

            string serviceAddressString = serviceAddress.ToString();
            
            AddToCache(serviceKey, serviceAddressString);
            return serviceAddressString;
        }

        throw new ArgumentException($"seems like the service your are trying to access ({serviceKey}) does not exist ");
    }

    private void AddToCache(string serviceKey, string serviceAddress)
    {
        _cache.Set(serviceKey, serviceAddress);
    }
}

Para poder utilizar la librería debemos añadirla al contenedor de dependencias, así como la configuración del cliente, donde debemos especificar al url en la que consul esta ubicado:

public static IServiceCollection AddDiscovery(this IServiceCollection services, IConfiguration configuration)
{
    return services.AddSingleton<IConsulClient, ConsulClient>(provider => new ConsulClient(consulConfig =>
        {
            var address = configuration["Discovery:Address"];
            consulConfig.Address = new Uri(address);
        }))
        .AddSingleton<IServiceDiscovery, ConsulServiceDiscovery>();
}

 

Además debemos actualizar los proyectos que van a utilizar dicha funcionalidad; 

Lo primero es añadir la configuración correcta al fichero appsettings.json

{
  ...
  "Discovery": {
    "Address": "http://localhost:8500"
  },
  ...
}

Nota: en nuestro caso, el service discovery va a ser parte del core de las aplicaciones y va a estar configurado en DefaultDistribtWebApplication, por lo que dicha información debe ser añadida a todos los servicios.

 

Una vez hacemos esto, debemos actualizar las librerías/servicios para que cojan la URL del service discovery en vez de desde la configuración.

 

Para el caso de Vault por ejemplo simplemente creamos un método para coger dicha url y lo añadimos  a la configuración.

public static void AddSecretManager(this IServiceCollection serviceCollection, IConfiguration configuration)
{
    //TODO: create an awaiter project instead of .result everywhere in the config
    string discoveredUrl = GetVaultUrl(serviceCollection.BuildServiceProvider()).Result;
    serviceCollection.AddVaultService(configuration, discoveredUrl); 
}

private static async Task<string> GetVaultUrl(IServiceProvider serviceProvider)
{
    var serviceDiscovery = serviceProvider.GetService<IServiceDiscovery>();
    return await serviceDiscovery?.GetFullAddress(DiscoveryServices.Secrets)!;
}

Nota: En nuestro ejemplo las URL son estáticas, si en tu servicio puede ser que cambien, tendrás que administrar esa rotación manualmente en el código.

Además, cuando haces .Result bloqueas el hilo, si lo haces al inicio de la app no tiene mucha importancia, pero no ejecutes dicha acción fuera de la inicialización. 

Pero cuando configuramos no podemos ejecutar código asíncrono, así que es lo que hay.

 

Vemos como si ejecutamos el código funciona sin problemas:

leer registro con consul

Es muy común también una práctica que es tener todas las keys de los servicios que vamos a utilizar en un fichero común, para así no tener que recordar las keys y utilizar la referencia como tal:

 

public class DiscoveryServices
{
    public const string RabbitMQ = "RabbitMQ";
    public const string Secrets = "SecretManager";
    
    public class Microservices
    {
        public const string Emails = "EmailsApi";
        public const string Orders = "OrdersAPi";
        public const string Products = "ProductsAPi";
        public const string Subscriptions = "SubscriptionsAPi";
    }
}

 

Finalmente solo nos queda actualizar todos los ficheros appsettings y las librerías que se vean afectadas. 

No olvides que ya puedes quitar de los appsettings la referencia a todos los hostnames/urls, Incluidos rabbitMQ o Vault entre otros servicios. 

 

 

Conclusión

  • En este post hemos visto qué es un registro de servicios en una arquitectura distribuida
  • Qué es Consul y como configurarlo con docker
  • Cómo implementar Consul con C#

 


Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2024 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café