IoT Jobs

IoT Job はリモートでの一連の操作を1つまたは複数のデバイスに送信して実行するサービスです。

ジョブで利用するトピック

AWS IoT ではジョブで利用するための MQTT の API として以下のトピックを提供しています。

  • $aws/things/<thingName>/jobs/notify (or $aws/things/<thingName>/jobs/notify-next)
  • $aws/things/<thingName>/jobs/get/accepted
  • $aws/things/<thingName>/jobs/get/rejected
  • $aws/things/<thingName>/jobs/jobId/get/accepted
  • $aws/things/<thingName>/jobs/jobId/get/rejected

なお、デバイス側でジョブを処理するプログラムのワークフローやサンプルについては、下記 URL が参考になります。

ここでは、サンプルのジョブを作成し、エミュレータ (device_emulator.py) を使ってジョブを実行します。このエミュレータを使うと、バルクでプロビジョニングした全デバイスが IoT Core と接続し、ジョブの実行を待ち受けます。

エミュレータは、ジョブドキュメントの内容に応じて異なる動きをします。エミュレータは MQTT の API からジョブを取得し、ジョブの実行結果をレポートします。詳しくは device_emulator.py をご参照ください。

以下のコマンドを実行するとエミュレータが起動します。コマンド中の bulky-NAME-YYYY-MM-DD_H-M-S の部分は、3-1. Bulk Device Provisioning で作成されたフォルダ名に変更してください。

# ホームディレクトリへ移動します
cd
# エミュレータを実行します
./device_emulator.py -d bulky-NAME-YYYY-MM-DD_H-M-S -i $IOT_ENDPOINT --cacert ~/root.ca.bundle.pem

サンプルのジョブドキュメント

以下のジョブドキュメントが ~/job-agent/job-document-sysinfo.json に配置されています。

{
    "operation": "sys-info",
    "sys-info": "uptime",
    "topic": "sys/info"
}

上記のドキュメントは、デバイスに対してシステムの起動時間を問い合わせ、AWS IoT の sys/info というトピックに結果をレポートすることを指示しています。

Cloud9 でデバイスのエミュレータを実行しているターミナルとは別のターミナルを開き、以下のコマンドを実行します。ジョブドキュメントを S3 バケットにコピーし、コピーされたことを確認します。

image
cd
aws s3 cp job-agent/job-document-sysinfo.json s3://$S3_BUCKET/

aws s3 ls s3://$S3_BUCKET/

NAMEの環境変数を再度設定します。 続いて、ジョブを作成します。ジョブの対象に3-1でバルクプロビジョニングで作成したグループを指定します。

NAME=[YOUR_NAME]  # 例: NAME=watanabe
# モノの ARN は IoT Core のマネジメントコンソールや aws iot list-things で確認できます
THING_GROUP_ARN=$(aws iot describe-thing-group --thing-group-name bulky-group-$NAME | jq -r '.thingGroupArn')

# ジョブを作成します
aws iot create-job --job-id  $NAME-`date +%Y%m%d-%H%M%S` \
    --targets $THING_GROUP_ARN \
    --document-source https://s3.amazonaws.com/$S3_BUCKET/job-document-sysinfo.json  \
    --presigned-url-config "{\"roleArn\":\"$ARN_IOT_PROVISIONING_ROLE\", \"expiresInSec\":3600}"

以下のような出力が返ってきます。

{
    "jobArn": "arn:aws:iot:[AWS_REGION]:[AWS_ACCOUNT_ID]:job/$JOB_ID",
    "jobId": "$JOB_ID"
}

もう一つのターミナルで、デバイスエミュレータの出力を確認しましょう。

ジョブがないときは以下のような出力が表示されます。

device_emulator.bulky-NAME-Prod-N: NO jobs available

ジョブを作成すると、以下の行を含む出力が表示されます。

device_emulator.bulky-NAME-Prod-N: JOBS AVAILABLE

続いて、いくつかの出力が続いて出力されます。

# 受け取ったジョブのIDと内容
019-10-10 23:50:59,383 INFO: device_emulator: job_id: $JOB_ID job_document: {'operation': 'sys-info', 'sys-info': 'uptime', 'topic': 'sys/info'}
...

# ジョブで要求された出力(uptime)をトピックに配信
2019-10-10 23:51:05,390 INFO: device_emulator: PUBLISH UPTIME - topic: sys/info message: {'thing-id': 'bulky-NAME-Prod-N', 'uptime': '1 day, 22:07:22.070000'}

# ジョブの完了(成功)を通知
2019-10-10 23:51:08,394 INFO: device_emulator: UpdateJobExecution: topic: $aws/things/bulky-NAME-Prod-N/jobs/JOB_ID/update message: {'status': 'SUCCEEDED', 'clientToken': '87c20aec-7565-4822-9d3b-e30232cc6d02'}

# 再びジョブ待ちの状態に遷移
2019-10-10 23:51:18,364 INFO: device_emulator.bulky-NAME-Prod-N: NO jobs available

ジョブの経過を確認

job-document-upgrade.json を開いてみましょう。

{
    "operation": "upgrade",
    "firmware_version": "v1.2",
    "URL": "https://example.com/firmware/v1.2.zip"
}

今回は、デバイス側で実行に時間がかかるジョブを実行し、デバイス側の進捗状況を確認します。

このジョブドキュメントを再度 S3 のバケットにコピーし、グループに対してジョブを実行してみましょう。

aws s3 cp job-agent/job-document-upgrade.json s3://$S3_BUCKET/

aws iot create-job --job-id $NAME-upgrade-`date +%Y%m%d-%H%M%S` \
    --targets $THING_GROUP_ARN \
    --document-source https://s3.amazonaws.com/$S3_BUCKET/job-document-upgrade.json  \
    --presigned-url-config "{\"roleArn\":\"$ARN_IOT_PROVISIONING_ROLE\", \"expiresInSec\":3600}"

IoT Core のマネジメントコンソールを開き、管理 -> ジョブ -> 作成した Job ID からジョブの状況を確認しましょう。

このジョブは2割の確率で失敗するようになっているので、結果を見ると、いくつかのデバイスが失敗になっている場合があります。また、ジョブが成功したデバイスのシャドウを見ると、firmaware_versionv1.2 に更新されていることが確認できます。

image
※ 特定のデバイスでジョブがキューのまま実行されない場合は、device_emulator を Ctrl + C で一旦止めて、再度実行してみてください。

ジョブの途中中断

job-document-reject.json を開いてみましょう。

{
    "operation": "unknown"
}

このジョブはデバイス側で未定義のオペレーションを指定しているため、デバイス側から拒否されます。

一定の割合以上ジョブが失敗したり拒否された場合に、ジョブを途中で中断することができます。

このジョブドキュメントを同様に S3 のバケットにコピーし、以下の設定でジョブを実行してみましょう。

aws s3 cp job-agent/job-document-reject.json s3://$S3_BUCKET/

aws iot create-job --job-id $NAME-reject-`date +%Y%m%d-%H%M%S` \
    --targets $THING_GROUP_ARN \
    --document-source https://s3.amazonaws.com/$S3_BUCKET/job-document-reject.json  \
    --presigned-url-config "{\"roleArn\":\"$ARN_IOT_PROVISIONING_ROLE\", \"expiresInSec\":3600}"  \
    --job-executions-rollout-config maximumPerMinute=1  \
    --abort-config "criteriaList=[{failureType=REJECTED,action=CANCEL,thresholdPercentage=50,minNumberOfExecutedThings=2}]"

device_emulator を実行しているターミナルを確認すると、先ほどの upgrade のジョブではバルクで生成した全デバイスに対して一気にジョブが実行されたのに対して、こちらでは、まず1台のデバイスのみに対してジョブが実行されることが確認できます。

そして、2台のデバイスに対してジョブが失敗した時点でジョブがキャンセルされ、それ以降はデバイスでジョブが実行されないことが確認できます。

Continuous Job

Continuous Job を使うと、作成されたジョブは終了せず、対象としたグループに後から追加されたデバイスに対してデプロイを続行します。例えば、デバイスのバッテリー残量が80%以上で、ファームウェアバージョンがv1.0という動的グループを作成し、この動的グループに対してジョブを作成することで、バッテリー残量が少ないデバイスに対しては、充電が一定値を上回った場合のみにファームウェア更新を実行することができるようになります。

先ほど作成した動的グループに対して、upgrade のジョブを継続ジョブとして設定しましょう。

THING_GROUP_ARN=$(aws iot describe-thing-group --thing-group-name "Dynamic_${NAME}_v1" | jq -r '.thingGroupArn')

aws iot create-job --job-id  $NAME-continuous-`date +%Y%m%d-%H%M%S` \
    --targets $THING_GROUP_ARN \
    --document-source https://s3.amazonaws.com/$S3_BUCKET/job-document-upgrade.json  \
    --presigned-url-config "{\"roleArn\":\"$ARN_IOT_PROVISIONING_ROLE\", \"expiresInSec\":3600}"  \
    --target-selection CONTINUOUS

AWS IoT Core のコンソールから、bulky-NAME-PoC-N のデバイスの属性の hardwareModelv1.0 に変更すると、変更したデバイスに対してもジョブが適用されることを確認しましょう。

image
image

署名付き URL

デバイスをセキュリティで保護し、ジョブドキュメント自体に含まれるデータ以外のデータに時間制限付きでアクセスできるようにするには、署名付き Amazon S3 URL を使用できます。例えば、ファームウェアの更新データを Amazon S3 バケットに配置し、ジョブドキュメント内のデータにプレースホルダーリンクを追加することができます。ジョブサービスは、ジョブドキュメントのリクエストを受け取ると、ジョブドキュメントを解析してプレースホルダーリンクを探し、それを署名付き Amazon S3 URL に置き換えます。

例えば以下のようなジョブドキュメントを作成することで、署名付き URL を利用できます。

{
    "operation": "upgrade",
    "version": "v1.2",
    "URL": "${aws:iot:s3-presigned-url:https://s3.amazonaws.com/YOUR_BUCKET/path/to/your/file.zip}"
}