Greengrass(V1)のコアデバイスにLambdaをデプロイ・実行してみた

#はじめに
Greengrassの特徴の1つとして、エッジ環境でLambdaを実行できることがあげられます。
前回Greengrass(V1)をインストールしたJetson nanoに対して以下を実施したので、その結果をまとめます。

* Lambdaを作成してエッジ環境（以下、Jetson nano）にデプロイ
* Jetson nanoにデプロイしたLambdaの動作確認
 * IoT Coreに永続的にMQTTメッセージを送る
 * IoT CoreからのMQTTメッセージでLambdaを起動→IoT CoreへMQTTメッセージを送る

参考（前回）：[Greengrass(V1)をクイックスタートでインストールしてみる](https://qiita.com/kt-yt/items/e2db8520c207861f2f5f)
#1　Lambda関数の作成
Jetson nano上で実行するLambda関数を用意します。
今回は「AWS IoT Greengrass Core SDK for Python」を利用しました。
#1-1 greengrasssdk for Python のダウンロード
今回はgreengrass sdk for Pythonを利用しました。
利用にあたっては、公式ドキュメントの記載通り、デプロイするLambda関数のパッケージに含めるSDKの用意が必要です。
> Greengrass Lambda 関数で SDK を使用するには、AWS Lambda にアップロードする Lambda 関数デプロイパッケージに SDK を含めます。
出典：[SDKs 関数のGreengrass Lambda](https://docs.aws.amazon.com/ja_jp/greengrass/v1/developerguide/lambda-functions.html#lambda-sdks)

1.「AWS IoT Greengrass Core SDK for Python」のGitHubレポジトリにアクセスし、Code > Download ZIP の順にクリック
Github：[AWS IoT Greengrass Core SDK for Python](https://github.com/aws/aws-greengrass-core-sdk-python)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/0bfb98b9-10f8-9dcd-f021-0cd39e1a2785.png)
2.ダウンロードした「aws-greengrass-core-sdk-python-master.zip」を解凍する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/06126a0c-3972-dfd6-ed57-c5f8c1c5be93.png)
#1-2　Lambda関数の作成（ローカルでの作業）
今回は以下2点のプログラムを作成しました。

* test_function.py：一定時間ごとにメッセージを送り続ける
* test_function2.py：メッセージを受けた際に起動してメッセージを送る

1.ローカルで以下の通りそれぞれプログラムを作成した

```python:test_function.py
import greengrasssdk
import logging
import sys
from threading import Timer
import json

logger = logging.getLogger(__name__)
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)

client = greengrasssdk.client("iot-data")

def test_function_1():
    client.publish(
        topic="test/function1",
        payload=json.dumps(
            {
                "function": "test_function_1", 
                "message": "This is Test",
                "version":  "1.0"
            }
        )
    )

    Timer(5, test_function_1).start()

test_function_1()
```
```python:test_function2.py
import greengrasssdk
import logging
import sys
from threading import Timer
import json

logger = logging.getLogger(__name__)
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)

client = greengrasssdk.client("iot-data")

def test_function_2(event, context):
    client.publish(
        topic="test/result",
        payload=json.dumps(
            {
                "function": "test_function_2", 
                "message": "This is Test",
                "version":  "1.0"
            }
        )
    )
```
2.作成したプログラムと「greengrasssdk」フォルダをまとめて圧縮
※test_function.py、test_function2.py双方で同様に実施
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/00e6cef8-9ce6-1420-604c-1ef4b8f322ff.png)
#1-3　Lambda関数の作成（IoT Coreでの作業①）
1.AWS IoT Coreのマネジメントコンソールで 左のメニューから Greengrass > クラシック(V1) > グループ の順にクリック
2.一覧で作成したグループ名をクリックし、Lambdaをクリック
※既に存在する「Greengrass_HelloEorld_S2kUw」はGreengrassインストール時にデプロイされたLambda関数
　使わないようであれば、… > 関数の削除 から削除する（エッジ側はデプロイ時に削除が反映される）
　今回は削除したので、以降のスクショに上記関数は登場しません
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/8f4de928-837e-4c21-854a-955d6fcb2c36.png)
3.Lambdaの追加 > 新しいLambdaの作成 をクリック
※別タブでLambdaのマネジメントコンソールが開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/7de32ac4-8240-c38f-eb20-93e21cbf41b1.png)
#1-4　Lambda関数の作成（Lambdaでの作業）
ローカルで作成したプログラムをLambdaにアップロードし、Lambda関数を作成します。

1.Lambdaのマネジメントコンソールで以下の通り関数を作成する

* 一から作成
* 関数名：test_function　※任意の関数名
* ランタイム：Python 3.7

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/7c1f0633-2110-f1c1-5d37-cc70bed910ba.png)
2.関数が作成されたら、コードタブで アップロード元 > zipファイル の順にクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/ec7bb85e-effb-e8c5-44d6-011cb449aa75.png)
3.ファイル選択ウィンドウで先ほど作成したzipファイル（プログラムと「greengrasssdk」をまとめて圧縮したもの）を選択して「保存」をクリック
※これにより、以下の通り作成した関数とSDKをLambda関数に反映できる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/03b60d20-29f0-c067-4451-e349a0840fa6.png)
4.コードタブでランタイム設定の「編集」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/0e8e8a6a-6e48-03e3-9380-9b00818d024f.png)
5.ハンドラを「[プログラムファイル名].[関数名]」に修正して「保存」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/0a25bf48-dda8-8359-f9bd-fcef7381b11b.png)
参考：[プログラム初心者がAWS Lambda(Python)でハマった7個のこと](https://blog.serverworks.co.jp/tech/2016/08/31/lambda-point/)

6.関数画面で アクション > 新しいバージョンを発行 をクリックし、「発行」をクリックする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/35c8f50b-97da-f1ca-7825-6b15da366939.png)
7.アクション > エイリアスを作成 をクリック
8.以下の通りエイリアス設定を入力し、「保存」をクリックする

* 名前：test_function ※任意のエイリアス名
* バージョン：1

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/a7ae1035-982c-8718-5dc4-1bf326db765b.png)
※ここまでの作業をtest_function.py、test_function2.py共に実施し、Lambda関数を2つ作成した
#1-5　Lambda関数の作成（IoT Coreでの作業②）
Jetson nano上で実行するLambda関数が作成できたので、デプロイを行います。

1．IoT Coreマネジメントコンソールを開いていたタブに戻り、「既存のLambdaを使用」をクリック
2.作成したLambda関数を選択して「次へ」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/0d0a5d67-d82a-e3b5-5773-21795b9f1c0c.png)
3.作成したエイリアスを選択して「完了」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/1ae89437-a551-1230-a99a-8f121ef3a54e.png)
4.作成した2つのLamnda関数を追加すると、以下の通りGreengrassグループに表示される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/369373b9-e9fc-15f2-222d-5831e42c519f.png)
5.上記画面で ... > 設定の編集 の順にクリック
6.「Lambdaのライフサイクル」をそれぞれ以下の通りとし、「更新」をクリックする

* test_function：存続期間が長く無制限に稼働する関数にする
* test_function2：オンデマンド関数

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/afbec066-cfa5-e92c-6292-58a9aba9f21d.png)
#2　エッジ環境へのデプロイ
デプロイの前にグループにサブスクリプションを追加します。
今回は以下3点を追加しました。

* Jetson nano上のLambdaから定期的にIoT Coreへメッセージを送る(test_function)
* IoT CoreからJetson nano上のLambdaへメッセージを送る(test_function2)
* Jetson nano上のLambdaからイベント時にIoT Coreへメッセージを送る(test_function2)

##2-1　test_function1 -> IoT Core
1.対象のGreengrassグループ画面で サブスクリプション > 「サブスクリプションの追加」をクリックする
※以下画像に既に追加されているサブスクリプションはGreengrass Coreインストール時にあわせて設定されたものです
　不要なら ... > 削除 で削除します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/05daa903-f815-0746-8c4b-96cb1b8b4e8d.png)
2.ソースとターゲットの「選択」をクリック > それぞれ以下を選択 > 「次へ」をクリックする

* ソース：test_function　※作成したLambda関数
* ターゲット：IoT Cloud

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/cfc5be32-8610-2a07-e26e-7605347d275a.png)
3.トピックのフィルターに「test/function1」と入力して「次へ」をクリック
※ここで記載しているトピックは作成したLambda関数のtest_function_1で指定しているtopic
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/3f5747ab-2a4b-0d6b-098e-975b9663125a.png)
4.内容を確認して「完了」をクリックすると、以下の通りサブスクリプションがGreengrassグループに追加される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/ad509569-e11d-224c-e140-0170cc7a2d2d.png)
##2-2　IoT Core -> test_function2
1.先ほど同様に サブスクリプション > 「サブスクリプションの追加」をクリックする
2.ソースとターゲットの「選択」をクリック > それぞれ以下を選択 > 「次へ」をクリックする

* ソース：IoT Cloud
* ターゲット：test_function2　※作成したLambda関数

3.トピックのフィルターに「test/function2」と入力して「次へ」をクリック
※ここで記載しているトピックは作成したLambda関数のtest_function_2で指定しているもの
4.内容を確認して「完了」をクリックする
##2-3　test_function2 -> IoT Core
1.先ほど同様に サブスクリプション > 「サブスクリプションの追加」をクリックする
2.ソースとターゲットの「選択」をクリック > それぞれ以下を選択 > 「次へ」をクリックする

* ソース：test_function2　※作成したLambda関数
* ターゲット：IoT Cloud

3.トピックのフィルターに「test/result」と入力して「次へ」をクリック
※ここで記載しているトピックは作成したLambda関数のtest_function_2で指定しているtopic
4.内容を確認して「完了」をクリックする
※この段階の前にGreengrass_HelloWorld_S2kUw1 -> IoT Cloudは削除したためスクショには載っていない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/0a778ca8-be45-60d7-9f20-c59e05927343.png)
##2-4　エッジ環境（Jetson nano）へのデプロイ
ここまでグループに追加してきたLambda関数、サブスクリプションをエッジ環境にGreengrassグループとしてデプロイします。

1.Jetson nano上でGreengrassが起動していることを確認する
※起動していないとデプロイできない

```
#Greengrassが起動しているか確認
ps aux | grep -E "greengrass.*daemon"

#起動していない場合は以下のコマンドで起動
sudo /greengrass/ggc/core/greengrassd start
```
2.IoT Coreのマネジメントコンソールで 左のメニューから Greengrass > クラシック(V1) > グループ の順にクリック
3.一覧で作成したグループ名をクリックして詳細画面に遷移し、アクション > デプロイの順にクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/b8956655-ee68-fda3-7407-6bff9cda6d21.png)
4.「正常に完了しました」と表示されることを確認する
#3　動作検証
Jetson nanoにデプロイしたLambda関数が起動していることを確認します。
##3-1　継続的なMQTTメッセージの確認（test_function1）
test_function_1 は5秒ごとに test/function1 にMQTTでメッセージを送ります。
IoT Coreでサブスクライブ出来ることを以下で確認します。

1.IoT Coreのマネジメントコンソールで 左メニューからテスト をクリック
2.「トピックのフィルター」に「test/function1」と入力して「サブスクライブ」をクリックする
※グループのサブスクリプションでtest_function -> IoT Cloudeに対して設定したtopic
3.以下の通り、Lambda関数で指定したjson形式のメッセージが5秒おきに届くことを確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/e3870d17-f831-8be1-ff71-5eb68f80697c.png)
##3-2　MQTTメッセージをトリガにJetson nano上のLambdaが起動することの確認（test_function2）
1.同じくIoT Coreのマネジメントコンソールで 左メニューからテスト をクリック
2.「トピックのフィルター」に「test/result」と入力して「サブスクライブ」をクリックする
※グループのサブスクリプションでIoT Cloud -> test_function2に対して設定したtopic
3.「トピックに公開する」タブで「トピックのフィルター」に「test/function2」と入力して「サブスクライブ」をクリックする
※グループのサブスクリプションでtest_function2 -> IoT Cloudに対して設定したtopic
4.以下の通り、Lambda関数で指定したjson形式のメッセージが届くことを確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/113f18b7-7986-60eb-7921-444751b1fc72.png)
#4　エッジ環境のLambdaの更新
作成した関数をAWS上で修正してJetson nano上に改めてデプロイします。
今回はtest_function_2で確認しました。
##4-1 関数の修正
1.Lambdaのマネジメントコンソールに移動し、関数 > test_function2 の順にクリック
2.今回は通知するメッセージのみ以下の通り修正する

```python:test_function2.py
#変更後抜粋
#messageを"This is Test"から変更、versionを"1.0"から変更
payload=json.dumps(
    {
        "function": "test_function_2", 
        "message": "This is Update Test", 
        "version":  "2.0"
    }
)
```
3.「Deploy」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/ed22db93-de48-543a-3830-7640fbec1ae7.png)
4.アクション > 新しいバージョンを発行 > 発行 の順にクリック
5.test_function2 に戻り、エイリアスタブでエイリアスを選択して「編集」をクリックする
※「1-5　Lambda関数の作成（IoT Coreでの作業②）」で作成したエイリアス
　Greengrassグループはこのエイリアスに紐づいたバージョンをデプロイしている
※なおGreengrassは「$LATEST」バージョンのエイリアスはサポートしていない
　参考：[エイリアスまたはバージョンによる Lambda 関数のリファレンス](https://docs.aws.amazon.com/ja_jp/greengrass/v1/developerguide/lambda-functions.html)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/f535ac13-d99f-600f-0794-e7f552b9e30b.png)
6.先ほど発行したバージョンを選択して「保存」をクリックする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/8053b095-8386-37df-8706-1dcc2df54024.png)
7.IoT Coreのマネジメントコンソールに移動し、左のメニューから Greengrass > クラシック(V1) > グループ > 作成したグループ名 の順にクリック
8.アクション > デプロイ の順にクリックし、「正常に完了しました」と表示されることを確認する
##4-2　動作検証（3-2同様の手順）
1.IoT Coreのマネジメントコンソールで 左メニューからテスト をクリック
2.「トピックのフィルター」に「test/result」と入力して「サブスクライブ」をクリックする
3.「トピックに公開する」タブで「トピックのフィルター」に「test/function2」と入力して「サブスクライブ」をクリックする
4.以下の通り、4-1で更新したメッセージが届くことを確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/5ad4301d-e53e-2217-4180-9632e4bf82f6.png)
#5　トラブルシューティング（エラー調査・解消まで）
以上の手順は成功したあとに逆算で整理したものになります。
初回デプロイ後、Lambda関数からメッセージが届かなかったので、以下の通り調査や改修をしました。
こんな失敗がありうるということのサンプルとして掲載します。
##5-1　エラーの確認
以下のログを確認することで今回は以降の各種修正を行いました。

```
#ログが吐かれているディレクトリに移動
sudo cd /greengrass/ggc/var/log/user/[リージョン名]/[アカウントID]/

#ログの確認
sudo view [関数名].log

#IoT Coreからのメッセージを受けてLambdaが起動しているか確認しているときは以下を利用
sudo tail -f [関数名].log
```

##5-2　各エラー集
###①ハンドラの指定が誤っている
ハンドラをデフォルトのlambda_function.lambda_handlerとしていたら以下の通りエラーとなった
「1-4　Lambda関数の作成（Lambdaでの作業）」の手順5の通りハンドラを「[プログラムファイル名].[関数名]」にすることで解消した

```
#ログファイルより抜粋。
[2021-03-10T15:09:37.981+09:00][FATAL]-lambda_runtime.py:142,Failed to import handler function "lambda_function.lambda_handler" due to exception: No module named 'lambda_function'
[2021-03-10T15:09:37.981+09:00][FATAL]-lambda_runtime.py:382,Failed to initialize Lambda runtime due to exception: No module named 'lambda_function'
```
###②ライブラリのimportが抜けている/間違っている
ケアレスミスだが、以下の間違いがあった

* `imort greengrasssdk` とすべきところが `import greengrass` となっていた
* `import json` が抜けていた

test_function、test_function2共に上記を修正→再デプロイして解消した
###③ハンドラ修正後Lambdaのバージョン発行＋エイリアスの更新をしていなかった
コードを変更した際は「Deploy」が必要なのでバージョン発行＋エイリアスの更新をしていたが、ハンドラのみ更新した際は「Deploy」が不要だったのでこの手順を実施しなかった。
その結果、Greengrassグループを更新しても修正したハンドラが反映されておらず、①と同様のエラーが発生した
ハンドラの修正した後もバージョン発行＋エイリアスを更新してか再デプロイすることで解消した
###④test_function2からMQTTでメッセージを送るサブスクリプションの設定が漏れていた
上記①～③の対応後test_functionからは5秒おきにメッセージが来るものの、test_function2からのメッセージをIoT Coreでサブスクライブできなかった
ただし、tail -F で実行時のログを確認する限りではエラーも発生していなかった

```
#tail -f test_function2.log で確認しながらIoT Coreよりtest/function2にメッセージを送信
[2021-03-10T18:44:01.357+09:00][INFO]-lambda_runtime.py:149,Running [arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:test_function2:5]
[2021-03-10T18:44:01.357+09:00][INFO]-ipc_client.py:210,Getting work for function [arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:test_function2:5] from http://localhost:8000/2016-11-01/functions/arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:test_function2:5/work
[2021-03-10T18:44:01.366+09:00][INFO]-ipc_client.py:220,Got work item with invocation id [5f863386-a701-4d72-4433-230edec50652]
[2021-03-10T18:44:01.369+09:00][INFO]-ipc_client.py:185,Posting work for function [arn:aws:lambda:::function:GGRouter] to http://localhost:8000/2016-11-01/functions/arn:aws:lambda:::function:GGRouter
[2021-03-10T18:44:01.371+09:00][INFO]-ipc_client.py:195,Work posted with invocation id [60b57292-e6d0-49df-6d0f-47405d88e38d]
[2021-03-10T18:44:01.372+09:00][INFO]-ipc_client.py:241,Posting work result for invocation id [5f863386-a701-4d72-4433-230edec50652] to http://localhost:8000/2016-11-01/functions/arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:test_function2:5/work
[2021-03-10T18:44:01.373+09:00][INFO]-ipc_client.py:250,Posted work result for invocation id [5f863386-a701-4d72-4433-230edec50652]
[2021-03-10T18:44:01.374+09:00][INFO]-ipc_client.py:210,Getting work for function [arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:test_function2:5] from http://localhost:8000/2016-11-01/functions/arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:test_function2:5/work
```
結論としては、Greengrassグループにtest_function2 -> IoT Cloudのサブスクリプションを設定していなかったことが原因だった
test_function2を起動するためメッセージを送ることまでは頭にあったものの、起動後にメッセージを送る際の設定が抜けていた
「2-3　test_function2 -> IoT Core」の手順を実施し、再デプロイすることで解消した
#6　備忘録
Greengrassグループをデプロイする際、Lambdaが一時的に停止する
公式ドキュメントにも以下の通り記載がある
> デプロイ中、コアデバイスの Greengrass デーモンプロセスは停止し、再起動します。
参考：[AWS IoT Greengrass グループを AWS IoT Greengrass Core にデプロイする](https://docs.aws.amazon.com/ja_jp/greengrass/v1/developerguide/deployments.html)

```
#tail -F でLambda関数のログを確認中にデプロイすると以下のログが出力された
[2021-03-10T18:48:39.84+09:00][INFO]-lambda_runtime.py:366,Caught signal 15. Stopping runtime.
```
#7　おわりに
Lambdaのデプロイまではトントン拍子でしたが、以降のエラー解消に苦戦しました。
ログにエラーが出るようであれば、原因の確認と解消はいくぶん楽だったのですが、サブスクリプションの設定漏れに気づくまで結構時間がかかってしまいました。

とはいえ、エッジ環境でLambdaを利用するまでの流れが掴めたので、以降いろいろと試してみたいと思います。
#8　参考文献（文中で登場していないもの）

* [AWS IoT Greengrass Core での Lambda 関数の実行](https://docs.aws.amazon.com/ja_jp/greengrass/v1/developerguide/lambda-functions.html)
* [AWS IOT GREENGRASS(V1)入門ハンズオン](https://aws-iot-greengrass-for-beginners.workshop.aws/)
 * 今回範囲であれば、「4.1 LAMBDAファンクションの作成」以降を参考にしています
* [【Part3】Lambda関数を作り、エッジ用に設定する](https://note.com/imaimai/n/nb46fcb843d12)
 * リンク先の記事内でPart3以外の記事もいずれも大いに参考にさせていただきました
* [Jetson NanoでAWS IoT Greengrassを使う Lambda編](https://blog.serverworks.co.jp/2020/09/07/155239)