Lors de la conception de solutions basées sur l'architecture des microservices, nous sommes souvent confrontés à l'exigence d'une gestion rapide et simple de l'ensemble du système, d'une automatisation la plus élevée possible, sans l'ajustement nécessaire des composants individuels.
C'est un véritable défi et c'est pourquoi j'ai décidé de préparer un tutoriel qui montre comment établir une architecture de microservices de la manière la plus simple possible, qui peut être rapidement et très facilement mise à l'échelle et adaptée aux exigences du client.
Je ne voulais pas interférer avec le code et les paramètres des services individuels, mais contrôler le système uniquement en orchestrant les conteneurs dans Docker.
Le résultat est une architecture de microservice simple qui peut être facilement mise à l'échelle avec seulement quelques modifications dans les paramètres du conteneur, tout le reste est fourni par Ocelot en tant que passerelle/équilibreur de charge et Consul en tant qu'agent de découverte de services.
Une telle architecture nous permet de redéployer un seul service sans coordonner le déploiement au sein d'autres services. Le service redéployé est automatiquement enregistré lors de la découverte du service et immédiatement disponible via la passerelle. Vous pouvez imaginer à quel point cela représente un grand coup de pouce pour chaque équipe de développement !
Bien sûr, l’utilisation d’un service de passerelle unique devient un point de défaillance unique pour notre architecture. Nous devons donc en déployer au moins deux instances pour bénéficier d’une haute disponibilité. Mais je vous laisse jouer avec ce problème.
Dans ma démo précédente, j'ai montré comment implémenter Ocelot en tant que passerelle de service et équilibreur de charge avec Eureka pour une découverte de service. Au lieu de cela, Eureka, cette démo utilise Consul pour une découverte de service.
Consul est une solution de maillage de services fournissant un plan de contrôle complet avec des fonctionnalités de découverte, de configuration et de segmentation de services. Chacune de ces fonctionnalités peut être utilisée individuellement selon les besoins, ou elles peuvent être utilisées ensemble pour créer un maillage de services complet. Consul nécessite un plan de données et prend en charge à la fois un modèle d'intégration proxy et natif. Consul est livré avec un simple proxy intégré pour que tout fonctionne immédiatement, mais prend également en charge les intégrations de proxy tiers telles que Envoy.
Les principales caractéristiques de Consul sont :
Découverte de services : les clients de Consul peuvent enregistrer un service, tel que API ou MySQL, et d'autres clients peuvent utiliser Consul pour découvrir les fournisseurs d'un service donné. En utilisant DNS ou HTTP, les applications peuvent facilement trouver les services dont elles dépendent.
Vérification de l'état : les clients Consul peuvent fournir n'importe quel nombre de vérifications de l'état, soit associées à un service donné ("le serveur Web renvoie-t-il 200 OK"), soit au nœud local ("l'utilisation de la mémoire est-elle inférieure à 90 %"). Ces informations peuvent être utilisées par un opérateur pour surveiller l'état du cluster, et elles sont utilisées par les composants de découverte de services pour acheminer le trafic loin des hôtes défectueux.
KV Store : les applications peuvent utiliser le magasin de clés/valeurs hiérarchiques de Consul à de nombreuses fins, notamment la configuration dynamique, le marquage des fonctionnalités, la coordination, l'élection des dirigeants, etc. L'API HTTP simple le rend facile à utiliser.
Communication de service sécurisée : Consul peut générer et distribuer des certificats TLS pour les services afin d'établir des connexions TLS mutuelles. Les intentions peuvent être utilisées pour définir quels services sont autorisés à communiquer. La segmentation des services peut être facilement gérée avec des intentions pouvant être modifiées en temps réel au lieu d'utiliser des topologies de réseau complexes et des règles de pare-feu statiques.
Multi Datacenter : Consul prend en charge plusieurs centres de données prêts à l'emploi. Cela signifie que les utilisateurs de Consul n'ont pas à se soucier de la création de couches d'abstraction supplémentaires pour s'étendre à plusieurs régions.
Consul est conçu pour être convivial à la fois pour la communauté DevOps et les développeurs d'applications, ce qui le rend parfait pour les infrastructures modernes et élastiques.
Source : introduction du Consul
Un élément clé du didacticiel est l'utilisation de Consul pour découvrir dynamiquement les points de terminaison du service. Une fois qu'un service est enregistré auprès de Consul, il peut être découvert à l'aide d'un DNS typique ou d'une API personnalisée.
Consul fournit des contrôles de santé sur ces instances de service. Si l'une des instances de service ou le service lui-même est défectueux ou échoue à sa vérification de l'état, le registre en sera alors informé et évitera de renvoyer l'adresse du service. Le travail que ferait l'équilibreur de charge est géré par le registre dans ce cas.
Étant donné que nous utilisons plusieurs instances du même service , Consul enverrait du trafic de manière aléatoire vers différentes instances. Il équilibre ainsi la charge entre les instances de services.
Consul gère les défis de détection des pannes et de répartition de la charge sur plusieurs instances de services sans avoir besoin de déployer un équilibreur de charge centralisé.
Il gère automatiquement le registre, qui est mis à jour lorsqu'une nouvelle instance du service est enregistrée et devient disponible pour recevoir du trafic. Cela nous aide à faire évoluer facilement les services.
Avant d'entrer dans les détails de mise en œuvre de l'auto-inscription sur Consul, examinons comment fonctionne réellement la découverte de services avec l'auto-inscription.
Dans un premier temps, une instance de service s'enregistre auprès du service de découverte de services en fournissant son nom, son identifiant et son adresse. Une fois que cette passerelle est en mesure d'obtenir l'adresse de ce service en interrogeant la découverte du service Consul par son nom/ID.
L'élément clé à noter ici est que les instances de service sont enregistrées avec un ID de service unique afin de lever l'ambiguïté entre les instances de service qui s'exécutent sur le même agent de service Consul. Il est nécessaire que tous les services aient un identifiant unique par nœud , donc si les noms peuvent entrer en conflit (notre cas), des identifiants uniques doivent être fournis.
Voyons comment nous pouvons implémenter l'auto-enregistrement dans l'application .NET. Tout d’abord, nous devons lire la configuration requise pour la découverte de services à partir des variables d’environnement transmises via le fichier 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 ;
}
}
Après avoir lu la configuration requise pour accéder au service de découverte de services, nous pouvons l'utiliser pour enregistrer notre service. Le code ci-dessous est implémenté en tant que tâche d'arrière-plan (service hébergé), qui enregistre le service dans Consul en remplaçant les informations précédentes sur le service, le cas échéant. Si le service s'arrête, il est automatiquement désinscrit du registre 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 ) } " ) ;
}
}
}
Une fois que nous avons enregistré nos services dans le service de découverte de services, nous pouvons commencer à implémenter l'API Gateway.
Ocelot nécessite que vous fournissiez un fichier de configuration contenant une liste de routes (configuration utilisée pour mapper la requête en amont) et une configuration globale (autre configuration comme la QoS, la limitation de débit, etc.). Dans le fichier ocelot.json ci-dessous, vous pouvez voir comment nous transmettons les requêtes HTTP. Nous devons préciser quel type d'équilibreur de charge nous utiliserons, dans notre cas il s'agit d'un « RoundRobin » qui parcourt les services disponibles et envoie des requêtes aux services disponibles. Il est important de définir Consul en tant que service de découverte de services dans GlobalConfiguration pour 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
}
}
}
Voici quelques explications nécessaires sur les paramètres de ServiceDiscoveryProvider dans la section GlobalConfiguration :
Après avoir défini notre configuration, nous pouvons commencer à implémenter API Gateway. Ci-dessous, nous pouvons voir la mise en œuvre du service Ocelot API Gateway, qui utilise notre fichier de configuration ocelot.json et Consul comme registre de services.
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 ( ) ;
Comme mentionné précédemment, nous conteneuriserons tous les services avec Docker, y compris Consul , en utilisant les distributions Lightweight GNU/Linux pour les conteneurs.
Le fichier docker-compose.yml avec la configuration pour tous les conteneurs ressemble à ceci :
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
Notez que nos services ne contiennent aucun fichier de configuration, pour cela nous allons utiliser le fichier 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
Pour exécuter le fichier de composition, ouvrez Powershell et accédez au fichier de composition dans le dossier racine. Exécutez ensuite la commande suivante : docker-compose up -d --build --remove-orphans qui démarre et exécute tous les services. Le paramètre -d exécute la commande détachée. Cela signifie que les conteneurs s'exécutent en arrière-plan et ne bloquent pas votre fenêtre Powershell. Pour vérifier tous les conteneurs en cours d'exécution, utilisez la commande docker ps .
Le Consul offre une interface utilisateur Web agréable dès la sortie de la boîte. Vous pouvez y accéder sur le port 8500 : http://localhost:8500. Regardons quelques-uns des écrans.
La page d'accueil des services Consul UI avec toutes les informations pertinentes liées à un agent Consul et à la vérification des services Web.
Faisons plusieurs appels via API Gateway : http://localhost:9500/api/values. L'équilibreur de charge parcourra les services disponibles, enverra des requêtes et renverra des réponses :
Les systèmes de microservices ne sont pas faciles à construire et à entretenir. Mais ce tutoriel a montré à quel point il est facile de développer et de déployer une application avec une architecture de microservices. HashiCorp Consul offre un support de premier ordre pour la découverte de services, le contrôle de l'état, le stockage clé-valeur et les centres de données multiples. Ocelot, en tant que passerelle, communique avec succès avec le registre des services Consul et récupère les enregistrements de services, l'équilibreur de charge parcourt les services disponibles et envoie des requêtes. L’utilisation des deux facilite considérablement la vie des développeurs confrontés à de tels défis. Êtes-vous d'accord?
Apprécier!
Licence sous MIT. Contactez-moi sur LinkedIn.