目次
AWS IoT Jobsでファイルを有効期限付きURLで配布したい
デバイスのファームウェアのアップデートといった用途で、AWS IoT Jobsを使ってファイルをデバイスに配布できるか、またその配布に有効期限を付けられるかを確認したかったので調べてみました。しかし、AWSマネジメントコンソールで一連の操作を完結する記事を見つけられませんでした。そこで、これからAWS IoTを使う方向けに、この一連の操作をAWSマネジメントコンソールで行った場合の手順をお伝えします。
環境
以下の環境で試しました。
- Windows 10 (お試しなのでデバイスを模して普段使うラップトップを使います)
- Node.jsインストール済み (サンプルプログラムで使います)
- ポート8883を開放済み (AWS IoT CoreとMQTTで通信するのに使用します)
SDKの準備
AWS IoT Coreサービスを開き、左ペインの「オンボード」の「開始方法」をクリックします。
「開始方法」をクリックします。
「開始方法」をクリックします。
プラットフォームの選択は「Windows」を、AWS IoT デバイスSDKの選択は「Node.js」を選択します。
任意の名前を入力します。
「Windows」をクリックして接続キットのダウンロードを開始します。
ダウンロードした圧縮ファイルを解凍します。圧縮ファイルには以下のファイルが含まれています。
ファイル名 | 説明 |
---|---|
miso_things_1.cert.pem | クライアント証明書 |
miso_things_1.private.key | クライアント証明書の秘密鍵 |
miso_things_1.public.key | クライアント証明書の公開鍵 |
start.sh | メッセージを送受信するスクリプト |
「次のステップ」をクリックすると、デバイスの設定とテストの画面になります。
接続キットを解凍したフォルダでPowerShellを開き、以下のコマンドで実行権限を付与します。
1 |
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process |
AWS IoT SDKをインストールします。
1 2 |
npm init npm install aws-iot-device-sdk |
開始スクリプトstart.ps1を起動します。
1 |
.\start.ps1 |
デバイスからのメッセージが表示されます。ここで失敗する場合は、ファイアウォールのポート開放ができていないか、開発環境のセキュリティポリシーによりはじかれている可能性があります。
デバイスへメッセージを送信してみます。
送信したメッセージがPowerShell上に表示されます。
1 2 3 4 5 |
Downloading AWS IoT Root CA Certificate from AWS... Running pub/sub sample application... connect message topic_1 Hello, miso_thing1 |
次の画面に進み、「完了」をクリックします。
モノが追加できたことを確認できます。
ここまででモノの追加、AWS IoT SDKのインストール、AWS IoT Coreとの送受信の確認ができました。
S3バケットの準備
S3バケットを準備します。このバケットには、AWS IoT Coreのジョブの内容を記載するジョブドキュメントとデバイスに配信するファイルを置きます。
S3のサービスを開き、「バケットを作成する」をクリックします。
任意のバケット名を入力します。バケット名はS3の既存バケットの中で一意の名前である必要があります。「次へ」をクリックします。
オプションの設定はデフォルトのまま「次へ」をクリックします。
アクション許可の設定はデフォルトのままにし、「次へ」をクリックします。
「バケットを作成」をクリックします。
バケットが作成されました。
IAMロールの準備
AWS IoT Coreサービスが、Amazon S3の特定のバケットからダウンロードできるようにするためのIAMロールを作成します。
ポリシーの作成
S3からのダウンロードを許可するIAMポリシーを作成します。IAMポリシーとは、AWSリソースにアクセスするための権限設定のことです。デフォルトでAWSが様々なポリシーを用意してくれていますが、特定のAmazon S3のバケットからのダウンロードだけを許可するポリシーがないため新たに用意してみます。
IAMサービスを開き、IAMサービスの左ペインの「ポリシー」をクリックします。
「ポリシーの作成」をクリックします。
「サービスの選択」をクリックします。
検索欄に「S3」と入力し、表示された「S3」をクリックします。
次にアクションを開き、検索欄に「ListBucket」と入力し、「ListBucket」をチェックします。
今度は検索欄に「GetObject」と入力し、「GetObject」をチェックします。
次にリソースを開き、「bucket」の「ARNの追加」をクリックします。
「Bucket name *」の入力欄にアクセスを許可したいバケット名を入力し、「追加」をクリックします。
「object」の「ARNの追加」をクリックします。
「Bucket name *」に先程と同じバケット名を入力し、「Object name *」は「すべて」をチェックします。
「ポリシーの確認」をクリックします。
「名前」に任意の名前を入力します。「説明」にはポリシーの説明を設定します。その後、「ポリシーの作成」をクリックします。
ポリシーが作成されました。
ロールの作成
左ペインの「ロール」をクリックし、「ロールの作成」をクリックします。
「S3」のサービスをクリックします。
ユースケースの選択で「S3」を選択し、「次のステップ:アクセス権限」をクリックします。
検索欄に先程作成したポリシー名を入力し、表示されたポリシーをチェックします。その後、「次のステップ:タグ」をクリックします。
デフォルトのまま「次のステップ:確認」をクリックします。
ロール名とロールの説明に任意の内容を入力し、「ロールの作成」をクリックします。
ロールが作成されました。
ロールの信頼関係の編集
IoT Coreのモノは、ここで作成したロールの権限を取得することになりますので、IoT Coreのサービスを信頼するように設定する必要があります。
先程作成したロールをクリックします。
「信頼関係」タブを開き、「信頼関係の編集」をクリックします。
デフォルトでは以下のように信頼関係が設定されています。
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "s3.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } |
信頼するサービスに「iot.amazonaws.com」を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "s3.amazonaws.com", "iot.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } |
「信頼ポリシーの更新」をクリックします。
信頼されたエンティティにIoT Coreのサービスが登録されました。
ロールエイリアスの作成
AWSのサービスを直接呼び出すデバイスは、 IoT Coreに接続するときにどのARNロールを使用するかを知る必要があります。ARNロールをハードコーディングすると、ロールのARNが変わるたびにデバイスを更新することになってしまい、手間です。そのため、ロールエイリアスを用意します。そうすることで、ロールのARNが変更された場合には、ロールエイリアスを更新するだけで済みます。
IoT Coreのサービスを開き、左ペインの「安全性」の「ロールエイリアス」を開き、「ロールエイリアスを作成」をクリックします。
名前に任意の内容を入力し、エイリアスのロールは先程作成したロールを選択します。その後、「ロールエイリアスを作成」をクリックします。
ロールエイリアスが作成されました。
デバイス証明書にポリシーをアタッチ
ポリシーをデバイス証明書にアタッチします。デバイス証明書にアタッチされているポリシーは、デバイスにロールを引き受ける権限を付与する必要があります。「iot:AssumeRoleWithCertificate」アクションへのアクセス許可をロールエイリアスに付与します。
AWS IoT Coreの左ペインの「安全性」>「ポリシー」をクリックし、「作成」をクリックします。
任意の名前を入力し、アクションに「iot:AssumeRoleWithCertificate」と入力します。リソースARNには自動で入力されます。ここに表示される数字列はAWSのアカウントIDです。
リソースARNの「topic/replaceWithATopic」を「rolealias/先程作成したロールエイリアス名」に変更します。「許可」をチェックし、「作成」をクリックします。
「ステートメントの追加」をクリックし、同様の手順で以下の表のアクションを追加します。※iot:AssumeRoleWithCertificateは上記手順で登録済み
アクション | リソースARN |
---|---|
iot:AssumeRoleWithCertificate | arn:aws:iot:ap-northeast-1:[AWSアカウントID]:rolealias/ |
iot:Connect |
arn:aws:iot:ap-northeast-1:[AWSアカウントID]:client/miso* |
iot:Publish |
arn:aws:iot:ap-northeast-1:[AWSアカウントID]: |
iot:Subscribe |
arn:aws:iot:ap-northeast-1:[AWSアカウントID]: |
iot:Receive |
arn:aws:iot:ap-northeast-1:[AWSアカウントID]: |
ポリシーが作成されました。
証明書をアタッチするモノを開きます。
「セキュリティ」を開きます。
証明書を開きます。
「ポリシー」を開き、「ポリシーのアタッチ」をクリックします。
先程作成したポリシーをチェックし、「アタッチ」をクリックします。
ポリシーがアタッチされました。
ジョブ
ファイルダウンロードのジョブを準備し、実際にジョブを実行してみます。
S3にファイルを作成
S3のバケットを開き、「アップロード」をクリックします。
ファームウェアのファイルを模したファイルを用意します。お試しなのでファイルの中身は以下のようにしました。
1 |
This is the file to try downloading from S3. |
S3にドラッグアンドドロップします。
「アップロード」をクリックします。
ファイルのアップロードができました。
次にジョブドキュメントを以下のように作成します。ジョブドキュメントは、UTF-8 でエンコードされた JSON ドキュメントで、デバイスがジョブを実行するために必要な情報を含みます。ジョブドキュメントは、S3 バケットに格納することも、ジョブを作成するコマンドにインラインで含めることもできます。ジョブドキュメントの例は、AWS IoT SDK for JavaScriptのjobs-agent.js が参考になります。
1 2 3 4 5 |
{ "operation": "update", "version": "1", "url": "${aws:iot:s3-presigned-url:https://s3.amazonaws.com/miso/firmware_v1.txt}" } |
- operation
- AWS IoT SDKにどういうジョブを実行するのか指定できます。プログラムの中でジョブごとにやらせたいことを定義できます。
- version
- バージョン番号を想定した項目です。このようにAWS IoT SDKに通知したい項目を自由に追加できます。
- url
- デバイスをセキュリティで保護し、ジョブドキュメント自体に含まれるデータ以外のデータに時間制限付きでアクセスできるようにするために、署名付きAmazon S3 URLを使用します。データをAmazon S3 バケットに配置し、ジョブドキュメント内のデータにプレースホルダーリンクを追加することができます。AWS IoT Coreのジョブサービスが、ジョブドキュメントのリクエストを受け取ると、このジョブドキュメントを解析してプレースホルダーリンクを探し、それを署名付きAmazon S3 URLに置き換えます。
以下の形式で使用します。
1 |
${aws:iot:s3-presigned-url:https://s3.amazonaws.com/bucket/key} |
bucketはS3バケット名、keyはオブジェクト名を指定します。デバイスに通知される署名付きAmazon S3 URLは以下のような形になります。
1 |
https://miso.s3.ap-northeast-1.amazonaws.com/firmware_v1.txt?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEHYaDmFwLW5vcnRoZWFzdC0xIkgwRgIhAPdhX4PXjREAMicBsCWNseaJQ2uVjd1ipNZvzjtgs4BkAiEAhzU1LJucpL9%2Fw4K6GLv%2Fjjdt8rX6pLKlqjdXTDUU%2Bigq3QIIcBAAGgw4MTU1ODEyODI4MDAiDMZkSBoFbP0QTPvbyCq6AmlDLQuMSDPY8UmbjQJAqBShI0AjDnpOJUBvE9qPYFFlxom%2FzP58nEaFcxJX0GekH%2BQK1%2Fr7GjNsgXFK6ujZG9mmXIEO6DCVuo70fCWPr9d0Kr6hrVrTmOnC02OC9fsAWUWq8tr5IKkpaGA0qJv1NXOaRdn4v6tumhUNwcAANmV1J5eyfC2Vrv%2BsGgFyaPiv5HvBnbPAEHUJi%2BCqZMv3EJlyWhNFAe4krtJrr2g2Wsthib1MLyQW%2FS1UEM9DZvsIbP71DZn5zILx3X76HT7nvrv6EL9%2FOOtsmcemFTYoPm6EzoJNcNH4Rmk9lx63%2BWyJV2ePZJJQhPOLWn0XFXKzcLRcXyckXytw0X%2Fy26eCwGXQ6GNwNDqMi%2FhdbkNLVKQCj%2FtCI1PSoJYJIo7cG9c1Rgieq2PqUEY8kIHOMITRt%2FoFOr4BPVS%2BTFp9PFXO%2B8Y%2FRM7JdkI1xC%2BMnqCF4XpWsx0rjs9RPc7dUs%2Fd%2F6YHx%2FyqidPJ%2BMem7rTNScxbroc3X7NfQPlcZjiDAkJ%2FS6RVnNjXrSq4AGJEGoc8VL6veIVUtb%2FiMLVT1041x5k1de88Cv0L95O%2F3%2FKTRoGq67Z2S760qKmsgvj5C9DYmASaF6m96HTHPP5MEL3gUNq5o5wg%2BiXtLoqmTUYqY9TrQXiT9%2F%2BJaDLNOum96PU9CswG8g%2BbtA%3D%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20200901T062156Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Credential=ASIA33ZDOYHYLOMO7543%2F20200901%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Signature=d2019627b0b3fb909eeb90c3b8dbe8d0825ed4d5f2d42218429ce3e91c8a3086 |
先程の要領で作成したジョブドキュメントをS3バケットにアップロードします。
ジョブの作成
実際にジョブを作成して、ジョブを実行してみます。
IoT Coreサービスの「ジョブ」を開き、「ジョブを作成する」をクリックします。
「カスタムジョブの作成」をクリックします。
「ジョブID」と「説明」に任意の内容を入力し、「更新するデバイスの選択」の「選択」をクリックします。
対象のモノをチェックし、「ジョブファイルの追加」の「選択」をクリックします。
先程作成したジョブドキュメントを選択します。
「事前署名リソースURL」を「ジョブファイルは構成済みでありURLの事前署名を行う」を選択し、S3読み込み用のロールを選択します。その後、「次へ」をクリックします。
高度な設定はデフォルトのまま「作成」をクリックします。
ジョブの作成が完了しました。もしもここで失敗する場合は、IAMロールの信頼関係の設定誤りの可能性があります。作成したジョブを開きます。
現在のジョブの状況が表示されます。
ジョブを実行するNode.jsのプログラム
署名付きAmazon S3 URLからダウンロードするプログラムを以下のように作成してみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
const jobsModule = require("./node_modules/aws-iot-device-sdk").jobs; const cmdLineProcess = require("./node_modules/aws-iot-device-sdk/examples/lib/cmdline"); const isUndefined = require("./node_modules/aws-iot-device-sdk/common/lib/is-undefined"); const https = require("https"); const fs = require("fs"); function jobsConnect(args) { var thingName = args.thingName; /* S3 のバケットにいれた JSON に定義した operation と同じにすると operatioName を 引数に持つ subscribeToJobs() が呼び出されます。同じにしない場合 operationName を引数に持たない subscribeToJobs() が呼び出されます。 */ var operationName = "update"; /* jobs モジュールは MQTT インスタンスをエクスポートし、引数で指定されたAWS IoT エンドポイントへの接続を試みます。接続されると、アプリケーションが処理できる イベントが発行されます。 */ const jobs = jobsModule({ // クライアント証明書の秘密鍵ファイルのパス keyPath: args.privateKey, // クライアント証明書ファイルのパス certPath: args.clientCert, // CA 証明書ファイルのパス caPath: args.caCert, // AWS IoT への接続に使用するクライアント ID clientId: args.clientId, // リージョン https://docs.aws.amazon.com/ja_jp/general/latest/gr/rande.html region: args.region, // 再接続時間 (デフォルトは 1000) baseReconnectTimeMs: args.baseReconnectTimeMs, // Ping の時間間隔 (デフォルト 300秒) keepalive: args.keepAlive, // プロトコル。設定されている場合、環境変数 AWS_SESSION_TOKEN を上書きする protocol: args.Protocol, // ポート番号 port: args.Port, // 接続に使用するAWS IoTエンドポイント host: args.Host, debug: args.Debug, }); ////////////////////////////////////////////////////////////////////////////// // Events ////////////////////////////////////////////////////////////////////////////// // mqtt.Client() イベント。正常な (再) 接続時に呼び出されます。 jobs.on("connect", function () { console.log("event: connect"); }); // mqtt.Client() イベント。切断後に呼び出されます。 jobs.on("close", function () { console.log("event: close"); }); // mqtt.Client() イベント。再接続の開始時に呼び出されます。 jobs.on("reconnect", function () { console.log("event: reconnect"); }); // mqtt.Client() イベント。オフラインになったときに呼び出されます。 jobs.on("offline", function () { console.log("event: offline"); }); // mqtt.Client() イベント。接続できないまたはエラー発生時に呼び出されます。 jobs.on("error", function () { console.log("event: error"); }); // mqtt.Client() イベント jobs.on("message", function (topic, payload) { console.log("event: message", topic, payload.toString()); }); /* thingName という名前のモノのジョブ実行通知をサブスクライブします。 operationName が指定されている場合、実行の準備ができたジョブのジョブドキュメン トに operationName に一致する値を持つ operation というプロパティが含まれている 場合にのみ、このコールバックが呼び出されます。 */ jobs.subscribeToJobs(thingName, operationName, function (err, job) { console.log("shoot job handler invoked, jobId: " + job.id.toString()); if (isUndefined(err)) { // ジョブが処理中であることを AWS IoT Jobs マネージャに通知する。 job.inProgress( { operation: operationName, step: "download" }, function (err) { try { // ジョブを実行する。 var file = ''; console.log(job.document.url); var req = https.get(job.document.url, function(res) { res.setEncoding('utf8'); res.on('data', function(data) { file += data; }); res.on('end', function() { try { fs.writeFileSync("download.txt", file); } catch(err) { console.log(err); } // ジョブが完了したことを AWS IoT Jobs マネージャに通知する。 job.succeeded( { operation: operationName, step: "finished all steps" }, function (err) {} ); console.log("Job Succeeded"); }); }); req.on('error', function(err) { notificateFailJob(job, operationName, err); }); } catch (err) { // ジョブが失敗したことを AWS IoT Jobs マネージャに通知する。 notificateFailJob(job, operationName, err); } } ); } else { console.log(err); } }); /* thingName という名前のモノのジョブ実行通知をサブスクライブします。 operationName に一致しない値を持つ operation というプロパティが含まれている 場合、このコールバックが呼び出されます。 */ jobs.subscribeToJobs(thingName, function (err, job) { console.dir(job, {depth:3}); if (isUndefined(err)) { console.log("default job handler invoked, jobId: " + job.id.toString()); job.failed( { operation: job.operation, errorCondition: "not yet implemented", }, function (err) {} ); } else { console.log(err); } }); // ジョブ実行通知を開始した時に引数のコールバックが実行されます jobs.startJobNotifications(thingName, function (err) { if (isUndefined(err)) { console.log("startJobNotifications completed for thing: " + thingName); } else { console.log(err); } }); } function notificateFailJob(job, op, err) { console.log(err); job.failed( { operation: op, errorCondition: "execution error" }, function (err) {} ); console.log("Job Failed"); } //////////////////////////////////////////////////////////////////////////////// // Main //////////////////////////////////////////////////////////////////////////////// if (require.main === module) { cmdLineProcess("for AWS Jobs Connection", process.argv.slice(2), jobsConnect); } |
プログラムを起動する時の引数は –help で確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
Options -i, --client-id=ID use ID as client ID -H, --host-name=HOST connect to HOST (overrides --aws-region) -p, --port=PORT connect to PORT (overrides defaults) -P, --protocol=PROTOCOL connect using PROTOCOL (mqtts|wss) -k, --private-key=FILE use FILE as private key -c, --client-certificate=FILE use FILE as client certificate -a, --ca-certificate=FILE use FILE as CA certificate -f, --certificate-dir=DIR look in DIR for certificates -F, --configuration-file=FILE use FILE (JSON format) for configuration -r, --reconnect-period-ms=VALUE use VALUE as the reconnect period (ms) -K, --keepalive=VALUE use VALUE as the keepalive time (seconds) -t, --test-mode=[1-n] set test mode for multi-process tests -T, --thing-name=THINGNAME access thing shadow named THINGNAME -d, --delay-ms=VALUE delay in milliseconds before publishing -D, --debug print additional debugging information Default values client-id $USER<random-integer> protocol mqtts private-key private.pem.key client-certificate certificate.pem.crt ca-certificate root-CA.crt reconnect-period-ms 3000ms delay-ms 4000ms test-mode 1 |
-H は AWS IoT Coreの左ペインの「設定」で確認できるエンドポイントを指定します。
作成したプログラムを起動してみます。
1 |
node .\jobs_connect.js -k .\miso_thing1.private.key -c .\miso_thing1.cert.pem -a .\root-CA.crt -H [エンドポイント] -T miso_thing1 -i miso_thing1 |
ジョブの完了がAWS IoT Coreに通知され、無事にジョブが完了しました。ここで失敗する場合は、デバイス証明書にアタッチしたポリシーに誤りがある場合や、ジョブドキュメントの内容に誤りがあることが考えられます。
プログラムを実行したフォルダに生成されている download.txt を開いてみます。ファイルの中身がS3バケットのファイルと一致しており、正しくダウンロードできたことを確認できました。
1 |
This is the file to try downloading from S3. |
おわりに
無事にファイルダウンロードのジョブを実行することができました。これでデバイスのファームウェアの更新や設定ファイルの送信といった用途に使うことができます。今回はお試しでしたので、本番環境では以下のようなことをきちんと検討する必要があると考えています。
- rootユーザーで一連の操作をしましたが、運用ではIAMユーザーで操作することになるので、少し操作内容が変わるはず
- モノのグループをうまく活用することで、多くのデバイスに少ない操作でまとめてファイルダウンロードの実行を通知できるようになるはず
また、今回はファイルダウンロードでしたが、IAMロールのポリシーのGetObjectといった部分をPutObjectといった書き込みの内容にすれば、S3へのファイルアップロードもすんなりと実現できるでしょう。それを使ってログファイルをアップロードすれば、遠隔地からログの解析もできるようになります。
最後に、今回はジョブの記事を書かせていただきましたが、また機会があればデバイスシャドウを試した記事を書いてみたいです。というのも、今回の記事は情報量が多く、デバイス証明書にアタッチするポリシーの説明ができなかったためです。どうぞ次回をお楽しみに!
執筆者プロフィール
-
IoTの製品企画を担当。IoTを使ってお客様に価値を提供できるように奮闘中。
最近はこれまでのIoT製品と組み合わせた分析ソリューションを企画中。