AWS IoT Things Graphで遊んでみた

ステークホルダーが多いケースで有用そうなサービスでした
2021.04.26

概要

こんにちは、yoshimです。
「AWS IoT Things Graph」で遊んでみたのでブログにしてみました。

目次

1.はじめに

「AWS IoT Things Graph」について遊んでみたので、その内容を記述します。
基本的にこちらに書いてある内容をベースにしつつ、理解を深めるためにちょこちょこ違うことをしています。
「AWS IoT Things Graph」は、下記の通りIoTアプリケーションを視覚的に開発するためのサービスなので、そのようなサービスに興味がある方向けのエントリーです。

AWS IoT Things Graph は、さまざまなデバイスやウェブサービスを容易に視覚的に接続して IoT アプリケーションを構築できるサービスです。

参照: AWS IoT Things Graph

最終的には、下記のような処理フローをGUIで作成します。
これだけ見ても理解しづらいと思いますが、「AWS IoT Things Graph」を私なりの言葉で表現するなら「デバイスのあれやこれやを抽象化&定義することで、デバイスの低レイヤーを意識することなくフローを開発できる」というもので、下記はその具体的なフローの開発画面です。

(これは「myMotionSensor」でのある条件(StateChanged)をトリガーに「myCamera」である処理(capture)を実行し、その結果を用いて「myScreen」である処理(display)をする、というフローです)

2.IAMロールの作成

後ほど定義する「フロー」というものを「AWS IoT Things Graph」が実行するためのサービスロールを作成します。
(「フロー」については後ほど言及しますが、複数のデバイスをまたいだ一連のジョブと思ってください)
作成したIAMロールの「信頼関係」と「アタッチしたIAMポリシー」は下記の通りです。

信頼関係
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "iotthingsgraph.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

IAMロールにアタッチしたポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "iot:Connect",
            "Resource": "arn:aws:iot:{リージョン}:{AWSアカウントID}:client/${iot:Connection.Thing.ThingName}"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "iot:Publish",
            "Resource": "arn:aws:iot:{リージョン}:{AWSアカウントID}:topic/*/*"
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": "iot:DescribeEndpoint",
            "Resource": "*"
        }
    ]
}

3.モノの登録

「AWS IoT Things Graph」で利用するモノを3つ登録します。
(「blog-camera」、「blog-screen」、「blog-ms」の3点)
この過程では特筆することはありませんが、下記に今回の手順を記述しますので気になる方はご確認ください。

モノの登録

まずは検証用の緩いポリシーを作成し、

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:*",
      "Resource": "arn:aws:iot:<リージョン>:<AWSアカウントID>:topic/*/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Connect"
      ],
      "Resource": [
        "arn:aws:iot:<リージョン>:<AWSアカウントID>:client/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Subscribe"
      ],
      "Resource": [
        "arn:aws:iot:<リージョン>:<AWSアカウントID>:topicfilter/*/*"
      ]
    }
  ]
}

「AWS IoT Core」のコンソール画面からポチポチとモノを登録していき、証明書や秘密鍵をダウンロードします。

今回は簡単な検証が目的なので、秘密鍵と証明書ファイルをダウンロードし、ローカルのPCをデバイスとして検証を進めます。
また、「AWS IoTのルートCA」はAmazon Root CA 1をコピペして保存しておきます。

最後に、ポリシーをモノにアタッチします。
こんな感じでモノを3つ登録します。

4.データモデル(TDM)の作成

続いてデータモデル(TDM)を作成します。
TDMはデバイスの様々な要素(ex.機能)を抽象化した定義です。
本エントリーではこれ以上言及しませんが、気になる方はこちらをご参照ください。

モデルの作成はコンソール画面からしました。

下記が実際に作成した定義です。
それぞれ参照元と殆ど同じですが、今後カスタマイズすることを想定して別モデルとして作成してみました。

myCamera
type myCameraDM @deviceModel(id: "urn:tdm:<リージョン>/default:deviceModel:myCameraDM", capability: "urn:tdm:<リージョン>/<accountId>/default:capability:myCameraCapability") {
    ignore: void
  }

  type myCameraCapability @capabilityType(id: "urn:tdm:<リージョン>/<accountId>/default:capability:myCameraCapability") {
    STATE: myCameraState @state(id: "urn:tdm:<リージョン>/<accountId>/default:state:myCameraState")
    capture: myCapture @action(id: "urn:tdm:<リージョン>/<accountId>/default:action:myCapture")
  }

  type myCameraState @stateType(id: "urn:tdm:<リージョン>/<accountId>/default:state:myCameraState") {
    mylastClickedImage: Uri @property(id: "urn:tdm:aws:property:String")
  }

  type myCapture @actionType(id: "urn:tdm:<リージョン>/<accountId>/default:action:myCapture") {
    return: myCameraStateProperty @property(id: "urn:tdm:<リージョン>/<accountId>/default:property:myCameraStateProperty")
  }

  type myCameraStateProperty @propertyType(id: "urn:tdm:<リージョン>/<accountId>/default:property:myCameraStateProperty", instanceOf: "urn:tdm:<リージョン>/<accountId>/default:state:myCameraState") {
    ignore: void
  }


query myCamera @device(id: "urn:tdm:<リージョン>/<accountId>/default:device:myCamera",
            deviceModel: "urn:tdm:<リージョン>/<accountId>/default:deviceModel:myCameraDM") {
        MQTT {
            CameraCapability(id: "urn:tdm:<リージョン>/<accountId>/default:capability:myCameraCapability") {
                state {
                    mylastClickedImage(name: "lastImage", property: "urn:tdm:aws:property:String")
                }
                Action(name: "capture") {
                    Publish {
                        Request(topic: "$macro(${systemRuntime.deviceId}/capture)") {
                            params
                        }
                        Response(topic: "$macro(${systemRuntime.deviceId}/capture/finished)") {
                            responsePayload(property: "urn:tdm:<リージョン>/<accountId>/default:property:myCameraStateProperty")
                        }
                    }
                }
            }
        }
}

参照: iot-tg-examples-rpicamera.md

myScreen
query myScreen @device(id: "urn:tdm:<リージョン>/<accountId>/default:device:myScreen", deviceModel: "urn:tdm:<リージョン>/<accountId>/default:deviceModel:myScreenDM") {
  MQTT {
    myScreenCapability(id: "urn:tdm:<リージョン>/<accountId>/default:capability:myScreenCapability") {
      state {
        mycurrentDisplayImage(name: "displayUri", property: "urn:tdm:aws:property:String")
      }
      Action(name: "display") {
        params {
          param(name: "imageUrl", property: "urn:tdm:aws:property:String")
        }
        Publish {
          Request(topic: "$macro(${systemRuntime.deviceId}/display)") {
            params {
              param(name: "imageUri", property: "urn:tdm:<リージョン>/<accountId>/default:property:myCameraStateProperty", value: "${imageUrl.value}")
            }
          }
        }
      }
    }
  }
}

type myScreenDM @deviceModel(id: "urn:tdm:<リージョン>/<accountId>/default:deviceModel:myScreenDM", capability: "urn:tdm:<リージョン>/<accountId>/default:capability:myScreenCapability") {
  ignore: void
}

type myScreenCapability @capabilityType(id: "urn:tdm:<リージョン>/<accountId>/default:capability:myScreenCapability") {
  STATE: myScreenState @state(id: "urn:tdm:<リージョン>/<accountId>/default:state:myScreenState")
  display: myDisplay @action(id: "urn:tdm:<リージョン>/<accountId>/default:action:myDisplay")
}

type myScreenState @stateType(id: "urn:tdm:<リージョン>/<accountId>/default:state:myScreenState") {
  mycurrentDisplayImage: String @property(id: "urn:tdm:aws:property:String")
}

type myDisplay @actionType(id: "urn:tdm:<リージョン>/<accountId>/default:action:myDisplay") {
  imageUrl: String @property(id: "urn:tdm:aws:property:String")
}

参照: iot-tg-examples-rpiscreen.md

myMotionSensor
query myMotionSensor @device(id: "urn:tdm:<リージョン>/<accountId>/default:device:myMotionSensor", deviceModel: "urn:tdm:<リージョン>/<accountId>/default:deviceModel:myMotionSensorDM") {
  MQTT {
    myMotionSensorCapability(id: "urn:tdm:<リージョン>/<accountId>/default:capability:myMotionSensorCapability") {
      state {
        myisMotionDetected(name: "isMotionDetected", property: "urn:tdm:aws:property:Boolean")
      }
      Event(name: "StateChanged") {
        Subscribe(topic: "$macro(${systemRuntime.deviceId}/motion)") {
          responsepayload(property: "urn:tdm:<リージョン>/<accountId>/default:property:myMotionSensorStateProperty")
        }
      }
    }
  }
}

type myMotionSensorDM @deviceModel(id: "urn:tdm:<リージョン>/<accountId>/default:deviceModel:myMotionSensorDM", capability: "urn:tdm:<リージョン>/<accountId>/default:capability:myMotionSensorCapability") {
  ignore: void
}

type myMotionSensorCapability @capabilityType(id: "urn:tdm:<リージョン>/<accountId>/default:capability:myMotionSensorCapability") {
  STATE: myMotionSensorState @state(id: "urn:tdm:<リージョン>/<accountId>/default:State:myMotionSensorState")
  StateChanged: myMotionSensorEvent @event(id: "urn:tdm:<リージョン>/<accountId>/default:event:myMotionSensorEvent")
}

type myMotionSensorState @stateType(id: "urn:tdm:<リージョン>/<accountId>/default:State:myMotionSensorState") {
  myisMotionDetected: Boolean @property(id: "urn:tdm:aws:property:Boolean")
}

type myMotionSensorEvent @eventType(id: "urn:tdm:<リージョン>/<accountId>/default:event:myMotionSensorEvent", payload: "urn:tdm:<リージョン>/<accountId>/default:property:myMotionSensorStateProperty") {
  ignore: void
}

type myMotionSensorStateProperty @propertyType(id: "urn:tdm:<リージョン>/<accountId>/default:property:myMotionSensorStateProperty", instanceOf: "urn:tdm:<リージョン>/<accountId>/default:State:myMotionSensorState", description: "Property representing the motion sensor state") {
  ignore: void
}

参照: iot-tg-examples-motionsensor.md

5.モノとデータモデル(TDM)を関連付け

続いて、先ほど定義したモデル(デバイス)とモノを関連づけます。

先ほどのモデル定義で「urn:tdm:{リージョン}/{AWS アカウントID}/default:device:{デバイス名}」で「myCamera」を定義したので、指定できるようになっていますね。
早速、「モノ(blog_camera)」と「デバイス(myCamera)」を関連づけます。

同様に「myScreen」、「myMotionSensor」の関連付けも行いました。

6.フローの作成

続いてAWSコンソール画面上でフローを作成します。
下記のように先ほど登録したデバイスが確認できるので、早速ドラッグ&ドロップでフローを定義してみます。

デバイス、処理の関係性を指定して、

トリガーを指定します。
「条件イベント」では、先ほどのモデル定義で指定した「StateChanged」を指定します。
(「capability」に登録した「event」)

myMotionSensorの「stateChanged」をトリガーとしてmyCameraで実行するアクションを定義します。

myCameraのモデル定義で定義した「capture」というアクションを実行します。
また、本アクションで出力される変数名を「cameraResult」とここで定義しました。
下記にmyCameraのモデル定義の一部を再掲します。
このアクションで利用するトピックやpayloadはここで定義されていることがわかります。

Action(name: "capture") {
    Publish {
        Request(topic: "$macro(${systemRuntime.deviceId}/capture)") {
            params
        }
        Response(topic: "$macro(${systemRuntime.deviceId}/capture/finished)") {
            responsePayload(property: "urn:tdm:<リージョン>/<accountId>/default:property:myCameraStateProperty")
        }
    }
}

続いて、「myCamera」のアクションの後に実行するアクションを定義します。 「capabilityType」に登録した「display」アクションを実施します。

下記にモデル定義の一部を再掲します。
利用するトピックやパラメータの定義が記述されていることがわかります。

Action(name: "display") {
  params {
    param(name: "imageUrl", property: "urn:tdm:aws:property:String")
  }
  Publish {
    Request(topic: "$macro(${systemRuntime.deviceId}/display)") {
      params {
        param(name: "imageUri", property: "urn:tdm:<リージョン>/<accountId>/default:property:myCameraStateProperty", value: "${imageUrl.value}")
      }
    }
  }
}

フローの定義が終わったので公開します。

続いて、フローの設定をします。
今回は「クラウド」にデプロイすることとします。

「AWS IoT Things Graph」がフローを実行する権限を付与するために、「2.IAMロールの作成」で作成したIAMロールを指定します。
また、フローを実行した結果のメトリクスをcloudWatchで確認できるようなので、メトリクスを有効化してみました。

続いて、ここで先ほど定義したデバイスの定義とモノをマッピングします。

トリガーやその後のアクションについて意図した内容になっていることを確認して、問題ないので作成します。

さて、これで「フロー設定」の作成が完了しました。
まだ「フロー設定」を作成しただけでありデプロイされていない状態です。

7.フロー設定のデプロイ

続いて、先ほど定義したフロー設定をデプロイします。

デプロイが無事完了したら、このステータスから確認できます。
とても簡単にデプロイできますね。

8.フローの挙動確認

先ほどデプロイしたフローを実際に実行してみます。
このフローは下記のようなことをするので、ここで出てくる4つのトピックをAWSコンソール画面上でサブスクライブしてフローがちゃんと実行されていることを確認します。

フローの処理について

今回定義したフローでは、下記のような処理が実行されます。

- 1.motion sensorが「{IoTモノの名前(motion sensor)}/motion」トピックにパブリッシュ
- 2.「AWS IoT Things Graph」がこれをサブスクライブして「{IoTモノの名前(camera)}/capture」トピックにパブリッシュ    
- 3.cameraがこれをサブスクライブして、アクション(capture)をして、その結果をStringにして「{IoTモノの名前(camera)}/capture/finished」トピックにパブリッシュ  
- 4.「AWS IoT Things Graph」が「{IoTモノの名前(camera)}/capture/finished」トピックをサブスクライブして、その結果を「{IoTモノの名前(screen)}/display」トピックにパブリッシュ
- 5.screenが「{IoTモノの名前(screen)}/display」トピックをサブスクライブして、displayアクションを実行

NOTE:
上記の「1」、「3」、「5」はデバイス側で実行されるためデバイス側で処理を記述する必要があります。逆に言うと、「AWS IoT Things Graph」の「フロー」でAWS側で対応できるのはこの「2」、「4」の部分です。

こちらのスクリプトを利用し、それぞれのデバイスの処理を実行しました。
上記「フローの処理について」の「1」、「3」を実行する処理です。

スクリプトの実行について

下記の通り「motion sensor」と「camera」の2つのPythonスクリプトを実行します。
(今回はフローの挙動確認なので「display」についてはトピックをサブスクライブして確認するのでスクリプト実行は不要です)
フローの中で利用するトピックパスとして「IoTのモノの名前」を指定しているため、このコマンドの引数でもIoTのモノの名前を引数に入れています。

  • 1.motion sensor
python ms/cloudms.py -e {AWS IoTのデータエンドポイント} \
-r {ルート証明書のパス} \
-c {デバイス証明書のパス} \
-k {秘密鍵のパス} \
-n {IoTのモノの名前}
  • 2.camera
python camera/cloudcamera.py \
-e {AWS IoTのデータエンドポイント} \
-r {ルート証明書のパス} \
-c {デバイス証明書のパス} \
-k {秘密鍵のパス} \
-n {IoTのモノの名前}

まず、デバイスから「{IoT モノ名}/motion」トピックにパブリッシュされ、

「{IoT モノ名}/motion」トピックをサブスクライブしていたフローが「{IoTモノの名前(camera)}/capture」トピックにパブリッシュしてくれて、

「{IoTモノの名前(camera)}/capture」トピックをサブスクライブしているカメラデバイスは、アクション(capture)し、その結果を「{IoTモノの名前(camera)}/capture/finished」トピックにパブリッシュし

「{IoTモノの名前(camera)}/capture/finished」トピックをサブスクライブしていたフローが「{IoTモノの名前(screen)}/display」トピックにパブリッシュしていることが確認できました。

また、AWSコンソール画面上から対象のフローの実行履歴を確認することもできます。

実行履歴の中身を確認できるのは便利でいいですね。

以上で、フローが意図した通り挙動していることが確認できました。

9.まとめ

長くなりましたが、「AWS IoT Things Graph」で遊ぶことで、本サービスに対する理解を深めることができました。
「デバイスのモデル」ごとの「定義(ex.アクション、状態として持つ値)」と「利用するトピックのパス構成」を事前に決めることで、デバイスとクラウド上のIFを抽象化した状態で整理でき、その結果デバイス側の低レイヤーの仕様を気にしすぎることなく開発ができるようになるサービスだと理解しました。

本エントリーが「AWS IoT Things Graph」を使ってみたい方の一助にでもなれば幸いです。

10.参照