plugin-discovery

The cluster discovery module enables you to write your applications in a microservice-oriented way. It provides service discovery and communication. It was specially designed to work with thorin apps. This plugin acts as the client that communicates with a discovery registry. sconfig.io offers deep integration with their self-hosted discovery service, at discovery.sconfig.io.

You can also choose to host your own discovery gateway, by setting up your own registry from the git repo and direct your clients to work with it.

Installation and usage
#install the plugin
npm i --save thorin-plugin-discovery@1.x
'use strict';
//app.js entry file
const thorin = require('thorin');

thorin.addPlugin(require('thorin-plugin-discovery')); // <-- add this line
thorin.run((err) => {});
Getting started with the sconfig.io discovery registry
  • Download the UNLOQ app from the App Store or from Google Play and create an account
  • Login with UNLOQ at sconfig.io
  • Create an application with an environment and a minimal description and make sure to toggle Use sconfig discovery. A default API key will be generated for you and the Discovery key will appear in your application's details page.
  • Use the discovery key provided by sconfig io to place under your plugin's token configuration
If your application already uses sconfig.io for config management and have checked the use sconfig discovery field, the plugin will automatically request the registry's token from sconfig, so you do not need to manually place it in your configuration.
Getting started with your self-hosted registry

The discovery registry is an open source node.js application that handles your cluster's discovery and feeds information about the microservices it works with. You can setup your own self-hosted registry by visiting the official repo. All you need is redis and node.js > 4.x. We suggest using nginx as the reverse proxy and TLS termination handler.

Once you've setup your registry, just override the default gateway configuration to your registry's /dispatch full URL. and you're done!

Default configuration
  • gatewayhttps://discovery.sconfig.io/dispatch the registry full dispatch URL to work with.
  • dispatchPath/dispatchthe dispatch path configured in your microservices. This should be the same for all your microservices
  • tokenprocess.env.DISCOVERY_TOKENyour registry's discovery token used to authorize requests
  • interval18000the number of milliseconds between registry announces. This is automatically calculated based on your specified TTL
  • cachetruecache the cluster information locally using thorin.persist, so that in the event of a failure in the registry, you still have the latest representation of your cluster to work with.
  • retry1the number of retries before marking a microservice as unavailable, when you dispatch an action to a microservice
  • timeout3000the default microservice response timeout
  • servicemicroservice config, see below the information about your microservice node. This is where you define how other nodes can contact this one.

Microservice configuration

  • service.typethorin.appthe type of the application (mailer, notifier, api, etc), defaults to the thorin application name
  • service.namethorin.idthe unique identifier of the application, used in logging and such, defaults to the thorin application id
  • service.protohttpthe default protocol to use for inter-communication
  • service.ttl60the time-to-live in seconds used by the registry. If no announces were made by this node to the registry in the configured seconds, the registry will automatically remove the node from the cluster when other microservices announce their presence.
  • service.tags[]additional tags that you can attach to your microservice information
  • service.hostinternalspecifies the publicly available hostname used by other microservices to connect to this microservice. The values it can take are listed bellow.
  • service.port{transport.http.port}specifies the HTTP port the microservice binded to. If a http transport is registered, it will default to the configured transport port.
  • service.path{transport.http.actionPath}specifies the dispatch path that your microservice uses to handle actions. If a http transport is present, we will use its configured actionPath

The service.host possible values

  • internal - use the first private IPv4 from 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, useful when the microservices are in a private network that use the same private block.
  • public - use the first publicly accessible IPv4 address offered by all the network interfaces.
  • {CIDR Block} - if you specify a CIDR block, we will use the first IP address that matches the given block. This is useful when your machine has more than one IP address and you want to use one from a specific block.
  • {static IP address} - if you specify a static IP address, we will use it.
  • {static domain} - if you specify a domain name, we will use it.
Plugin functionality
pluginObj.getRegistry() : Object
Returns the current state of the registry, containing all available microservices.
'use strict';
thorin.plugin('discovery').getRegistry(); // returns
/* 
{ api: 
   [ { name: 'api-xxxxx',
       host: '1.2.3.4',
       proto: 'http',
       port: 14000,
       path: '/dispatch',
       tags: [],
       ttl: 60,
       sid: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
       healthy: true } 
   ],
  mailer: 
   [ { name: 'mailer-xxxxx',
       host: '2.3.4.5',
       proto: 'http',
       port: 14001,
       path: '/dispatch',
       tags: [],
       ttl: 60,
       sid: 'yyyyyyyyyyyyyyyyyyyyy',
       healthy: true } ]
}
*/
pluginObj.elect(serviceName) : Object
Manually elect a service node that can be used to dispatch an action.
pluginObj.refresh() : Promise
Manually refresh the registry by announcing our presence and updating the local registry information
pluginObj.getServiceKey() : string
Returns the registry service authorization key assigned by the registry server. This can be used as an additional authorization token between microservice communication.
pluginObj.dispatch(serviceName, action, payload, opt) : Promise
Performs an HTTP request to the given service, using the action and payload specified. The registry should ultimately have multiple serviceName nodes and the plugin will elect() one. This functionality will essentially enable cross-service communication.
This is where the configured retries is useful. If setting retry to a higher number, in the event of a microservice being offline, the plugin will elect a new one and try to dispatch the action to it.
  • serviceNamestring the service name we want to dispatch an action to
  • actionstringthe action name we want to dispatch
  • payloadobject if specified, the action payload we want to send (essentially the BODY data).
  • optobject if specified, additional options to use while using thorin.util.fetch.
Extended Thorin.Action

The discovery plugin will extend the thorin.Action class, adding additonal functionality such as request proxying. The request proxying is actually adding a custom middleware in the use chain, that will use the intent input() as the payload to dispatch an action to another microservice.

actionObj.proxy(serviceName, opt)
Allows the action handlers of one service to proxy the incoming input to another service using the discovery system. The service`s result is then placed under the intent`s result.
  • serviceNamestring the name of the service we want to call, see pattern below
  • opt.actionstring the action we want to dispatch to the service, defaults to the current action name.
  • opt.payloadobject the base payload that we will be override by the intent input.
  • opt.rawInputfalse if set to true, we will use the intent`s rawInput object and not the filtered input.
Note: the name of the serviceName must have the prefix discovery# so that the plugin knows that we are going to proxy to a discovery service, and not an internal proxy. The pattern is discovery#{serviceName}
'use strict';
// The sender app
thorin.dispatcher
   .addAction('myAction')
   .use((intentObj, next) => {
      intentObj.input('someValue', 1);    // override the intent`s input.
      next();
   })
   .before('proxy', (intentObj, serviceData) => {
      console.log('Will proxy action to ${serviceData.ip}');
   })
   .proxy('discovery#myOtherService', {
      action: 'some.other.custom.action'  // override the default "myAction"
   })
   .after('proxy', (intentObj, response) => {
      console.log(`Proxy successful. Response:`, response);
   })
   .use((intentObj) => {
      console.log("myOtherService responded with: ", intentObj.result());
      // here is where we can mutate the result of the intent to send back to the client.
      intentObj.result("wasCalled", true);
      intentObj.send();
   });

'use strict';
// The receiver app
thorin.dispatcher
   .addAction('some.other.custom.action')
   .authorize('discovery#proxy') // verifies the incoming authorization token to be from an application within the cluster
   .use((intentObj, next) => {
      log.info(`Got called the microservice: ${intentObj.data('proxy_name')} with: ${intentObj.rawResult}`);
      intentObj.result({
         serviceResult: 'right here'
      }).send();
   });

Security concerns

Whenever a service will call another service, it will also generate an Authorization HTTP header that uses a signed token to identify the service that is initiating the request. The signature is then verified by the service that is expecting the action by using the discovery#proxy authorization middleware.

The discovery#proxy authorization middleware will place under the intent`s data object the proxy_name, which is the name of the service that is calling, and the proxy_type key, which specifies the service`s type.

A short example can be found in the todo-mailer and todo-app example applications.

Do you have a question or is something missing?

You can always create a new issue on GitHub or contact one of the core founders by chat.