Building a Microservices Control Plane using NATS and Docker Engine v1.12

Wally Quevedo — August 22, 2016

For those of you who might have read the previous post I did on Docker + NATS for Microservices , you’ll recall we took an in-depth look at using Docker Compose with NATS.

That blog post was done a few days prior to DockerCon, and as expected Docker made many interesting announcements at DockerCon. One of the main things announced that week was Docker 1.12.

1.12 includes Swarm mode which can be used for assembling a cluster of Docker Engines and running workloads on them.

In this blog post, we share a basic example of how showing some of these new features available in Docker 1.12 to bring up a NATS cluster that our clients running can then connect against.

Requirements

  • Docker Engine 1.12
  • Docker Swarm cluster prepared with a manager and at least a couple of workers https://docs.docker.com/engine/swarm/
  • Latest release of the NATS Server, which supports cluster auto-discovery for clients. This way, if we add more nodes to the cluster the clients become aware of the full topology dynamically. Awesome stuff!

Step 1

We will first need to create an overlay network for the cluster, here named nats-cluster-example, along with an initial NATS server using latest Docker:

docker network create --driver overlay nats-cluster-example

docker service create --network nats-cluster-example \
                           --name nats-cluster-node-1 nats:0.9.4 -DV

Step 2

Next, we confirm which node of the Docker Swarm NATS Server ended up running on, and confirm its IP:

  docker service ps nats-cluster-node-1
  ID                         NAME                   IMAGE       NODE    DESIRED STATE  CURRENT STATE           ERROR
  b81qv4ljs7g7b52vmnlojktug  nats-cluster-node-1.1  nats:0.9.4  node-2  Running        Running 32 seconds ago

  docker network inspect nats-cluster-example
  [
      {
          "Name": "nats-cluster-example",
          "Id": "944z42rg2hjwgsv9xubmomvi5",
          "Scope": "swarm",
          "Driver": "overlay",
          "EnableIPv6": false,
          "IPAM": {
              "Driver": "default",
              "Options": null,
              "Config": [
                  {
                      "Subnet": "10.0.1.0/24",
                      "Gateway": "10.0.1.1"
                  }
              ]
          },
          "Internal": false,
          "Containers": {
              "e0d105ed7703aa5de939861f75087671c96128fbe799bf6ba38cd15c55aaef07": {
                  "Name": "nats-cluster-node-1.1.0j0wde7hbqn735eqgpslkxowf",
                  "EndpointID": "e798782eaac9bbd36decf3c6e5defd56faa88a3c7a09df608ffd1f2ce1969ed8",
                  "MacAddress": "02:42:0a:00:01:03",
                  "IPv4Address": "10.0.1.3/24",
                  "IPv6Address": ""
              }
          },
          "Options": {
              "com.docker.network.driver.overlay.vxlanid_list": "258"
          },
          "Labels": {}
      }
  ]

On node-2 of the Docker Swarm, notice that the NATS server is now running:

  docker logs e0b4d7b2f7f3
  [1] 2016/08/15 11:31:41.680139 [INF] Starting nats-server version 0.9.4
  [1] 2016/08/15 11:31:41.680217 [DBG] Go build version go1.6.3
  [1] 2016/08/15 11:31:41.680259 [INF] Starting http monitor on 0.0.0.0:8222
  [1] 2016/08/15 11:31:41.680348 [INF] Listening for client connections on 0.0.0.0:4222
  [1] 2016/08/15 11:31:41.680377 [DBG] Server id is JngwpOevXl1rhAovFO8Su6
  [1] 2016/08/15 11:31:41.680386 [INF] Server is ready
  [1] 2016/08/15 11:31:41.680731 [INF] Listening for route connections on 0.0.0.0:6222

Step 3

Next, we will create another service which connects to this server within the overlay network; note that we have an initial IP for connecting to the server:

docker service create --name ruby-nats --network nats-cluster-example wallyqs/ruby-nats:ruby-2.3.1-nats-v0.8.0 -e '
  NATS.on_error do |e|
    puts "ERROR: #{e}"
  end
  NATS.start(:servers => ["nats://10.0.1.3:4222"]) do |nc|
    inbox = NATS.create_inbox
    puts "[#{Time.now}] Connected to NATS at #{nc.connected_server}, inbox: #{inbox}"

    nc.subscribe(inbox) do |msg, reply|
      puts "[#{Time.now}] Received reply - #{msg}"
    end

    nc.subscribe("hello") do |msg, reply|
      next if reply == inbox
      puts "[#{Time.now}] Received greeting - #{msg} - #{reply}"
      nc.publish(reply, "world")
    end

    EM.add_periodic_timer(1) do
      puts "[#{Time.now}] Saying hi (servers in pool: #{nc.server_pool}"
      nc.publish("hello", "hi", inbox)
    end
  end
'

Step 4

Now we are ready to add more nodes to the cluster via more docker services:

docker service create --network nats-cluster-example \
			   --name nats-cluster-node-2 nats:0.9.4 -DV -cluster nats://0.0.0.0:6222 -routes nats://10.0.1.3:6222

Add in more replicas of the subscriber:

docker service scale ruby-nats=3

Then confirm the distribution on our Docker Swarm cluster:

docker service ps ruby-nats
ID                         NAME         IMAGE                                     NODE    DESIRED STATE  CURRENT STATE          ERROR
25skxso8honyhuznu15e4989m  ruby-nats.1  wallyqs/ruby-nats:ruby-2.3.1-nats-v0.8.0  node-1  Running        Running 2 minutes ago  
0017lut0u3wj153yvp0uxr8yo  ruby-nats.2  wallyqs/ruby-nats:ruby-2.3.1-nats-v0.8.0  node-1  Running        Running 2 minutes ago  
2sxl8rw6vm99x622efbdmkb96  ruby-nats.3  wallyqs/ruby-nats:ruby-2.3.1-nats-v0.8.0  node-2  Running        Running 2 minutes ago  

The sample output after adding more NATS server nodes to the cluster, is below - and notice that the client is dynamically aware of more nodes being part of the cluster via auto discovery!:

[2016-08-15 12:51:52 +0000] Saying hi (servers in pool: [{:uri=>#<URI::Generic nats://10.0.1.3:4222>, :was_connected=>true, :reconnect_attempts=>0}]
[2016-08-15 12:51:53 +0000] Saying hi (servers in pool: [{:uri=>#<URI::Generic nats://10.0.1.3:4222>, :was_connected=>true, :reconnect_attempts=>0}]
[2016-08-15 12:51:54 +0000] Saying hi (servers in pool: [{:uri=>#<URI::Generic nats://10.0.1.3:4222>, :was_connected=>true, :reconnect_attempts=>0}]
[2016-08-15 12:51:55 +0000] Saying hi (servers in pool: [{:uri=>#<URI::Generic nats://10.0.1.3:4222>, :was_connected=>true, :reconnect_attempts=>0}, {:uri=>#<URI::Generic nats://10.0.1.7:4222>, :reconnect_attempts=>0}, {:uri=>#<URI::Generic nats://10.0.1.6:4222>, :reconnect_attempts=>0}]

Sample output after adding more workers which can reply back (since ignoring own responses):

[2016-08-15 16:06:26 +0000] Received reply - world
[2016-08-15 16:06:26 +0000] Received reply - world
[2016-08-15 16:06:27 +0000] Received greeting - hi - _INBOX.b8d8c01753d78e562e4dc561f1
[2016-08-15 16:06:27 +0000] Received greeting - hi - _INBOX.4c35d18701979f8c8ed7e5f6ea

Conclusion

Built-in Docker Swarm mode in Docker Engine can be very handy for deploying a distributed system where NATS is being used for handling the internal communication among components via an overlay network, and external communication is being exposed through a load balancer. This in combination with the cluster discovery features from latest NATS releases make microservices operations a breeze, and incredibly flexible and scalable.

Want to get involved in the NATS Community and learn more? We would be happy to hear from you, and answer any questions you may have!

Follow us on Twitter: @nats_io


Back to Blog