AWS IoT Greengrass ハンズオン

## はじめに
今回はAWS IoTで何ができるのかを学習するにあたり、以下のことを実施しました。自身の記録用として残していますが、どなたかの学習のヒントになれば幸いです。
* AWS IoT Greengrassって何？を理解するために、いくつかのサイトでイメージをつかむ。
* 実際に手を動かして、つかんだイメージを少し具体的に理解する。

## 参考サイト
「はじめに」の2点を実施する際に、以下のサイトをメインに参考にしました。どちらも非常にわかりやすかったです。
* [EurekaBox コラム AWS Greengrass](!https://www.eureka-box.com/media/tag/AWSGreengrass) 
→3つコラムをすべて読むと1時間くらいかかりますが、とても読みやすく整理されています。
* [AWS IoT Greengrass V2 入門ハンズオン](!https://catalog.us-east-1.prod.workshops.aws/workshops/5ecc2416-f956-4273-b729-d0d30556013f/ja-JP)
→基本的にはこちらのハンズオンにしたがって実施。

## やってみる
コラムを読んだ後にこちらのハンズオンを実施したので、Greenglassの使い方がより具体的にイメージできました。今回のハンズオンで構成する内容は以下の図の内容になります。「Greengrass client software」がすべて単一のCloud9上に構築されるので、エッジデバイスとIoTデバイスを意識しながら進めていくのがポイントかなと思いました。（EurekaBox コラムを読んでイメージが持ててたのでよかった！）
なお、今回の記事では細かいステップ（Cloud9で設定した部分など）は割愛して、より覚えておきたい部分をメインに記載してます。ですのでハンズオンを実際に実施する場合はぜひ公式URLを参照してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/d1449d17-a8ab-4fba-f6de-e9ecb91af6f6.png)

### 環境の用意
まずはハンズオン用の環境を用意します。上述の通り今回はCloud9を利用していきますが、Cloud9セットアップ用のCloudFormationテンプレートは[公式ハンズオンの中で用意されている](!https://catalog.us-east-1.prod.workshops.aws/workshops/5ecc2416-f956-4273-b729-d0d30556013f/ja-JP/chapter2-letsbegin/10-step1)ので、こちらを利用します。
テンプレートの実行結果として以下のようなリソースが作成されました！こちらのCloud9を利用していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/ec39330b-44d7-6e02-a6a2-cfeea72c31e9.png)

次にGreengrassのServiceRoleを作成していきます。Cloud9ターミナルからロールを作成します。
```terminal
aws iam create-role --role-name Greengrass_ServiceRole --assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
   {
      "Effect": "Allow",
      "Principal": {
      "Service": "greengrass.amazonaws.com"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
      "ArnLike": {
         "aws:SourceArn": "arn:aws:greengrass:[region]:[account-id]:*"
      },
      "StringEquals": {
         "aws:SourceAccount": "[account-id]"
      }
      }
   }
]
}'
```
作成したロールに`AWSGreengrassResourceAccessRolePolicy`ポリシーをアタッチします。
```terminal
aws iam attach-role-policy --role-name Greengrass_ServiceRole --policy-arn arn:aws:iam::aws:policy/service-role/AWSGreengrassResourceAccessRolePolicy
```
さらに以下のコマンドでロールとアカウントを結び付けます、ここでは`aws greengrassv2`コマンドを用いています。
```terminal
aws greengrassv2 associate-service-role-to-account --role-arn [role-arn] --region [region]
```
AWS IoTコンソールからロールとアカウントが紐づけられたことが確認できます。ひとまず環境セットアップはここまでです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/a0d64a6a-2f8a-8b27-0fee-a909c08f5413.png)

### Greengrassのセットアップ
次にGreengrass自体のセットアップを実施していきます。以下の図のように、まずはAWSコンソールでAWS IoT Coreの初期設定を行い、次にCloud9からGreengrassV2をインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/1fdef8df-9cf2-c9fa-37d0-9ab9ba8a9ff7.png)

まずはコンソールからCore Devicesの設定を行っていきます。デフォルト値で以下のように進めていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/91fa8b3e-3f0e-ad63-9889-7d9ecb278d66.png)

Step3の画面でCloud9（コアデバイス）で実施するGreengrassのインストーラー実行コマンドが表示されるので、コピーしてCloud9で実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/917750f0-8e06-d776-4bd2-4d7e02d8715a.png)

インストール完了まで待ちます。以下のようになればOK。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/3b846045-0f52-4487-9eff-d6f65b9add54.png)

AWSコンソール画面に戻って、コアデバイスを確認すると追加したデバイスが表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/46be08c2-5c35-810b-6015-7b5644bdf7d5.png)
つまり、GreengrassのコアデバイスとAWS IoT Core（コンソール）の接続が完了したことになります。ちなみに上記インストーラーの実行により、Things、ポリシー、デバイス証明書などもプロビジョニングされています。

次に再度IAMのセットアップを実施します。ここでは以降のハンズオンで必要な以下のアクションを許可するポリシーを作成します。
* カスタムコンポーネントのパブリッシュ (S3 を使用)
* MQTT ブリッジ
以下が作成するポリシー。ポリシー名は`workshop_s3_iot_gg_policy `。
```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "S3BucketActions",
      "Effect": "Allow",
      "Action": [
        "s3:CreateBucket",
        "s3:ListAllMyBuckets",
        "s3:GetBucketLocation",
        "s3:PutObject",
        "s3:GetObject"
      ],
      "Resource": ["arn:aws:s3:::*"]
    },
    {
      "Effect": "Allow",
      "Action": ["iot:*"],
      "Resource": "*"
    },
    {
      "Sid": "GreengrassActions",
      "Effect": "Allow",
      "Action": ["greengrass:*"],
      "Resource": "*"
    }
  ]
}
```
上記ポリシーを既存の`GreengrassV2TokenExchangeRole`にアタッチします。

### コンポーネントの作成
次に、コンポーネントと呼ばれるモジュール化されたアプリケーションを開発し、デプロイしていきます。このアプリケーション部分はGreengrass Core（今回はCloud9）で実行されるロジックである理解です。今回はCloud9でコンポーネントを作成していますが、この部分はおそらくLambdaとかでも書けるはずです。以下の図のLambda Functionの部分を今回はCloud9で直接開発するイメージでしょうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/4f5bd974-8007-dcad-3e34-8b1665fa9064.png)

開発するコンポーネント（アプリロジック）は以下です。詳細な開発手順（フォルダ作成とか）は公式ハンズオンを参照してください。
```python:hello_world.py
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0
import sys
import datetime
import time

while True:

    message = f"Hello, {sys.argv[1]}! Current time: {str(datetime.datetime.now())}."

    # Print the message to stdout.
    print(message)

    # Append the message to the log file.
    with open('/tmp/Greengrass_HelloWorld.log', 'a') as f:
        print(message, file=f)

    time.sleep(1)


```
なお、同じCloud9上で開発しちゃっているので少し分かりづらいですが、この時点ではまだGreengrass Core（デバイス）にはこのコンポーネント（アプリ）はデプロイされていません。

デプロイするには**レシピ**と呼ばれる`YAML`もしくは`json`で書かれたファイルが必要です。ハンズオンではjsonで記載しています。
```json:com.example.HelloWorld-1.0.0.json
{
  "RecipeFormatVersion": "2020-01-25",
  "ComponentName": "com.example.HelloWorld",
  "ComponentVersion": "1.0.0",
  "ComponentDescription": "My first AWS IoT Greengrass component.",
  "ComponentPublisher": "Amazon",
  "ComponentConfiguration": {
    "DefaultConfiguration": {
      "Message": "world"
    }
  },
  "Manifests": [
    {
      "Platform": {
        "os": "linux"
      },
      "Lifecycle": {
        "Run": "python3 -u {artifacts:path}/hello_world.py '{configuration:/Message}'\n"
      }
    }
  ]
}
```
いよいよデプロイです。以下のコマンドでデプロイします。
```terminal
sudo /greengrass/v2/bin/greengrass-cli deployment create \
  --recipeDir ~/environment/GreengrassCore/recipes \
  --artifactDir ~/environment/GreengrassCore/artifacts \
  --merge "com.example.HelloWorld=1.0.0"
```
デプロイ後に以下のようなコマンドでログが見れたらデプロイに成功しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/42151c77-f3d9-c789-a233-9fd99c9915a7.png)

さて、ここまででGreengrass Coreのデバイスにデプロイは完了していますが、実際には他に複数のデバイスが存在しており、同じロジックを適用したいケースもあるはずです。そのような場合はこの作成したコンポーネントをS3にアップロードして、AWSコンソールからデプロイします。この部分の手順については[公式のハンズオンの他のデバイスにデプロイするためにコンポーネントを AWS にパブリッシュする](!https://catalog.us-east-1.prod.workshops.aws/workshops/5ecc2416-f956-4273-b729-d0d30556013f/ja-JP/chapter4-createfirstcomp/30-step3)を参照してください。
手順を正常に完了すると、以下のようにコンソールからコンポーネント一覧で確認することができるようになり、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/4497b6f5-6a56-3115-2aa0-59b3bab7d949.png)
指定するデプロイ先にコンポーネントをデプロイできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/2aa50221-ce54-dc31-87b9-2f28fc37e323.png)

### メッセージのPub/Subの実装
次に各コンポーネント間でメッセージ交換を行うためのPub/Subの実装をしていきます。以下の図がイメージつきやすいでしょうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/c8390d72-6689-15a5-d759-a467a8a94f9b.png)

（※引用　Eureka Box（ユーリカボックス）【体験版】AWS Greengrass =連載第２回=
https://member.eureka-box.com/products/4/categories/4011892/posts/13450002）
>メッセージ交換は「Pub/Sub型のトピック通信モデル」で行われます。ご存知の方は多いかと思いますが、Pub/Sub型の通信は基本的に、データを送る人(Publisher)がトピックと呼ばれるメッセージの内容を送ると、そのトピックに興味を持っている人、トピックを購読したい人(Subscriber)にデータが配信されるという仕組みです。

ハンズオンでは以下を実装していきます。
* センサーデータを読み込んでトピックに発行し続けるコンポーネント(Publisher)
* そのトピックのログを購読してメッセージを受信するコンポーネント(Subscriber)

なお、このハンズオンでは上記のどちらもGreengrass Core（Cloud9上）にデプロイされます。実際のセンサーを積んだデバイスも、現状はCloud9が肩代わりしているようなイメージです。

**パブリッシャーコンポーネントの実装**
こちらの内容も詳細は[公式ハンズオン](!https://catalog.us-east-1.prod.workshops.aws/workshops/5ecc2416-f956-4273-b729-d0d30556013f/ja-JP/chapter5-pubsubipc/10-step1)を参照してください。大まかな手順は以下です。
1. センサーの動きをするダミーのスクリプト（`dummy_sensor.py`）を作成する。
1. メッセージをPublishするPublisherコンポーネント（`example_publisher.py`）を記述する。
1. デプロイするためのレシピファイル（`com.example.Publisher-1.0.0.json`）を作成する。
1. デプロイコマンドを実行してコンポーネントデプロイする。

**サブスクライバーコンポーネントの実装**
こちらの内容も詳細は[公式ハンズオン](!https://catalog.us-east-1.prod.workshops.aws/workshops/5ecc2416-f956-4273-b729-d0d30556013f/ja-JP/chapter5-pubsubipc/20-step2)を参照してください。大まかな手順は以下です。
1. メッセージをSubscribeするSubscriberコンポーネント（`example_subscriber.py`）を記述する。
1. デプロイするためのレシピファイル（`com.example.Subscriber-1.0.0.json`）を作成する。
1. デプロイコマンドを実行してコンポーネントデプロイする。

デプロイ後に以下のコマンドを実行して受信しているメッセージが表示され続ければ成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/c872b89a-84ca-5f79-d163-c2e2b5533617.png)

### Greengrass Coreデバイスを各IoTデバイスのゲートウェイとして機能させる実装
ここまでの実装では、GreengrassCodeデバイス（Cloud9）のみで完結するような構成ですが、実際は複数のデバイスからメッセージを受け取るな利用シーンは多いはずです。以降の手順ではMQTTブリッジを導入することにより、Greengrass Coreデバイスをゲートウェイとして利用するような構成を構築していきます。MQTTとはGreengrassで用いられる通信プロトコルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/5c73f125-0f47-a505-4226-822c16700454.png)

まずはMQTTを利用するために、MQTTブローカーをGreengrassCoreデバイスにデプロイします。コンソール画面から以下のコンポーネント（これらはAWSにより`パブリックコンポーネント`として用意されています）を選択します。
※☑がついていないコンポーネントは、以降のハンズオンでデプロイしたものです。この時点では無視してください。なお、デプロイ時の詳細手順は[公式ハンズオン](!https://catalog.us-east-1.prod.workshops.aws/workshops/5ecc2416-f956-4273-b729-d0d30556013f/ja-JP/chapter6-mqtt-broker/10-mqtt-broker-1#mqtt)を参照してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/fd388b92-4351-52f1-59ec-52d80f9dfb38.png)

次にGreengrassCoreにメッセージを送るデバイスを作成します。ただしこのハンズオンではやはりCloud9で模擬的にデバイスを作成しているので、この部分が若干わかりづらく感じるかもです。具体的には`Mosquitto`クライアントというものを利用して、デバイスのシュミレーションをしています。作業環境はCloud9です。
以下のように模擬クライアントを作成していきます。
```terminal
cd ~/environment
mkdir -p devices
cd devices
THING_NAME=MyClientDevice1
aws iot create-thing --thing-name $THING_NAME
CERTIFICATE_ARN=$(aws iot create-keys-and-certificate --private-key-out $THING_NAME.key --certificate-pem-out $THING_NAME.crt --query "certificateArn" --out text --set-as-active)
aws iot attach-thing-principal --thing-name $THING_NAME --principal $CERTIFICATE_ARN
```
次に模擬クライアント（Cloud9）からGreengrassCore（Cloud9）へMQTTを使った通信のテストを実施します。
`mosquitto`クライアントをインストールし、
```terminal
sudo apt install mosquitto-clients
```
ローカルブローカーサーバー証明書の CA 証明書を取得します。これは、クライアントがブローカーのエンドポイントを検証するために使用されます。
```terminal
cd ~/environment/devices
sudo cp /greengrass/v2/work/aws.greengrass.clientdevices.Auth/ca.pem .
sudo chmod 444 ca.pem
```
以下のコマンドでサブスクライブを開始し、
```terminal
cd ~/environment/devices
mosquitto_sub -h localhost -p 8883 -q 0 -t clients/+/hello/world --cafile ca.pem --cert MyOtherClientDevice1.crt --key MyOtherClientDevice1.key -i MyOtherClientDevice1
```
さらに以下のコマンドでメッセージを発行します。
```terminal
cd ~/environment/devices
mosquitto_pub -h localhost -p 8883 -q 0 -t clients/test/hello/world --cafile ca.pem --cert MyClientDevice1.crt --key MyClientDevice1.key -m "{\"message\": \""$2" @ "$(date +"%m-%d-%Y-%H-%M-%s")"\"}" -i MyClientDevice1
```
サブスクライバー側のターミナルで無事にメッセージ受信を確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/a670c4f8-26de-6684-b991-39b511044957.png)

この時点でGreengrassCore（ローカルブローカー）から AWS IoT Core にメッセージをリレーするよう Bridge コンポーネントを設定をしたため、AWSマネジメントコンソール にアクセスし`clients/#`をサブスクライブすることでテストすることができます。同じメッセージが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/68681b67-3e5f-e4f3-4658-df3e803e6ad2.png)

### Greengrass Coreに機械学習コンポーネントをデプロイ
さらにハンズオンではAWSパブリックに用意されている機械学習のコンポーネントもデプロイし、エッジデバイスで機械学習させ、その結果をAWSコンソールで確認するような手順も含まれています。この場合はAWSにより準備されているコンポーネントをデプロイしているだけなので簡単にできるので、ぜひ[公式ハンズオン](!https://catalog.us-east-1.prod.workshops.aws/workshops/5ecc2416-f956-4273-b729-d0d30556013f/ja-JP/chapter7-ml)を参照してやってみてください。画像分類の機械学習コンポーネントをデプロイしています。

### 

## つまづいたポイント
上記までのハンズオン内容の記載には含んでいませんが、オプションで[6.2 独自の MQTT ブローカー [Mosquitto] (Optional)](!https://catalog.us-east-1.prod.workshops.aws/workshops/5ecc2416-f956-4273-b729-d0d30556013f/ja-JP/chapter6-mqtt-broker/20-step1)を実施した際に、メッセージの発行がうまくできませんでした。継続調査をしたいと思います。同じところでハマった方などいましたら、記事などUP頂けると助かります！

## おわりに
今回はハンズオンに習って一通りGreengrassを使っていました。概念を理解した後に実際に手を動かしてみると、やはりより具体的な利用イメージがわきました。実際の利用ケースではコンポーネント実装をLambda側で開発したり、IoTデバイスを複数用意したりすると思います。そのような本番ワークロード経験を積んでいきたいです。

以上。