Raspberry PiとAWSを繋いでみる ~AWS IoTのジョブ~

AWS IoTのジョブを使ってみます。AWS IoT Device Clientのチュートリアル完結。
2023.04.24

こんにちは。CX事業本部のKyoです。引き続きラズパイとAWS IoT Coreを触っていきます。

前回はこちら。

概要

AWS IoTからAWS IoT Device Clientをセットアップしたラズパイに対して、ジョブを実行させます。ジョブ、という言葉の定義は公式ドキュメントに以下のように記されています。

ジョブは、AWS IoT に接続された 1 つ以上のデバイスに送信され実行されるリモート操作です。例えば、一連のデバイスに対して、アプリケーションのダウンロードとインストール、ファームウェア更新の実行、再起動、証明書のローテーション、またはリモートトラブルシューティングオペレーションの実行を指示するジョブを定義できます。

具体的には以下の手順を行います。

AWS IoT Device Client でのリモートアクション (ジョブ) をデモンストレーションする

ステップ 1: ジョブを実行するために Raspberry Pi を準備する

Raspberry Pi をプロビジョニングしてジョブをデモンストレーションする

前々回、前回でも行ったラズパイへの証明書セットアップ、AWS IoTへのリソース作成(デバイスのプロビジョニング、ポリシーの作成、ポリシーのアタッチ)を行いました。

今回のポリシーはこのような形です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Connect"
      ],
      "Resource": [
        "arn:aws:iot:<リージョン>:<アカウントID>:client/<デバイス名>"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Publish"
      ],
      "Resource": [
        "arn:aws:iot:<リージョン>:<アカウントID>:topic/test/dc/pubtopic",
        "arn:aws:iot:<リージョン>:<アカウントID>:topic/$aws/events/job/*",
        "arn:aws:iot:<リージョン>:<アカウントID>:topic/$aws/events/jobExecution/*",
        "arn:aws:iot:<リージョン>:<アカウントID>:topic/$aws/things/<デバイス名>/jobs/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Subscribe"
      ],
      "Resource": [
        "arn:aws:iot:<リージョン>:<アカウントID>:topicfilter/test/dc/subtopic",
        "arn:aws:iot:<リージョン>:<アカウントID>:topic/$aws/events/jobExecution/*",
        "arn:aws:iot:<リージョン>:<アカウントID>:topicfilter/$aws/things/<デバイス名>/jobs/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Receive"
      ],
      "Resource": [
        "arn:aws:iot:<リージョン>:<アカウントID>:topic/test/dc/subtopic",
        "arn:aws:iot:<リージョン>:<アカウントID>:topic/$aws/things/<デバイス名>/jobs/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:DescribeJobExecution",
        "iot:GetPendingJobExecutions",
        "iot:StartNextPendingJobExecution",
        "iot:UpdateJobExecution"
      ],
      "Resource": [
        "arn:aws:iot:<リージョン>:<アカウントID>:topic/$aws/things/<デバイス名>"
      ]
    }
  ]
}

AWS IoT Device Client を設定してジョブエージェントを実行する

おなじみのAWS IoT Device Clientの設定ファイルを作成します。今回はjobsが有効化されていますね。

{
  "endpoint": "",
  "cert": "~/certs/jobs/device.pem.crt",
  "key": "~/certs/jobs/private.pem.key",
  "root-ca": "~/certs/AmazonRootCA1.pem",
  "thing-name": "",
  "logging": {
    "enable-sdk-logging": true,
    "level": "DEBUG",
    "type": "STDOUT",
    "file": ""
  },
  "jobs": {
    "enabled": true,
    "handler-directory": ""
  },
  "tunneling": {
    "enabled": false
  },
  "device-defender": {
    "enabled": false,
    "interval": 300
  },
  "fleet-provisioning": {
    "enabled": false,
    "template-name": "",
    "template-parameters": "",
    "csr-file": "",
    "device-key": ""
  },
  "samples": {
    "pub-sub": {
      "enabled": false,
      "publish-topic": "",
      "publish-file": "",
      "subscribe-topic": "",
      "subscribe-file": ""
    }
  },
  "config-shadow": {
    "enabled": false
  },
  "sample-shadow": {
    "enabled": false,
    "shadow-name": "",
    "shadow-input-file": "",
    "shadow-output-file": ""
  }
}

ステップ 2: AWS IoT でジョブを作成して実行する

ジョブのジョブドキュメントを作成して保存する

まずジョブの内容を示したジョブドキュメントをJSON形式で作成します。今回の内容はHello Worldの出力です。

{
    "operation": "echo",
    "args": ["Hello world!"]
}

このJSONは任意のS3にアップロードしておきます。この際にジョブドキュメントのURIをメモしておきます。

より具体的なジョブドキュメントについてはAWS IoT Device Clientのリポジトリにサンプルがあったので覗いてみるといいと思います。

https://github.com/awslabs/aws-iot-device-client/tree/main/sample-job-docs

AWS IoTで 1 つの IoT デバイスに対してジョブを実行する

ラズパイ側でAWS IoT Device Client を起動します。

./aws-iot-device-client --config-file ~/dc-configs/dc-jobs-config.json

ラズパイ側のターミナルに以下のようなログが表示されたら準備完了です。

2023-04-0XTXX:XX:XX.XXXZ [INFO]  {JobsFeature.cpp}: No pending jobs are scheduled, waiting for the next incoming job

ローカル側から以下のコマンドを実行します。

aws iot create-job \
        --job-id hello-world-job-1 \
        --document-source "<ジョブドキュメントのURI>" \
        --targets "<デバイスのARN>" \
        --target-selection SNAPSHOT

ラズパイ側で以下のようにジョブの実行が確認できました。

2023-04-07T10:31:45.524Z [DEBUG] {JobsFeature.cpp}: We have not seen a job yet, this is not a duplicate job notification
2023-04-07T10:31:45.524Z [DEBUG] {JobsFeature.cpp}: Attempting to update job execution status!
2023-04-07T10:31:45.525Z [INFO]  {JobsFeature.cpp}: Executing job: hello-world-job-1
2023-04-07T10:31:45.525Z [DEBUG] {Retry.cpp}: Retryable function starting, it will retry until success
2023-04-07T10:31:45.525Z [INFO]  {JobEngine.cpp}: About to execute step with name: echo
2023-04-07T10:31:45.525Z [DEBUG] {JobsFeature.cpp}: Created EphemeralPromise for ClientToken 7SlC3wUiQ7 in the updateJobExecution promises map
2023-04-07T10:31:45.525Z [DEBUG] {JobEngine.cpp}: Assuming executable is in PATH
2023-04-07T10:31:45.525Z [INFO]  {JobEngine.cpp}: About to execute: echo  Hello world!
2023-04-07T10:31:45.526Z [DEBUG] {JobEngine.cpp}: Child process now running
2023-04-07T10:31:45.526Z [DEBUG] {JobEngine.cpp}: Child process about to call execvp
2023-04-07T10:31:45.531Z [DEBUG] {JobEngine.cpp}: Parent process now running, child PID is 11214
2023-04-07T10:31:45.535Z [DEBUG] {11214}:  Hello world!
2023-04-07T10:31:45.536Z [DEBUG] {JobEngine.cpp}: JobEngine finished waiting for child process, returning 0
2023-04-07T10:31:45.536Z [INFO]  {JobsFeature.cpp}: Job exited with status: 0
2023-04-07T10:31:45.536Z [INFO]  {JobsFeature.cpp}: Job executed successfully!
2023-04-07T10:31:45.536Z [DEBUG] {JobsFeature.cpp}: Attempting to update job execution status!
2023-04-07T10:31:45.537Z [DEBUG] {Retry.cpp}: Retryable function starting, it will retry until success
2023-04-07T10:31:45.537Z [DEBUG] {JobsFeature.cpp}: Created EphemeralPromise for ClientToken 7ud3y1DPkb in the updateJobExecution promises map
2023-04-07T10:31:45.620Z [DEBUG] {JobsFeature.cpp}: Ack received for PublishUpdateJobExecutionStatus with code {0}
2023-04-07T10:31:45.621Z [DEBUG] {JobsFeature.cpp}: Removing ClientToken 7SlC3wUiQ7 from the updateJobExecution promises map
2023-04-07T10:31:45.621Z [DEBUG] {JobsFeature.cpp}: Success response after UpdateJobExecution for job hello-world-job-1
2023-04-07T10:31:45.630Z [DEBUG] {JobsFeature.cpp}: Ack received for PublishUpdateJobExecutionStatus with code {0}
2023-04-07T10:31:45.631Z [DEBUG] {JobsFeature.cpp}: Removing ClientToken 7ud3y1DPkb from the updateJobExecution promises map
2023-04-07T10:31:45.631Z [DEBUG] {JobsFeature.cpp}: Success response after UpdateJobExecution for job hello-world-job-1
2023-04-07T10:31:46.395Z [INFO]  {JobsFeature.cpp}: No pending jobs are scheduled, waiting for the next incoming job

マネジメントコンソールからもジョブの状態が確認できました。

おわりに

今回はAWS IoTのジョブ機能を使ってみました。イメージとしてはSSMのRun Commandのような印象を受けました。色々なことができそうなので活用方法を探っていければと思います。