Create an IoT network using the Syntropy Stack (Part 2): Using Ansible, Docker, Mosquitto and NodeJS

Craig
15 min readFeb 1, 2021

--

It’s all play and no work

Familiarity with Ansible, NodeJS, Docker, Virtual Machines / Cloud Computing, and the command-line is recommended for following this guide.

Welcome to chapter 2 of 4 in the series. The goal of the series is to explore different ways in which to work with the Syntropy Stack. Each post aims to achieve the same goal: Create an MQTT-powered network with a Broker, Publisher, and Subscriber. The previous post walks you through creating the same MQTT network manually using Docker and the Syntropy UI. If you haven’t given it a read yet, it provides some more in-depth detail on the services and applications themselves, so I recommend reading through it even if you don’t intend on setting up the network manually, as it’ll help you to familiarize yourself with the applications, concepts and terminology we use in this guide. In this chapter, we’ll focus on creating the same network and infrastructure using Ansible.

  • Part 1: Accessing and configuring your VMs manually, launching your apps with docker-compose, and creating your network using the Syntropy UI.
  • This Chapter: Using Ansible to automate the provisioning of the VMs, for deploying the services, and for creating the network
  • Part 3: Provisioning your VMs with Ansible and creating your network manually using the Syntropy CTL (Computational Topology Library) command line utility.
  • Part 4: Using the Docker CLI to launch our services manually and the Syntropy NAC (Network As Code) command line utility to create our network.

Getting started with Ansible

I have to admit that I was relatively new to Ansible when I set about creating this guide. I can confidently say now, though, that after using it for this project, I’m kind of obsessed. It’s incredibly simple, yet so powerful. For me, it’s one of those muse-like technologies. The ones that, as you’re learning them, inspire idea after idea of what to build with it. It took a little while to get comfortable with the style of Ansible’s YAML indentation, but once you get the hang of interpreting the logs and errors it gets easier.

The primary focus of this guide, apart from showcasing the Syntropy Stack, is to show you how to use it in conjunction with Ansible. There are probably hundreds of excellent “getting started with Ansible” guides out there, so we’ll assume you have at least some basic knowledge of Ansible, though I will go into a bit of detail with some of the Ansible functionality we use. The most important thing to know is that every operation we want Ansible to perform is described in a playbook (which is just a YAML template).

Ansible Playbooks offer a repeatable, re-usable, simple configuration management and multi-machine deployment system, one that is well suited to deploying complex applications. If you need to execute a task with Ansible more than once, write a playbook and put it under source control. Then you can use the playbook to push out new configuration or confirm the configuration of remote systems.

Let’s get started

Clone the syntropy-devops-integrations repo on Github. We’ll be working in the mqtt-mosquitto-nodejs-ansible folder.

If you’d like to see me go through the process of setting up my own network, a screen recording with commentary can be found here:
https://www.loom.com/share/3b61a2087f834c8883f31e92d64ac103

Here’s a checklist of what you’ll need to set up your three nodes.

  • You need to have a Syntropy Stack account, as well as an active Agent Token.
  • 3 Linux VMs running on separate cloud providers
  • A control node for Ansible — this is just a fancy way of saying “a computer to run Ansible on” (it can be your local machine or a VM). Ansible should already be installed on this machine, if it’s not, do so now.
  • Your control node should also have Python ≥ 3.6 for the Ansible dependencies we’ll be working with

Here’s a simple diagram of the network we’ll be building:

Each node is running the Syntropy Agent, its own Docker network, and a service

I’ve chosen to use Digital Ocean, AWS, and Google Cloud Platform as the cloud providers for my VMs. You can find more details about why I chose them, and info on setting up your own, in Part 1.

Project Structure

Let’s examine the project structure and what each part does:

Craigson:mqtt-mosquitto-nodejs-ansible craig$ tree
.
├── README.md
├── deploy_broker.yaml
├── deploy_network.yaml
├── deploy_publisher.yaml
├── deploy_subsciber.yaml
├── provision_hosts.yaml
├── roles
│ ├── create_app_image
│ │ └── tasks
│ │ └── main.yaml
│ ├── create_docker_network
│ │ └── tasks
│ │ └── main.yml
│ ├── create_syntropy_network
│ │ └── tasks
│ │ └── main.yml
│ ├── install_wireguard
│ │ ├── files
│ │ │ └── wireguard.conf
│ │ └── tasks
│ │ └── main.yaml
│ ├── launch_mosquitto
│ │ ├── files
│ │ │ └── mosquitto.conf
│ │ └── tasks
│ │ └── main.yml
│ ├── launch_nodejs
│ │ └── tasks
│ │ └── main.yml
│ ├── launch_syntropy_agent
│ │ └── tasks
│ │ └── main.yml
│ ├── tasks
│ │ └── main.yaml
│ └── update_cache
│ └── tasks
│ └── main.yml
├── secrets.yaml
├── src
│ ├── publisher
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ └── publisher.js
│ └── subscriber
│ ├── Dockerfile
│ ├── package.json
│ └── subscriber.js
└── syntropyhosts

Playbooks

These are the YAML files located at the project’s root. We’ll execute these using the ansible-playbook command from the command-line. Let’s see what each one does.

provision_hosts : Provisions all your VMs by installing Docker, Wireguard, and the necessary dependencies to run your applications.

deploy_broker.yml : Deploys the Mosquitto Broker and Syntropy Agent

deploy_publisher.yaml : Deploys the NodeJS Publisher and Syntropy Agent

deploy_subscriber.yaml : Deploys the NodeJS Subscriber and Syntropy Agent

deploy_network.yaml : Creates a Syntropy network and connects the Broker to the Publisher and Subscriber endpoints.

Secrets

The secrets.yaml file contains information you wouldn’t want to commit, such as your API key. Any variables from the secrets file can be loaded into your playbooks using the include_vars (ansible.builtin.include_vars) module.

Roles

This is where the Ansible magic happens! If you aren’t familiar with roles, they’re a handy way to modularize your playbooks. In the same way that a function is a modularized, reusable piece of code for performing a single task, a role contains a task (or a collection of tasks) that should focus on performing a single job/action. We’ll discuss some of our roles in more detail later on.

Src

The src folder contains, you guessed it, the source code for our NodeJS Publisher and Subscriber apps. The apps themselves are described in more details in Part 1.

Syntropy Hosts

The syntropyhosts file, known as an “inventory” file, contains details about your host VMs, such as the username and IP addresses.

Creating your network

1. Authentication

Ensure that you have access to your VMs via SSH and that they’ve been added them to your list of authorized keys so that Ansible has unfettered access to them.

Rename the sample.secrets.yamlfile to secrets.yamland add your Agent Token (generated via Syntropy UI) to the api_key variable.

secrets.yaml---
api_key: "your_api_key"

Next, we need to generate an API Token (not to be confused with your Agent Token, ie. your API key). To generate an API Token, install the Syntropy CLI.

pip3 install syntropycli

Generate an API Token by logging in using the CLI:

syntropyctl login {syntropy stack user name} { syntropy stack password}

Copy the API token this command outputs and add it to your ENV. I’ve placed mine in my .bashrc. You’ll need to add the API URL, as well as your username in password.

export SYNTROPY_API_SERVER=https://controller-prod-server.syntropystack.comexport SYNTROPY_API_TOKEN=”your_syntropy_api_token”export SYNTROPY_PASSWORD=”your_syntropy_password”export SYNTROPY_USERNAME=”your_syntropy_username”

If you do place it in your .bashrc , remember to source the file from your current terminal window so that the variables are available in your ENV.

source ~/.bashrc

You can check your env (on Mac), by simply typing env in your terminal window and hitting return .

2. Prepare your inventory

Update the syntropyhosts file to include login credentials for your VMs. This file is known as an inventory in Ansible.

Each host is given a name, designated by the [ ] . If you require a PEM file for SSH authentication, assign it to ansible_ssh_private_key_file . The [all:vars] tells Ansible to make these variables available to all hosts.

ansible_host represents the IP address of your VM, whereas your SSH user is assigned to ansible_user .

3. Install the Syntropy Galaxy collection and configure Ansible

Ansible has great documentation, so definitely check it out if you’re curious about what it’s capable of, or if you aren’t sure about what something's doing. We’ll be using the Syntropy Ansible Galaxy Collection. Ansible Galaxy houses content created by the Ansible community. Collections contain useful playbooks, roles and modules for us to include in our own playbooks.

This Galaxy collection requires Python ≥ 3.6 for the required dependencies. If you’re working on a Mac, like me, the standard python version installed is usually 2.7, so we’ll be using python3 and pip3.

Check that you have the correction versions installed.

$ python3 --version
Python 3.7.5

Install the collection:

ansible-galaxy collection install git@github.com:SyntropyNet/syntropy-ansible-collection.git

Navigate to your local ansible directory, for example on Mac OS:

cd /Users/{user}/.ansible/collections/ansible_collections/syntropynet/syntropy

Install the Python dependencies.

pip3 install -U -r requirements.txt

To make the log output from your Ansible CLI easier to read, create an Ansible config file and place it in your home directory.

~/.ansible.cfg

And add the following to it:

[defaults]
stdout_callback=yaml
# use stdout_callback when running adhoc commands too
bin_ansible_callbacks = True
interpreter_python = auto_silent
remote_tmp = /tmp/ansible-$USER

Without the config, your log output looks like this:

TASK [create_docker_network : Create Docker network] *****************************************************************
ok: [broker] => {"ansible_facts": {"docker_network": {"Attachable": false, "ConfigFrom": {"Network": ""}, "ConfigOnly": false, "Containers": {"1893dcb898d5299c5dd9cf5a2219f2e2f08956507afc56d86bfe64cb96e18837": {"EndpointID": "7b9ec7d78666c965795c34db03465562e4bb7f5bdfa21de71d5f69824c3b48a7", "IPv4Address": "172.20.0.2/24", "IPv6Address": "", "MacAddress": "02:42:ac:14:00:02", "Name": "mosquitto"}}, "Created": "2021-01-11T17:10:29.613448381Z", "Driver": "bridge", "EnableIPv6": false, "IPAM": {"Config": [{"Subnet": "172.20.0.0/24"}], "Driver": "default", "Options": null}, "Id": "9e6daec0c1bb385fc2d6459655602ab5ce127505e7e6eaea091c9e5af7b5a1f0", "Ingress": false, "Internal": false, "Labels": {}, "Name": "syntropynet", "Options": {}, "Scope": "local"}}, "changed": false, "network": {"Attachable": false, "ConfigFrom": {"Network": ""}, "ConfigOnly": false, "Containers": {"1893dcb898d5299c5dd9cf5a2219f2e2f08956507afc56d86bfe64cb96e18837": {"EndpointID": "7b9ec7d78666c965795c34db03465562e4bb7f5bdfa21de71d5f69824c3b48a7", "IPv4Address": "172.20.0.2/24", "IPv6Address": "", "MacAddress": "02:42:ac:14:00:02", "Name": "mosquitto"}}, "Created": "2021-01-11T17:10:29.613448381Z", "Driver": "bridge", "EnableIPv6": false, "IPAM": {"Config": [{"Subnet": "172.20.0.0/24"}], "Driver": "default", "Options": null}, "Id": "9e6daec0c1bb385fc2d6459655602ab5ce127505e7e6eaea091c9e5af7b5a1f0", "Ingress": false, "Internal": false, "Labels": {}, "Name": "syntropynet", "Options": {}, "Scope": "local"}}

After adding your config, it’ll look like this:

TASK [create_docker_network : Create Docker network] *****************************************************************
ok: [broker] => changed=false
ansible_facts:
docker_network:
Attachable: false
ConfigFrom:
Network: ''
ConfigOnly: false
Containers:
1893dcb898d5299c5dd9cf5a2219f2e2f08956507afc56d86bfe64cb96e18837:
EndpointID: 7b9ec7d78666c965795c34db03465562e4bb7f5bdfa21de71d5f69824c3b48a7
IPv4Address: 172.20.0.2/24
IPv6Address: ''
MacAddress: 02:42:ac:14:00:02
Name: mosquitto
Created: '2021-01-11T17:10:29.613448381Z'
Driver: bridge
EnableIPv6: false
IPAM:
Config:
- Subnet: 172.20.0.0/24
Driver: default
Options: null
Id: 9e6daec0c1bb385fc2d6459655602ab5ce127505e7e6eaea091c9e5af7b5a1f0
Ingress: false
Internal: false
Labels: {}
Name: syntropynet
Options: {}
Scope: local
network:
Attachable: false
ConfigFrom:
Network: ''
ConfigOnly: false
Containers:
1893dcb898d5299c5dd9cf5a2219f2e2f08956507afc56d86bfe64cb96e18837:
EndpointID: 7b9ec7d78666c965795c34db03465562e4bb7f5bdfa21de71d5f69824c3b48a7
IPv4Address: 172.20.0.2/24
IPv6Address: ''
MacAddress: 02:42:ac:14:00:02
Name: mosquitto
Created: '2021-01-11T17:10:29.613448381Z'
Driver: bridge
EnableIPv6: false
IPAM:
Config:
- Subnet: 172.20.0.0/24
Driver: default
Options: null
Id: 9e6daec0c1bb385fc2d6459655602ab5ce127505e7e6eaea091c9e5af7b5a1f0
Ingress: false
Internal: false
Labels: {}
Name: syntropynet
Options: {}
Scope: local

That induces much less anxiety! By placing the .ansible.cfg file in your home directory ( ~/ ), you’re making it global. You can place an additional .ansible.cfg in your project root and it will override any settings that overlap with the global config.

4. Configure your playbooks

As I mentioned before, there’s really not much configuration or code for you to write. However, there are a few small changes you need to make.

In each of the deploy_broker.yaml , deploy_publisher.yaml and deploy_subscriber.yaml files, change the agent_provider ID to match whatever cloud provider you’ve chosen for that respective service. You can find a list of IDs for the supported cloud providers here.

Feel free to change the network_name to in the deploy_network.yaml file, though that’s certainly not a requirement.

5. Ansible: It’s game time!

Believe it or not, that’s all we need to do, all that’s left to do is run the playbooks and our network will be online! Now might be a good time to put on a pair of raggedy overalls and take a look under the hood. Ansible describes a playbook as follows:

A playbook is composed of one or more ‘plays’ in an ordered list. The terms ‘playbook’ and ‘play’ are sports analogies. Each play executes part of the overall goal of the playbook, running one or more tasks. Each task calls an Ansible module.

Let’s breakdown what’s going on in our playbooks before we run them.

As we explained a little earlier, we’ve broken out most of our functionality into roles, which we’re then able to share between different playbooks. Here’s the YAML template for our deploy_broker playbook:

name : This gives our play a name, which shows up in the console like this:

PLAY [Deploy Broker] **********************************************

hosts : refers to which host we want to run the playbook on. You can designate every host using all , but we only want the broker which we defined in our syntropyhosts file. Remember that Ansible will SSH into your host and perform all the tasks designated in the playbook.

vars : this sets global variables that we can access in our roles using the "{{my_var}}" syntax.

roles : This is where the magic happens, the roles defined here execute synchronously and in order

Let’s take a closer look at the create_docker_network role:

As you can see, we have two tasks defined. The first uses the Ansible debug module (ansible.builtin.debug) to print a message to the console containing the subnet that we defined in the deploy_broker.yaml file.

The second tasks uses the docker_network module (community.general.docker_network) to define a docker network named syntropynet , set the driver to bridge and set the subnet to whatever we defined earlier, in this case 172.20.0.0/24 .

Next, let’s take a quick look at the launch_syntropy_agent role as it’s used by all three of our services.

Perhaps the first thing you’ll notice is that this looks remarkably similar to a docker-compose file, which makes sense given they’re both just YAML templates. You can see we’re using the docker_container module which allows us to structure it like we’re using docker-compose. We’re also pulling in the api_key from the secrets.yaml file using "{{api_key}}" , along with the rest of the variables defined in the parent playbook.

Lastly, I want to show you the launch_mosquitto role, just because it shows one additional concept of copying files across to the host. Take a look at the file structure:

├── launch_mosquitto
│ ├── files
│ │ └── mosquitto.conf
│ └── tasks
│ │ └── main.yml

All roles have their tasks defined in /<role>/tasks/main.yml , but for this role, you’ll see the mosquitto.conf in the files/ directory.

Ansible’s built-in copy module copies files from the Ansible Controller (your local machine) to the host. You don’t have to use the relative or absolute path of the mosquitto.conf file as it appears in the files/ directory of the role, whereas the dest represents the absolute path on the host, ie. the root. We can then reference this path when mounting the volume.

Another important property to bring to your attention is the purge_networks . This removes the default docker bridge network. This caught me out at the beginning as it was creating overlapping networks in my Syntropy Agents. The default docker bridge network is created on 172.17.0.0/24 , so all three agents had the same subnet, which isn’t allowed in a Syntropy Network. This issue was solved by adding the purge_networks: yes .

6. Provision your VMs

Each VM requires Docker, Wireguard, and some additional Python dependencies to be installed. If you’re using the same VMs you used for Part 1. Repetitive tasks like installing dependencies are perfect for Ansible. Ensure you have SSH access to all of your VMs, as Ansible needs to be able to secure shell into your hosts on your behalf. To provision your VMs, run the following command from the root of your project folder. This will likely take a few minutes to complete.

ansible-playbook deploy_broker.yaml -i syntropyhosts -vv

As I mentioned before, you’re passing your inventory ( syntropyhosts ) file with the -i flag. The -v flag stands for verbose.

-v, --verbose         verbose mode (-vvv for more, -vvvv to enable
connection debugging)

Here’s what the play that we executed looks like in the playbook:

Because we specified all hosts, we’ll execute the tasks on all the VMs and they’ll be provisioned in parallel. It’ll run each task on each VM before moving on to the next task, then move on to the next role when all the previous role’s tasks are complete until all VMs are provisioned. If you want to confirm if everything is installed correctly, you should see the following output at the end of your log output.

provision_hosts log output

The PLAY RECAP in the log output will show you if any tasks failed. You can see from me running provision_hosts.yaml on my VMs that all 8 tasks succeeded and 0 failed.

7. Deploy your Services

Deploy the Broker

ansible-playbook deploy_broker.yaml -i syntropyhosts -vv

As an example, the output of the deploy_broker playbook looks like this (though bear in mind I’ve left out the -v flag to remove the verbose log output):

PLAY [Deploy Broker] *************************************************************************************************TASK [Gathering Facts] ***********************************************************************************************
ok: [broker]
TASK [create_docker_network : debug] *********************************************************************************
ok: [broker] =>
msg: Create docker network on subnet - 172.20.0.0/24"
TASK [create_docker_network : Create Docker network] *****************************************************************
ok: [broker]
TASK [launch_syntropy_agent : include_vars] **************************************************************************
ok: [broker]
TASK [launch_syntropy_agent : debug] *********************************************************************************
ok: [broker] =>
msg: creating "mqt_2_broker"
TASK [launch_syntropy_agent : Launch Syntropy Agent] *****************************************************************
[DEPRECATION WARNING]: The container_default_behavior option will change its default value from "compatibility" to
"no_defaults" in community.general 3.0.0. To remove this warning, please specify an explicit value for it now. This
feature will be removed from community.general in version 3.0.0. Deprecation warnings can be disabled by setting
deprecation_warnings=False in ansible.cfg.
ok: [broker]
TASK [launch_mosquitto : debug] **************************************************************************************
ok: [broker] =>
msg: Launching Mosquito
TASK [launch_mosquitto : Copy Mosquitto conf file] *******************************************************************
ok: [broker]
TASK [launch_mosquitto : Launch Mosquitto] ***************************************************************************
[DEPRECATION WARNING]: Please note that docker_container handles networks slightly different than docker CLI. If you
specify networks, the default network will still be attached as the first network. (You can specify purge_networks to
remove all networks not explicitly listed.) This behavior will change in community.general 2.0.0. You can change the
behavior now by setting the new `networks_cli_compatible` option to `yes`, and remove this warning by setting it to
`no`. This feature will be removed from community.general in version 2.0.0. Deprecation warnings can be disabled by
setting deprecation_warnings=False in ansible.cfg.
ok: [broker]
PLAY RECAP ***********************************************************************************************************
broker : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Deploy the Publisher.

ansible-playbook deploy_publisher.yaml -i syntropyhosts -vv

Deploy the subscriber.

ansible-playbook deploy_subscriber.yaml -i syntropyhosts -vv

Login to the Syntopy UI to confirm you see your endpoints.

MQT2 endpoints online

I’d also recommend SSHing into both your Publisher and Subscriber and following the logs ofnodejs-publisher and nodejs-subscriber containers so you can see when they connect and start sending/receiving messages.

sudo docker logs --follow <container_name>

and you’ll see output showing the Publisher|Subscriber has initialized.

Initializing Subscriber | Publisher

8. Create your network

Finally, all that’s left to do is to create your network and connect the endpoints. We’ll do this using, you guessed it, an Ansible playbook. Be before we do, though, let’s take a quick look at the create_syntropy_network role’s tasks in the main.yaml file.

Here we’re using the syntropy_network module using its Fully Qualified Collection Namespace (FQCN), ie. syntropynet.syntropy.syntropy_network . This module is part of the ansible-galaxy collection we downloaded earlier. You can see that we’re creating a p2m network, ie. a Point-to-Multipoint Protocol network. We do this because we only need to make the broker-subscriber and broker-publisher connections, as all communication on our MQTT network takes place via the Broker. Because we tagged our Syntropy Agents with the mqtt tag, we can now use that to identify them when deploying our network. We then tell our mqt_2_broker to connect_to our mqtt tag, ie. all our agents that have that tag.

It’s time to create your network.

ansible-playbook deploy_network.yaml -i syntropyhosts -vv

Open the Syntropy UI and check your network is connected.

Your Broker should be connected to the Publisher and Subscriber.

If you go back to your SSH sessions for the Publisher and Subscriber, you should see something like the following:

Publisher log output:

Initializing PublisherEstablished connection with Broker[sending] January 11th 2021, 10:53:05 pm[sending] January 11th 2021, 10:54:05 pm[sending] January 11th 2021, 10:55:05 pm

Subscriber log output:

Initializing SubscriberEstablished connection with Broker[subscribed] topic: hello_syntropy[subscribed] topic: init[received][hello_syntropy] Powered by **Syntropy Stack**: January 11th 2021, 10:53:05 pm[received][hello_syntropy] Powered by **Syntropy Stack**: January 11th 2021, 10:54:05 pm[received][hello_syntropy] Powered by **Syntropy Stack**: January 11th 2021, 10:55:05 pm
Photo by NeONBRAND on Unsplash

Congratulations, you’ve just used Ansible to create your very own secure, optimized network between cloud providers!

Some parting thoughts…

I really enjoyed putting together this guide as it gave me the opportunity to sink my teeth into Ansible. While it might seem like overkill for this particular example, I feel it serves the purpose of illustrating just how simple it can be to use. Once your playbooks are written, it only takes a matter of minutes to provision your servers, deploy your applications, and create your Syntropy Network. Just think how easy it would be for us to scale up to 10, 100, even a 1000 nodes. That, in my (play)book, is pretty damn cool.

’til next time!

--

--

Craig

I’m a Creative Technologist, based in Brooklyn NY. My interests span hardware, software, and cloud computing. Find me at https://craigpickard.net