ServiceStack Plugin for Service Discovery using Consul.io
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.

README.md

ServiceStack.Discovery.Consul

Build status NuGet version

A plugin for ServiceStack that provides transparent client service discovery using Consul.io as a Service Registry

This enables distributed servicestack instances to call one another, without either knowing where the other is, based solely on a copy of the .Net CLR request type.

Your services will not need to take any dependencies on each other and as you deploy updates to your services they will automatically be registered and used without reconfiguring the existing services.

The automatic and customisable health checks for each service will also ensure that failing services will not be used, or if you run multiple instances of a service, only the healthy and most responsive service will be returned.

RequestDTO Service Discovery

Requirements

A consul agent must be running on the same machine as the AppHost.

Quick Start

Install the package https://www.nuget.org/packages/ServiceStack.Discovery.Consul

PM> Install-Package ServiceStack.Discovery.Consul

Add the following to your AppHost.Configure method

public override void Configure(Container container)
{
    SetConfig(new HostConfig
    {
        // the url:port that other services will use to access this one
        WebHostUrl = "http://api.acme.com:1234",

        // optional
        ApiVersion = "2.0",             
        HandlerFactoryPath = "/api/"
    });

    // Register the plugin, that's it!
    Plugins.Add(new ConsulFeature());
}

To call external services, you just call the Gateway and let it handle the routing for you.

public class MyService : Service
{
    public void Any(RequestDTO dto)
    {
        // The gateway will automatically route external requests to the correct service
        var internalCall = Gateway.Send(new InternalDTO { ... });
        var externalCall = Gateway.Send(new ExternalDTO { ... });
    }
}

It really is that simple!

Running your services

Before you start your services, you'll need to download consul and start the agent running on your machine.

Using the binary

The following will create an in-memory instance which is useful for testing

consul.exe agent -dev -advertise="127.0.0.1"

You should now be able see the Consul Agent WebUI link appear under Plugins on the metadata page.

Using the official docker image

docker pull consul
docker run -dp 8500:8500/tcp --name=dev-consul consul agent -dev -ui -client 0.0.0.0

This will create an in-memory instance using the official docker image

Under the covers...

Automatic Service Registration

Automatic Service Registration

Once you have added the plugin to your ServiceStack AppHost and started it up, it will self-register:

  • AppHost.AfterInit - Registers the service and it's operations in the service registry.
  • AppHost.OnDispose - Unregisters the service when the AppHost is shutdown.

Health checks

Default Health Checks

Each service can have any number of health checks. These checks are run by Consul and allow service discovery to filter out failing instances of your services.

By default the plugin creates 2 health checks

  1. Heartbeat : Creates an endpoint in your service http://locahost:1234/reply/json/heartbeat that expects a 200 response
  2. If Redis has been configured in the AppHost, it will check Redis is responding

NB From Consul 0.7 onwards, if the heartbeat check fails for 90 minutes, the service will automatically be unregistered

You can turn off the default health checks by setting the following property:

new ConsulFeature(settings => { settings.IncludeDefaultServiceHealth = false; });

Custom health checks

You can add your own health checks in one of two ways

1. Define your own health check delegate.

new ConsulFeature(settings =>
{
    settings.AddServiceCheck(host =>
    {
        // your code for checking service health
        if (...failing check)
            return new HealthCheck(ServiceHealth.Critical, "Out of disk space");
        if (...warning check)
            return new HealthCheck(ServiceHealth.Warning, "Query times are slow than expected");
            
        ...ok check 
        return new HealthCheck(ServiceHealth.Ok, "working normally");
    },
    intervalInSeconds: 60 // default check once per minute,
    deregisterIfCriticalAfterInMinutes: null // deregisters the service if health is critical after x minutes, null = disabled by default
    );
});

If an exception is thrown from this check, the healthcheck will return Critical to consul along with the exception

2. Specifying HTTP or TCP endpoints

new ConsulFeature(settings =>
{
    settings.AddServiceCheck(new ConsulRegisterCheck("httpcheck")
    {
        HTTP = "http://myservice/custom/healthcheck",
        IntervalInSeconds = 60
    });
    settings.AddServiceCheck(new ConsulRegisterCheck("tcpcheck")
    {
        TCP = "localhost:1234",
        IntervalInSeconds = 60
    });
});

http checks must be GET and the health check expects a 200 http status code

_tcp checks expect an ACK response.

Discovery

It is important to understand that in order to facilitate seamless service to service calls across different apphosts, there are a few opinionated choices in how the plugin works with consul.

Firstly, the only routing that is supported, is the default pre-defined routes

The use of the Service Gateway, also dictates that the 'IVerb' interface markers must be specified on the DTO's in order to properly send the correct verb.

Secondly, lookups are 'per DTO' type name - This enables the service, apphost or namespaces to change over time for a DTO endpoint. By registering all DTO's in the same consul 'service', this allows seamless DNS and HTTP based lookups using only the DTO name. Each service or apphost will not be shown in consul as a separate entry but rather 'nodes' under a single 'api' service.

For this reason, it is expected that:

  • DTO names are not changed over time breaking the predefined routes.
  • DTO names 'globally unique' in all discovery enabled apphosts to avoid DTO name collisions.

Registering in this way allows for the most efficient lookup of the correct apphost for a DTO and also enables DNS queries to be consistent and 'guessable'.

# {dtoName}.{serviceName}.{type}.consul
hellorequest.api.service.consul

Changing the service name per apphost, makes it impossible to simply query a consul datacenter in either http or dns for the a dto's endpoint.

Excluding RequestDTO's

If there are types that you want to exclude from being registered for discovery by other services, you can use one of the following options:

The ExcludeAttribute : Feature.Metadata or Feature.ServiceDiscovery are not registered

[Exclude(Feature.ServiceDiscovery | Feature.Metadata)]
public class MyInternalDto { ... }

The RestrictAttribute. Any type that does not allow RestrictAttribute.External will be excluded. See the documentation for more details

[Restrict(RequestAttributes.External)]
public class MyInternalDto { ... }

Customisable discovery

The default discovery mechanism uses the ServiceStack request types to resolve all of the services capable of processing the request. This means that you should always use unique request names across all your services for each of your RequestDTO's To override the default which uses Consul, you can implement your own IServiceDiscovery<TServiceModel, TServiceRegistration> client to use whatever backing store you want.

new ConsulFeature(settings =>
{
    settings.AddServiceDiscovery(new CustomServiceDiscovery());
});
public class CustomServiceDiscovery : IServiceDiscovery<TServiceModel, TServiceRegistration>
{
    ...
}

Configuring the external Gateway

By default a JsonServiceClient is used for all external Gateway requests. To change this default, or just to add additional client configuration, you can set the following setting:

new ConsulFeature(settings =>
{
    settings.SetDefaultGateway(baseUri => new JsvServiceClient(baseUri) { UserName = "custom" });
});

You can then continue to use the Gateway as normal but any external call will now use your preferred IServiceGateway

public class EchoService : Service
{
    public void Any(int num)
    {
        // this will use the JsvServiceClient to send the external DTO
        var remoteResponse = Gateway.Send(new ExternalDTO());
    }
}

Tags

you can add your own custom tags to register with consul. This can be useful when you override the default 'IDiscoveryTypeResolver' or want to register different regions or environments for services

new ConsulFeature(settings => { settings.AddTags("region-us-east", "region-europe-west", "region-aus-east"); });

Example

The following shows the services registered with consul and passing health checks and the services running on different IP:Port/Paths

Services