フリートプロビジョニングで作成したモノのシャドウをフリートインデックスでクエリする

AWS IoTでフリートインデックスを使って、シャドウの状態によるモノのクエリを試したいと思います。

今回使ったコードはこちらにあります。
https://github.com/sabmeua/aws-iot-device-fleet-test

# フリートプロビジョニング
まず、デバイスを複数作りたいですが、準備するのは面倒なので、フリートプロビジョニングを使います。

```bash
$ git clone https://github.com/sabmeua/aws-iot-device-fleet-test.git
$ cd aws-iot-device-fleet-test
$ ./setup.sh
```

`./setup.sh` ではAWS IoTのプロビジョニングテンプレートとそのブートストラップテンプレート、IAMロールなどを作成します。
TerraformにAWS IoTのI/Fがなかったためaws cliでやっています。あとでリソース削除したい場合は手動でやらないといけないのでご注意ください。

モノ側は、デバイスを用意はできないので、dockerでalpine上でpython SDKを使って行います。docker-composeで複数のモノを立ち上げるので、イメージのビルドもdocker-composeで行います。

```bash
$ docker-compose build
```

このイメージは https://github.com/aws-samples/aws-iot-fleet-provisioning フリートプロビジョニングのサンプル実装をチェックアウトしてきます。コンテナを起動すると `run_device.sh` から、プロビジョニングとシャドウのアップデートを開始するようになっています。 

# シャドウのアップデート

シャドウのアップデートは下のようなスクリプトになっています。

```python:updateDeviceShadow.py
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTShadowClient
import time
import json
import random

def callback(payload, status, token):
    if status == "timeout":
        print(f'Timeout: {token}')
    if status == "accepted":
        payloadDict = json.loads(payload)
        print(f'Accepted: {token}')
        print(payload)
    if status == "rejected":
        print("Rejected: " + token)

with open('device_info.json') as f:
    dev = json.load(f)

shadowClient = AWSIoTMQTTShadowClient(dev['thingName'])
shadowClient.configureEndpoint(dev['endpoint'], 8883)
shadowClient.configureCredentials(
    f'{dev["certPath"]}/{dev["rootCert"]}',
    f'{dev["certPath"]}/{dev["privateKey"]}',
    f'{dev["certPath"]}/{dev["cert"]}')
shadowClient.configureAutoReconnectBackoffTime(1, 32, 20)
shadowClient.configureConnectDisconnectTimeout(10)
shadowClient.configureMQTTOperationTimeout(5)

print(f'Start updating the battery status : {dev["thingName"]}')

shadowClient.connect()
shadowHandler = shadowClient.createShadowHandlerWithName(dev['thingName'], True)
shadowHandler.shadowDelete(callback, 5)

prop = {'state':{'reported':{'batt': 100.0}}}
while True:
    batt = random.betavariate(6, 3) * 100
    prop['state']['reported']['batt'] = batt
    shadowHandler.shadowUpdate(json.dumps(prop), callback, 5)
    if batt < 50:
        print(f'Stop updating due to the battery remaining is less than 50%')
        break
    time.sleep(30)

```

プロビジョニングで取得したデバイス毎の証明書を使って、30秒おきに `reported.batt` というバッテリ残量を模したシャドウのプロパティを　`betavariate(6, 3)`のランダム値で更新します。
この値が50%を切ったら更新を停止するようにしています。

# フリートインデックスの有効化

フリートインデックスが作成されるように設定を変更します。フリートインデックスの作成は課金されるので、不要なら、あとで設定を戻すように注意してください。
AWSマネジメントコンソールのAWSIoTのコンソールから ”設定 -> フリートインデックス作成の設定” で下記のように設定します。

![スクリーンショット 2020-05-31 6.47.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130580/4f3ad11b-413d-8ce7-98e0-afe561e28146.png)

以上で準備完了です。

# 実行

docker-composeで５台起動してみます。
１台ごとのプロビジョニングは下記のように表示されれば成功です。

```bash
$ docker-compose up --scale fleet_device=5
Starting aws-iot-device-fleet-test_fleet_device_1 ... done
Starting aws-iot-device-fleet-test_fleet_device_2 ... done
Starting aws-iot-device-fleet-test_fleet_device_3 ... done
Starting aws-iot-device-fleet-test_fleet_device_4 ... done
Starting aws-iot-device-fleet-test_fleet_device_5 ... done
Attaching to aws-iot-device-fleet-test_fleet_device_1, aws-iot-device-fleet-test_fleet_device_2, aws-iot-device-fleet-test_fleet_device_3, aws-iot-device-fleet-test_fleet_device_4, aws-iot-device-fleet-test_fleet_device_5
fleet_device_3  |                  ______   __                   __ 
fleet_device_3  |                 / ____/  / /  ___     ___     / /_
fleet_device_3  |                / /_     / /  / _ \   / _ \   / __/
fleet_device_3  |               / __/    / /  /  __/  /  __/  / /_  
fleet_device_3  |              /_/      /_/   \___/   \___/   \__/  
fleet_device_3  |                                                   
fleet_device_3  | 
fleet_device_3  |     ____                  _      _             _            
fleet_device_3  |    / __ \_________ _   __(_)____(_)___  ____  (_)___  ____ _
fleet_device_3  |   / /_/ / ___/ __ \ | / / / ___/ / __ \/ __ \/ / __ \/ __ `/
fleet_device_3  |  / ____/ /  / /_/ / |/ / (__  ) / /_/ / / / / / / / / /_/ / 
fleet_device_3  | /_/   /_/   \____/|___/_/____/_/\____/_/ /_/_/_/ /_/\__, /  
fleet_device_3  |                                                    /____/   
fleet_device_3  | 
fleet_device_3  |                                                              
fleet_device_3  |                                                              
fleet_device_3  |  ____________________________________________________________
fleet_device_3  | /_____/_____/_____/_____/_____/_____/_____/_____/_____/_____/
fleet_device_3  |                                                              
fleet_device_3  |                                                              
fleet_device_3  | 
fleet_device_3  | ##### CONNECTING WITH PROVISIONING CLAIM CERT #####
fleet_device_3  | ##### SUCCESS. SAVING KEYS TO DEVICE! #####
fleet_device_3  | ##### CREATING THING ACTIVATING CERT #####
fleet_device_3  | ##### CERT ACTIVATED AND THING device-1590879565906 CREATED #####
fleet_device_3  | ##### CONNECTING WITH OFFICIAL CERT #####
fleet_device_3  | ##### ACTIVATED AND TESTED CREDENTIALS (0c2fa73a60-private.pem.key, 0c2fa73a60-certificate.pem.crt). #####
fleet_device_3  | ##### FILES SAVED TO /aws-iot-fleet-provisioning/certs #####
fleet_device_3  | {'service_response': '##### RESPONSE FROM PREVIOUSLY FORBIDDEN TOPIC #####'}
fleet_device_3  | Start updating the battery status : device-1590879565906
```

複数起動するとMQTTのコネクションがTimeoutしてプロビジョニングが失敗することがありました。
これについて、なぜかわかっていませんが、一度プロビジョニングして証明書を取得したデバイスのコンテナは、２回目以降の起動で再度プロビジョニングは行わないようにしているので、 `docker-compose stop / start` を繰り返したら全てのデバイスのプロビジョニングが出来ます。

シャドウの更新が行われている状態では、下記のように、シャドウのトピックのMQTTのペイロードのログが表示されます。
このログでは、途中でデバイス５がバッテリー残量50%を切って終了しています。
このように状態の変化が起きている状態でフリートインデックスのクエリをしてみます。

```bash
fleet_device_3  | Start updating the battery status : device-1590879565906
fleet_device_1  | Start updating the battery status : device-1590879257006
fleet_device_2  | Start updating the battery status : device-1590879253998
fleet_device_5  | Start updating the battery status : device-1590879255597
fleet_device_4  | Start updating the battery status : device-1590879572901
fleet_device_3  | Accepted: 9b33888e-862e-453c-ad1a-d7ee626b0324
fleet_device_3  | {"version":15,"timestamp":1590884818,"clientToken":"9b33888e-862e-453c-ad1a-d7ee626b0324"}
fleet_device_1  | Accepted: aa3911c3-4da8-4cce-a746-4583ab85b238
fleet_device_1  | {"version":10,"timestamp":1590884818,"clientToken":"aa3911c3-4da8-4cce-a746-4583ab85b238"}
fleet_device_2  | Accepted: 792d90a4-82c7-425a-b66d-56339a7bedef
fleet_device_2  | {"version":7,"timestamp":1590884819,"clientToken":"792d90a4-82c7-425a-b66d-56339a7bedef"}
fleet_device_5  | Accepted: 429820d6-7dba-4a87-8589-3da5b9cef61e
fleet_device_5  | {"version":11,"timestamp":1590884819,"clientToken":"429820d6-7dba-4a87-8589-3da5b9cef61e"}
fleet_device_4  | Accepted: 0d72b28b-23d3-4023-bb3e-2728c55b4c0f
fleet_device_4  | {"version":6,"timestamp":1590884819,"clientToken":"0d72b28b-23d3-4023-bb3e-2728c55b4c0f"}
fleet_device_3  | Accepted: b3552043-6167-4b60-8bfc-1b1a5312b828
fleet_device_3  | {"state":{"reported":{"batt":54.56623259478952}},"metadata":{"reported":{"batt":{"timestamp":1590884821}}},"version":17,"timestamp":1590884821,"clientToken":"b3552043-6167-4b60-8bfc-1b1a5312b828"}
fleet_device_5  | Stop updating due to the battery remaining is less than 50%
fleet_device_1  | Accepted: e04057ed-7c12-4a74-bc8e-4e4496111803
fleet_device_1  | {"state":{"reported":{"batt":55.60684342775718}},"metadata":{"reported":{"batt":{"timestamp":1590884821}}},"version":12,"timestamp":1590884821,"clientToken":"e04057ed-7c12-4a74-bc8e-4e4496111803"}
fleet_device_2  | Accepted: dceb237f-0a90-437b-8d04-1aa03c661830
fleet_device_2  | {"state":{"reported":{"batt":55.73068072917988}},"metadata":{"reported":{"batt":{"timestamp":1590884821}}},"version":9,"timestamp":1590884821,"clientToken":"dceb237f-0a90-437b-8d04-1aa03c661830"}
fleet_device_4  | Accepted: 5f301135-2715-41d5-a7ec-372a0c251540
fleet_device_4  | {"state":{"reported":{"batt":78.55606282570174}},"metadata":{"reported":{"batt":{"timestamp":1590884821}}},"version":8,"timestamp":1590884821,"clientToken":"5f301135-2715-41d5-a7ec-372a0c251540"}
aws-iot-device-fleet-test_fleet_device_5 exited with code 0
fleet_device_3  | Accepted: b4161f0d-5b33-43ae-b8c8-a06ee42a6417
fleet_device_3  | {"state":{"reported":{"batt":60.10810045271757}},"metadata":{"reported":{"batt":{"timestamp":1590884851}}},"version":18,"timestamp":1590884851,"clientToken":"b4161f0d-5b33-43ae-b8c8-a06ee42a6417"}

```

# フリートインデックスのクエリ

クエリはとりあえずはaws cliで試すことができます。
まず、シャドウのバッテリ残量でクエリしてみます。値が50~80、80~100でレポートされているモノを `shadow.reported.batt:{50 TO 80}` のようなクエリで抽出することができました。

```bash
$ aws iot search-index --query-string 'shadow.reported.batt:{50 TO 80}'
{
    "things": [
        {
            "thingName": "device-1590879565906",
            "thingId": "2c387914-1426-4076-a8de-24876b9d213e",
            "thingTypeName": "device-type-1",
            "thingGroupNames": [
                "test-devices"
            ],
            "shadow": "{\"reported\":{\"batt\":58.422574552849426},\"metadata\":{\"reported\":{\"batt\":{\"timestamp\":1590887635}}},\"version\":26}",
            "connectivity": {
                "connected": true,
                "timestamp": 1590887601460
            }
        },
        {
            "thingName": "device-1590879253998",
            "thingId": "5da7ed87-9535-451a-8211-77fa1936a67c",
            "thingTypeName": "device-type-1",
            "thingGroupNames": [
                "test-devices"
            ],
            "shadow": "{\"reported\":{\"batt\":71.36448863151011},\"metadata\":{\"reported\":{\"batt\":{\"timestamp\":1590887633}}},\"version\":32}",
            "connectivity": {
                "connected": true,
                "timestamp": 1590887599149
            }
        },
        {
            "thingName": "device-1590879255597",
            "thingId": "e4bdd3c6-85b1-4d47-9947-621a0ed5dcdd",
            "thingTypeName": "device-type-1",
            "thingGroupNames": [
                "test-devices"
            ],
            "shadow": "{\"reported\":{\"batt\":70.52375514441887},\"metadata\":{\"reported\":{\"batt\":{\"timestamp\":1590887604}}},\"version\":26}",
            "connectivity": {
                "connected": true,
                "timestamp": 1590887599730
            }
        }
    ]
}
$ aws iot search-index --query-string 'shadow.reported.batt:{80 TO 100}'
{
    "things": [
        {
            "thingName": "device-1590879255597",
            "thingId": "e4bdd3c6-85b1-4d47-9947-621a0ed5dcdd",
            "thingTypeName": "device-type-1",
            "thingGroupNames": [
                "test-devices"
            ],
            "shadow": "{\"reported\":{\"batt\":83.16154864112754},\"metadata\":{\"reported\":{\"batt\":{\"timestamp\":1590887634}}},\"version\":27}",
            "connectivity": {
                "connected": true,
                "timestamp": 1590887599730
            }
        }
    ]
}
```

次に、バッテリ残量50%を切って、更新を停止してしまったモノを抽出します。 `connectivity.connected` というインデックスが作成されており、これでクエリできます。

```bash
$ aws iot search-index --query-string 'connectivity.connected:false'    
{
    "things": [
        {
            "thingName": "device-1590879257006",
            "thingId": "6b9efeb0-a668-4c2c-83b5-8a0ced063bf0",
            "thingTypeName": "device-type-1",
            "thingGroupNames": [
                "test-devices"
            ],
            "shadow": "{\"reported\":{\"batt\":40.81584157412556},\"metadata\":{\"reported\":{\"batt\":{\"timestamp\":1590887635}}},\"version\":29}",
            "connectivity": {
                "connected": false,
                "timestamp": 1590887635292
            }
        }
    ]
}
```

# まとめ

今回は5台でシャドウも1プロパティだけでしたが、フリートインデックスを使って、基本的なモノの検索をすることができました。
`connectivity.connected` による検索が可能なのは、デバイスの管理にとても有効だと思いました。