【AWS IoT Jobs】有効期限付きのURLでファイルを配信する【S3 Presigned URL】

# はじめに
AWS IoT Jobsを使ってデバイスにファイルを配信する際には、デバイスからHTTPSでダウンロード可能なURLを指定することが多いかと思います。
公式ドキュメント上はS3 Presigned URLを使うことが想定されていますが、これはいったいどういうものか、なぜ使うのか、といった観点での説明が足りないように思えるので、これらを補足する意味で記事にまとめます。


# なぜS3 Presigned URLを使うのか
## 通信方向・プロトコル
まずはS3 Presigned URLのことを考える前に、なぜHTTPSを利用するのか、何故デバイスからGetする形でファイルを取得するのか、という点を考えます。

原理上は、デバイス側で何らかMQTT TopicをSubscribeしておいて、MQTT Brokerから当該TopicにファイルをPublishすることで、ファイルの送達は可能です。
しかしながらこれは、全てがうまく動いているとき／うまく動くと保証されているときのみ有効な手段です。まず、当該ファイルがPublishされる前に、デバイス側は確実にMQTT TopicをSubscribeした状態になっている必要があります。また、ファイルの送信中に通信が切断された場合は、リトライする必要があります。

またMQTT Brokerからデバイスに対してQoS1でメッセージを送信することで、メッセージのロストはなくなりますが、
AWSのMQTT Brokerでは、QoS1のメッセージを保持できる件数には限りがあります。

いずれにせよ、IoTシステムにおいて、デバイス側とクラウド側の通信処理を考える際には、「**すべてがうまく動かない**」ということを前提に考える必要があります。

この観点から出発すると、デバイス側からの操作を起点に考えた場合、以下の条件を満たす構成が一番都合がよいことに気づきます。

- ファイル配信に関する情報（JobDocument）はいつでも・何度でも取得可能
- 配信ファイル自体を取得する操作は、デバイスから何度でもリトライできる

AWS IoT Jobsの仕様においては、すでに前者の要件は考慮されています。
後者の要件を満たすためには、HTTPSでファイルをダウンロードできるようにしておいて、デバイス側からGetする、という方式がベストであるように見えます。

## セキュリティ
上記までの構成そのままでは、だれでも情報を取得できるという状態になります。
つぎに、予め許可されたデバイスに対してのみ、情報・ファイルの取得が可能なようにする方法を考えます。

### ファイル配信に関する情報（JobDocument）の取得
> - ファイル配信に関する情報（JobDocument）はいつでも・何度でも取得可能

まずこの操作は、デバイス側からMQTT over TLSで行われるを前提とします（AWS APIで取得することも可能ですが、ここでは考慮しません）。AWS IoTのMQTT BrokerとMQTT通信する際には、有効なクライアント証明書・秘密鍵を持っていないと行えませんので、ここでセキュリティを担保できることになります。
どのデバイスがどのMQTT Topicを操作できるかは、AWS IoTのPolicyで設定することが可能です

### 配信ファイルの取得
> - 配信ファイル自体を取得する操作は、デバイスから何度でもリトライできる

次に、上記の操作に関して、許可したデバイスのみファイルをダウンロードできるようにする方法を考えます。

まず、S3 Bucket上のファイルをダウンロードさせる場合、2つの方法があります。

- AWS S3 API経由で取得する（≒AWS CLIやAWS SDKを利用する）
- HTTPSを利用して取得する

前者の方法を利用する場合、デバイス側はAWS Credentials（AccessKey/SecretAccessKey、Temporary Credentialsの場合はさらにSessionToken）を持っている必要があります。

デバイスからのリクエストに応じてTemporary Credentialsを返却する、デバイスはこれを使ってAWS S3 API経由でファイルを取得する、という方法で原理上は実現可能です。
ただしこの方法は、デバイスからのリクエストに応じてTemporary Credentialsを返却する仕組みを作りこむ必要があり、少し大仰です。

つぎに、HTTPSでファイルを取得する方式を考えます。

クライアントに権限があるかどうかをチェックするために、MQTT over TLS用のクライアント証明書を利用できればベストですが、現状ではS3はそこまで対応していません（おそらく今後も対応されないと思います）。

では、クライアントからのリクエストに応じ、一時的に有効なダウンロードURL（HTTPSベース）を発行することはどうでしょうか。**S3 Presigned URL**を利用することで、これを実現できます。

ダウンロードURLはJobDocument内に記載されてデバイスに渡されます。JobDocumentを渡す操作において権限チェックが行われるため、セキュリティ上も問題ありません。

なお、S3 Presigned URLについての説明が長くなったので、下記記事に個別にまとめました。
[【AWS S3】S3 Presigned URLの仕組みを調べてみた - Qiita](https://qiita.com/tmiki/items/87697d3d3d5330c6fc08)


# 試してみる
## 環境を準備する
試した環境は以下のようになります。

- AWSアカウント
	- アカウントID: 4**********8
- S3
    - Bucket名: investigate-iot-jobs
    - 配信ファイル: s3://investigate-iot-jobs/example-distfile.bin
    - JobDocumentファイル: s3://investigate-iot-jobs/example-job-document.json
- IAM Role（配信ファイルのPresigned URLを生成するためのRole）
    - role-investigate-iot-jobs
    - Permissions: 後述するinline policyを付与
- AWS IoT Thing（配信対象のデバイス）
    - Thing Name: example-device01

```json:role-investigate-iot-jobsのinline&nbsp;policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::investigate-iot-jobs/*"
        }
    ]
}
```

S3 Bucketには、配信ファイルとJobDocumentファイルが必要です。

まず、JobDocumentファイル「example-job-document.json」として下記JSONファイルを作成します。

```json:example-job-document.json
{
	"distFileUrl": "${aws:iot:s3-presigned-url:https://s3.amazonaws.com/investigate-iot-jobs/example-distfile.bin}"
}
```

example-distfile.binは、1MBのバイナリファイルを生成したものとなります。このファイルは何でもよいです。

```
$ dd if=/dev/zero of=example-distfile.bin bs=1024 count=1024
1024+0 records in
1024+0 records out
1048576 bytes (1.0 MB) copied, 0.00191433 s, 548 MB/s

$ ls -la example-distfile.bin
-rw-rw-r-- 1 vagrant vagrant 1048576 Apr 14 09:51 example-distfile.bin
```

上記2つのファイルとS3 Bucket格納します。最終的に下記のように2つのファイルが格納されます。

```
$ aws s3 ls --recursive  s3://investigate-iot-jobs/
2019-04-14 09:52:20    1048576 example-distfile.bin
2019-04-14 09:54:59        120 example-job-document.json
```

AWS IoT Jobsでのファイル配信対象となるデバイスが必要ですので併せて作成します。
example-device01という名称で作成しました。

```:aws&nbsp;iot&nbsp;create-thing
$ aws iot create-thing --thing-name example-device01
{
    "thingArn": "arn:aws:iot:ap-northeast-1:4**********8:thing/example-device01",
    "thingName": "example-device01",
    "thingId": "81******-****-****-****-**********f6"
}
```

## IoT Jobを作成する
この作業はAWS Management Consoleから行ってみます。
以下の内容でJobを作ります。

- JobID: example-job01
- Select devices to update: example-device01
- Add a job file: s3://investigate-iot-jobs/example-job-document.json
- Pre-sign resource URLs
    - I want to pre-sign my URLs and have configured my job file.
    - Role: role-investigate-iot-jobs

まず、下記のURL等からAWS IoT CoreのDashboardを開きます。
https://ap-northeast-1.console.aws.amazon.com/iot/home?region=ap-northeast-1#/dashboard

次に、「Manage」->「Jobs」を選択します。遷移した画面で「Create a job」ボタンを押します。
![2019-04-14_19h13_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137777/6a118610-5c1b-11a1-5b05-88511e61e2b1.png)

どのようなジョブを作成するかを選択する画面になりますので「Create custom job」ボタンを押します。
![2019-04-14_19h13_09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137777/54dd086a-35f3-154d-aee0-66bb6fedaa81.png)

具体的な値を設定する画面に遷移しますので、上述した内容でJobを作っていきます。入力したら「Next」ボタンを押します。
![2019-04-14_19h16_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137777/089a6421-ccf1-c3dd-885b-0a5fd54e2d87.png)

次のページは、特に変更する点はありません。そのまま「Create」ボタンを押します。
![2019-04-14_19h16_43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137777/d5f7a3b4-d7f6-9038-4c39-b226154b2022.png)

作成が完了すると、以下のように作成されたJobが表示されます。
![2019-04-14_19h16_49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137777/6ee3cc6d-21ab-e284-2332-737549250168.png)

Jobをクリックすると、詳細を参照することが可能です。
![2019-04-14_19h16_58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137777/3dbdfe5c-89f5-487f-267b-4a1bfc4cb67a.png)

## 作成したIoT JobをAWS CLIから参照する
AWS CLIからは、iot list-jobsおよびiot describe-jobで参照することができます。

```:aws&nbsp;iot&nbsp;list-jobs
$ aws iot list-jobs
{
    "jobs": [
        {
            "status": "IN_PROGRESS",
            "jobArn": "arn:aws:iot:ap-northeast-1:4**********8:job/example-job01",
            "jobId": "example-job01",
            "lastUpdatedAt": 1555237008.749,
            "targetSelection": "SNAPSHOT",
            "createdAt": 1555237002.922
        }
    ]
}
```

```:aws&nbsp;iot&nbsp;describe-job
$ aws iot describe-job --job-id example-job01
{
    "documentSource": "https://investigate-iot-jobs.s3.ap-northeast-1.amazonaws.com/example-job-document.json",
    "job": {
        "status": "IN_PROGRESS",
        "jobArn": "arn:aws:iot:ap-northeast-1:4**********8:job/example-job01",
        "jobProcessDetails": {
            "numberOfQueuedThings": 1,
            "numberOfInProgressThings": 0,
            "numberOfSucceededThings": 0,
            "numberOfTimedOutThings": 0,
            "numberOfCanceledThings": 0,
            "numberOfFailedThings": 0,
            "numberOfRemovedThings": 0,
            "numberOfRejectedThings": 0
        },
        "presignedUrlConfig": {
            "expiresInSec": 300,
            "roleArn": "arn:aws:iam::4**********8:role/role-investigate-iot-jobs"
        },
        "jobId": "example-job01",
        "lastUpdatedAt": 1555237008.749,
        "targetSelection": "SNAPSHOT",
        "timeoutConfig": {},
        "jobExecutionsRolloutConfig": {
            "maximumPerMinute": 1000
        },
        "targets": [
            "arn:aws:iot:ap-northeast-1:4**********8:thing/example-device01"
        ],
        "createdAt": 1555237002.922
    }
}
```

## デバイスからJobの情報を取得する
ではいよいよ、デバイスからMQTT経由で作成したJobの情報を取得します。
ただし、実際に組み込み系デバイスでMQTT経由でJobを取得するためには、それ相応のプログラムを実装する必要があり、冗長です。
本稿では、AWS Management ConsoleのMQTTクライアント機能を利用し、デバイスから行う操作を疑似的に再現することとします。

デバイスからJobを取得するフローは、下記ドキュメントの「DescribeJobExecution」に従います。

Using the AWS IoT Jobs APIs - AWS IoT -> DescribeJobExecution
https://docs.aws.amazon.com/iot/latest/developerguide/jobs-api.html#mqtt-describejobexecution

これはjobIdで指定したJobの内容を取得することができます。
MQTTのTopic名にjobIdを指定しますが、「$next」という特殊な文字列を設定することで、当該Thingに割り当てられたJobのうち実行待ちのもの（statusがIN_PROGRESSかQUEUEDのもの）のを対象とすることができます。


まず、AWS IoT CoreのDashboardから「Test」をクリックし、MQTTクライアントを実行します。
以下のMQTT TopicをSubscribeします。「Subscription topic」にSubscribeしたいTopicを入力し「Subscribe to topic」ボタンを押します。

- \$aws/things/example-device01/jobs/$next/get/accepted
- \$aws/things/example-device01/jobs/$next/get/rejected

![2019-04-15_00h29_40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137777/5a8bfc7a-cff3-9ad0-900d-7558b86b1c82.png)
![2019-04-15_00h30_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137777/6bf36e58-3ab7-d4b6-de8b-1241ff5d6887.png)

次に、「Publish to a topic」リンクをクリックします。
「Specify a topic and a message to publish with a QoS of 0.」の欄に下記のTopic名を入力します。
メッセージ本文は何でもよいです。ここでは空オブジェクト「{}」を指定します。
入力後、「Publish to topic」ボタンをクリックします。

- \$aws/things/example-device01/jobs/$next/get

すると、「\$aws/things/example-device01/jobs/$next/get/accepted」TopicにメッセージがPublishされます。
![2019-04-15_00h34_09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137777/80d83984-c4b5-cbbe-ac49-d6c8b0434c8f.png)

「\$aws/things/example-device01/jobs/$next/get/accepted」Topicを見てみると、下記のようなJSONドキュメントがPublishされていることが分かります。
![2019-04-15_00h42_33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137777/317ba617-998b-a3bb-1a82-7d2f95804d90.png)

```json:\$aws/things/example-device01/jobs/$next/get/accepted
{
  "timestamp": 1555256042,
  "execution": {
    "jobId": "example-job01",
    "status": "QUEUED",
    "queuedAt": 1555237004,
    "lastUpdatedAt": 1555237004,
    "versionNumber": 1,
    "executionNumber": 1,
    "jobDocument": {
      "distFileUrl": "https://investigate-iot-jobs.s3.ap-northeast-1.amazonaws.com/example-distfile.bin?X-Amz-Security-Token=AgoGb3JpZ2luEGMaDmFwLW5vcnRoZWFzdC0xIoACYYh9OutqTnIbbvusQBKUckbKpj6MHQj3UQ7CzQUAIHc%2F5XkrAWB5lF5ZqG%2Ff16K25ZzSIHcbx9o62M1e0f8k2gY1ere2rGjt4REKavic6I%2BjpvQzUyTXaoZTTbbEwRc8QgrwwPgy5Qt6EdxC5z3zDchn3t%2FzKj5l1RgQ7fxwisics%2Fmiy80xI%2FYJ2CS2gIV3mzPbNaUAq1%2FoPxEGxPhMVKXztsTp70%2Fny0FSoaDkI4xW1VomKmFvnG%2Fz5F5KVNHOSZdEwyvzvgqXE9iuAubHdBKsTQoNCYHfLEoQ3Cn%2Beg92r6ysnrZgRWevLJlF9v2ae7IlPmV7%2B9uGjbA3FdHmGyqPAgjZ%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAAaDDQ5NzMzOTA2MzAwOCIMYC6D7wfC2Yvm23yxKuMBfBo17VFQlPA%2F64fqRyYGdofOpIQskk1V3dMsMA%2Bu2PMusYItU5%2F5bTUu%2Fh7s1%2FFN4gPal6Y1cN4S4UzqVBWRq7siOzm%2Bc0yte%2BSHTE6YxcfphgOBJ6PwCfuZa9t3%2FlUCTiTYposI8yxaVsJk8Sb%2BG8jVLXLgJDMrAchhGGR2wlRGGd92bIKMheuK5en1Jxucucvfoi9BFmJAE2zWhmQNzw1j3LYa%2BbtyTqezBZQg1UW7D1wdHwqMu1zm37MI6jLpVi%2BJOkIEAXr123MX0SkpCwJKGwHFwQsduTovZ9UsxfwoSSww6qXN5QU%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20190414T153402Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Credential=ASIAXHS5X3LQE6XTCHWH%2F20190414%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Signature=5e93dbde98e7c15bb603eec07f09ae796e4423942e26e413fb36cc13f670f068"
    }
  }
}
```

ここで重要な項目は「execution.jobDocument」になります。これは先ほど作ってS3上に格納したもJobDocumentファイルの内容が入ります。
ただし、1点大きく違う点があります。「distFileUrl」の値が書き換わり、実際のPresigned URLになっていることです。
ちなみに、Job作成時に有効期限を「5 minutes」に設定しているので、上記のPresigned URLの有効期限は5分になります。

もともとのファイルは下記の内容でした。「\${aws:iot:s3-presigned-url:.......}」の項目が、実際のPresigned URLに置き換わっています。

```json:example-job-document.json
{
	"distFileUrl": "${aws:iot:s3-presigned-url:https://s3.amazonaws.com/investigate-iot-jobs/example-distfile.bin}"
}
```


## JobDocumentに記載のPresigned URLの仕様
まずPresigned URLは、MQTT経由で取得した場合のみ置き換えられます。

Using the AWS IoT Jobs APIs - AWS IoT -> GetJobDocument
https://docs.aws.amazon.com/iot/latest/developerguide/jobs-api.html

GetJobDocumentというAPIがありますが、ここのNoteにその旨、記載があります。

> GetJobDocument
> Gets the job document for a job.
> 
> Note
> 
> Placeholder URLs are not replaced with presigned Amazon S3 URLs in the document returned. Presigned URLs are generated only when the AWS IoT Jobs service receives a request over MQTT.


Presigned URLは、デバイスから取得のリクエストが行われた場合に生成されます。
したがって、Presigned URLの有効期限が切れた場合は、デバイス側から再度JobDocumentを取得することで、新たなPresigned URLを得ることができます。

Managing Jobs - AWS IoT
https://docs.aws.amazon.com/iot/latest/developerguide/create-manage-jobs.html

> Managing Jobs
>
> When a device requests the job document, AWS IoT generates the presigned URL and replaces the placeholder URL with the presigned URL. Your job document is then sent to the device.

# おわりに
現実的にAWS IoTを利用してリモートデバイスをコントロールする場合、MQTTが主な通信手段となるでしょう。この構成でファイル配信する場合、JobDocumentにPresigned URLを記載し、デバイスからダウンロードさせる方式が現時点ではベストな方式になると思います。