How to connect to AWS IoT Core with MQTT.js

Implementing a basic IoT client application

Author's image
Tamás Sallai
3 mins

MQTT client application

In the previous article we discussed the backend side for the resources we need to configure on AWS IoT Core to allow a device to connect via MQTT. These include the thing, the certificate, the appropriate policy, and it outputs the data needed by the client.

In this article, we'll implement a basic NodeJs application that uses MQTT.js to publish and subscribe to MQTT topics. This can then form the basis of an IoT-driven solution built on AWS.

Setting up the environment

The Terraform stack we built previously outputs a couple of values that the client app will need:

ca = <<EOT
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
...
-----END CERTIFICATE-----

EOT
cert = <<EOT
-----BEGIN CERTIFICATE-----
MIICuTCCAaGgAwIBAgIRAJxkojqslH8aV89AbIM2qI8wDQYJKoZIhvcNAQELBQAw
...
-----END CERTIFICATE-----

EOT
iot_endpoint = "a192wytv5ywpbg-ats.iot.eu-central-1.amazonaws.com"
key = <<EOT
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAzy4j8/DSDF/DFbeT3LzqzwsRt0J4+2N4HLyF4uBD4GGk+DIE
...
-----END RSA PRIVATE KEY-----

EOT
thing_name = "thing_658f63be58a9b9d4"

To move these to the NodeJs app, extract them to environment variables:

CA=$(terraform output -raw ca) \
	IOT_ENDPOINT=$(terraform output -raw iot_endpoint) \
	CERT=$(terraform output -raw cert) \
	KEY=$(terraform output -raw key) \
	THING_NAME=$(terraform output -raw thing_name) \
	node index.js

Then inside the app extract them from the process.env:

const {IOT_ENDPOINT, CA, CERT, KEY, THING_NAME} = process.env;

Installing dependencies

As NodeJs does not provide built-in support to connect to MQTT endpoints, we'll need a library to do that. I could find only a single project for this: MQTT.js. There are others that implement MQTT over WebSocket, but that's a different scheme and it works a bit differently than standard MQTT in AWS.

To install MQTT.js:

npm i mqtt

Connection

The MQTT connection needs a couple of arguments:

const opt = {
	host: IOT_ENDPOINT,
	protocol: "mqtt",
	clientId: THING_NAME,
	clean: true,
	key: KEY,
	cert: CERT,
	ca: CA,
	reconnectPeriod: 0,
};

const client  = connect(opt);

The host defines the URL to connect, and it is the IoT endpoint for the AWS account (or more precisely, a region in the account). The protocol is mqtt as we want direct MQTT connection and not one over WebSockets.

Moving forward, the clientId is the thing name, as required by AWS. Then the key and the cert are for the client authentication, as AWS trusts this certificate. Finally, the ca is AWS's root certificate so that the client can verify the endpoint.

Then add an event listener for the connection:

client.on("connect", () => {
	// ...
})

Publishing and subscribing

After the connection is established, the client can publish values to topics:

client.publish(
	`$aws/things/${THING_NAME}/shadow/name/test/update`,
	JSON.stringify({state: {reported: {value: data}}})
);

The first argument is the topic. On the backend-side we implemented a "walled garden" permission model that restricts all devices to topics under $aws/things/<thing name>/, so the client needs to specify a topic with that prefix. In this case, this is the test shadow.

The second argument is the payload. Shadows define a strict structure the clients need to follow with a state property with a reported or desired field. In this example, the client updates the reported state of the test shadow with {value: data}.

Subscribing to changes

Subscribing works similarly, as it also needs a topic:

client.subscribe(`$aws/things/${THING_NAME}/shadow/name/test/update/documents`, (err) => {
	// handle subscribe error or success
});

The topic for a shadow is defined in the shadow documentation. The /shadow/name/test/update/documents listens for changes to the test shadow.

Receiving the events is done via the connection object:

client.on("message", (topic, message) => {
	console.log("[Message received]: " + JSON.stringify(JSON.parse(message.toString()).current.state, undefined, 2));
});

Whenever the shadow is updated, this function will be called.

Conclusion

The above implementation gives a bare-bones but complete functionality for a device running NodeJs to connect to AWS IoT Core. It connects securely using mutual TLS authentication, can publish to topics, and can receive data in real-time.

January 10, 2023
In this article