Backstory
For the last couple of weeks, I've been playing with AWS Greengrass for work. At first, I did some manual setups, but eventually, I needed to automate the deployments and subscription setups. After diving into the documentation and a few chats with AWS Support, I've managed to do a proper CloudFormation stack and do a deployment.
In this post, I'll try to give a worthy walkthrough if you ever want to play with AWS Greengrass. If this is your first time hearing Greengrass, I'll kindly point out the official documentation, here, which obviously does a pretty good job of covering what it is.
Prerequisite
As usual, we would require some setup before we start writing some code. Some of these steps are pretty usual, such as an AWS Account. However, I'll try to give you some instruction about others, as they are unique to this tutorial.
- An AWS account, which you can open easily at aws.amazon.com.
- Install and configure AWS CLI on your computer.
- Install NodeJS. Version 12.X will suffice.
- Install Serverless framework. See serverless.com.
- Install Docker.
After step 5, we need to fetch the Docker image of Greengrass as we will run the core on our laptop as if it's an IoT device.
Let's pull the Docker image of Greengrass. First, we need to log in to the AWS IoT Greengrass registry in Amazon ECR. If this runs correctly, we'll see Login Succeeded
output in our terminal.
$ aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin https://216483018798.dkr.ecr.us-west-2.amazonaws.com
Then, we can pull the Docker image.
$ docker pull 216483018798.dkr.ecr.us-west-2.amazonaws.com/aws-iot-greengrass:latest
Once it's finished, we can move on to the code. Don't worry, we'll get back to this Docker image eventually, but first, we need to get some credentials and do some work.
Coding
Now, before we start working on all the connections, we need a simple serverless function to send a message to a Greengrass topic. So, let's start building it.
Lambda Function
OK. To keep things simple, we are once again going to use the Serverless framework.
$ mkdir greengrassLambda && cd $_
$ serverless create --template aws-nodejs
Let's also init npm and install AWS Greengrass SDK.
$ npm init -y
$ npm install aws-greengrass-core-sdk --save
We are going to update our serverless.yml
file as well.
service: greengrasslambda
frameworkVersion: "2"
provider:
name: aws
runtime: nodejs12.x
stage: dev
region: eu-west-1
functions:
counter:
handler: handler.handler
Now, we can write our function in handler.js
which is going to be a simple counter that will push messages to our choice of a topic, sample/counter
.
'use-strict';
const ggSdk = require('aws-greengrass-core-sdk');
const iotClient = new ggSdk.IotData();
const util = require('util');
let counter = 0;
module.exports.handler = (event, context, callback) => {
counter++;
try {
iotClient.publish({
topic: 'sample/counter',
payload: JSON.stringify({
message: util.format('Sent from Greengrass Core. Invocation Count: %s', counter)
}),
queueFullPolicy: 'AllOrError',
}, () => {
callback(null, true);
});
} catch (error) {
console.log(error);
}
return;
};
Alright. Looks like this part is done, so let's deploy this and start our CloudFormation template for our AWS Greengrass group.
$ sls deploy
AWS Greengrass Group
This part is a bit tricky. There is an example CloudFormation template online for Greengrass, but, to understand what we need to create, you might need to play a bit on AWS Console. I did many manual setups, breaking stuff and more to understand the needs here. I'll try to give you as much insight as possible and break the CloudFormation template into pieces before giving you the whole file.
Also, there is a step which needed to be done manually here. That is, creating a certificate. As far as I can tell, CloudFormation currently doesn't support this. There is an API to handle this but, we'll do this part manually for now.
First, we'll go to the IoT Core service on AWS Console. Navigate to the Certificates menu and click create.
Then, progress as recommended and create the certificates. After it's done, download all three files, activate the certificates and click done. We'll attach the policy using CloudFormation later in the post.
Now we can start writing our CloudFormation template. Let's keep it in the same repository as our lambda function, so, we should create a templates
folder and in it, a file named greengrass.json
. We'll take three parameters. LambdaName
, LambdaVersion
, and CertificateHash
. Since Greengrass doesn't support $LATEST
tag, this will make our job a lot easier. For CertificateHash
, we'll take the value as cert/xxxxx...
.
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "AWS IoT Greengrass template for RustyNeuron tutorial.",
"Parameters": {
"LambdaName": {
"Type": "String"
},
"LambdaVersion": {
"Type": "String"
},
"CertificateHash": {
"Type": "String"
}
},
"Resources": {},
"Outputs": {}
}
Above is the basis of our template. Let's start filling up our Resources
. Now, in order this to work, we need several resources in order, as listed below.
1. AWS::IoT::Thing
2. AWS::Greengrass::CoreDefinition
3. AWS::Greengrass::CoreDefinitionVersion
4. AWS::Greengrass::FunctionDefinition
5. AWS::Greengrass::FunctionDefinitionVersion
6. AWS::Greengrass::LoggerDefinition
7. AWS::Greengrass::LoggerDefinitionVersion
8. AWS::Greengrass::SubscriptionDefinition
9. AWS::Greengrass::SubscriptionDefinitionVersion
10. AWS::Greengrass::Group
11. AWS::IoT::Policy
12. AWS::IoT::PolicyPrincipalAttachment
13. AWS::IoT::ThingPrincipalAttachment
Let's start with the top three. We'll name our thing, Neuron
.
{
...
"Resources": {
// COPY FROM HERE
"NeuronCore": {
"Type": "AWS::IoT::Thing",
"Properties": {
"ThingName": "NeuronCore"
}
},
"NeuronCoreDefinition": {
"Type": "AWS::Greengrass::CoreDefinition",
"Properties": {
"Name": "NeuronCoreDefinition"
}
},
"NeuronCoreDefinitionVersion": {
"Type": "AWS::Greengrass::CoreDefinitionVersion",
"Properties": {
"CoreDefinitionId": {
"Ref": "NeuronCoreDefinition"
},
"Cores": [
{
"Id": "NeuronCore",
"CertificateArn": {
"Fn::Join": [
":",
[
"arn:aws:iot",
{
"Ref": "AWS::Region"
},
{
"Ref": "AWS::AccountId"
},
{
"Ref": "CertificateHash"
}
]
]
},
"ThingArn": {
"Fn::Join": [
":",
[
"arn:aws:iot",
{
"Ref": "AWS::Region"
},
{
"Ref": "AWS::AccountId"
},
"thing/NeuronCore"
]
]
}
}
]
}
},
// TO HERE
},
...
}
OK. This looks promising. Let's add the Function
definitions. Here, we need to be careful about a couple of things. First, IsolationMode
must be NoContainer
as Greengrass is running on our computer. Second, in the FunctionConfiguration
, we mark the Pinned
value as false
. This is because we designed our lambda function to run on-demand.
{
...
"Resources": {
...
// COPY FROM HERE
"NeuronFunctionDefinition": {
"Type": "AWS::Greengrass::FunctionDefinition",
"Properties": {
"Name": "NeuronFunctionDefinition"
}
},
"NeuronFunctionDefinitionVersion": {
"Type": "AWS::Greengrass::FunctionDefinitionVersion",
"Properties": {
"FunctionDefinitionId": {
"Fn::GetAtt": [
"NeuronFunctionDefinition",
"Id"
]
},
"DefaultConfig": {
"Execution": {
"IsolationMode": "NoContainer"
}
},
"Functions": [
{
"Id": "NeuronLambda",
"FunctionArn": {
"Fn::Join": [
":",
[
"arn:aws:lambda",
{
"Ref": "AWS::Region"
},
{
"Ref": "AWS::AccountId"
},
"function",
{
"Ref": "LambdaName"
},
{
"Ref": "LambdaVersion"
}
]
]
},
"FunctionConfiguration": {
"Pinned": false,
"Timeout": 5,
"EncodingType": "json",
"Environment": {
"Execution": {
"IsolationMode": "NoContainer",
"RunAs": {
"Uid": "1",
"Gid": "10"
}
}
}
}
}
]
}
},
// TO HERE
},
...
}
For Logger
definitions, we need two types of loggers. One for CloudWatch and one for the local logs. If you have a problem of seeing logs on CloudWatch, we might need to alter the Greengrass service policy to allow logging. We'll get to that at the end of our article.
{
...
"Resources": {
...
// COPY FROM HERE
"NeuronLoggerDefinition": {
"Type": "AWS::Greengrass::LoggerDefinition",
"Properties": {
"Name": "NeuronLoggerDefinition"
}
},
"NeuronLoggerDefinitionVersion": {
"Type": "AWS::Greengrass::LoggerDefinitionVersion",
"Properties": {
"LoggerDefinitionId": {
"Ref": "NeuronLoggerDefinition"
},
"Loggers": [
{
"Id": "NeuronLoggerCWSystem",
"Type": "AWSCloudWatch",
"Component": "GreengrassSystem",
"Level": "INFO"
},
{
"Id": "NeuronLoggerCWLambda",
"Type": "AWSCloudWatch",
"Component": "Lambda",
"Level": "INFO"
},
{
"Id": "NeuronLoggerLocalSystem",
"Type": "FileSystem",
"Component": "GreengrassSystem",
"Level": "INFO",
"Space": 25600
},
{
"Id": "NeuronLoggerLocalLambda",
"Type": "FileSystem",
"Component": "Lambda",
"Level": "INFO",
"Space": 25600
}
]
}
},
// TO HERE
}
...
}
Next, we have Subscriptions
. Here, we need two subscriptions.
- From lambda to IoT cloud. This is to listen to the messages that are being published by our lambda function.
- From IoT cloud to our lambda. This is to easily trigger our lambda function, using the MQTT client on IoT Console.
{
...
"Resources": {
...
// COPY FROM HERE
"NeuronSubscriptionDefinition": {
"Type": "AWS::Greengrass::SubscriptionDefinition",
"Properties": {
"Name": "NeuronSubscriptionDefinition"
}
},
"NeuronSubscriptionDefinitionVersion": {
"Type": "AWS::Greengrass::SubscriptionDefinitionVersion",
"Properties": {
"SubscriptionDefinitionId": {
"Ref": "NeuronSubscriptionDefinition"
},
"Subscriptions": [
{
"Id": "Subscription1",
"Source": {
"Fn::Join": [
":",
[
"arn:aws:lambda",
{
"Ref": "AWS::Region"
},
{
"Ref": "AWS::AccountId"
},
"function",
{
"Ref": "LambdaName"
},
{
"Ref": "LambdaVersion"
}
]
]
},
"Subject": "sample/counter",
"Target": "cloud"
},
{
"Id": "Subscription2",
"Source": "cloud",
"Subject": "sample/trigger",
"Target": {
"Fn::Join": [
":",
[
"arn:aws:lambda",
{
"Ref": "AWS::Region"
},
{
"Ref": "AWS::AccountId"
},
"function",
{
"Ref": "LambdaName"
},
{
"Ref": "LambdaVersion"
}
]
]
}
}
]
}
},
// TO HERE
}
...
}
We are very close. Now that every component definition is done, we can write the group's definition and link the components we wrote.
{
...
"Resources": {
...
// COPY FROM HERE
"NeuronGroup": {
"Type": "AWS::Greengrass::Group",
"Properties": {
"Name": "NeuronGroup",
"RoleArn": {
"Fn::Join": [
":",
[
"arn:aws:iam:",
{
"Ref": "AWS::AccountId"
},
"role/service-role/Greengrass_ServiceRole"
]
]
},
"InitialVersion": {
"CoreDefinitionVersionArn": {
"Ref": "NeuronCoreDefinitionVersion"
},
"FunctionDefinitionVersionArn": {
"Ref": "NeuronFunctionDefinitionVersion"
},
"SubscriptionDefinitionVersionArn": {
"Ref": "NeuronSubscriptionDefinitionVersion"
},
"LoggerDefinitionVersionArn": {
"Ref": "NeuronLoggerDefinitionVersion"
}
}
}
},
// TO HERE
}
...
}
All good. Let's add the policy definitions now.
{
...
"Resources": {
...
// COPY FROM HERE
"NeuronPolicy": {
"Type": "AWS::IoT::Policy",
"Properties": {
"PolicyName": "NeuronPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iot:Publish",
"iot:Subscribe",
"iot:Connect",
"iot:Receive"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"iot:GetThingShadow",
"iot:UpdateThingShadow",
"iot:DeleteThingShadow"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"greengrass:*"
],
"Resource": [
"*"
]
}
]
}
}
},
"NeuronPolicyAttachmet": {
"Type": "AWS::IoT::PolicyPrincipalAttachment",
"Properties": {
"PolicyName": {
"Ref": "NeuronPolicy"
},
"Principal": {
"Fn::Join": [
":",
[
"arn:aws:iot",
{
"Ref": "AWS::Region"
},
{
"Ref": "AWS::AccountId"
},
{
"Ref": "CertificateHash"
}
]
]
}
},
"DependsOn": "NeuronPolicy"
},
"NeuronBaseCertificateAttachment": {
"Type": "AWS::IoT::ThingPrincipalAttachment",
"Properties": {
"ThingName": "NeuronCore",
"Principal": {
"Fn::Join": [
":",
[
"arn:aws:iot",
{
"Ref": "AWS::Region"
},
{
"Ref": "AWS::AccountId"
},
{
"Ref": "CertificateHash"
}
]
]
}
},
"DependsOn": "NeuronCore"
}
// TO HERE
}
...
}
At this point, our Resources are done. However, we need one more piece to make it right. We need the command to make a new deployment of our group. To do this, we are going to define an output.
{
...
"Resources": {
...
},
"Outputs": {
// COPY FROM HERE
"CommandToDeployGroup": {
"Value": {
"Fn::Join": [
" ",
[
"groupVersion=$(cut -d'/' -f6 <<<",
{
"Fn::GetAtt": [
"NeuronGroup",
"LatestVersionArn"
]
},
");",
"aws --region",
{
"Ref": "AWS::Region"
},
"greengrass create-deployment --group-id",
{
"Ref": "NeuronGroup"
},
"--deployment-type NewDeployment --group-version-id",
"$groupVersion"
]
]
}
}
// TO HERE
}
}
Alright. Whew... Let's apply this, shall we? First, we validate the template we created.
$ aws cloudformation validate-template --template-body file://templates/greengrass.json --region eu-west-1
It should give us the following output.
{
"Parameters": [
{
"ParameterKey": "LambdaName",
"NoEcho": false
},
{
"ParameterKey": "CertificateHash",
"NoEcho": false
},
{
"ParameterKey": "LambdaVersion",
"NoEcho": false
}
],
"Description": "AWS IoT Greengrass template for RustyNeuron tutorial."
}
(END)
Right now, we are ready to create our stack on AWS. So, let's go. This will take a while to complete, but hopefully, it will succeed. Make sure to replace the xxxxx...
part with your certificate hash.
$ aws cloudformation create-stack --stack-name RustyNeuronTutorial \
--template-body file://templates/greengrass.json --region eu-west-1 \
--parameters ParameterKey=LambdaName,ParameterValue=greengrasslambda-dev-counter \
ParameterKey=LambdaVersion,ParameterValue=1 \
ParameterKey=CertificateHash,ParameterValue=cert/xxxxx...
Running IoT Core on Docker
Do you remember that we downloaded some keys and certificates when we started this tutorial? Right, we need them now. To keep things in one place, let's create a greengrass
folder in the home root, the ~
. However, this is up to you. Just remember to change the paths when we run docker
.
$ cd ~
$ mkdir -p Development/greengrass && cd $_
$ mkdir certs
$ mkdir config
Now, we'll copy the three files we got when we created the certificate in the certs
folder. Additionally, we need the root.ca.pem
file, which is distributed by AWS. To get that, the following command must be run in the certs
folder.
$ cd certs # /Users/$USER/Development/greengrass/certs
$ sudo wget -O root.ca.pem https://www.amazontrust.com/repository/AmazonRootCA1.pem
Now, we'll write our config.json
file. This file, if you create your Greengrass group, will be included in the certificate tar file. However, it's not present if you create your certificate stand-alone.
In the config
folder, we create a config.json
file with the following content.
{
"coreThing": {
"caPath": "root.ca.pem",
"certPath": "xxxxxxxxxx-certificate.pem.crt",
"keyPath": "xxxxxxxxxx-private.pem.key",
"thingArn": "arn:aws:iot:eu-west-1:AWS_ACCOUNT_ID:thing/NeuronCore",
"iotHost": "xxxxxxxxxxxxxx-ats.iot.eu-west-1.amazonaws.com",
"ggHost": "greengrass-ats.iot.eu-west-1.amazonaws.com",
"keepAlive": 600
},
"runtime": {
"cgroup": {
"useSystemd": "yes"
}
},
"managedRespawn": false,
"crypto": {
"principals": {
"SecretsManager": {
"privateKeyPath": "file:///greengrass/certs/xxxxxxxxxx-private.pem.key"
},
"IoTCertificate": {
"privateKeyPath": "file:///greengrass/certs/xxxxxxxxxx-private.pem.key",
"certificatePath": "file:///greengrass/certs/xxxxxxxxxx-certificate.pem.crt"
}
},
"caPath": "file:///greengrass/certs/root.ca.pem"
}
}
You can get your AWS_ACCOUNT_ID
by running aws sts get-caller-identity
in your terminal. The masked parts of certPath
, keyPath
, privateKeyPath
, and certificatePath
are the hash of the files we downloaded. You can find the iotHost
in the thing's Interract
menu as shown in the image below.
In the end, your folder structure should look like this.
$ tree .
.
├── certs
│ ├── xxxxxxxxxx-certificate.pem.crt
│ ├── xxxxxxxxxx-private.pem.key
│ ├── xxxxxxxxxx-public.pem.key
│ └── root.ca.pem
└── config
└── config.json
2 directories, 5 files
We seem to be ready. Let's start our IoT core using Docker. Make sure to change the $USER
in the command.
$ docker run --rm --init -it --name aws-iot-greengrass \
--entrypoint /greengrass-entrypoint.sh \
-v /Users/$USER/Development/greengrass/certs:/greengrass/certs \
-v /Users/$USER/Development/greengrass/config:/greengrass/config \
-p 8883:8883 \
216483018798.dkr.ecr.us-west-2.amazonaws.com/aws-iot-greengrass:latest
We should get something like this.
Greengrass successfully started with PID: 13
Once we see that our container is running, we can make a group deployment and test it.
AWS Greengrass Group Deployment
We have two options here. One, we can go to the IoT console and click deploy or we can use AWS CLI. Remember that we defined an Output
in our CloudFormation template. Let's get that command.
Navigate to the AWS CloudFormation and select RustyNeuronTutorial
stack. Go to the Outputs
tab.
$ groupVersion=$(cut -d'/' -f6 <<< arn:aws:greengrass:eu-west-1:AWS_ACCOUNT_ID:/greengrass/groups/GREENGRASS_GROUP_ID/versions/GREENGRASS_GROUP_VERSION_ID ); \
aws --region eu-west-1 greengrass create-deployment \
--group-id GREENGRASS_GROUP_ID \
--deployment-type NewDeployment \
--group-version-id $groupVersion
This should be done in no time and we should see the successful deployment on IoT Console.
Testing
Alright. We are in the home stretch. Let's test this whole setup. Go to the Test
page in the IoT Console. Then, subscribe to sample/counter
topic to listen to our lambda function.
And now, publish to sample/trigger
topic. Remember, we set two subscriptions for both ways. We should successfully get the invocation count message published by our lambda function.
Logging
Logs will stream to two places. CloudWatch and local file system. We configured our group for both, but probably you won't be able to see the logs on CloudWatch. So, let's see the logs in the local file system first.
First, we should connect to our Docker container.
$ docker exec -it aws-iot-greengrass /bin/bash
Logs are kept under /greengrass/ggc/var/log/
as two parts: system
and user
. To follow the user
logs, you can run the following command in your container.
$ tail -f /greengrass/ggc/var/log/user/eu-west-1/AWS_ACCOUNT_ID/LAMBDA_NAME.log
When it comes to CloudWatch logs, we need to give the Greengrass_ServiceRole
access to CloudWatch. You could give full access or if you want more granular access, you can attach the following policy to the service role.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:PutMetricFilter",
"logs:PutRetentionPolicy",
"logs:DescribeLogStreams"
],
"Resource": [
"arn:aws:logs:*:*:*"
]
}
]
}
Go to IAM
console and find the Greengrass_ServiceRole
. Then, in the Permissions
tab, click the Attach policies
. Here, you can either select CloudWatchLogsFullAccess
or click Create policy
and paste the above summary. It's up to you. I'll choose CloudWatchLogsFullAccess
. When you trigger your function, like in the Testing
section, you'll be able to see the log groups with the /aws/greengrass/
prefix in CloudWatch.
Deleting
To remove the CloudFormation stack properly, first, you need to reset the deployments of your group. You can do that with the following command.
$ aws greengrass reset-deployments --group-id GREENGRASS_GROUP_ID --region eu-west-1 --force
Then, to remove the CloudFormation stack, you'd need the following command.
$ aws cloudformation delete-stack --stack-name RustyNeuronTutorial --region eu-west-1
Source Code and Troubleshooting
The whole thing, except for the certificates and configs, is in this repository; aoozdemir/greengrass-example.
If you encounter any error or problem, I suggest you go to the AWS Documentation. If you find any errata or any suggestion to do things more efficiently, please contact me.
See you in another post!
Top comments (0)