Playing with AWS Greengrass
Tags: serverless, aws, tutorial
Published: 2020-10-22 17:48:51

#### 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](https://docs.aws.amazon.com/greengrass/latest/developerguide/what-is-gg.html), 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.

1. An AWS account, which you can open easily at [aws.amazon.com](https://aws.amazon.com).
2. Install and configure AWS CLI on your computer.
3. Install NodeJS. Version 12.X will suffice.
4. Install Serverless framework. See [serverless.com](https://serverless.com).
5. 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.

```bash
$ 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.

```bash
$ 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.

```bash
$ mkdir greengrassLambda && cd $_
$ serverless create --template aws-nodejs
```

Let's also init npm and install AWS Greengrass SDK.

```bash
$ npm init -y
$ npm install aws-greengrass-core-sdk --save
```

We are going to update our `serverless.yml` file as well.

```yml
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`.

```js
'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.

```bash
$ 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.

![AWS IoT Core Certificates.](https://rustyneuron.net/assets/greengrass-tutorial/iot-certificate-create.png)

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.

![AWS IoT Core Certificate Create One Click.](https://rustyneuron.net/assets/greengrass-tutorial/iot-certificate-create-one-click.png)

![AWS IoT Core Certificate Create Done.](https://rustyneuron.net/assets/greengrass-tutorial/iot-certificate-create-done.png)

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...`.

```json
{
  "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.

```json
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`.

```json
{
  ...
  "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**.

```json
{
  ...
  "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.

```json
{
  ...
  "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.

1. From lambda to IoT cloud. This is to listen to the messages that are being published by our lambda function.
2. From IoT cloud to our lambda. This is to easily trigger our lambda function, using the MQTT client on IoT Console.

```json
{
  ...
  "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.

```json
{
  ...
  "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.

```json
{
  ...
  "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.

```json
{
  ...
  "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.

```bash
$ aws cloudformation validate-template --template-body file://templates/greengrass.json --region eu-west-1
```

It should give us the following output.

```bash
{
    "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.

```bash
$ 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...
```

![AWS IoT Stack Complete.](https://rustyneuron.net/assets/greengrass-tutorial/iot-stack-complete.png)

---

##### 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`.

```bash
$ 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.

```bash
$ 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.

```json
{
  "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.

![AWS IoT Host.](https://rustyneuron.net/assets/greengrass-tutorial/iot-host.png)

In the end, your folder structure should look like this.

```bash
$ 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.

```bash
$ 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.

```bash
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.

![AWS IoT Command.](https://rustyneuron.net/assets/greengrass-tutorial/iot-deploy-command.png)

```bash
$ 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.

![AWS IoT Deployment Done.](https://rustyneuron.net/assets/greengrass-tutorial/iot-deploy-success.png)

---

#### 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.

![AWS IoT Test.](https://rustyneuron.net/assets/greengrass-tutorial/iot-test.png)

![AWS IoT Test Sub.](https://rustyneuron.net/assets/greengrass-tutorial/iot-test-subscribe.png)

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.

![AWS IoT Test Pub.](https://rustyneuron.net/assets/greengrass-tutorial/iot-test-publish.png)

---

#### 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.

```bash
$ 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.

```bash
$ 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.

```json
{
  "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.

```bash
$ 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.

```bash
$ 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](https://github.com/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!