AWSで遊ぶ 〜M5StickC (ESP32) とAWS IoTとLambdaを繋ぐ〜

前回、M5StickCとAWS IoTをMQTTで繋ぎましたが、これだと単に土管を作っただけみたいなものであまり面白くないので、もう少し複雑な処理を行えるよう、AWS Lambdaを連携させてみます。

やりたいことのイメージ

こんな感じで、M5StickCからMQTTで送った情報をLambdaで処理し、その処理結果をM5StickCに返します。Lambdaの処理内容については今回は特に追求しないため、適当に値を2倍にして返す、ぐらいにしておこうと思います。

AWS側の作業

前提条件

前回作成したモノやポリシー、証明書をそのまま引き継いで使用するので、お手数ですが前回分の内容(←M5StickCとAWS IoTのMQTT双方向通信)を実施した上で以下をご参照頂きますよう、宜しくお願い致します。

IAM設定

作業するIAMユーザ(あるいは、ユーザの所属するグループ)のポリシーですが、まず前提として以下のポリシーが設定されているものとします。

  • AWSLambdaFullAccess
  • CloudWatchFullAccess
  • AWSIoTFullAccess

前回時点のものから、AWSLambdaFullAccessを追加した形になります。例によって、FullAccessで良いのかという気がしますが、現時点ではこれでご了承ください。

 

さて、これで少なくともAWSコンソール上で作業ユーザがLambdaをなんやかんやすることができるようになりますが、これだけだとちょっと困ります。

上の「やりたいことのイメージ」を見ればわかりますが、今回作成するLambda関数はAWS IoTの管理するMQTTのトピックにアクセス(Publish)する必要があります。ということは、(作業ユーザではなく)作成するLambda関数に対して、AWS IoTのトピックにPublishする許可を与えてあげる必要があります。これを実現するのがIAMの『ロール』という仕組みなわけですが、上記のポリシーだと、Lambda関数を作成しようとしても、このロールの付与ができません。ロールの付与はあくまでIAMの機能なので、IAMに対するポリシーを何も設定していない状態では、設定できないのは当然といえば当然です。

しかし、諸々の許可を管理するIAMのFullAccessを作業用ユーザに対して付与するのは流石にいかがなものかと思います。ということで、作業用ユーザに対して、IAMの『ロールの付与』という権限だけを付与してあげるようにします。

以下の手順は、このあたりを参考にさせて頂きました。

まず、管理者ユーザでIAMの管理画面から「ポリシー」の設定に入ります。

ポリシーを新規に作成します。

ビジュアルエディタで設定しても良いですが、今回はコピペで済むようにJSONで設定します。JSONの入力画面になったら、以下をコピペして「ポリシーの確認」に進みます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "*"
        }
    ]
}

名前は適当に、例えば”AllowPassRole”などにして、ポリシーの作成を完了させます。

その後、作成したポリシーを作業用のユーザ(グループ)にアタッチしてください。

これで、少なくとも作業用ユーザは『(Lambda含む)任意のリソースに対してロールを付与する』という行為だけは許可されることになりました。ただ、あくまで許可したのはロールの『付与』だけで、ロールの『作成』を許可したわけではありません。作成まで許可してしまうと、結構強い権限になってしまう気がしますので。ということで、管理者ユーザのまま、今回使用するロールを作成していきます。ちなみに、「どうせ自分一人でやってることだし、いちいちロールを作るのに管理者ユーザに切り替えるのは面倒だ!」という方は、PassRoleだけではなくCreateRoleの権限まで作業用ユーザ(グループ)に付与すれば可能だとは思いますが、少なくとも私はやっていないので、正確なところはわかりません。

それでは、今回使用するLambda用のロールを作成していきます。後段で出てくる話の先出しになってしまいますが、Lambda関数に付与するロールには「CloudWatchにログを送信する権限」が付与されていることを求められます。これを具体的に書き記したものが、AWSで初めから定義されている”AWSLambdaBasicExecutionRole”で、その内容は以下になります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

したがって、こちらをベースに、今回必要になる権限(=AWS IoTのMQTTトピックに対するPubishする権限)を追加します。先ほどと同様に新規のポリシー作成画面に入り、JSONで以下のように設定してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "iot:Publish",
            "Resource": "arn:aws:iot:[region]:[AWS account ID]:topic/myTopic/fromCloud"
        }
    ]
}

今回はかなり対象(リソース)を絞っています。

名前は適当に”AllowIoTPubForLambda”とかにして、ポリシーの作成を完了させます。ここで設定したポリシーを、ロールに対して紐付けます。

「ロール」から「ロールの作成」に入ります。

エンティティの種類を「AWSサービス」として、ロールを使用するサービスとしてLambdaを選択して次に進みます。

先で作成したポリシー(”AllowIoTPubForLambda”)を選択して次に進みます。

タブは将来的に開発規模が大きくなれば設定した方が良いのかと思いますが、今回は設定なしで進みます。

名前は適当に”AWSLambdaExecutionWithIoTPub”などとして、ロールの作成を完了させます。

おつかれさまでした。ここまで済んだら、作用業ユーザでログインし直して、以下の作業に進みます。

ところで、「Lambda → AWS IoT方向(Publish)の権限はこのロール作成で良いとして、AWS IoT → Lambda方向のロール作成はいらんの?」と思った方もおられるかもしれません。少なくとも自分はそう思ったのですが、これは(作業用ユーザで作業していても)AWS IoT側でLambdaを呼び出すルールを作るときに、AWS IoT → Lambdaのアクセス権限付与は勝手にやってくれるようです。このあたりの詳細は、すみません、よくわかっていませんが、とりあえず気にせず先に進みます。

Lambda関数の作成

まずは、AWS IoTと連携させるLambda関数を先に作っておきます。とりあえず形だけ先に作ることにして、Lambdaのソースコードはまた後から書き直します。

Lambdaの管理画面の「関数」から、「関数の作成」に進みます。

「一から作成」を選択して、例えば以下のように設定します。

関数名 respond_to_IoT_message
ランタイム Python 3.7
アクセス権限 既存のロールを使用する: AWSLambdaExecutionWithIoTPub

大事なのは、言わずもがな「アクセス権限」です。ここで、先ほど管理者用ユーザで作成したロールを選択します。

これでとりあえずLambda関数が作成されたので、次にこれをAWS IoTと連携するよう設定します。

AWS IoT Coreの管理画面に入り、「ACT」の「ルールの作成」に進みます。

ルールの名前はとりあえず

myM5StickCRule

とかにします。続いてルールクエリステートメントですが、ここは

SELECT * FROM ‘myTopic/fromDevice’

こんな感じで、Lambda関数の起動のトリガーとなるトピック名をFROM以下に指定するようにします。

続いて「1つ以上のアクションを設定する」の方の「アクションの追加」に進みます。その下のエラーアクションも本当なら何か設定した方がよいのかもしれませんが、ここはいつか理解が深まったら設定することにして先に進みます。

アクションの選択画面になるので、「メッセージデータを渡すLambda関数を呼び出す」を選択して「アクションの設定」に進みます。

関数名の選択画面になるので、先ほど作成した関数”respond_to_IoT_message”を選択して「アクションの追加」に進みます。

ルールの作成画面に戻ってくるので、必要であればタグを設定して、先に進みます。

ルールが正常に作成されたら、もう一度Lambdaの管理画面に戻り、関数”respond_to_IoT_message”の設定画面を開きます。

すると、関数のトリガーとしてAWS IoTが紐づけられていることがわかります。これでAWS IoT → Lambdaの連携設定が済んだので、後はLambda → AWS IoTへメッセージをPublishするためのソースコードを作成します。

開発規模が大きくなってきたら、「コードエントリタイプ」で「.zipファイルをアップロード」とする形でソースを作成した方がよいのでしょうが、今回はイベントをキャッチしてメッセージをPublishするだけの短いコードですので、「コードをインラインで編集」で、そのままコンソール上でソースコードを編集してしまいます。

今回作成する内容は以下になります。

import json
import boto3

# Publish先のトピックを指定
PUB_TOPIC = 'myTopic/fromCloud'

print('Loading function')  

# AWS IoT Data Planeオブジェクト取得
iot = boto3.client('iot-data')

# Lambdaメイン関数
def lambda_handler(event, context):
    # メッセージ内容を取得
    rx_value = int(event["message"])
    print("RX:", rx_value)  

    # 送信メッセージの内容を作成
    tx_value = str(rx_value * 2)
    print("TX:", tx_value)

    # 送信メッセージを作成
    payload = {
        "message": tx_value
    }

    try:
        iot.publish(
            topic = PUB_TOPIC,
            qos   = 0, # 届こうが届くまいが1回だけ送信
            payload=json.dumps(payload)
        )
        print("Publish Succeeded!")

        return "Succeeeded."

    except Exception as e:
        print(e)
        print("Publish Failed.")
        return "Failed."

以下、Lambdaを使いこなしている方々からすれば当然のことなのでしょうが、自分は使用するのが今回が初めてでしたので、自分用のメモになります。

  • boto3は、AWSをPythonから操作するための公式ライブラリ。
  • Lambda内でprintを使用すると、CloudWatch Logsでprintの出力内容を確認できる。
  • boto3.client()で、使いたいAWSサービスのオブジェクトを取得できる。AWS IoT Data Planeオブジェクトの詳細はこちらを参照。
  • lambda_handlerのevent引数(通常はdictタイプ)から、呼び出し元から送られたデータを参照できる。event, contextの詳細についてはこちらを参照。

上記のソースコードを作成したら、「保存」で完了です。設定画面には他にも色々入力項目がありますが、今回はそこはスルーです。必要に応じて、今後利用するようにします。

作成したLambda関数が意図通り動作しているかをチェックするために、ここでテストを実施しても良いと思います。

Lambda関数の管理画面右上から、以下のような感じでテストイベントを作成します。

その後、Lambda関数の管理画面右上から「テスト」を実行して、実行結果で特にエラーを吐くことなく成功していればOKです。

以上で、AWS側の作業はおしまいです。おつかれさまでした。

デバイス側の作業

今回はデバイス側の作業は特にありません。前回作成して書き込んだソースコードをそのまま使用できます。

動作確認

それでは、動作確認していきます。M5StickCを起動して数秒〜10秒ぐらい待って、”Connected to AWS.”とディスプレイに表示されてからAボタン(M5ボタン)を押します。押すたびに”TX”の数字がカウントアップされていきますが、僅かのディレイの後、”RX”の値が”TX”の2倍の値になって表示されればOKです。確かにLambdaで処理された値が返ってきています。

最後に、M5StickC側の動作検証の様子を動画で載せておきます。

 

最初だけ”TX”が表示されてから”RX”の表示に少し間があるのは、おそらく最初のLambda呼び出しだけコールドスタートになっているからですね。

 

ということで、AWS IoTに対して送ったデータをLambdaで処理して送り返してもらう、というところまでやってみました。今回は単純に値を2倍にして返しているだけですが、その気になれば、例えばLambda上でscikit-learnを使用して機械学習の予測モデルを適用した結果を返してもらうということも可能になるわけで、クラウドのコンピューティングパワーを活用するための準備がこれで整ったことになります。

といっても、「じゃあそれで何すんねん」というところがまだ特に決まっているわけでもないので、まだまだお勉強フェーズは続きます。前回、今回とデバイス側がトリガーになって処理が始まるようになっていたので、次回はCloudWatchなど使ってみて、クラウド側がトリガーになってデバイス側にメッセージを送るパターンを試してみようかなと思っています。が、相変わらず予定は未定です。

参考:

 

以下は自分が勉強用にパラパラと読んでいる本です。広範囲に渡るAWSサービスの概要を知るのに丁度良い感じがしています。