How to connect to AWS IoT Core with MQTT.js
Implementing a basic IoT client application
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.