.NET Microservices : Service Discovery (Part 3: Configuring Commands HTTP Service to Consul and consuming it)
Hello again on this third part of our Service Discovery tutorial for .NET core Microservices.
If you haven't seen the beginning of the series I recommand you check the first post about Consul & RabbitMQ and the second post about .NET code to access services with Consul, along with the base course from Les Jackson.
This post is part of a whole:
- .NET Microservices: Service Discovery (Part 1: Consul & RabbitMQ Deployment)
- .NET Microservices: Service Discovery (Part 2: Reaching RabbitMQ through Consul)
- .NET Microservices: Service Discovery (Part 3: Configuring Commands HTTP Service to Consul and consuming it)
- .NET Microservices: Service Discovery (Part 4: Configuring Platform gRPC Service to Consul and consuming it)
We successfully deployed Consul to register our Microservices, we set up RabbitMQ to register automatically to Consul, and we updated our Microservices to call Consul in order to get the RabbitMQ configuration dynamically.
Several services such as RabbitMQ offer some elements or plugins to configure automatically to Consul, I strongly recommand to check out before doing anything complicated because as you've seen, RabbitMQ was quite easy to set up.
So what do we want to do now? Well the whole point was to remove the dependency between our microservices, so we are going to register one of the services to Consul and ask Consul to deliver the configuration to whomever needs it at runtime.
I used some parts of another of TechyMaki's video about Consul, find out more here:
We are going to register the CommandService HTTP Service first, simply because it's the most trivial to configure and to call.
Registering our Commands Service to Consul
Configuring our appsettings
Let's focus on CommandService project.
First things first, we are going to need to configure a few things for our appsettings files. The logic here will be reversed, instead of configuring the addresses and ports of the other services, we're going to configure our own address, port and consul info (id & name).
When we wanted to call CommandService
from PlatformService
, we configured the following address locally like that: "CommandsService": "http://localhost:6000"
in the PlatformService project.
Now, we want to configure the two appsettings of CommandService this way:
Locally, our server is running in localhost
on port 6000, which is what we configured.
In our kubernetes cluster, we configured our service to run on the commands-clusterip-srv
address, with a classic port of 80.
I chose commandshttp
as Id & Name for Consul and we're going to use it now.
Hosted Service
We are going to use a HostedService
. The principle is pretty simple, you implement an interface (IHostedService
) that has two methods, StartAsync()
and StopAsync()
, and we will register this class to our AspNet startup system.
The Hosted Service will trigger the StartAsync()
method just before starting the application, and will trigger StopAsync()
right before the application stops. Remember this is sequentially triggered at startup so if you need a hosted service, you should keep these two implementation minimal not to overload your overall system.
This is actually perfect because it's the exact lifecycle we want for our Consul Registration: register the service at startup, deregister it at shutdown. Life is good.
ConsulRegisterHostedService
We are going to create a new class, ConsulRegisterHostedService
that we will place in the same folder as the rest of our Consul Services (DiscoveryServices). It will implement the IHostedService
interface. We will need the IConsulClient
we injected before and the IConfiguration
to get our configuration. It should look like that at first:
Registering our Hosted Service
Now we're going to register our Hosted Service. It's pretty standard, just go to your Program.cs
(or Startup.cs
) and use the services.AddHostedService<T>()
method:
Just start your application with dotnet run
to check you have the register log in the console, then kill it with CTRL+C and check you have the deregister log like that:
StartAsync() and StopAsync() implementations
The implementation is quite simple, Consul has everything ready for us for registration and deregistration with the AgentServiceRegistration
class. We're going to register in StartAsync()
and deregister on StopAsync()
. Side-note, just as a security we're going to deregister the existing id before registering it.
We're going to use everything we put in our appsettings, just like that in StartAsync()
:
And for the StopAsync()
implementation:
Pretty straightforward, just put a try-catch block (especially on startup) because something could go wrong and we don't want a HostedService to fail ungracefully. For instance, if Consul server was not available.
One improvement could be a retry-mechanism in case Consul is not available; we're not going to bother here but this might be a good idea in production to avoid orphaned services.
Now check that you correctly register your service in your Consul management page (http://localhost:8500):
Now kill you app and check you no longer have the service; it should be good!
If you stay on the page you should have an error message:
Congratulations!
Kubernetes build
Last step for our CommandService configuration, rebuild, push and restart your deployment:docker build -t xxx/commandservice .
docker push xxx/commandservice
kubectl rollout restart deployment commands-depl
Now check that your service is correctly registered with the production address:
All set regarding CommandService HTTP configuration to Consul! Not that difficult right?
Getting CommandsHttp configuration from Consul
Okay now let's get back to our PlatformService
, trust me this will go very smoothly from now on due to our configuration on the previous post.
First, you can remove the "CommandsService"
parameter from all the appsettings (dev & prod): we won't need it anymore!
Get back to ConsulService.cs
, we're going to configure our new id (commandshttp):
Now get back to our HttpCommandDataClient
, we're going to replace what is no longer needed with our Consul service call. What we currently have:
So the two steps are
- Remove
IConfiguration
and addIConsulRegistryService
, just like we did for RabbitMQ previously - Replace our configuration hard-coded values with a consul service call
I'll skip the constructor and private fields to focus on the SendPlatformToCommandAsync
which should look something like this:
Now start your CommandService locally with dotnet run
, check it is correctly registered, start your PlatformService with dotnet run
and check that the CommandHttp is correctly found and called; you're all set!
All that is left is to just build/push/rollout your image for platformservice and check that everything runs correctly.
And that's it!
Additional side-notes
First of all, we implemented our IConsulRegistryService.GetService()
to be synchronous with a « dirty » var serviceQueryResult = _consulClient.Health.Service(serviceName).Result;
. It was necessary considering we were using it in a constructor, but that is quite a waste when we're using it in an asynchronous method such as SendPlatformToCommandAsync()
. If you want to go to production, I would refactor this a bit.
Secondly, we could wonder why we're calling our _consulRegistryService
each time. It seems quite a waste of time: we could just load it in the constructor and keep it safe in a field.
…BUT…
That would defeat the whole purpose of the operation! In that ecosystem, the services come and go, live and die, and we want to have the most accurate configuration possible, so we NEED to call the service each time actually to be sure to have a service that is alive and healthy at this precise moment, not during initialization.
An additional implementation could be to listen to the updates from Consul in the ConsulRegistryService
to keep a track of the services, it could save a few calls to Consul. I think it might be overkill but I'd like your opinion on this !
Thank you for reaching the end of the part 3 of that tutorial and I hope that you enjoyed it!
Right now you should have nearly all the keys to switch to a fully functional Service Discovery system in your Microservices!
Next up we're going to do something similar with gRPC, with just a little configuration twist that deserves its own post in my opinion.
See you!
This post is part of a whole:
- .NET Microservices: Service Discovery (Part 1: Consul & RabbitMQ Deployment)
- .NET Microservices: Service Discovery (Part 2: Reaching RabbitMQ through Consul)
- .NET Microservices: Service Discovery (Part 3: Configuring Commands HTTP Service to Consul and consuming it)
- .NET Microservices: Service Discovery (Part 4: Configuring Platform gRPC Service to Consul and consuming it)