AWS IoT でできることとハマったところ

私は今[ウミトロン株式会社](https://umitron.com/ja/)で、水産養殖に使うIoTデバイス、およびそれを用いたサービスの開発をしています。

デバイスの中には Go 製のプログラムが入ったりしているのですが（詳しくは[Go5カレンダーに書いた記事](https://qiita.com/negito6/items/33f298f471b985f69058)をご覧ください）、その中で AWS IoT を利用しています。AWS IoT を導入するか迷っている人、今実装中の人に参考になるかもしれないことを書きます。

# 便利なこと
AWS IoT がサービスとして提供している機能はたくさんあるのですが、開発していて特に「これができるようになって便利だ！」と思ったことを挙げます

## スマホアプリ/ウェブブラウザとデバイスを、サーバレスで双方向に繋ぐことができる
スマートフォン向けアプリやウェブブラウザ（以下全てアプリと書きます）からデバイスの様子を見ることができるようになっています。アプリからは AWS IoT の MQTT トピックを Subscribe していて、デバイスが Publish した内容を直接受け取ることが可能です。

AWS IoT 導入以前は、アプリから参照することのできるデータは、デバイスから HTTPS で送ってくるデータしかありませんでした。また、アプリからそのデータを取りに行くのも HTTPS なので、多数のユーザーが高頻度で状態を確認できる構成にするのは大変です。

デバイス/アプリの双方が MQTT 通信することで、負荷を気にすることなく高頻度で状態の送受信を行うことができるようになりました。

## AWS IoT Job を使って、アップデート処理などを非同期で行うことができる
ジョブを登録すると、デバイスがジョブの MQTT トピックを Subscribe したタイミングで処理を実行させることができます。

デバイスプログラムのアップデート処理を登録しておけば、デバイスがオフラインになっているときでも非同期でアップデートを行わせることができます。デバイスのオンライン状態を気にせず運用ができるようになるので、海上で使っているデバイスのように、電源切るオペレーションがあるときに特に有用です。

## ログ保存をサーバレスで行える
AWS IoT には Rule といって MQTT で送られたデータに対する操作を登録することができます。主に下記の二つを指定します。

* どのデータに対して作用させるかを条件付けをする SQL
* SQL によって適合したデータに対して何をするか（Action）

Action は選択肢として、S3 や DynamoDB などを指定することができるので、あらかじめ Rule を登録しておけば、デバイス側からログを MQTT Topic に Publish するだけ保存されるようになります。
https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/iot-rule-actions.html

## 飛んでくるデータを、SQS を挟んでサーバアプリケーションで処理できる
上記の Action として SQS を指定することで、デバイスからのデータ送信に対して非同期処理を動かすことが簡単にできます。サーバアプリケーションが Ruby on Rails であれば Shoryuken を使うことで、ウェブサーバの負荷を気にすることなくデバイスからの情報を待ち受けることができます。

# ハマったところ
## Thing id の名前空間が Region の中に一つしかない
AWS IoT は、プロジェクトのような概念がないので、ある名前 (Thing name) のデバイスを Region の中に一つしか作ることができません (Thing group の機能はありますが、groupが別だとしても名前がかぶるデバイスを作ることはできません)。そこで、サービスインする時に、Thing name にサービス名や環境名を含めることで区別させました。

それでも、 Rule の SQL を書くときに手間が発生します。 SQL が対象とするのはその Region のMQTT トピック全てです。複数運用しているサービスのうちのどれで扱っているデバイスのみで処理するときは、何かしら WHERE 文で頑張るしかありません。

## デバイスが Disconnect していることを知る方法
AWS IoT ではデバイスがオンラインかどうか（MQTT 接続されているかどうか）を知る直接の方法はありません。
オンライン状態の把握を最も簡単に実現するのであれば、デバイス状態を記録するDevice shadowに、デバイスがオフラインになる前/オンラインになった後にオンラインステータスを書き込んでおく、といった方法が考えられるかと思います。

しかし、海の上でデバイスを動かしていると、ネットワークが不通になることがよく起こります。そういう時には、オフラインのステータスを書き込むことはできません。

Disconnect は、AWS IoT ではライフサイクルイベントの一種として扱われていて、Connected / Disconnect が通知される MQTT トピックがあります。しかしずっとそれを Subscribe するのも大変です。Rule を使ってログを書き込んでおく方法もありますが、それだと見に行くデータソースが増えてしまいます。

そこで、Rule を使ってDevice shadowに書き込むようにしています[^1]。

[^1]: 実際には、Rule から device shadow に Republish しているのではなく、一度 Lambda を介してshadowに書き込んでいます。 Republish ができなかったとかそういったことだったかと記憶していますが、正確な理由は忘れてしまいました。

## Android SDK のコードの、 Cognito を使って認証するときに使う関数の引数
Android アプリから、AWS の Cognito を使って接続しているのですが、実装当時、ここでかなり躓きました。AWS の AccountId と Cognito IdentityPoolId は要らない実装をしたのですが、なぜかそれに適した（適していると感じる）インターフェースを持つクラスが見つかりませんでした。

そこで結局、

```kotlin
val identityPoolId = null
val accountId = null
val provider = AWSEnhancedCognitoIdentityProvider(accountId, identityPoolId) 
```

こんなことをやりました。引数が null でいいなら、そもそも省略できるインターフェースが欲しかったところです。

(SDK のコードが更新される可能性があるかと思われますので、実装の参考にするときは最新のコードを確認してください。)

## Rule と device のプログラムのバージョン
作っているサービスのある新機能を AWS IoT Rule で実現したい、と思った時に、Device のプログラムの古いバージョンは対応できないっていうことがあります。実際にそういうケースに遭遇し、Rule SQL の `WHERE` 文で判定を行いたかった...

AWS IoT を実装してサービスインした当初は MQTT にプログラムのバージョンで送っていなかったので、判定に色々苦労しました。Shadow に書いておくとか、毎回の Publish する json につけておくとか、早めにしておくとよかったなと思います。

## AWS IoT Policy と `${iot:Connection.Thing.ThingName}` が `ClientId` と同じ値になってしまう
デバイスからは、証明書を使ってサーバに接続しています。デバイスから二つのコネクションを貼りたいことが開発途中に発生し、（MQTT ブローカーに接続するClientId がかぶるわけにはいかないので）ClientId を変えて接続をするようにしました。

その際に、AWS IoT の Policy に埋め込める `${iot:Connection.Thing.ThingName}` を使おうとしたのですが、なぜか `ThingName` が `ClientId` と同じになってしまいました。本来は `ThingName` は登録していある Thing の名前で、証明書を使っていることから AWS IoT 側で判断できそうなものですが。。。

(仕様が変更されている可能性もあるので、実装の参考にするときは最新のドキュメントを参照してください)