3 Recomendações para Consumir Microsserviços com Segurança e Resiliência

Uma arquitetura baseada em microsseriços exige que prestemos atenção tanto na escrita destes, quanto nos códigos que os consomem.

Neste post, compartilho três recomendações sobre como consumir microsserviços de forma elegante e adequada.

O código-fonte para o exemplo deste post está disponível em meu github.

Se tiver interesse em entender mais sobre microsserviços, recomendo que acesse o Guia de Conteúdo para Microsserviços deste site.

1. Abstraia o interface para seu microsserviço

O acesso a um microsserviço deve ser abstraído da lógica principal de sua aplicação.

 

Minha recomendação é criar interfaces apropriadas para o negócio, isolando o código faz chamadas remotas.

public interface IEchoService
{
    Task Echo(string message,
        CancellationToken cancellationToken = default(CancellationToken)
        );
}

Dessa forma, o código que irá consumir o microsserviço ficará muito mais simples de ler e entender.

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task Echo(EchoViewModel model)
{
    model.Result = await _echoService.Echo(model.Message);
    return View(model);
}

O que temos é uma chamada assíncrona simples. Veja como não há nenhuma menção ao fato de estar sendo feito um request para outro serviço.

Outra vantagem dessa abordagem, é que fica mais simples escrever testes de unidade. Afinal, não estamos “acoplando” nosso código a conexões HTTP.

2. Isole requisições HTTP em um cliente apropriado

Chamadas HTTP para um microsserviço exigem configurações apropriadas de segurança e tratamento de falhas. Recomendo implementar essa lógica uma única vez reaproveitando a implementação sempre que necessário.

Atualmente, venho adotando uma versão adaptada do código de exemplo fornecido pela Microsoft.

public class StandardHttpClient : IHttpClient, IDisposable
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly HttpClient _httpClient;

    public StandardHttpClient(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
        _httpClient = new HttpClient();
    }

    public async Task GetStringAsync(
        string uri, 
        Authorization authorization = default(Authorization),
        CancellationToken cancellationToken = default(CancellationToken)
        )
    {
        var message = new HttpRequestMessage(HttpMethod.Get, uri)
            .CopyAuthorizationHeaderFrom(_httpContextAccessor.HttpContext)
            .Apply(authorization);

        var response = await _httpClient.SendAsync(message, cancellationToken);

        if (response.StatusCode == HttpStatusCode.InternalServerError)
        {
            throw new HttpRequestException();
        }

        return await response.Content.ReadAsStringAsync();
    }

    public async Task PutAsync(
        string uri, 
        T item, string requestId = null, 
        Authorization authorization = default(Authorization),
        CancellationToken cancellationToken = default(CancellationToken))
    {
        var requestMessage = new HttpRequestMessage(HttpMethod.Put, uri)
            .CopyAuthorizationHeaderFrom(_httpContextAccessor.HttpContext)
            .Apply(authorization);
            
        requestMessage.Content = new StringContent(
            JsonConvert.SerializeObject(item), 
            System.Text.Encoding.UTF8, "application/json"
            );
            
        if (requestId != null)
        {
            requestMessage.Headers.Add("x-requestid", requestId);
        }

        var response = await _httpClient.SendAsync(requestMessage, cancellationToken);

        if (response.StatusCode == HttpStatusCode.InternalServerError)
        {
            throw new HttpRequestException();
        }

        return response;
    }

    public void Dispose()
    {
        _httpClient?.Dispose();
    }
}

Chamo especial atenção, nesse código, para a utilização da chave de identificação no PUT (adicionando o cabeçalho x-requestid). É parte da implementação do lado cliente para minha recomendação de como lidar com falhas de conexão.

Quanto a autorização, tento obter do próprio contexto sempre que possível.

public static HttpRequestMessage CopyAuthorizationHeaderFrom(
    this HttpRequestMessage request,
    HttpContext context
)
{
    var authorizationHeader = context
        .Request
        .Headers["Authorization"];

    if (!string.IsNullOrEmpty(authorizationHeader))
    {
        request.Headers.Add("Authorization", new string[] { authorizationHeader });
    }

    return request;
}

Se não estiver disponível, dou opção para o código consumidor prover a informação de autenticação apropriada.

public struct Authorization
{
    public string Token { get; }
    public string Method { get; }

    public Authorization(
        string token = null,
        string method = "Bearer"
    )
    {
        Token = token;
        Method = method;
    }

    public static readonly Authorization Empty = new Authorization();

    public static implicit operator Authorization(string token) =>
        new Authorization(token);

    public bool IsEmpty => Token == null;
}

3. Implemente os padrões Retry e Circuit Breaker de forma transparente

Como destaquei em um post anterior, no lado cliente (microsserviço ou aplicativo que está fazendo uma requisição) é fundamental implementar:

  • estratégias de proteção ao servidor (microsserviço que está sendo consumido), como a implementação do padrão Circuit Breaker
  • estretégias de repetição de requisição, como a implementação do padrão Retry

Tenho feito essa implementação com um “adapter” para o cliente Http que suporta políticas do Polly. Mais uma vez, é importante destacar que esta é uma implementação adaptada de um código de exemplo fornecido pela Microsoft.

public delegate IEnumerable PolicyFactory(string origin);

public class ResilientHttpClient : IHttpClient
{
    private readonly PolicyFactory _policyFactory;
    private readonly StandardHttpClient _standardHttpClient;

    private readonly ConcurrentDictionary<string, PolicyWrap> _policyWrappers =
        new ConcurrentDictionary<string, PolicyWrap>();

    public ResilientHttpClient(
        PolicyFactory policyFactory,
        IHttpContextAccessor accessor
        )
    {
        _policyFactory = policyFactory;
        _standardHttpClient = new StandardHttpClient(accessor);

    }

    public Task GetStringAsync(string uri, Authorization authorization = default(Authorization),
        CancellationToken cancellationToken = default(CancellationToken)) => HttpInvoker(
        new Uri(uri).GetOriginFromUri(),
        () => _standardHttpClient.GetStringAsync(
            uri, authorization, cancellationToken
        )
    );

    public Task PutAsync(string uri, T item, string requestId = null, Authorization authorization = default(Authorization),
        CancellationToken cancellationToken = default(CancellationToken)) => HttpInvoker(
        new Uri(uri).GetOriginFromUri(),
        () => _standardHttpClient.PutAsync(
            uri, item, requestId, authorization, cancellationToken
        )
    );
        
    private async Task HttpInvoker(string origin, Func<Task> action)
    {
        var normalizedOrigin = origin?.Trim()?.ToLower();

        if (!_policyWrappers.TryGetValue(normalizedOrigin, out var policyWrap))
        {
            var policies = _policyFactory(normalizedOrigin).ToArray();
            policyWrap = Policy.WrapAsync(policies);
            _policyWrappers.TryAdd(normalizedOrigin, policyWrap);
        }

        return await policyWrap.ExecuteAsync(action, new Context(normalizedOrigin));
    }
}

public static class UriExtensions
{
    public static string GetOriginFromUri(this Uri uri) =>
        $"{uri.Scheme}://{uri.DnsSafeHost}:{uri.Port}";
}

Repare que tanto o Retry quanto o Circuit Breaker são injetados através da especificação de um Delegate.

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
//services.AddSingleton<IHttpClient, StandardHttpClient>();
services.AddSingleton(sp => (PolicyFactory)((origin) =>
{
    var logger = sp.GetRequiredService<ILogger>();
    return new Policy[]
    {
        Policy.Handle()
            .WaitAndRetryAsync<HttpRequestException>(
                // number of retries
                6,
                // exponential backofff
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                // on retry
                (exception, timeSpan, retryCount, context) =>
                {
                    var msg = $"Retry {retryCount} implemented with Polly's RetryPolicy " +
                                $"of {context.PolicyKey} " +
                                $"at {context.ExecutionKey}, " +
                                $"due to: {exception}.";
                    logger.LogWarning(msg);
                    logger.LogDebug(msg);
                }),
        Policy.Handle()
            .CircuitBreakerAsync<HttpRequestException>( 
                // number of exceptions before breaking circuit
                5,
                // time circuit opened before retry
                TimeSpan.FromMinutes(1),
                (exception, duration) =>
                {
                    // on circuit opened
                    logger.LogTrace("Circuit breaker opened");
                },
                () =>
                {
                    // on circuit closed
                    logger.LogTrace("Circuit breaker reset");
                })
    };
}));
services.AddSingleton<IHttpClient, ResilientHttpClient>();

Concluindo

A escrita de código que consome microsserviços não é necessariamente difícil. Mas, precisa ser cuidadosa para evitar excessiva complexidade acidental. Minha abordagem, tem sido construir abstrações e implementar meu código através dessas abstrações.

  • Abstraindo o acesso ao microsserviço em uma classe de serviço com interface amigável
  • Abstraindo complexidades de acesso Http através de um cliente especializado com customizações e padrões que estou adotando
  • Abstraindo implementação de padrões de resiliência através do uso de uma biblioteca apropriada.

Comentários?
Capa: Nicolas Picard

Compartilhe este insight:

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Mais insights para o seu negócio

Veja mais alguns estudos e reflexões que podem gerar alguns insights para o seu negócio:

No último post desta série, tratamos da “Lei do Retorno Acelerado”. Sabemos que negócios digitais tem crescimento potencialmente exponencial. Neste...
[tweet]Transformação Digital é sobre como o negócio será impactado (transformado) pela adoção de recursos digitais.[/tweet] Portanto, começando uma nova série...
In the previous post, I mentioned Mario Fusco who wrote a blog post series questioning the way Java developers are...
Em um post anterior, indiquei que um servidor de identidades seria uma bela alternativa para um “primeiro microsserviço”. Neste post,...
If you ask me one tip to improve the performance of your applications, it would be: Design your objects to...
Gosto bastante da abordagem de Caitie McCaffrey para explicar sagas. Neste post, me inspiro na linha de raciocínio dela para...
× Precisa de ajuda?