Ao projetar soluções baseadas na arquitetura de microsserviços, muitas vezes nos deparamos com a necessidade de gerenciamento rápido e fácil de todo o sistema, a maior automação possível, sem o ajuste necessário de componentes individuais.
Este é um verdadeiro desafio e por isso decidi preparar um tutorial que demonstra como estabelecer uma arquitetura de micro serviços da forma mais simples possível, que possa ser rápida e facilmente dimensionada e adaptada às necessidades do cliente.
Eu não queria interferir no código e nas configurações de serviços individuais, mas sim controlar o sistema apenas orquestrando contêineres no Docker.
O resultado é uma arquitetura simples de microsserviços que pode ser facilmente dimensionada com apenas algumas alterações nas configurações do contêiner. Todo o resto é fornecido pelo Ocelot como gateway/balanceador de carga e pelo Consul como agente de descoberta de serviço.
Tal arquitetura nos permite a redistribuição de um único serviço sem coordenar a implantação dentro de outros serviços. O serviço reimplantado é registrado automaticamente na descoberta do serviço e fica imediatamente disponível através do gateway. Você pode imaginar o quão grande isso é um impulso para cada equipe de desenvolvimento!
Claro, usar um único serviço de gateway se torna um ponto único de falha em nossa arquitetura, então precisamos implantar pelo menos duas instâncias dele para ter alta disponibilidade. Mas vou deixar esse problema para você brincar.
Na minha demonstração anterior, mostrei como implementar o Ocelot como gateway de serviço e balanceador de carga junto com o Eureka para uma descoberta de serviço. Em vez de Eureka, esta demonstração usa Consul para uma descoberta de serviço.
Consul é uma solução de malha de serviço que fornece um plano de controle completo com descoberta de serviço, configuração e funcionalidade de segmentação. Cada um desses recursos pode ser usado individualmente conforme necessário ou em conjunto para construir uma malha de serviço completa. Consul requer um plano de dados e oferece suporte a um modelo de integração proxy e nativo. O Consul vem com um proxy integrado simples para que tudo funcione imediatamente, mas também oferece suporte a integrações de proxy de terceiros, como o Envoy.
As principais características do Consul são:
Service Discovery : Os clientes do Consul podem cadastrar um serviço, como api ou mysql, e outros clientes podem usar o Consul para descobrir provedores de um determinado serviço. Usando DNS ou HTTP, os aplicativos podem encontrar facilmente os serviços dos quais dependem.
Verificação de integridade : os clientes Consul podem fornecer qualquer número de verificações de integridade, tanto associadas a um determinado serviço ("o servidor web está retornando 200 OK"), quanto ao nó local ("a utilização da memória está abaixo de 90%"). Essas informações podem ser usadas por um operador para monitorar o funcionamento do cluster e são usadas pelos componentes de descoberta de serviço para rotear o tráfego para longe de hosts não íntegros.
Armazenamento KV : os aplicativos podem fazer uso do armazenamento hierárquico de chave/valor do Consul para uma série de finalidades, incluindo configuração dinâmica, sinalização de recursos, coordenação, eleição de líder e muito mais. A API HTTP simples facilita o uso.
Comunicação segura de serviço : Consul pode gerar e distribuir certificados TLS para serviços para estabelecer conexões TLS mútuas. As intenções podem ser usadas para definir quais serviços têm permissão para se comunicar. A segmentação de serviços pode ser facilmente gerenciada com intenções que podem ser alteradas em tempo real, em vez de usar topologias de rede complexas e regras de firewall estáticas.
Multi Datacenter : Consul oferece suporte a vários datacenters prontos para uso. Isso significa que os usuários do Consul não precisam se preocupar em construir camadas adicionais de abstração para crescer em múltiplas regiões.
O Consul foi projetado para ser amigável tanto para a comunidade DevOps quanto para desenvolvedores de aplicativos, tornando-o perfeito para infraestruturas modernas e elásticas.
Fonte: Introdução do Cônsul
Uma parte importante do tutorial é o uso do Consul para descobrir pontos de extremidade de serviço dinamicamente. Depois que um serviço é registrado no Consul, ele pode ser descoberto usando DNS típico ou API personalizada.
A Consul fornece verificações de integridade nessas instâncias de serviço. Se uma das instâncias de serviço ou o próprio serviço não estiver íntegro ou falhar na verificação de integridade, o registro saberá disso e evitará retornar o endereço do serviço. O trabalho que o balanceador de carga faria é feito pelo registro neste caso.
Como usamos várias instâncias do mesmo serviço , o Consul enviaria tráfego aleatoriamente para instâncias diferentes. Assim, equilibra a carga entre instâncias de serviços.
Consul lida com desafios de detecção de falhas e distribuição de carga em múltiplas instâncias de serviços sem a necessidade de implantar um balanceador de carga centralizado.
Ele gerencia automaticamente o registro, que é atualizado quando alguma nova instância do serviço é registrada e fica disponível para receber tráfego. Isso nos ajuda a dimensionar facilmente os serviços.
Antes de entrar nos detalhes de implementação de como implementar o autorregistro no Consul, vamos ver como a descoberta de serviço com autorregistro realmente funciona.
Num primeiro passo, uma instância de serviço regista-se no serviço de descoberta de serviços, fornecendo o seu nome, ID e endereço. Após este gateway ser capaz de obter o endereço deste serviço consultando a descoberta do serviço Consul por seu nome/ID.
O principal a ser observado aqui é que as instâncias de serviço são registradas com um ID de serviço exclusivo para eliminar a ambiguidade entre instâncias de serviço que estão em execução no mesmo agente de serviço Consul. É necessário que todos os serviços tenham um ID exclusivo por nó ; portanto, se os nomes puderem entrar em conflito (nosso caso), os IDs exclusivos deverão ser fornecidos.
Vejamos como podemos implementar o autorregistro no aplicativo .NET. Primeiro, precisamos ler a configuração necessária para descoberta de serviço a partir de variáveis de ambiente, que foram passadas pelo arquivo docker-compose.override.yml .
public static class ServiceConfigExtensions
{
public static ServiceConfig GetServiceConfig ( this IConfiguration configuration )
{
ArgumentNullException . ThrowIfNull ( configuration ) ;
ServiceConfig serviceConfig = new ( )
{
Id = configuration . GetValue < string > ( "ServiceConfig:Id" ) ,
Name = configuration . GetValue < string > ( "ServiceConfig:Name" ) ,
ApiUrl = configuration . GetValue < string > ( "ServiceConfig:ApiUrl" ) ,
Port = configuration . GetValue < int > ( "ServiceConfig:Port" ) ,
ConsulUrl = configuration . GetValue < Uri > ( "ServiceConfig:ConsulUrl" ) ,
HealthCheckEndPoint = configuration . GetValue < string > ( "ServiceConfig:HealthCheckEndPoint" ) ,
} ;
return serviceConfig ;
}
}
Depois de ler a configuração necessária para acessar o serviço de descoberta de serviço, podemos usá-lo para registrar nosso serviço. O código abaixo é implementado como uma tarefa em segundo plano (serviço hospedado), que registra o serviço no Consul, substituindo as informações anteriores sobre o serviço, caso existam. Se o serviço for encerrado, seu registro será automaticamente cancelado no registro do Consul.
public class ServiceDiscoveryHostedService (
ILogger < ServiceDiscoveryHostedService > logger ,
IConsulClient client ,
ServiceConfig config )
: IHostedService
{
private AgentServiceRegistration _serviceRegistration ;
/// <summary>
/// Registers service to Consul registry
/// </summary>
public async Task StartAsync ( CancellationToken cancellationToken )
{
_serviceRegistration = new AgentServiceRegistration
{
ID = config . Id ,
Name = config . Name ,
Address = config . ApiUrl ,
Port = config . Port ,
Check = new AgentServiceCheck ( )
{
DeregisterCriticalServiceAfter = TimeSpan . FromSeconds ( 5 ) ,
Interval = TimeSpan . FromSeconds ( 15 ) ,
HTTP = $ "http:// { config . ApiUrl } : { config . Port } /api/values/ { config . HealthCheckEndPoint } " ,
Timeout = TimeSpan . FromSeconds ( 5 )
}
} ;
try
{
await client . Agent . ServiceDeregister ( _serviceRegistration . ID , cancellationToken ) . ConfigureAwait ( false ) ;
await client . Agent . ServiceRegister ( _serviceRegistration , cancellationToken ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
logger . LogError ( ex , $ "Error while trying to deregister in { nameof ( StartAsync ) } " ) ;
}
}
/// <summary>
/// If the service is shutting down it deregisters service from Consul registry
/// </summary>
public async Task StopAsync ( CancellationToken cancellationToken )
{
try
{
await client . Agent . ServiceDeregister ( _serviceRegistration . ID , cancellationToken ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
logger . LogError ( ex , $ "Error while trying to deregister in { nameof ( StopAsync ) } " ) ;
}
}
}
Depois de registrar nossos serviços no serviço de descoberta de serviços, podemos começar a implementar a API Gateway.
Ocelot exige que você forneça um arquivo de configuração que contenha uma lista de rotas (configuração usada para mapear solicitações upstream) e uma configuração global (outras configurações como QoS, limitação de taxa, etc.). No arquivo ocelot.json abaixo, você pode ver como encaminhamos solicitações HTTP. Temos que especificar que tipo de balanceador de carga usaremos, no nosso caso é um “RoundRobin” que percorre os serviços disponíveis e envia solicitações aos serviços disponíveis. É importante definir o Consul como um serviço de descoberta de serviço em GlobalConfiguration para ServiceDiscoveryProvider .
{
"Routes" : [
{
"Servicename" : " ValueService " ,
"DownstreamPathTemplate" : " /{everything} " ,
"DownstreamScheme" : " http " ,
"UpstreamPathTemplate" : " /{everything} " ,
"UpstreamHttpMethod" : [ " GET " ],
"UseServiceDiscovery" : true ,
"RouteIsCaseSensitive" : false ,
"LoadBalancerOptions" : {
"Type" : " RoundRobin "
},
"QoSOptions" : {
"ExceptionsAllowedBeforeBreaking" : 3 ,
"DurationOfBreak" : 5000 ,
"TimeoutValue" : 2000
}
}
],
"GlobalConfiguration" : {
"RequestIdKey" : " OcelotRequestId " ,
"UseServiceDiscovery" : true ,
"ServiceDiscoveryProvider" : {
"Host" : " consul " ,
"Port" : 8500 ,
"Type" : " PollConsul " ,
"PollingInterval" : 100
}
}
}
Aqui estão algumas explicações necessárias para as configurações do ServiceDiscoveryProvider na seção GlobalConfiguration :
Depois de definirmos nossa configuração podemos começar a implementar o API Gateway. Abaixo podemos ver a implementação do serviço Ocelot API Gateway, que utiliza nosso arquivo de configuração ocelot.json e Consul como registro de serviço.
IHostBuilder hostBuilder = Host . CreateDefaultBuilder ( args )
. UseContentRoot ( Directory . GetCurrentDirectory ( ) )
. ConfigureWebHostDefaults ( webBuilder =>
{
webBuilder . ConfigureServices ( services =>
services
. AddOcelot ( )
. AddConsul < MyConsulServiceBuilder > ( )
. AddCacheManager ( x =>
{
x . WithDictionaryHandle ( ) ;
} )
. AddPolly ( ) ) ;
webBuilder . Configure ( app =>
app . UseOcelot ( ) . Wait ( ) )
. ConfigureAppConfiguration ( ( hostingContext , config ) =>
{
config
. SetBasePath ( hostingContext . HostingEnvironment . ContentRootPath )
. AddJsonFile ( "appsettings.json" , false , true )
. AddJsonFile ( $ "appsettings. { hostingContext . HostingEnvironment . EnvironmentName } .json" , true , true )
. AddJsonFile ( "ocelot.json" , false , true )
. AddEnvironmentVariables ( ) ;
} )
. ConfigureLogging ( ( builderContext , logging ) =>
{
logging . ClearProviders ( ) ;
logging . AddConsole ( ) ;
logging . AddDebug ( ) ;
} ) ;
} ) ;
IHost host = hostBuilder . Build ( ) ;
await host . RunAsync ( ) ;
Como mencionado anteriormente, iremos conteinerizar todos os serviços com Docker, incluindo Consul , usando distribuições leves GNU/Linux para contêineres.
O arquivo docker-compose.yml com configuração para todos os contêineres é semelhante a este:
services :
services :
consul :
image : hashicorp/consul
container_name : consul
command : consul agent -dev -log-level=warn -ui -client=0.0.0.0
hostname : consul
networks :
- common_network
valueservice1.openapi :
image : valueservice.openapi:latest
container_name : valueservice1.openapi
restart : on-failure
hostname : valueservice1.openapi
build :
context : .
dockerfile : src/ValueService.OpenApi/Dockerfile
networks :
- common_network
valueservice2.openapi :
image : valueservice.openapi:latest
container_name : valueservice2.openapi
restart : on-failure
hostname : valueservice2.openapi
build :
context : .
dockerfile : src/ValueService.OpenApi/Dockerfile
networks :
- common_network
valueservice3.openapi :
image : valueservice.openapi:latest
container_name : valueservice3.openapi
restart : on-failure
hostname : valueservice3.openapi
build :
context : .
dockerfile : src/ValueService.OpenApi/Dockerfile
networks :
- common_network
services.gateway :
image : services.gateway:latest
container_name : services.gateway
restart : on-failure
hostname : services.gateway
build :
context : .
dockerfile : src/Services.Gateway/Dockerfile
networks :
- common_network
networks :
common_network :
driver : bridge
Observe que nossos serviços não contêm nenhum arquivo de configuração, para isso usaremos o arquivo Docker-compose.override.yml :
services :
consul :
ports :
- " 8500:8500 "
valueservice1.openapi :
# Swagger UI: http://localhost:9100/index.html
# http://localhost:9100/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
- ServiceConfig__ApiUrl=valueservice1.openapi
- ServiceConfig__ConsulUrl=http://consul:8500
- ServiceConfig__HealthCheckEndPoint=healthcheck
- ServiceConfig__Id=ValueService.OpenApi-9100
- ServiceConfig__Name=ValueService
- ServiceConfig__Port=8080
ports :
- 9100:8080
depends_on :
- consul
valueservice2.openapi :
# Swagger UI: http://localhost:9200/index.html
# http://localhost:9200/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
- ServiceConfig__ApiUrl=valueservice2.openapi
- ServiceConfig__ConsulUrl=http://consul:8500
- ServiceConfig__HealthCheckEndPoint=healthcheck
- ServiceConfig__Id=ValueService.OpenApi-9200
- ServiceConfig__Name=ValueService
- ServiceConfig__Port=8080
ports :
- 9200:8080
depends_on :
- consul
valueservice3.openapi :
# Swagger UI: http://localhost:9300/index.html
# http://localhost:9300/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
- ServiceConfig__ApiUrl=valueservice3.openapi
- ServiceConfig__ConsulUrl=http://consul:8500
- ServiceConfig__HealthCheckEndPoint=healthcheck
- ServiceConfig__Id=ValueService.OpenApi-9300
- ServiceConfig__Name=ValueService
- ServiceConfig__Port=8080
ports :
- 9300:8080
depends_on :
- consul
services.gateway :
# Call first available service: http://localhost:9500/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
ports :
- 9500:8080
depends_on :
- consul
- valueservice1.openapi
- valueservice2.openapi
- valueservice3.openapi
Para executar o arquivo de composição, abra o Powershell e navegue até o arquivo de composição na pasta raiz. Em seguida, execute o seguinte comando: docker-compose up -d --build --remove-orphans que inicia e executa todos os serviços. O parâmetro -d executa o comando desanexado. Isso significa que os contêineres são executados em segundo plano e não bloqueiam a janela do Powershell. Para verificar todos os contêineres em execução, use o comando docker ps .
O Consul oferece uma interface de usuário web agradável pronta para uso. Você pode acessá-lo na porta 8500 : http://localhost:8500. Vejamos algumas das telas.
A página inicial dos serviços Consul UI com todas as informações relevantes relacionadas a um agente Consul e verificação de serviço web.
Vamos fazer diversas chamadas através do API Gateway: http://localhost:9500/api/values. O balanceador de carga percorrerá os serviços disponíveis e enviará solicitações e retornará respostas:
Os sistemas de microsserviços não são fáceis de construir e manter. Mas este tutorial mostrou como é fácil desenvolver e implantar uma aplicação com arquitetura de microsserviços. HashiCorp Consul oferece suporte de primeira classe para descoberta de serviços, verificação de integridade, armazenamento de valores-chave e multidata centers. Ocelot, como gateway, comunica-se com sucesso com o registro de serviço Consul e recupera registros de serviço, faz um loop do balanceador de carga através dos serviços disponíveis e envia solicitações. O uso de ambos facilita significativamente a vida dos desenvolvedores que enfrentam esses desafios. Você concorda?
Aproveitar!
Licenciado pelo MIT. Contate-me no LinkedIn.