Building Secure Distributed Javascript Microservices with RabbitMQ and SenecaJS

Published

To take Microservices into production, you need to make sure they are communicating securely and reliably. We explore using RabbitMQ as an alternative transport for SenacaJS microservices and show you how easy it can be to plug Compose RabbitMQ into your microservices stack.

Creating the Microservices

In a previous article, we demonstrated how to build microservices with Javascript using SenecaJS. SenecaJS is a library for building Microservices in Javascript that uses JSON messages and executes actions depending on whether the message matches a pre-defined pattern. By default, SenecaJS transports these messages between microservices using HTTP and has a direct TCP message transport bundled with it as well.

Let's take a look at this by setting up a new project and creating two simple SenecaJS Microservices and connecting them together. Create a new directory to house your project and initialize a new NPM module there:

$ mkdir seneca-compose
$ cd seneca-compose
$ npm init
...
...
...
$ npm install --save seneca

Now, we'll call our first service Foo:

// foo.js
var seneca = require('seneca')();

seneca  
  .add({
      "role": "foo", 
      "cmd": "ping"
  }, (args, done) => {
    done(null, {result: "Hi there"});
  })
  .listen();

This creates a simple microservice that receives messages via HTTP on port 10101 (the default for the SenecaJS HTTP transport) and listens for messages that have a pattern with a role or foo and a cmd of ping. SenecaJS will pattern match on the messages you send and execute that function if the message matches that pattern. Your messages can have any format you'd like. However, it's a strong convention in SenecaJS to use the keys role and cmd to identify a service you'd like to call and the action you'd like to perform in that service.

Our second microservice, bar, will use HTTP as well, but since port 10101 is already being used by our first microservice, we'll configure our second service to use port 10102.

// bar.js
var seneca = require('seneca')();

seneca  
  .add({
      "role": "bar",
      "cmd": "ping"
  }, (args, done) => {
    done(null, {result: "Hi there"});
  })
  .listen({"type": "http", "port": 10102});

Finally, let's create a small example application that connects to both of these microservices as a client and executes actions on each of them:

// app.js
var seneca = require('seneca');

var foo = seneca()  
    .client()
    .act({"role": "foo", "cmd": "ping"}, function(err, response) {
        if (err) console.error(err);
        else console.log(response);
    });


var bar = seneca()  
    .client(10102)
    .act({"role": "bar", "cmd": "ping"}, (err, response) => {
        if (err) console.error(err);
        else console.log(response);
    });

To run the application, you'll need to run all 3 processes at the same time. You can do so in three separate terminals, or you can run them all in the background like the following:

$ node foo.js &
$ node bar.js &
$ node app.js

The startup order is important here - make sure you start the app.js program last. You should see output like the following:

{"kind":"notice","notice":"hello seneca pv2qmop9xejh/1485838502653/5138/3.2.2/-","level":"info","when":1485838502673}
{"kind":"notice","notice":"hello seneca gf039p7ud4l5/1485838502755/5138/3.2.2/-","level":"info","when":1485838502758}
{ result: 'Hi there' }
{ result: 'Hi there' }

Using RabbitMQ as a Secure Message Transport

SenecaJS command pattern matching doesn't stop at application code. All of the services in SenecaJS use pattern matching, including the transport layer. This means you can build an application using one transport system and then swap it out with a different one later on in development with minimal impact to your existing system. To demonstrate this, let's switch out our HTTP transport with one that can talk AMQP. That's the protocol that RabbitMQ uses natively and plugging into RabbitMQ also gives you a whole new flexible layer in your application stack.

The first thing we'll need to do is spin up an RabbitMQ deployment on Compose. Since RabbitMQ on Compose uses TLS encryption, your messages can be safely passed from one microservice to another. Log into your Compose account and click on the deployments tab and click Create Deployment.

Then, select RabbitMQ from the list of available deployments. The default deployment settings will be sufficient for now, but if you want to change RAM settings or the name of the deployment you can do so here. When you're ready, click Create Deployment:

Once your deployment is created you can use the data browser to create a new user. The user credentials you create now will be used to connect to this deployment of RabbitMQ:

Next, set the user's permissions to allow for configuring, writing, and reading all resources. In production, you'll be able to improve the security of your application by changing the user permissions so they only have access to the resources they need to use. For this small example, we'll allow our user to be as permissive as possible. The user permissions are defined by regular expressions that match the resources for each access permission:

Now, let's go back to our microservices and configure them to use RabbitMQ. First, install the seneca-amqp-transport node module by using the npm command:

$ npm install --save seneca-amqp-transport

Then, we'll configure our Seneca application to use the seneca-amqp-transport plugin. Copy the url from the Connection strings section of the deployment page:

Notice that RabbitMQ on Composes uses a URL with the amqps:// protocol. This is how we know our connection will be encrypted using SSL. Paste the URL with the new connection settings into each microservice:

var seneca = require('seneca');

var foo = seneca()  
    .client({
        type: 'amqp',
        pin: 'role:foo,cmd:*',
        url: "amqps://<user>:<password>@portal1966-10.groovy-rabbitmq-64.jwo.composedb.com:15907/groovy-rabbitmq-64"
      })
    .act({"role": "foo", "cmd": "ping"}, function(err, response) {
        if (err) console.error(err);
        else console.log(response);
    });

var bar = seneca()  
    .client({
        type: 'amqp',
        pin: 'role:bar,cmd:*',
        url: "amqps://<user>:<password>@portal1966-10.groovy-rabbitmq-64.jwo.composedb.com:15907/groovy-rabbitmq-64"
    })
    .act({"role": "bar", "cmd": "ping"}, (err, response) => {
        if (err) console.error(err);
        else console.log(response);
    });

Inspecting Messages with the Admin UI

One of the major benefits of using RabbitMQ as a message transport is the ability to view connections between microservices and the messages being sent over those connections. RabbitMQ on Compose provides a handy administrative UI so you can quickly and easily view different properties of your messages.

To access it, log into your RabbitMQ deployment and scroll down to the Connection Strings section. Beneath the URL for the Admin UI, click on the open button to open the UI in a browser tab.

From there, you can view stats about the various connections, channels, and queues being used by RabbitMQ. You can also details of the messages being sent between different microservices there as well.

Summing Up

Transporting of messages is a critical piece of any microservice-based application, and using RabbitMQ to transport those messages makes your application more robust and fault-tolerant. SenecaJS with its pluggable architecture makes swapping out transport mechanisms easy and fast.


If you have any feedback about this or any other Compose article, drop the Compose Articles team a line at articles@compose.com. We're happy to hear from you.

Image by Clint Adair
John O'Connor
John O'Connor is a code junky, educator, and amateur dad that loves letting the smoke out of gadgets, turning caffeine into code, and writing about it all. Love this article? Head over to John O'Connor’s author page and keep reading.