AWS Compute Blog
Implementing a Serverless AWS IoT Backend with AWS Lambda and Amazon DynamoDB
Ed Lima
Cloud Support Engineer
Does your IoT device fleet scale to hundreds or thousands of devices? Do you find it somewhat challenging to retrieve the details for multiple devices? AWS IoT provides a platform to connect those devices and build a scalable solution for your Internet of Things workloads.
Out of the box, the AWS IoT console gives you your own searchable device registry with access to the device state and information about device shadows. You can enhance and customize the service using AWS Lambda and Amazon DynamoDB to build a serverless backend with a customizable device database that can be used to store useful information about the devices as well as helping to track what devices are activated with an activation code, if required.
You can use DynamoDB to extend the AWS IoT internal device registry to help manage the device fleet, as well as storing specific additional data about each device. Lambda provides the link between AWS IoT and DynamoDB allowing you to add, update, and query your new device database backend.
In this post, you learn how to use AWS IoT rules to trigger specific device registration logic using Lamba in order to populate a DynamoDB table. You then use a second Lambda function to search the database for a specific device serial number and a randomly generated activation code to activate the device and register the email of the device owner in the same table. After you’re done, you’ll have a fully functional serverless IoT backend, allowing you to focus on your own IoT solution and logic instead of managing the infrastructure to do so.
Prerequisites
You must have the following before you can create and deploy this framework:
- An AWS account
- An IAM user with permissions to create AWS resources (AWS IoT things and rules, Lambda functions, DynamoDB tables, IAM policies and roles, etc.)
- JS and the AWS SDK for JavaScript installed locally to test the deployment
Building a backend
In this post, I assume that you have some basic knowledge about the services involved. If not, you can review the documentation:
- Creating Lambda functions
- Creating DynamoDB tables
- Overview of AWS IoT
- Configuring AWS IoT rules to trigger newly-created Lambda functions
For this use case, imagine that you have a fleet of devices called “myThing”. These devices can be anything: a smart lightbulb, smart hub, Internet-connected robot, music player, smart thermostat, or anything with specific sensors that can be managed using AWS IoT.
When you create a myThing device, there is some specific information that you want to be available in your database, namely:
- Client ID
- Serial number
- Activation code
- Activation status
- Device name
- Device type
- Owner email
- AWS IoT endpoint
The following is a sample payload with details of a single myThing device to be sent to a specific MQTT topic, which triggers an IoT rule. The data is in a format that AWS IoT can understand, good old JSON. For example:
{
"clientId": "ID-91B2F06B3F05",
"serialNumber": "SN-D7F3C8947867",
"activationCode": "AC-9BE75CD0F1543D44C9AB",
"activated": "false",
"device": "myThing1",
"type": "MySmartIoTDevice",
"email": "not@registered.yet",
"endpoint": "<endpoint prefix>.iot.<region>.amazonaws.com"
}
The rule then invokes the first Lambda function, which you create now. Open the Lambda console, choose Create a Lambda function , and follow the steps. Here’s the code:
console.log('Loading function');
var AWS = require('aws-sdk');
var dynamo = new AWS.DynamoDB.DocumentClient();
var table = "iotCatalog";
exports.handler = function(event, context) {
//console.log('Received event:', JSON.stringify(event, null, 2));
var params = {
TableName:table,
Item:{
"serialNumber": event.serialNumber,
"clientId": event.clientId,
"device": event.device,
"endpoint": event.endpoint,
"type": event.type,
"certificateId": event.certificateId,
"activationCode": event.activationCode,
"activated": event.activated,
"email": event.email
}
};
console.log("Adding a new IoT device...");
dynamo.put(params, function(err, data) {
if (err) {
console.error("Unable to add device. Error JSON:", JSON.stringify(err, null, 2));
context.fail();
} else {
console.log("Added device:", JSON.stringify(data, null, 2));
context.succeed();
}
});
}
The function adds an item to a DynamoDB database called iotCatalog based on events like the JSON data provided earlier. You now need to create the database as well as making sure the Lambda function has permissions to add items to the DynamoDB table, by configuring it with the appropriate execution role.
Open the DynamoDB console, choose Create table and follow the steps. For this table, use the following details.
The serial number uniquely identifies your device; if, for instance, it is a smart hub that has different client devices connecting to it, use the client ID as the sort key.
The backend is good to go! You just need to make the new resources work together; for that, you configure an IoT rule to do so.
On the AWS IoT console, choose Create a resource and Create a rule , and use the following settings to point the rule to your newly-created Lambda function, also called iotCatalog.
After creating the rule, AWS IoT adds permissions on the background to allow it to trigger the Lambda function whenever a message is published to the MQTT topic called registration. You can use the following Node.js deployment code to test:
var AWS = require('aws-sdk');
AWS.config.region = 'ap-northeast-1';
var crypto = require('crypto');
var endpoint = "<endpoint prefix>.iot.<region>.amazonaws.com";
var iot = new AWS.Iot();
var iotdata = new AWS.IotData({endpoint: endpoint});
var topic = "registration";
var type = "MySmartIoTDevice"
//Create 50 AWS IoT Things
for(var i = 1; i < 51; i++) {
var serialNumber = "SN-"+crypto.randomBytes(Math.ceil(12/2)).toString('hex').slice(0,15).toUpperCase();
var clientId = "ID-"+crypto.randomBytes(Math.ceil(12/2)).toString('hex').slice(0,12).toUpperCase();
var activationCode = "AC-"+crypto.randomBytes(Math.ceil(20/2)).toString('hex').slice(0,20).toUpperCase();
var thing = "myThing"+i.toString();
var thingParams = {
thingName: thing
};
iot.createThing(thingParams).on('success', function(response) {
//Thing Created!
}).on('error', function(response) {
console.log(response);
}).send();
//Publish JSON to Registration Topic
var registrationData = '{\n \"serialNumber\": \"'+serialNumber+'\",\n \"clientId\": \"'+clientId+'\",\n \"device\": \"'+thing+'\",\n \"endpoint\": \"'+endpoint+'\",\n\"type\": \"'+type+'\",\n \"activationCode\": \"'+activationCode+'\",\n \"activated\": \"false\",\n \"email\": \"not@registered.yet\" \n}';
var registrationParams = {
topic: topic,
payload: registrationData,
qos: 0
};
iotdata.publish(registrationParams, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
// else Published Successfully!
});
setTimeout(function(){},50);
}
//Checking all devices were created
iot.listThings().on('success', function(response) {
var things = response.data.things;
var myThings = [];
for(var i = 0; i < things.length; i++) {
if (things[i].thingName.includes("myThing")){
myThings[i]=things[i].thingName;
}
}
if (myThings.length = 50){
console.log("myThing1 to 50 created and registered!");
}
}).on('error', function(response) {
console.log(response);
}).send();
console.log("Registration data on the way to Lambda and DynamoDB");
The code above creates 50 IoT things in AWS IoT and generate random client IDs, serial numbers, and activation codes for each device. It then publishes the device data as a JSON payload to the IoT topic accordingly, which in turn triggers the Lambda function:
And here it is! The function was triggered successfully by your IoT rule and created your database of IoT devices with all the custom information you need. You can query the database to find your things and any other details related to them.
In the AWS IoT console, the newly-created things are also available in the thing registry.
Now you can create certificates, policies, attach them to each “myThing” AWS IoT Thing then install each certificate as you provision the physical devices.
Activation and registration logic
However, you’re not done yet…. What if you want to activate a device in the field with the pre-generated activation code as well as register the email details of whoever activated the device?
You need a second Lambda function for that, with the same execution role from the first function (Basic with DynamoDB). Here’s the code:
console.log('Loading function');
var AWS = require('aws-sdk');
var dynamo = new AWS.DynamoDB.DocumentClient();
var table = "iotCatalog";
exports.handler = function(event, context) {
//console.log('Received event:', JSON.stringify(event, null, 2));
var params = {
TableName:table,
Key:{
"serialNumber": event.serialNumber,
"clientId": event.clientId,
}
};
console.log("Gettings IoT device details...");
dynamo.get(params, function(err, data) {
if (err) {
console.error("Unable to get device details. Error JSON:", JSON.stringify(err, null, 2));
context.fail();
} else {
console.log("Device data:", JSON.stringify(data, null, 2));
console.log(data.Item.activationCode);
if (data.Item.activationCode == event.activationCode){
console.log("Valid Activation Code! Proceed to register owner e-mail and update activation status");
var params = {
TableName:table,
Key:{
"serialNumber": event.serialNumber,
"clientId": event.clientId,
},
UpdateExpression: "set email = :val1, activated = :val2",
ExpressionAttributeValues:{
":val1": event.email,
":val2": "true"
},
ReturnValues:"UPDATED\_NEW"
};
dynamo.update(params, function(err, data) {
if (err) {
console.error("Unable to update item. Error JSON:", JSON.stringify(err, null, 2));
context.fail();
} else {
console.log("Device now active!", JSON.stringify(data, null, 2));
context.succeed("Device now active! Your e-mail is now registered as device owner, thank you for activating your Smart IoT Device!");
}
});
} else {
context.fail("Activation Code Invalid");
}
}
});
}
The function needs just a small subset of the data used earlier:
{
"clientId": "ID-91B2F06B3F05",
"serialNumber": "SN-D7F3C8947867",
"activationCode": "AC-9BE75CD0F1543D44C9AB",
"email": "verified@registered.iot"
}
Lambda uses the hash and range keys (serialNumber and clientId) to query the database and compare the database current pre-generated activation code to a code that is supplied by the device owner along with their email address. If the activation code matches the one from the database, the activation status and email details are updated in DynamoDB accordingly. If not, the user gets an error message stating that the code is invalid.
You can turn it into an API with Amazon API Gateway. In order to do so, go to the Lambda function and add an API endpoint, as follows.
Now test the access to the newly-created API endpoint, using a tool such as Postman.
If an invalid code is provided, the requester gets an error message accordingly.
Back in the database, you can confirm the record was updated as required.
Cleanup
After you finish the tutorial, delete all the newly created resources (IoT things, Lambda functions, and DynamoDB table). Alternatively, you can keep the Lambda function code for future reference, as you won’t incur charges unless the functions are invoked.
Conclusion
As you can see, by leveraging the power of the AWS IoT Rules Engine, you can take advantage of the seamless integration with AWS Lambda to create a flexible and scalable IoT backend powered by Amazon DynamoDB that can be used to manage your growing Internet of Things fleet.
You can also configure an activation API to make use of the newly-created backend and activate devices as well as register email contact details from the device owner; this information could be used to get in touch with your users regarding marketing campaigns or newsletters about new products or new versions of your IoT products.
If you have questions or suggestions, please comment below.