Amazon Web Services ブログ

AWS IoT Core でのタイムクリティカルなクラウドからデバイスへの IoT メッセージパターンの実装

この記事は Iacopo Palazzi によって投稿された Implementing time-critical cloud-to-device IoT message patterns on AWS IoT Core を翻訳したものです。

はじめに

デバイスからクラウド、クラウドからデバイスの通信に広く採用されている IoT(Internet of Things)の標準的な通信は通常の場合には非同期型で、イベント駆動型のパターンを実現し、回復力、コスト削減、障害耐性を確保することができます。しかし、さまざまな業界のお客様が、エッジデバイスのタイムクリティカルなロジックを確保するために、同期型の通信パターンを必要としています。

たとえば自動車メーカーは、コネクテッドビークルを遠隔操作し、サイドウィンドウを下げたり、車のアラームを解除したりといった操作を、顧客がタイムリーかつ効率的に行えるようにしたいと考えています。そのためには、お客様のコマンドを実行するために、車両とクラウド間のメッセージングがリアルタイムで行われることが必要とされます。しかしながら、このプロセスは非同期の IoT 通信パターンでは実現が困難です。なぜなら、一般的な非同期の IoT 通信は、デバイスがオフラインになったときでも、再びオンラインになるまでメッセージが存続する、通信の断絶を前提としたシナリオ向けに設計されているからです。同期通信の場合、この存続性は必要なく、代わりにメッセージは直ちに実行され、そうでなければ廃棄されることが求められます。

この記事では、AWS IoT の機能を使って、AWS 上で同期通信パターンを実装する方法を説明します。お客様は、HTTPクライアントを使用して、Amazon API Gateway のエンドポイントを呼び出します。このアクションは、リクエストを AWS Lambda 関数に転送し、エッジアプリケーションで実行されるロジックを呼び出します。

ソリューションの概要

我々の提案するソリューションは、IoT デバイス上で何らかのアプリケーションが実行されていて、そのアプリケーションが何らかのタスクを実行し、タスクの実行終了後にレスポンスを返す状況で動作します。このソリューションでは、HTTPクライアントが IoT デバイスに対してリクエストを発行し、即時のフィードバックを待ちます。また、アプリケーションは、設定されたタイムアウト時間までの特定の時間ウィンドウ内で実行する必要があり、時間内に実行できない場合は、クライアントに HTTP エラーを返します。

以下に、HTTP クライアントから開始され、デバイスロジックが行われた場合に戻り、特定のタイムアウト期間に応答が返されない場合(デバイスが接続されていない、またはその特定のリクエストに対してエッジロジックが実装されていないなど)にはエラーを返す一般的なリクエストのフローを示します。

  1. HTTPクライアントは、AWS Lambda 関数を外部に公開している Amazon API Gateway インスタンスにリクエストを送信します。
  2. Amazon API Gateway インスタンスは、リクエストを AWS Lambda 関数に転送します。
  3. AWS Lambda 関数は MQTT クライアントインスタンスを作成し、受け取ったHTTP リクエストを AWS IoT Core インスタンスとやり取りするためのチャネルとして使用します。
  4. MQTT クライアントが AWS IoT Core インスタンスに接続されると、リクエストのペイロードをデバイスと通信するための専用の AWS IoT Core トピックにリクエストが転送されます。
  5. AWS IoT Core インスタンスが AWS Lambda 関数からリクエストを受け取ると、以下のステップで同期的な通信を行います。
    1. AWS IoT Core は、デバイスに MQTT リクエストを転送します。
    2. AWS Lambda 関数インスタンスは、デバイスからの応答を待ちます、もしくはタイムアウトします。
    3. デバイスはリクエストを読み取り、それに関連するビジネスロジックを実行し、レスポンスを作成します。
    4. デバイスは、HTTP クライアントに転送されるように、確認応答メッセージとオプションのレスポンスを含む MQTT ペイロードを AWS IoT Core に返します。
  6. AWS Lambda 関数上の MQTT クライアントは、AWS IoT Core から、MQTT レスポンスを含むレスポンスを受信します。
  7. AWS Lambda 関数は、MQTT レスポンスから情報を取得し、Amazon API Gateway に返す HTTP レスポンスを生成します。
  8. Amazon API Gateway は、クライアントにレスポンスを転送します。
  9. クライアントは、デバイスが生成したレスポンスまたは AWS Lambda 関数が生成したタイムアウトのいずれかを含む HTTP レスポンスを受信します。

以下の図に、このソリューションを実装するための最小限のアーキテクチャーを示します。

ご自身の AWS アカウント上でのソリューションの実装

前提条件

このソリューションを実行するためには以下の前提条件を満たす必要があります。

  • IoT 通信シナリオ、MQTT プロトコル、イベントドリブン/非同期パターンに関する知識。
  • AWS のアカウント
  • Linux もしくはそれと同等の端末。
  • AWS Command Line Interface (CLI) がインストールされていること。AWS CLI のインストールと設定方法については、AWS CLI Documentation を参照ください。
  • AWS SAM CLI。AWS SAM CLI のインストールと設定方法については、AWS SAM CLI Documentation をご参照ください。
  • AWS Identity and Access Management (IAM) ユーザーで、CLI を使用して AWS リソースを作成するためのクレデンシャルを持っていること。
  • Python 3.9 がインストールされた環境。
  • コールバックロジックを実装するためのターゲット IoT デバイスまたはデバイスシミュレータ。
  • テストを実行するための HTTP クライアント。この手順では、cURL を使用します。

実装の仕様

この手順では、以下を指定することができる同期呼び出しの API の例を実装します。

  • Method: ターゲットデバイスで実行するアクションを示す。
  • Target: アクションを実行したいターゲットデバイスを示す。(例:ThingName)
  • Timeout: コマンドとレスポンスの実行時に設定したいタイムアウト値を指定する。

トピックの構成:

  • Outbound トピック: IoT デバイスにリクエストを送信します。
  • ACK トピック:IoT デバイスから AWS Lambda 関数に ack を送信します。

トピックを構成する階層構造は以下のようになります。

  • target: ターゲットデバイス(例:ThingName )、API コールからの Target パラメータから継承されます。ターゲットデバイスは、ルートトピック空間 「{target}/#」 のメッセージを購読する必要があります。
  • method: 実行するメソッドの識別子で、APIコールからの Target パラメータから継承される。この識別子は、レスポンスを提供するために、デバイスに認識させる必要があります。
  • client_prefix: クラウド上の API の識別子(例:”invocation”)で、アプリケーションスタック内で Lambda 環境変数として定義されます。
  • m_id: UUID4 を通して新しいリクエストごとにランダムに生成される値で、同じデバイスに送られた複数のリクエストが各リクエスターによって別々に管理されることを保証します。

このソリューションを実装するための手順は以下のようになります:

  • Step1: AWS IoT Core への同期型クライアントを実装した AWS Lambda 関数のデプロイメント。
  • Step2: Amazon API Gateway による HTTP API のデプロイと、バックエンドとしての Lambda 関数との連携。
  • Step3: AWS Lambda 関数のエンドポイントの取得。

このブログ記事では、非商用アカウントでのテストを目的として、Step1 と Step 2 のデプロイ可能なサンプル実装を提案します。

実装の設計方針

以下の章では提案する API のLambda バックエンドの主な設計方針を説明します。

  • AWS IoT Device SDK 2 for PythonWebsocket を介した MQTT メッセージのコネクション、パブリッシュ、サブスクリプションの実装に使用します。
  • Websocket ベースの MQTT クライアントを使用しているのは、X.509 証明書の代わりに IAM を通じて提供される、よりシンプルで効率的な認証メカニズムを利用するためです。この方法は、AWS IoT Core に接続するためのクライアント証明書のライフサイクルを維持する必要がないという利点があります。

AWS Lambda 関数と関連付けて使用される IAM ロールは GitHub リポジトリ上の template.yml にて確認が可能です。

  • このアプリケーションは、AWS Lambda とAmazon API Gateway をベースとしたクラウドネイティブアプリケーションの定義プロセスを簡素化する AWS Serverless Application Model(SAM)を使用してデプロイされます。

AWS IoT Core の同期型クライアントを AWS Lambda でデプロイする

Websocket を使った MQTT プロトコルで AWS IoT Core インスタンスと通信できる AWS Lambda 関数を作成する手順を以下のステップで説明します。

Step1 – 環境変数のセットアップ

1 – コマンドラインターミナルウィンドウを開く。

2 – コマンドを実行するために使用する作業ディレクトリを作成します。ここでは、/tmp/aws を例として説明します。

3 – 以下のリポジトリを、作業ディレクトリ /tmp/aws にチェックアウトします。

git clone https://github.com/aws-samples/time-critical-cloud-to-device-iot-message-patterns-on-aws-iot-core iot_direct_invocations

ブログ記事のコンテンツが /tmp/aws/iot_direct_invocations にコピーされます

4 – 次のステップで使用する AccountIDRegion の環境変数を定義します。

AccountID:以下のコマンドを実行し、awscli に設定されている AWS アカウント ID を取得します。

aws sts get-caller-identity

以下のようなレスポンスが得られます。(エラーの場合は、Configure the AWS CLI ガイドをご確認ください):

{
    "UserId": "AIDASAMPLEUSERID",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/DevAdmin"
}

“Account” の値をコピーし、環境変数として Export します。

export AccountID="123456789012"

Region: 使用したいリージョンを選び (例: eu-west-1 ) 以下のコマンドで Export します。

export Region="eu-west-1"

5 – Lambda 関数のインスタンスの名前を示す環境変数 FunctionName を定義します。(例: iot-lambda-function )

export FunctionName="iot-lambda-function"

注意: すでに存在する Lambda 関数の名前を設定しないようにしてください。もしすでに存在する名前に設定するとデプロイ時に名前の衝突によりエラーが生じます。

6 – 後に使用する AWS IoT Core インスタンスの URLを示す環境変数 IoTCoreEndpoint を定義します。

以下のコマンドを実行します。

aws iot describe-endpoint --endpoint-type iot:Data-AT

以下のようなレスポンスが得られます。

{
    "endpointAddress": "<instance_id>-ats.iot.<your_region>.amazonaws.com"
}

以下のように endpointAddress の値をコピーして Export します。

export IoTCoreEndpoint="<instance_id>-ats.iot.<your_region>.amazonaws.com"

Step2 – AWS Lambda 関数のデプロイ

ご自身の AWS Account でAWS Lambda 機能をデプロイする環境が整ったので、ターミナルを開いて /tmp/aws/iot_direct_invocations/sam/iot-lambda-client/に移動します。

このフォルダには、以下の要素をデプロイするために使用される SAM アーティファクトとテンプレートがあります。

  • IAM ロールとポリシー
  • API Gateway インスタンス
  • AWS Lambda 関数のコード(Pythonコードが /tmp/aws/iot_direct_invocations/sam/iot-lambda-client/ 以下にあります)

1 – SAM で CloudFormation テンプレートを構築する: 公開する AWS Lambda 関数に必要なすべての要素をデプロイする前に、以下のコマンドで CloudFormation テンプレートを構築する必要があります。

sam build

成功すると Build Succeeded と表示されるので次のステージに進みます。

2 – CloudFormation のスタックをデプロイ: ビルドが成功した場合、以下のコマンドで AWS Lambda 関数をデプロイします。

sam deploy \
    --resolve-s3 \
    --stack-name iot-lambda-client \
    --capabilities CAPABILITY_IAM \
    --parameter-overrides \
        AccountID=${AccountID} \
        Region=${Region} \
        LambdaName=${FunctionName} \
        IoTCoreEndpoint=${IoTCoreEndpoint} \
        ClientIDPrefix=invocation_client

コマンドの出力によりプロセスが正しく終了したかどうか判定することができます。

注意: コマンドは冪等性が担保されるので、SAM template や内容物に変更がない限り、何度実行しても同じ結果となります。しかしながら、この場合、Error: No changes to deploy. Stack iot-lambda-client is up to date のようなメッセージが表示されます。これはデプロイが失敗したのではなく、デプロイずべき変更がないことを示しています。

Step 3 – AWS Lambda 関数のエンドポイントを取得

AWS Lambda 関数をデプロイ後、HTTP クライアントを実行するために HTTP クライアントの URL を取得する必要があります。以下のようにコマンドを実行します。

aws cloudformation describe-stacks --stack-name iot-lambda-client

このコマンドは前のステップの SAM によって作成された iot-lambda-client という名前のCloudFormation のスタックの情報を取得します。出力は “.Stacks[].Outputs” というネストされた配列を含む json オブジェクトになります。配列の中にはキーバリューのペアがあり、その中に "OutputKey": "InvokeApi" という Key があるので、その "OutputValue" の値を取得します。以下のような値が取得できます。

"https://<id>.execute-api.<region>.amazonaws.com/Prod/invoke/"

この URL がインターネットから AWS Lambda 関数を実行するための URL です。以降のテストで使用するため、環境変数として Export しておきます。

export APIEndpoint="https://<id>.execute-api.<region>.amazonaws.com/Prod/invoke/"

ソリューションのテスト

ソリューションをテストするには異なる2つのステップが必要です。

  • AWS IoT Thing デバイスをシミュレートしたインスタンスを作成し、リクエストを待ち受ける
  • AWS Lambda 関数へ認証されたリクエストを実行する

AWS IoT Thing デバイスの準備

ソリューションをテストする効果的な方法は、上記の AWS IoT Core インスタンスに接続し、シナリオに沿ったやり取りをシミュレートするソフトウェアクライアントを作成することです。このクライアントは、AWS IoT thing によって表され、AWS Lambda 関数から来る呼び出しに応答します。

この例では、AWS IoT Core のレジストリに IoT thing を設定し、デバイス証明書と IoT ポリシーを IoT thing に関連付けます。デバイス証明書とデバイス秘密鍵は、AWS と通信するためにデバイスに提供されます。

ベストプラクティスとして、実際のプロビジョニングフローでは、パブリックインターネット上での秘密鍵の共有を避けるべきで、IoT デバイスの設計の一部としてプロビジョニングフローを組み込むことが推奨されます。

AWS は、AWS IoT Core ドキュメントの一部として、「Device Manufacturing and Provisioning with X.509 Certificates in AWS IoT Core」というホワイトペーパーを用意しています。この中で、デバイスプロビジョニングのためのオプションのリストと、実際のお客様のシナリオにどのように各オプションを使用するべきかを説明しています。

1 – デバイスシミュレータの作業ディレクトリ /tmp/aws/iot_direct_invocations/sam/test-client/ に移動します。

2 – コマンドラインターミナルウィンドウを開き、デバイス証明書とキーペアを生成するために以下のコマンドを実行すると、作業ディレクトリにファイルが作成されます。コマンドの出力から、certificateArncertificateId をコピーしてください。

aws iot create-keys-and-certificate \
    --certificate-pem-outfile "TestThing.cert.pem" \
    --public-key-outfile "TestThing.public.key" \
    --private-key-outfile "TestThing.private.key" \
    --region ${Region} \
    --set-as-active

以下のような出力が得られます。

{
    "certificateArn": "arn:aws:iot:<region>:<account_id>:cert/<certificate_id>",
    "certificateId": "<certificate_id>",
    "certificatePem": "-----BEGIN CERTIFICATE-----\n<certificate_data>\n-----END CERTIFICATE-----\n",
    "keyPair": {
        "PublicKey": "-----BEGIN PUBLIC KEY-----\n<public_key_data>\n-----END PUBLIC KEY-----\n",
        "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\n<private_key_data>\n-----END RSA PRIVATE KEY-----\n"
    }
}

certificateArn の値をメモし、次のステップで使用するために環境変数として Export しておきます。

export CertificateArn="arn:aws:iot:<region>:<account_id>:cert/<certificate_id>"

注意: AWS のセキュリティポリシーでは、秘密鍵はクラウド環境に送信することはできません。もし秘密鍵をなくしてしまった場合には、再び作成してください。

3 – 次に、シミュレータデバイスを表す AWS IoT thing を準備するために必要なリソースを作成する SAMスクリプトを準備します。ターミナルウィンドウを開き、作業ディレクトリ /tmp/aws/iot_direct_invocations/sam/test-client/ に移動し、SAMテンプレートをビルドします。

sam build

4 – ビルドプロセスが問題なく終了した場合、生成された AWS CloudFormation template を使用してデプロイを行うことができます。SAM の以下のコマンドを使用して IoT thing をデプロイします。

sam deploy \
     --resolve-s3 \
    --stack-name test-iot-thing \
    --parameter-overrides \
        Region=${Region} \
        AccountID=${AccountID} \
        ClientIDPrefix=invocation_client \
        TestingIoTThingCertificateARN=${CertificateArn}

デプロイが成功すると、ご自身の AWS アカウントの AWS IoT Console にて、登録された Thing、ポリシー、デバイス証明書を確認することができます。

5 – AWS IoT Thing デバイスがクラウド環境にプロビジョニングされると、シミュレーターを実行することができます。シミュレーターを実行するには以下のコマンドを実行します。

python index.py \
    --endpoint ${IoTCoreEndpoint} \
    --cert TestThing.cert.pem \
    --key TestThing.private.key \
    --client-id Device001

全てが正しくセットアップされている場合、クライアントが Waiting for command ... のメッセージを表示して停止します。この Python スクリプトはコマンドが AWS IoT Core インスタンスに転送されるのを待つデバイスをシミュレートしています。

AWS Lambda 関数 エンドポイントへの認証されたリクエストの実行

前のステップで定義したシミュレーションデバイスとのやりとりをテストするために、AWS Lambda 関数 API を公開している Amazon API Gateway のエンドポイントに向けて HTTP リクエストを送信する必要があります。

1 – Amazon API GatewayのエンドポイントURLを準備します:

実行する GET リクエストを表す URL は、以下のような構成になります。

export ENDPOINT="${APIEndpoint}?request=my_request_1&method=reqid-ABCD&target=Device001&timeout=3"

パラメータは以下の通りです。

  • request: AWS Lambda 関数に渡され、デバイスのロジックに直接転送されるリクエストを示す文字列です。AWS Lambda 関数は、パラメータ値の自体には関与しません。リクエストに含まれる URL の互換性のパラメータの問題を回避するため、base64エンコーディングを行った値を渡すことを推奨します。
  • method: デバイスのロジックとしてトリガーされるメソッドの識別子を表します。
  • target: リクエストを特定のデバイスにマッピングするために使用されるクライアント ID です。ステップ5 (-client-id) で使用したものと同じでなければなりません。
  • timeout: AWS Lambda 関数がデバイスの応答を待ち、呼び出し元にタイムアウトを返すまでの期間の値です。

2 – 認証されたリクエストを準備します

セキュリティのベストプラクティスとして、何らかの認証なしに API を公開するべきではありません。この例では、AWS IAM の認証を使用してAPIエンドポイントをデプロイしています。これは、公開された invoke API のリソースが、適切なポリシーが付与された IAM ユーザからのみ execute-api:Invoke リクエストを受け入れるよう設定されているということです。

まず、この例で使用されている IAM 認証情報は、このドキュメントに従って適切なポリシーがアタッチされた IAM ユーザーに関連付けられていることを確認します。次に、Step 1で作成されたエンドポイントに送信される全てのリクエストは、Signature Version 4 の署名プロセスに従って適切に署名されていることを確認します。

3 – http pythonクライアントを使用して、リクエストを実行します。

ここでは簡単にリクエストを発行できるようにするため、署名プロセスのロジックを実装するツールを提供します。これは、リポジトリのルートフォルダ /tmp/aws/iot_direct_invocations/ にある perform_authenticated_request.py という Python スクリプトです。

これにより、リポジトリのルートフォルダ(例:/tmp/aws/iot_direct_invocations/)に移動し、以下のコマンドを実行することで認証済みリクエストを実行することができます。

python perform_authenticated_request.py $ENDPOINT

このリクエストの出力はシナリオに応じて以下のようになります。

  • デバイスシミュレーターが接続されている:
{
    "result": "ok",
    "elapsed": "383",
    "response": "Your request was 'my_request_1'. Some random response: '333.0155808661127'"
 }
  • デバイスシミュレーターが接続されていない:
{
    "result": "timeout",
    "elapsed": "3795"
}

リソースのクリーンアップ

AWS アカウントへの不要な課金を防ぐために、このウォークスルーに使用した AWS リソースを削除することができます。これらの AWS リソースには、AWS IoT Core things と証明書、AWS Lambda 関数、およびAmazon API Gatewayが含まれます。クリーンアップを実行するには、AWS CLI、AWS Management Console、または AWS API を使用できます。このセクションでは、AWS CLI のアプローチを使用します。これらのリソースを保持したい場合は、このセクションを無視してください。

AWS IoT Core リソースのクリーンアップ

1 – IoT Policy からデバイス証明書をデタッチします

aws iot detach-policy --target "arn:aws:iot:<your_region>:<your_account_ID>:cert/<certificate_id>" --policy-name "TestPolicy"

2 – IoT Policy を削除します

aws iot delete-policy --policy-name TestPolicy

3 – テスト用の IoT Thing からデバイス証明書をデタッチします

aws iot detach-thing-principal --thing-name TestThing --principal arn:aws:iot:<your_region>:<your_account_ID>:cert/<certificate_id>

4 – AWS IoT Core からデバイス証明書を削除します

aws iot delete-certificate --certificate-id <certificate_id>

5 – AWS IoT Core から IoT Thing を削除します

aws iot delete-thing --thing-name TestThing

SAM による API Gateway と Lambda のリソースの削除

同期実行用のリソースに関連付けられた SAM の Stack を削除します。

sam delete --stack-name iot-lambda-client

まとめ

この投稿では、タイムクリティカルな通信シナリオにおいて同期パターンが必要な場合に、お客様が直面する主な課題について議論しました。このブログで提案したアーキテクチャと実装は、お客様のソリューションにこのような機能を実装するためのベースラインとして採用できるテストアーティファクトとなっています。

AWS IoT Core の使用方法の詳細についてはドキュメントをご参照ください。

AWSはフィードバックを歓迎します。感想や質問があれば、LinkedIn 経由でご連絡ください。

執筆者について

Iacopo は、ミラノを拠点とする AWS プロフェッショナルサービスチームで働く IoT エンジニアです。ソフトウェア開発と DevOps に情熱を持ち、それらを使って AWS の顧客のために堅牢でスケーラブルで革新的なアーキテクチャを実装しています。

Daniele Crestini

Daniele は、AWS プロフェッショナルサービスの IoT データコンサルタントです。AWS クラウド上で AWS IoT サービスを活用した革新的なソリューションを設計、構築し、AWS カスタマーのビジネスゴール達成を支援しています。

この記事はプロフェッショナルサービス本部 IoT コンサルタントの小林が翻訳しました。