AWS Greengrass on Raspberry Pi: Creating Core and Node Devices
This post will guide you through setting up a simulation of two independent devices communicating on the same Raspberry Pi. This can easily be extended for multiple Pi’s in a practical IoT network scenario.
Intro
Optimizing computing and data processing on the edge is something we think about a lot at TensorIoT, and we enjoy leveraging AWS Greengrass to innovate in these applications.
What is Greengrass? From AWS materials:
AWS Greengrass is software that lets you run local compute, messaging, data caching, sync, and ML inference capabilities for connected devices in a secure way. With AWS Greengrass, connected devices can run AWS Lambda functions, keep device data in sync, and communicate with other devices securely — even when not connected to the Internet.
So to reiterate, Greengrass helps abstract the implementation so you can focus on getting the devices to work together and start tackling the interesting computation needs of your specific data/use-case.
The two IoT devices we’ll set up in our Greengrass group are:
- A Greengrass Core device, which communicates with the AWS cloud, and takes messages from other devices on a local network.
- A Sensor device that generates readings and sends the data to the Core. It never communicates with the AWS cloud directly.
This is going to diverge from the online tutorials to rely on a scripting workflow
Setup
Note: this article was written using Greengrass 1.1.0. Very recently, 1.3.0 was released. In the documentation links you can toggle instructions between different versions.
- Set up AWS CLI to be authenticated correctly from your dev machine (In my case, a Mac. But this admin device could potentially be a bootstrapping server):
http://docs.aws.amazon.com/cli/latest/userguide/installing.html
testaws —-version
and test auth with something like:aws lambda list-functions
- Install the python AWS library (Faster to set up our stuff than using bash AWS CLI or UI workflow)
pip install boto3
- Follow this tutorial page to prep the Pi: http://docs.aws.amazon.com/greengrass/latest/developerguide/module1.html
Set up Devices in AWS IoT
Use the AWS console to create two “IoT Things”:
- group1_core
This will be the core device, and we’ll later create a group called group1 to correspond to it - pi1
This is the non-core device. For my test, this is also running on the same Raspberry Pi, and it communicates over localhost to the core process.
Download certs for these devices, as well as the root-CA.crt. Later we’ll transfer them to the pi.
Activate the certs.
Here’s a policy setup that should work for both devices:
Install the Greengrass-Core SDK on the Core device:
- Download the Greengrass SDK from the AWS IoT Console in the Software section
Note: My Pi was ARMv7l
2. scp downloads/greengrass-linux-armv7l-1.1.0.tar.gz me@pi-ipaddress:~/
3. ssh into Pi. sudo tar -zxvf greengrass-linux-armv7l-1.1.0.tar.gz -C /
Now you should be able to cd /greengrass
4. Enable cgroups for lambda sudo vim /etc/fstab
Add this line: cgroup /sys/fs/cgroup cgroup defaults 0 0
5. The certs for the core need to be transferred to /greengrass/certs
including the root-CA.crt
6. Create a new role in AWS IAM:
- For Role Type, choose AWS Greengrass Role
- Select AWSGreengrassResourceAccessRolePolicy
On the dev machine, add the ARN for the IAM role with this command
aws greengrass associate-service-role-to-account --role-arn arn:aws:iam::123451234510:role/GreengrassRole
7. sudo vim /greengrass/config/config.json
The file looks like this:
{
"coreThing": {
"caPath": "root-CA.crt",
"certPath": "2*******1-certificate.pem.crt",
"keyPath": "2*******1-private.pem.key",
"thingArn": "[THING_ARN_HERE]",
"iotHost": "[HOST_PREFIX_HERE].iot.[us-west-2 or other REGION].amazonaws.com",
"ggHost": "greengrass.iot.[AWS_REGION_HERE].amazonaws.com",
},
"runtime": {
"cgroup": {
"useSystemd": "yes"
}
}
}
Notes:
- The caPath, certPath is looking for the /greengrass/certs directory, so just put the file name
- Get your ARN for the core device in IoT > Manage >
- Get the custom IoT endpoint here at IoT > Settings:
8. Start the greengrass core process: sudo /greengrass/ggc/core/greengrassd start
Troubleshoot those steps at http://docs.aws.amazon.com/greengrass/latest/developerguide/gg-gs.html
Install Greengrass Core SDK module for Python:
1. Download the “Python Version 1.0.0” SDK. Scroll further down the download page from before:
2. transfer file to pi. For me this was like: scp downloads/greengrass-core-python-sdk-1.0.0.tar.gz me@pi-ip-address:~
3. ssh into Pi
4. tar –zxf greengrass-core-python-sdk-1.0.0.tar.gz
the unzipped folder should be called “aws_greengrass_core_sdk”
5. Unzip this nested file: unzip aws_greengrass_core_sdk/sdk/python_sdk_1_0_0.zip
6. Copy greengrasssdk directory into greengrass sdk files:
sudo cp –r aws_greengrass_core_sdk/sdk/python_sdk_1_0_0/greengrasssdk /greengrass/ggc/packages/1.1.0/runtime/python2.7
Now you should see greengrasssdk alongside greengrass_common, etc in that folder.
Set up Greengrass Group
We’ll use a Python script to make the set up faster. Set up / run this on the dev machine (Mac)
All these following script pieces will be in one script that runs at the end.
API docs: http://boto3.readthedocs.io/en/latest/reference/services/greengrass.html
Create a script called create_group1.py :
import boto3
import time
gg = boto3.client('greengrass')
# Setup device definitions:
core_def = gg.create_core_definition(
Name='group1_core',
InitialVersion={
'Cores': [{
'CertificateArn': 'see instructions below',
'Id': '1', # use an arbitrary unique ID
'SyncShadow': True,
'ThingArn': 'use group1 core ARN again'
}]
}
)
To get the certificate ARN, click on a specific device > Security, and click on the certificate. The next page will have the ARN:
Continue in the script:
device_arn = 'get pi1 device ARN from console'
device_def = gg.create_device_definition(
Name='pi1',
InitialVersion={
'Devices': [{
'CertificateArn': 'get device certificate arn from console like above',
'Id': '1', # an arbitrary unique ID
'SyncShadow': True,
'ThingArn': device_arn
}]
}
)
Create Lambda that will run on the Core device
On your dev machine create a lambda script corelambda.py with the code below.
This simple script just forwards data that triggers the lambda to AWS IoT as ‘topic_1’
import greengrasssdk # SDK that exists in the core, not in Cloud
import json
def handler(event, context):
client = greengrasssdk.client('iot-data')
client.publish(
topic='group1_core/topic_1',
qos=0,
payload=json.dumps(event))
Zip this file.
Go to UI and Create a lambda. You can use any arbitrary permission since it’s going to run locally. This one seems to
be just basic logging perms for a Lambda function:
- For “Runtime”, choose python2.7 — only python3 can run in greengrass at the moment.
- For “Code entry type”, choose “Upload a .ZIP file”, upload the corelambda.py script
- For Handler, enter the handler method from our script: “corelambda.handler”
Go to Actions > Publish New Version, then Publish.
You’ll use the resulting ARN below, it should look like: arn:aws:lambda:us-west-2:123451234510:function:messageLambda:1
Continue on create_group1.py:
# Create function definition:
fun_arn = ‘arn from above’
fun_def = gg.create_function_definition(
Name='corelambda1',
InitialVersion={
'Functions': [{
'Id': '1', # arbitrary unique id
'FunctionArn': fun_arn,
'FunctionConfiguration': {
'Executable': 'corelambda.handler',
'MemorySize': 128000,
'Timeout': 3}
}]
}
)
Set up subscriptions
In subscription versions, you outline the topics that each node is allowed to listen to or publish to. This includes what is allowed to be published to the cloud.
The syntax is “Publish from Source to Target”
# Create Subscription
sub_def = gg.create_subscription_definition(
Name='group1_subs',
InitialVersion={
'Subscriptions': [{
'Id': '1',
'Source': device_arn,
'Subject': 'pi1/topic_1',
'Target': fun_arn
}, {
'Id': '2',
'Source': fun_arn,
'Subject': 'group1_core/topic_1',
'Target': 'cloud'
}]
)
1. The first subscription sets our local core lambda to take messages from ‘pi1/topic_1’ from the sensor device.
(i.e., ‘pi1/topic_2’ would not trigger the lambda, unless we set up another lambda for that).
2. The second subscription allows the core to publish “group1_core/topic_1” to the cloud.
Deploy Greengrass setup to the core device
So far the Lambda scripts, subscriptions, configs only live in the AWS console. We need to actually deploy them to the core device. Continue on create_group1.py:
# Create Local Loggers
log_def = gg.create_logger_definition(
Name='local_loggers',
InitialVersion={
'Loggers': [{
'Id': '1',
'Component': 'GreengrassSystem',
'Level': 'INFO',
'Space': 5120,
'Type': 'FileSystem',
}, {
'Id': '2',
'Component': 'Lambda',
'Level': 'DEBUG',
'Space': 5120,
'Type': 'FileSystem',
}]
}
)# Create group
group_def = gg.create_group(
Name='group1',
InitialVersion={
'CoreDefinitionVersionArn': core_def['LatestVersionArn'],
'DeviceDefinitionVersionArn': device_def['LatestVersionArn'],
'FunctionDefinitionVersionArn': fun_def['LatestVersionArn'],
'LoggerDefinitionVersionArn': log_def['LatestVersionArn'],
'SubscriptionDefinitionVersionArn': sub_def['LatestVersionArn']
}
)print('New group id:', group_def['Id'])# Create connection information. TODO: How do nodes get to this when they're not on the internet?
gg.update_connectivity_info(
ThingName='group1_core',
ConnectivityInfo=[{
'HostAddress': '127.0.0.1', # core is listening on localhost
'Id': '1', # Arbitrary id we'll use later
'Metadata': 'localhost', # arbitrary description
'PortNumber': 8883
}]
)# Create deployment
deployment = gg.create_deployment(
DeploymentType='NewDeployment',
GroupId=group_def['Id'],
GroupVersionId=group_def['LatestVersion']
)
print('Finishing final step in 10 seconds')
import pprint
pp = pprint.PrettyPrinter()
time.sleep(10.5)
pp.pprint(gg.get_deployment_status(
DeploymentId=deployment['DeploymentId'],
GroupId=group_def['Id']
))
Finally, run the script. You might want to run it in the python interpreter so some of the variables holding Ids/Arns
can persist for troubleshooting and writing update scripts.
python
>>> exec(open(‘create_group1.py’).read())
Start the non-core device
We’re going to set up a device process that publishes a message called “pi1/topic_1” every 30 seconds.
- ssh into pi:
mkdir -p ~/iot_thing/certs
Transfer certs to that directory, including the root-CA.crt - Install the python IoT SDK:
pip install AWSIoTPythonSDK
- Create a script called sensor.py with the below code. You’ll need to update the path, cert-names, and ARNs at the top
import json
import logging
import random
import time
import uuid
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
from AWSIoTPythonSDK.core.greengrass.discovery.providers import DiscoveryInfoProviderHOME_PATH = ‘/home/me/iot_thing/certs/’
ca = HOME_PATH + ‘root-CA.crt’
crt = HOME_PATH + ‘a123123123-certificate.pem.crt’
key = HOME_PATH + ‘a123123123-private.pem.key’
IOT_ENDPOINT = ‘a1231231231231.iot.us-west-2.amazonaws.com’
CORE_ARN = ‘arn:aws:iot:us-west-2:123123123123:thing/group1_core’# optional logging actions
logger = logging.getLogger(‘AwsIoTPythonSDK.core’)
logger.setLevel(logging.DEBUG)
streamHandler = logging.StreamHandler()
formatter = logging.Formatter(‘%(asctime)s — %(name)s — %(levelname)s — %(message)s’)
streamHandler.setFormatter(formatter)
logger.addHandler(streamHandler)# Discover the core
diProvider = DiscoveryInfoProvider()
diProvider.configureEndpoint(IOT_ENDPOINT)
diProvider.configureCredentials(ca, crt, key)
diProvider.configureTimeout(10)discoveryInfo = diProvider.discover(‘pi1’)
infoObj = discoveryInfo.toObjectAtGroupLevel()
groupId = list(infoObj.keys())[0] # Just getting the first group from our list of groups
group = infoObj[groupId]
core = group.getCoreConnectivityInfo(CORE_ARN)
connectivityInfo = core.getConnectivityInfo(‘1’) # The arbitrary Id we put on our connection# Get groupCA, need that instead of the generic ca
caList = discoveryInfo.getAllCas()
_, ca_crt = caList[0]
group_ca_path = ‘%s%s_CA_%s.crt’ % (HOME_PATH, groupId, str(uuid.uuid4()))
with open(group_ca_path, ‘w’) as group_ca_file:
group_ca_file.write(ca_crt)
client = AWSIoTMQTTClient('pi1')
client.configureCredentials(group_ca_path, key, crt)
client.configureEndpoint(connectivityInfo.host, connectivityInfo.port)
client.connect()
counter = 0
while counter < 10
client.publish('pi1/topic_1', json.dumps({
'test_data': random.randint(0, 100),
'reading_id': counter
}), 0)
counter += 1
time.sleep(30) if counter < 9 else client.disconnect()
In this script we knew there was only one core we wanted to connect to, but there’s also a way to discover an unknown core which is interesting. Docs: https://github.com/aws/aws-iot-device-sdk-python#discoveryinfoprovider
4. Head to AWS console IoT > Test
Subscribe to “group1_core/topic_1” which is what we expect to receive from the core Lambda
5. Run the sensor.py script, you should see the json data is being forwarded by the greengrass core lambda and reaching
the console:
Troubleshoot the script errors in logs:
- /greengrass/ggc/var/log/user/
You’ll see something like us-west-2/lambda-id-number/pi1_topic1.py - /greengrass/ggc/var/log/system/runtime.log
/greengrass/ggc/var/log/system/python_runtime.log
Summary
We created thing definitions in AWS IoT: a Greengrass core and a sensor device. Then we set them up in a Greengrass group on the Raspberry Pi. We set up a Lambda function locally in the core process to handle messages received from the sensor. Finally, in the IoT console we proved that the Greengrass core was communicating with the cloud.
In future articles, we’ll introduce more nuanced workflows for the devices, and some more architecture to enhance the edge computing ability of the Greengrass core.