AWS IoT Coreのカスタムジョブでシェルスクリプトを配信して実行させる


はじめに

今までGreengrassのOTAアップデートを何度か試してきました。
ただ、それだけでなく更新用のファイルを配信→実行までしたい、ということでカスタムジョブを使ってみます。

今回は以下の記事を大いに参考にさせていただきました。
参考:AWS IoTのジョブ機能とモノの動的グループを利用してデバイスに配布するソフトウェアを管理する

1 必要なファイルの作成

今回はデバイスにシェルスクリプトを配信→実行ができるかまでを検証します。
実行できたことも分かるよう、シェルスクリプトは/tmp配下にログを吐くようなものを作成しました。

1.以下の通りシェルスクリプトを作成する

test.sh
#!/bin/bash

DIR=/tmp
FILE=$DIR/"update-test.log"

DATE=`date "+%Y-%m-%D %H:%M:%S"`

if [ -f $FILE ] ; then
    touch $FILE
fi

#/tmp配下に実行時刻を記録する「update-test.log」を作成
echo "$DATE update" >> $FILE

次に、ジョブドキュメントを作成します。
今回は参考サイトのものをそのまま利用します。

2 配信元S3の作成

1.配信元のS3バケットを作成する
2.S3バケットに test.sh をアップロードする
3.今回は作成したバケットを静的ウェブホスティングで公開し test.sh を取得できるようにする
4.S3のマネジメントコンソールで対象のバケット > アクセス許可タブ の順にクリック
5.ブロックパブリックアクセス (バケット設定)の「編集する」をクリック
6.「パブリックアクセスをすべてブロックする」からチェックを外し、「変更の保存」の順にクリック

7.プロパティタブ > 静的ウェブサイトホスティングの「編集する」の順にクリック
8.以下の通り設定を変更して、「変更の保存」をクリック

  • 静的ウェブサイトホスティング:有効にする
  • ホスティングタイプ:静的ウェブサイトをホストする
  • インデックスドキュメント:test.sh ※配信ファイル


9.アクセス許可タブ > バケットポリシーの「編集する」の順にクリック
10.以下の通りバケットポリシーを設定して「」をクリック

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::[バケット名]/*"
        }
    ]
}

11.オブジェクトタブでアップロードした配信ファイルをクリック > 「オブジェクトURL」を控える

3 ジョブ実行の準備

次に、ジョブドキュメントとデバイス側でジョブを受けて処理をするプログラムを用意します、
ジョブドキュメントは参考サイトのものをそのまま利用し、デバイスで実行するプログラムは参考サイトのモノに少し手を入れています。

1.ローカルでジョブドキュメントを作成する

update-test-0001.json
{
    "app_url": "先ほどの手順で控えた実行ファイルのオブジェクトURL",
    "app_version": "2.0"
}

2.作成したジョブドキュメントをS3にアップロードする
※任意のバケット
3.デバイスで実行プログラムを作成する
※参考サイト同様AWS IoT Device SDK for Pythonのsamples/jobsの jobsSample.py に少し手を加えた
 AWS IoT Device SDK for Pythonの導入は以下を参照のこと(下記手順のあと、作業ディレクトリにjobsSample.pyをコピーして実行した)
 参考:モジュール 4: AWS IoT Greengrass グループでのデバイスの操作

jobsSample.py
#主な修正箇所のみ記載

#---前略---

import threading
import logging
import time
import datetime
import argparse
import json
#以下のライブラリを追加でimport
import urllib.request
import os
import subprocess

#---中略---

    def executeJob(self, execution):
        print('Executing job ID, version, number: {}, {}, {}'.format(execution['jobId'], execution['versionNumber'], execution['executionNumber']))
        print('With jobDocument: ' + json.dumps(execution['jobDocument']))
        #参考サイトよりコピペ、ジョブドキュメントのURLをもとに配信ファイルをダウンロードする
        app_url = execution['jobDocument']['app_url']
        self.app_version = execution['jobDocument']['app_version']
        urllib.request.urlretrieve(app_url, 'app.sh')
        #配信ファイルに実行権限を付与して実行する
        os.chmod('test.sh', 0o777)
        subprocess.run('./test.sh')

#---後略---

4 カスタムジョブの実行

IoT Coreのマネジメントコンソールからジョブを実行し、test.sh の配信と実行を確認します。
なお、実行中 jobsSample.py から実行結果が $aws/events/ 配下のtopicに送られてくるので、テスト画面で $aws/events/# をサブスクライブしても状況を見ることができます。

1.IoT Coreのマネジメントコンソールで左のメニューから 管理 > ジョブ > ジョブを作成する の順にクリック
2.「カスタムジョブを作成」をクリック

3.以下の通り設定して「次へ」をクリック

  • ジョブID:任意の名称 ※ただし既存のジョブIDと被ってはいけない
  • 更新するデバイスの選択:任意のデバイスまたはグループ
  • ジョブファイルの追加:「3.ジョブ実行の準備」で作成したジョブドキュメントを選択
  • ジョブタイプ:ジョブは、選択したデバイス/グループへのデプロイ後に完了します (スナップショット)


4.デフォルト設定のまま「作成」をクリック
5.デバイス側で jobsSample.py を以下の通り実行する

python jobsSample.py --endpoint [エンドポイントのURL] \
 --rootCA [ルートCA証明書] --cert [デバイス証明書] --key [秘密鍵] \
 --thingName [モノの名前]

6.プロンプトが返ってきたら、マネジメントコンソールでジョブの実行状況を確認する

※詳細画面でジョブドキュメントも確認できる

5 実行結果の確認

まず、デバイスで jobsSample.py を実行したディレクトリ(カレントディレクトリ)に test.sh がダウンロードできていました。
以下の通り、権限も変更されています。

ls -la test.sh 
-rwxrwxrwx 1 root root 156  3月 29 20:05 test.sh

また、 test.sh を実行した証跡として期待通りログが出力されていました。

cat /tmp/update-test.log 
2021-03-03/29/21 20:05:22 update

6 おわりに

当初目的としていた以下は達成できました。

  • 実行ファイルを配信する
  • 配信したファイルを実行する

以下については今後もう少し工夫してみようと思います

  • デバイス側でジョブを処理するプログラムを手動実行しないで済むようにする
  • 署名付きURLを使ったジョブの実行

7 参考文献(本文中で登場していないもの)