motch のアーキテクチャを紹介

2019年度の未踏事業に採択されて, IoTデバイス管理アプリ「motch」を作りました.

arailly.hatenablog.com

私は主にバックエンドの開発を担当し, 以前から興味のあったサーバーレスアーキテクチャを駆使して実装しました. せっかくなので,そのアーキテクチャを紹介します.

アーキテクチャの全体像

全体像のアーキテクチャの概要図はこんな感じです.

f:id:arailly:20200301112321p:plain

開発環境

紆余曲折を経て,AWS SAM + LocalStackに落ち着きました. SAMはAPIやLambdaのエミュレーションができるので, ローカルでのテストが捗ります. ランタイムはNode.jsで,TypeScriptから生成していました.

サーバーレスあるあるですが, クラウド環境が楽な反面, ローカル環境が辛くなりがちです.

この辺りの知見もぜひ共有したいと思っています.

リクエスト処理

HTTPS

API Gateway + Lambda + DynamoDB で構成. 認証・認可はCognitoを使用しています.

f:id:arailly:20200318155602p:plain

MQTTS

IoT Core + SQS + Lambda + DynamoDB で構成.

IoTデバイスにとってHTTPSリクエストは重いので, MQTTと呼ばれるより軽量な通信プロトコルに対応しました. IoT Coreのトピックへのパブリッシュを生存報告とみなします. また,IoTデバイスが増えてもスケールするよう, メッセージは一旦SQSのキューにためてバッチ処理を行います.

f:id:arailly:20200318155559p:plain

なぜトピックへのパブリッシュを生存報告とするのか

AWS IoTを使った死活監視といえば, MQTT接続の切断イベントを利用することが多いと思います. しかし今回は,トピックへのパブリッシュを生存報告とみなし, 生存報告が途切れたら故障とみなす,というアプローチを取りました.

その理由は,想定している生存報告間隔が数十分から数時間と比較的長いからです. 頻繁にセンサーデータを送るシステムであれば常に接続している方がいいと思いますが, 数時間に一度しか通信を行わないのであれば, 生存報告のために接続し続けることによる消費電力が無視できないと考えました.

UIのリアルタイム更新

API Gateway (WebSocket Endpoint)+ Lambda + DynamoDB で構成.

APIGWのWebSocket Endpointによる双方向通信により実現しています. 接続時,切断時にそれぞれLambdaが起動し, 接続情報をDynamoDBで管理します. デバイスの状況に変化が生じたら, 接続情報を参照し, 即座にクライアントにデータを送信します.

f:id:arailly:20200318155606p:plain

死活監視アルゴリズム

DynamoDB Stream + Lambda + SQS で構成.

f:id:arailly:20200318155556p:plain

一番悩んだポイントです. motchでは,「生存報告が定期的に行われなくなると故障と判断する」 という故障検出アプローチを取っています. そのため,「生存報告が来ない」ことに反応する必要があります.

最も単純な解決法はポーリングだけどあんまりかっこよくありません. 一定時間おきにテーブルのスキャンが発生するのでスケールしませんし.

生存報告を受信すると,一定時間(生存報告間隔)後に故障検出を行うプロセスを生成し, 制限時間までに生存報告が来たらそのプロセスをキャンセルする, というアプローチを取りたかったのですが, サーバーレスなのでそんな器用なことはできません.

生存報告を受信すると, 一定時間後にLambdaが立ち上がる遅延起動のような機能があればいいのになあと思いましたが, 残念ながらそんなものは無かったので自分で再現することにしました.

色々調べてみると, SQSのメッセージは遅延時間付きで送信できることがわかりました. つまり,SQSにメッセージを送信すると, 遅延時間後にそれが反映されます. さらに,SQSのメッセージ受信をトリガーにしてLambdaを起動することができます.

そこで,これを使って, あるデバイスの生存報告を受信すると, 一定時間後にLambdaを起動してそのデバイスの生存報告状況を確認することができました.

これによって,インデックスを使えばテーブルのスキャンは発生しませんし, Lambdaが並列に起動するのでそこそこスケールします.

Lambdaの遅延起動のような機能を実装したい, という方がいればぜひ参考にしてください.

バイス情報・SDKの管理

DynamoDB Stream + Lambda + S3 + IoT Core で構成.

管理対象のデバイスが追加されると,認証情報を埋め込んだSDKを生成します. また,IoT Coreにもモノを作成し,証明書をアタッチします. これによって,SDKをインストールするだけで監視が可能になります.

f:id:arailly:20200318155551p:plain

SDKクラウドリソースのCI/CD

一応CI/CDも頑張っています. SDKの開発リポジトリを更新したら, SDKダウンロード用のS3バケットの中身を更新します. バックエンドも,developブランチにプッシュ・マージしたら ステージング環境にデプロイされ, masterブランチにマージしたら本番環境にデプロイするようにしています.

また,ほとんどのクラウドリソースはCloudFormationで管理しているので, IaCを実現できています.

f:id:arailly:20200318155548p:plain

ざっと解説するとこんなところです. 試したり作ったり壊したりを繰り返して, このアーキテクチャに落ち着きました.

ちょっとしたこだわりポイントは,固定費を0円にしたことです. これで,ユーザーが増えるまでは低コストでアプリを運用できます. その割には,ユーザーが増えてもそこそこスケールする構成にできたのではないかと思っています.

DynamoDBもセカンダリインデックスを使わず, パーティションキーとソートキーだけで高速にGETできるように設計しました.

アーキテクチャの今後の展望

HTTPSハンドラ

色々実装してわかったことは, Lambdaはちゃんと書かないとバグの温床になるということです. したがって,Lambdaの数を最小限にすることが重要です.

現状のアーキテクチャではHTTPハンドラが全てAPIGW + Lambdaなので, Lambdaが乱立しています. 多くのAPIはシンプルなCRUDしかしてないので, 全部AppSyncに乗せたいなあと思っています.

UIのリアルタイム更新

実は,このリアルタイム更新のためにAppSyncを使ったリアルタイムサブスクリプションを実装したのですが, IoTデバイスからGraphQLを叩くのが難しいことに気付いて結局無駄になっちゃいました..

それから大急ぎで実装したので,今回はAPIGWのWebSocketを採用しましたが, IoT Coreのトピックのリアルタイムサブスクリプションを使って実装することで こちらもLambdaレスにしたいです.

クラウドリソースの監視

かなりの数のクラウドリソースがあるので, 実運用時はCloudWatchを使ってちゃんとリソース監視しないと...

可用性

全てシングルAZなので, 東京リージョンで障害が起きたら終わりです. この辺りの知識が乏しいので, 時間があるときに勉強したいなあと思っています.

最後に

まだまだやれることは残っていますが, 未踏期間という限られた時間の中ではベストを尽くせたと思っています.

motchは3月中にリリース予定なので,応援してもらえると嬉しいです.

今後もアプリを改善して開発を継続する予定なので, ここはこうした方がいいよ,というフィードバックは大歓迎です.

あとは,せっかくサーバーレスで頑張ったので, AWSコミュニティやサーバーレスコミュニティで登壇もしてみたいなあと思ってます.

これからも頑張ります.