Greengrass(V1)のOTAアップデート前後にシェルスクリプトを実行する

#はじめに
前回Greengrass CoreのOTAアップデートを実施した際に以下の問題がありました。

* Lambdaが実行できなくなる
* グループのデプロイが出来なくなる

上記の問題はGreengrassを再起動することで解消しました。
参考：[Greengrass(V1)のOTAアップデートを実施する](https://qiita.com/kt-yt/items/b4ceaca58757c5bab225)

そこで、今回は（本質的な解決ではないものの）OTAアップデート後にGreengrassを再起動できるようにしました。
OTAアップデートの前後でシェルスクリプトを実施する機能があるので、これを設定してみます。

公式ドキュメントの以下を参考にしています。
参考：[init システムとの統合](https://docs.aws.amazon.com/ja_jp/greengrass/v1/developerguide/core-ota-update.html#integration-with-init)
#1　設定の変更
実行前後にシェルスクリプトを実行できるようconfigファイルを変更します。

```
cd /greengrass/config

sudo vim config.json
```
config.json内の`managedRespawn`を`true`にする

```json:config.json
{
    "coreThing": {
        "caPath": "/greengrass/certs/root.ca.pem",
        "certPath": "/greengrass/certs/8bd1570207.cert.pem",
        "keyPath": "/greengrass/certs/8bd1570207.private.key",
        "thingArn": "arn:aws:iot:ap-northeast-1:479448073833:thing/Greengrass_Test_Core_20210308",
        "iotHost": "a2iw7bft7mqw59-ats.iot.ap-northeast-1.amazonaws.com",
        "ggHost": "greengrass-ats.iot.ap-northeast-1.amazonaws.com",
        "keepAlive": 600
    },
    "runtime": {
        "cgroup": {
            "useSystemd": "yes"
        }
    },
    "managedRespawn": true,
    "crypto": {
        "caPath": "file:///greengrass/certs/root.ca.pem",
        "principals": {
            "SecretsManager": {
                "privateKeyPath": "file:///greengrass/certs/8bd1570207.private.key"
            },
            "IoTCertificate": {
                "certificatePath": "file:///greengrass/certs/8bd1570207.cert.pem",
                "privateKeyPath": "file:///greengrass/certs/8bd1570207.private.key"
            }
        }
    }
}
```
#2　シェルスクリプトの作成
実行するシェルスクリプトを作成します。
なお、公式ドキュメントにある通り、config.jsonで`managedRespawn`を`true`とした場合、以下の要件を満たす必要があります。
参考：[init システムとの統合 - OTA更新による管理された再生成](https://docs.aws.amazon.com/ja_jp/greengrass/v1/developerguide/core-ota-update.html#integration-with-init)

* /greengrass/usr/scripts ディレクトリに以下のスクリプトが存在している
 * ggc_pre_update.sh　※CoreのOTAアップデート開始前に実行する
 * ggc_post_update.sh　※CoreのOTAアップデート終了後に実行する
 * ota_pre_update.sh　※OTAエージェントのOTAアップデート開始前に実行する
 * ota_post_update.sh　※OTAエージェントのOTAアップデート終了後に実行する
* スクリプトは成功のリターンコードを返す必要がある
* スクリプトの所有者はroot、実行はrootのみが可能でなくてはならない
* ggc_pre_update.sh はGreengrassを停止する必要がある
* ggc_post_update.sh はGreengrassを起動する必要がある

##2-1　要件を満たすファイルの作成
以下の通りコマンドを実行します。

```
#ディレクトリの作成
cd /greengrass
sudo mkdir -p usr/scripts

#スクリプトの作成
cd usr/scripts
sudo touch ggc_pre_update.sh ggc_post_update.sh ota_pre_update.sh ota_post_update.sh

#権限の変更
sudo chmod 700 *sh

#確認
ls -la
-rwx------ 1 root root 1192  3月 17 12:07 ggc_post_update.sh
-rwx------ 1 root root  768  3月 17 12:55 ggc_pre_update.sh
-rwx------ 1 root root  206  3月 16 20:39 ota_post_update.sh
-rwx------ 1 root root  522  3月 17 12:49 ota_pre_update.sh
```
##2-2　シェルスクリプトの作成
公式ドキュメントを参考にスクリプトを作成します。

なお、今回はGreengrassの停止・起動・再起動はsystemctlコマンドで実行しています。
systemdでGreengrassを操作できるようにする設定は以下の記事を参考としてください。
参考：[Greengrass(V1)の自動起動設定&コアデバイスの接続情報をLambdaで取得 - 1　Greengrassの自動起動設定](https://qiita.com/kt-yt/items/877d735081adcee3da01#1greengrass%E3%81%AE%E8%87%AA%E5%8B%95%E8%B5%B7%E5%8B%95%E8%A8%AD%E5%AE%9A)

1.今回はログを出力するため、以下にディレクトリを作成した

```
cd /greengrass/usr
sudo mkdir log
```
2.以下の通り各スクリプトを作成する
※`set -euo pipefail`は公式ドキュメントに記載があるコマンドだが、コメントアウトしても問題なくOTAアップデートすることができた

```bash:ggc_pre_update.sh
#!/bin/bash
#エラーチェック用
set -euo pipefail

#ディレクトリとログファイル名の指定
DIR=/greengrass/usr/log
LOG=otaupdate.log

#開始時刻と開始時点のPIDの取得
START=`date "+%Y-%m-%D %H:%M:%S"`
PID=`cat /var/run/greengrassd.pid`

#ログ内容を書き込む関数の設定
function logging(){
        {
        echo "------------------------------------------------"
        echo "Greengrass Core OTA Update has started."
        echo "This script will stop Greengrass Core to OTA Update."
        echo ""
        echo "Script:/greengrass/usr/scripts/ggc_preupdate.sh"
        echo "PID:$PID"
        echo "Start:$START"
        }>> $DIR/$LOG
}

#ログファイルの存在チェックを行い、存在しない場合はファイルを作成後にログファイルに書き込みを行う
if [ -f $DIR/$LOG ]; then
        logging
else
        touch $DIR/$LOG
        logging
fi

#Greengrassを停止する（必須）
systemctl stop greengrass

#停止処理の終了時刻を取得
STOP=`date "+%Y-%m-%D %H:%M:%S"`

#停止時刻をログに書き込む
{
echo "Stop:$STOP"
echo ""
}>> $DIR/$LOG
```
```bash:ggc_post_update.sh
!/bin/bash
#エラーチェック用
set -euo pipefail

#ディレクトリとログファイル名、開始時刻の指定
DIR=/greengrass/usr/log
LOG="otaupdate.log"
START=`date "+%Y-%m-%D %H:%M:%S"`

#ログ内容を書き込む関数の設定
function logging(){
        {
        echo "Greengrass Core OTA Update has Completed."
        echo "Then this script will start Greengrass Core."
        echo ""
        echo "Script:/greengrass/usr/scripts/ggc_post_update.sh"
        echo "Start:$START"
        }>> $DIR/$LOG
}

#ログファイルの存在チェックを行い、存在しない場合はファイルを作成後にログファイルに書き込みを行う
if [ -f $DIR/$LOG ]; then
        logging
else
        touch $DIR/$LOG
        logging
fi

#Greengrassを開始する（必須）
systemctl start greengrass

#起動後のPIDと起動処理の終了時刻を取得
PID=`cat /var/run/greengrassd.pid`
STOP=`date "+%Y-%m-%D %H:%M:%S"`

#起動終了時刻と起動後のPIDをログファイルに書き込む
{
echo "Stop:$STOP"
echo "PID:$PID"
}>> $DIR/$LOG

#再起動処理の開始時刻を取得
#START2=`date "+%Y-%m-%D %H:%M:%S"`

#再起動開始をログファイルに書き込む
{
echo ""
echo "Then this script will restart Greengrass Core"
echo ""
echo "Start:$START2"
}>> $DIR/$LOG

#Greengrassの再起動
systemctl restart greengrass

#再起動後のPIDと再起動処理の終了時刻を取得
PID2=`cat /var/run/greengrassd.pid`
STOP2=`date "+%Y-%m-%D %H:%M:%S"`

#再起動終了時刻と再起動後のPIDをログファイルに書き込む
{
echo "Stop:$STOP2"
echo "PID:$PID2"
echo ""
echo "OTA Update has completed!!"
}>> $DIR/$LOG
```
※今回は動作確認とログ取得のためにota_pre_update.sh、ota_post_update.shともにスクリプトを書いていますが、この2つに関しては中身が空でもOTAアップデートは可能でした。

```bash:ota_pre_update.sh
#!/bin/bash
#エラーチェック用
set -euo pipefail

#ログのディレクトリとファイル名を指定、開始時刻を取得
DIR=/greengrass/usr/log
LOG=otaupdate.log
START=`date "+%Y-%m-%d %H:%M:%S"`

#ログ内容を書き出す関数の指定
function logging(){
        {
        echo "------------------------------------------------"
        echo "Greengrass OTA Agent OTA Update start."
        echo "This script only writes log file."
        echo ""
        echo "script:/greengrass/usr/scripts/ota_pre_update.sh"
        echo "Start:$START"
        }>> $DIR/$LOG
}

#ログファイルの存在チェックを行い、存在しない場合はログファイルを作成してからログの書き込みを行う
if [ -f $DIR/$LOG ]; then
        logging
else
        touch $DIR/$LOG
        logging
fi
```
```bash:ota_post_update.sh
#!/bin/bash
#エラーチェック用
set -euo pipefail

#ログのディレクトリとファイル名を指定、終了時刻を取得
DIR=/greengrass/usr/log
LOG=otaupdate.log
FINISH=`date "+%Y-%m-%d %H:%M:%S"`

#ログに書き込む
{
echo "Finish:$FINISH"
echo
echo "OTA Update has completed!!"
}>> $DIR/$LOG
```
#3　動作検証
実際にOTAアップデートを行い動作検証を行います。
今回は以下を確認します。

* シェルスクリプトが動いている
* GreengrassがOTAアップデート完了後に再起動されている
* （上記の再起動により）CoreのOTAアップデート後にもLambdaの実行、デプロイができる

##3-1　OTAアップデートの実施
以下の手順でOTAエージェント、Coreソフトウェア共にOTAアップデートを実施します。
OTAアップデートの詳細な手順は以下をご参照ください。
参考：[Greengrass(V1)のOTAアップデートを実施する](https://qiita.com/kt-yt/items/b4ceaca58757c5bab225)

1.コアデバイスでOTAエージェントを起動する
※2つ以上OTAエージェントが起動していると競合するのでプロセスをkillする

```
#OTAエージェントの起動
sudo /greengrass/ota/ota-agent/ggc-ota

#起動していることの確認
ps aux | grep -E "greengrass.*ota"
root     31159  0.0  0.1  79080  4488 ?        Ssl  12:53   0:00 /greengrass/ota/ota_agent_v1.0.0_8/ggc-ota -p 2813
```
2.IoT Coreのマネジメントコンソールで左のメニューから 管理 > ジョブをクリック
3.「作成」 > 「Core更新ジョブの作成」の順にクリック
4.以下の通りジョブを設定し、「作成」をクリック
※「作成」をクリックするとジョブが実行されるので注意

* 更新するデバイスの選択：※任意のモノ・モノのグループ（複数選択可）
* S3 URL 署名者ロール：※S3へのGet権限を持つIoT CoreのIAMロールを指定
* エージェントのログレベルを更新する：※任意のログレベル
* アーキテクチャタイプ：※対象デバイスのアーキテクチャタイプ
* 更新するGreengrass Coreのコンポーネント：※CoreかCore OTA エージェントを選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/95a739cc-c3cb-cbc4-ce6f-7e419038e5b2.png)
5.ジョブの一覧に作成したジョブが表示されるのでクリック
6.正常に完了することを確認する
※エラー時は「/var/log/greengrass/ota/ggc-ota.txt」のログを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/93812d78-a673-b276-b886-b6a8fc78aed1.png)
##3-2　検証結果
###シェルスクリプトの実行状況（再起動しているか含む）
シェルスクリプトに記載した通り「/greengrass/usr/log」配下にログが出力されていることを確認します。

```log:otaupdate.log
------------------------------------------------
Greengrass OTA Agent OTA Update start.
This script only writes log file.

script:/greengrass/usr/scripts/ota_pre_update.sh
Start:2021-03-16 20:41:45
Finish:2021-03-16 20:41:46

OTA Update has completed!!
------------------------------------------------
Greengrass Core OTA Update has started.
This script will stop Greengrass Core to OTA Update.

Script:/greengrass/usr/scripts/ggc_preupdate.sh
PID:23065
Start:2021-03-03/16/21 20:43:31
Stop:2021-03-03/16/21 20:43:42

Greengrass Core OTA Update has Completed.
Then this script will start Greengrass Core.

Script:/greengrass/usr/scripts/ggc_post_update.sh
Start:2021-03-03/16/21 20:43:45
Stop:2021-03-03/16/21 20:43:51
PID:13083

Then this script will restart Greengrass Core

Start:2021-03-03/16/21 20:43:51
Stop:2021-03-03/16/21 20:44:08
PID:14104

OTA Update has completed!!
------------------------------------------------
```
想定通りログが出力されており、シェルスクリプトが実行されたことが確認できます。
また、`systemctl restart greengrass`前後でPIDが変わっていることから、再起動も実施されたことが確認できます。
###CoreのOTAアップデート後にもLambdaの実行、デプロイができるか
IoT CoreのマネジメントコンソールからLambdaの実行とデプロイの実行を試します。

まず、以下の記事で作成した接続情報を取得するLambdaを動かします。
参考：[Greengrass(V1)の自動起動設定&コアデバイスの接続情報をLambdaで取得 - 2　Greengrass Coreデバイスの接続情報取得](https://qiita.com/kt-yt/items/877d735081adcee3da01#2greengrass-core%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%81%AE%E6%8E%A5%E7%B6%9A%E6%83%85%E5%A0%B1%E5%8F%96%E5%BE%97)

1.IoT Coreマネジメントコンソール上で左のメニューから テスト をクリック
2.「get/connection」をサブスクリプション
3.「cmd/connection」にメッセージを発行する
※この「cmd/connection」はIoT Cloud -> Lambdaで設定しているサブスクリプション
4.OTAアップデート後、Lambdaが起動したため結果が返ってきた

デプロイも以下の通り実施します。

1.IoT Coreマネジメントコンソール上で左のメニューから Greengrass > クラシック(V1) > グループ > 対象のグループ の順にクリック
2.アクション > デプロイ の順にクリック
3.デプロイが正常に完了することを確認できた

##3-3　再起動は不要…？
気になったので、ggc_pre_update.shから`systemstl restart greengrass`をコメントアウトしてCoreのOTAアップデートを行いました。
その後、Lambdaの起動状況とグループのデプロイを確認すると・・・**エラーなく実行できました！**

もしかしたら、と思い、シェルスクリプトを使わない方法で再度OTAアップデートを確認しました。
※config.jsonの`managedRespawn`を`false`に戻す
その結果・・・**エラーなくLambda実行・グループのデプロイができました！（？！）**

（当然といえば当然ですが）OTAアップデート後にGreengrass Coreの再起動がなくても問題なく動くようです。
であれば、いったい前回のエラーはなぜ起こったのでしょう…？
#4　トラブルシューティング
今回は（3-3のようなことを除けば）以下のような引っかかりポイントがありました

* OTAエージェントを起動し忘れており、いつまでもジョブのステータスが「キュー」のままだった
* 初歩的なシェルスクリプトのミス
 * 実行権限を付与していなかった
 * ログ出力先のディレクトリパスが間違っていた
 * コマンドのスペルミス

既に記載した通りですが、OTAアップデートでは**「/var/log/greengrass/ota/ggc-ota.txt」**にログが出力されます。
エラーが出る場合はこちらを確認してみてください。
#5　おわりに
CoreのOTAアップデートに伴う問題が結果的には解消しました。
ただ、やはり本質的な解決方法でなかったため、空振り感が否めず…。

一方で、OTAアップデート前後にシェルスクリプトが実行できることが確認できたのは良かったです。
OTAアップデートでは（スクリプトでも必須になっている通り）いったんGreengrassを停止するため、それに伴ってコアデバイスで実行しているLambdaも終了します。
終了前に処理を簡単に挟むことが出来るのは便利でしょう。

#6　参考文献（本文中に登場していないもの）

* [AWS IoT GreengrassのOTA更新をやってみた](https://qiita.com/moritalous/items/7ab82622dd6a2ec2f79c)
* [【 set 】コマンド――シェルの設定を確認、変更する](https://www.atmarkit.co.jp/ait/articles/1805/10/news023.html#opt)