LambdaとDynamoDBでIoTバックエンド開発

岡本忠浩氏:よろしくお願いします。「LambdaとDynamoDBでIoTバックエンド開発」というタイトルなんですけど、ちょっとスライドを作るのがギリギリになってしまって。至らないところがあるかもしれないんですが、お願いします。

自分はMMMという会社で働いている、岡本忠浩と申します。名乗るほどの者ではないと思うので、詳細はconnpassやTwitterを見てもらえるとありがたいです。

今日のテーマなんですけど、こういうタイトルでいろんな「これどうしてるの?」みたいなところを共有してこうかなと思います。けっこうランダムに、いろいろお話ししようかなと思っています。

「これどうしてるの?」というのがどういうことかというと、こういうことですね。

アプリケーションの管理とDynamoDB、あとはIoT周りのところを少し話そうかなという感じです。

今開発しているものなんですけど、いわゆる見守りスピーカー。家に置くような見守りスピーカーという機能がありまして、デバイスに対してメッセージ登録をして、人感センサーでメッセージを再生すると。それで、外出時のリマインダーだったり、天気予報の設定などもできるという感じになってます。モバイルアプリでも、APIを叩いてユーザー管理やビーコンの管理もできるという感じです。

なので、開発するものとしては、「モバイルアプリと通信するWeb API」と「AWS IoTからMQTTでつなぐバックエンド」というかたちになっています。

ちょっとよくわからないと思うので、概略を説明すると……まずデバイスがありまして、そこからセンサーだったり情報をMQTT経由でAWS IoTに送信します。それで、モバイルからAPIが叩かれて、API Gatewayで、Lambdaで処理しています。

大きく分けて、API GatewayとIoTの扱うLambdaが2つありまして、いろんなLambdaが動いているという状態です。それで、データベースはDynamoDBを使っている感じですね。

本当はもっとこの右側に、AmazonのPollyと書いてあるんですけど。これでテキストから音声合成をして、デバイス側に音声を伝える、みたいなこともやっていたり。あとはプッシュ通知だったりいろんな機能があるんですけど、一旦この概略図で紹介しています。

サーバーレスアプリケーションの管理

まず1個目、サーバーレスアプリケーションの管理です。どういうことをやるかというと、ビルドだったりパッケージングだったり、あとはデプロイテストとか環境変数の管理だったり、ロールを作ったりだとか、いろいろあると思うんですけど。

これはたぶんみなさんご存知だとは思うんですけど、AWS SAMというものを使っています。ベースとしてはCloudFormationなんですけど、Lambdaを使ったサーバーレスアプリケーションをいろいろ管理できるものになっています。

けっこういい感じにやってくれるというか、弊社でも今までにサーバーレスの開発の実績がありまして。慣れているというか、知見があります。それでこれを使っています。

AWS SAMの概要は今日はお話ししないんですけど、弊社でどうやっているかというのを紹介します。

全体的にDockerで作っていて、アプリケーションはGoで書いているんですけど、そのGoをビルドして、それをLambdaにパッケージングしてデプロイするところをAWS SAMでやっています。

それはCircleCI上で全部やっているんですけど、ローカル開発環境でもGoのユニットテスト環境というか、ユニットテストのコンテナを作っていて、DynamoDB Localというものをコンテナで動かして、それと通信してテストしていたりします。あとはStep Functions Localというものもあるんですけど、これはローカルだけで開発をしています。

その他、いろんなリソースをAWS SAMで管理していて。コンソールからチクチクやったりとかはほぼほぼなしで、全部コードで管理しているという感じですね。API Gatewayの設定だったり、S3のバケットを作ったり、SNSのイベントだったりとかも全部、SAMでやっています。

それで、SAMでまかなえない部分などは、別リポジトリというか、CloudFormationを生のやつを作っていて。KMSや各種ロールを作ったりとかは、別で任せています。

現在100個以上のLambda関数がありまして、けっこうもう……Lambda関数を作るためにSAMの設定を書く必要があって、大変になってしまうと。設定のし忘れだったりタイポだったり、あとは全部のLambda関数に共通のプレフィックスを付けたりするのが、手動でやっているとほぼ無理なので、template.yamlの自動生成をやっています。

template.yamlは、AWS SAMが使うCloudFormationベースのものなんですけど、それをアプリケーションコードから自動生成するスクリプトを書いています。

ちょっとサンプルなんですけど、例えばAPIだったら、Routesが書いてあるコードを読み込んで、CloudFormationのテンプレートに変換するスクリプトを書いています。

最終的にはyamlになって、自動的に生成されるという感じになっています。

これによってビジネスロジックに集中できて、Lambdaのことを考えることがあまりなくなっているので、いいかなと思っています。

DynamoDBのテーブル設計

次のテーマなんですけど、DynamoDBです。DynamoDBはNoSQLの、AWSのマネージドのデータベースです。ベストプラクティスはドキュメントにもきれいにまとまっていて、基本的にこれに従えばOKという感じにはなっています。けっこう読みごたえがあるので大変なんですけど。

あとは参考記事もあとで載せるので、これも見ていただければわかるかなという感じにはなっています。

ここでは大まかな手順だけ説明しようと思っていて、3つの手順になっています。

まず1個目ですね。1個目は本当にハードルを下げて、RDB風にER図を書きました。ふつうにRDBと同じようなER図を書くという感じになっていますね。

2個目に「アクセスパターンを列挙」というのがあるんですけど、RDBだとまずスキーマを考えて……これはリンクを載せているスライドから引用させていただいたんですけど、まずスキーマを考えて、SQLでどう取るかということだと思います。Dynamoの場合は、まずアクセスを考えて、それに合わせたデータというか、テーブル設計をする感じになっています。

アクセスパターンの列挙なんですけど、これは公式ドキュメントのをそのまま載せたんですけど、あるリソースをどう取るかとか、どういう検索があるかということを列挙していきます。ちょっとあとで説明します。

それをDynamoDBのスキーマに落とし込むんですけど、例えば簡単な例だと検索ですね。アクセスパターンが、例えばユーザーの一覧表示を考えると。それで、ユーザー名による並び替えや検索があると思うんですけど、そういうアクセスパターンごとにレコードを作るというのが、DynamoDBでは大事になってきます。

一番上が、すべての項目を乗せたベースのテーブルとなっていて、2、3個目のテーブルをグローバルセカンダリインデックスというんですけど、RDBのインデックスみたいなことを、実際にレコードを作ったDynamoDBでは実現してるという感じになっています。

2個目のテーブルがユーザー名による並び替えというアクセスパターンに対応したレコードになっています。(スライドの)SKと書いてある部分が、このインデックスにおけるパーティションキーというか、ユニークな識別子になっていて、ソートキーに実際のソートする値を書いていて、それでソートしたり、ほかの値で検索したりすることができます。

なんで分けているかというと、すべての項目を検索に含める必要はないときがあると思うんですけど、DynamoDBでは検索に必要な項目だけをスキーマに落とし込むという考え方がされます。

それで、実際にGoでどうやって書いているかというと、まずユーザーのストラクトというか、構造体を作りまして、それをベースのテーブルとフィルターのテーブルに対して展開するという書き方をしています。

ベースのテーブルとフィルターのテーブルもストラクトを分けていて、フィルターのテーブルには検索で使うものだけを入れるという感じです。これはアクセスパターンによっても、フィルター以外にもデータを作ったりということがあります。

スキーマに落とし込む際に、ユーザーのストラクトに共通のメソッドを付けています。まずこの1個目の「sortkey」という関数で、ソート対象になるものを指定しています。それで、ソート対象をループして、そこからフィルターレコードを作っていく感じになっています。

こんなふうに、あまり考えずにDynamoDBのコードを書けるようにしているというか。ちょっとコードは載せられなかったんですけど、ORM風なメソッドを追加したりモジュール分けをしたり、DynamoDBはなるべくアプリケーション実装では意識することなく、ロジックに集中できる体制になっています。

モジュール分けというのは、S3やIoTなどのサービスごとだったり、デバイスに対して音声を再生するサービスだったり。そういう適切なモジュール分けをやっています。

MQTTのTopic設計

次のテーマに入りますが、MQTTのTopic設計というものをやりました。

「MQTTのTopicとは?」ということなんですけど、RESTで言うところのエンドポイントみたいな感じです。「デバイスで人をディテクションしましたよ」というデータなんですけど。

それで、「IoT側からデバイスでイベントを検知して、IoT側にMQTTで通信をする」という場合と、「Lambda側からIoT側に操作をする」という2パターンがあります。

MQTT設計のホワイトペーパーがあるので、これをけっこう参考にしたんですけど、概要としては「Commands」というパターンと「Telemetry」というパターンがあります。

Commandsというのは、さっき言ったような、クラウドからデバイスに向けて操作をする場合や、双方向にコミュニケーションする場合のパターンです。Telemetryは、デバイスから何かを検知してクラウドに送信する場合ですね。例えば、電球の色をボタンを押して変えるといったパターンになります。Commandsのほうは、クラウドから何かをトリガーで再生させたりというパターンになっています。

具体的には、Telemetryはこういうシンタックスで設計するといいよ、ということが書いてあります。

今回のアプリケーションだと、(スライドの)dtというのはデータですね。Telemetryを送るデータという意味で、固定値になっています。アプリケーションも今回はspeaker-appにしていて、固定値になっています。

それで、contextというところで、実際の何が起きたかを指定します。例えば人を検知しましたとか、火を検知しましたとか。その次にthing-nameで、スピーカーのデバイスIDなどを指定して、IoTと通信をします。

Commandsのパターンも決まっているんですけど、Telemetryと違う点としては、req-typeというのが一番最後に書いてあります。Commandsの場合は双方向に通信をするので、reqかresを指定しています。実際にLambda側からリクエストを送って、デバイス側からレスが返ってくるまでを、Commandsでは責務として追っていて、実際には音声の再生などがこのアプリケーションでは多いというふうになっています。

DynamoDBのトランザクション

次々とテーマが変わるんですけど、「DynamoDBのトランザクションはどうしてますか」というところです。

去年、DynamoDBでトランザクションがサポートされたんですけど、一般的なトランザクションとは異なる制限があったりします。ちょっと全部は紹介しきれないんですけど、「同時に操作できるアイテムが10件まで」という制限がありまして、これはけっこう簡単に超過してしまうぞ、という事情があります。

例えば、今回で言うと、デバイスに関連するメッセージの音声だとか、リマインダーの音声だとか、天気の設定だったりを全部削除したい。メッセージが例えば5件登録されていて、各5件登録されているだけで制限を超過してしまうと。

かつ、それ以外にも、S3の音声ファイル消したり、IoTのシャドウという、デバイスが状態を持つようなデータも削除しなきゃいけないということがあって、10件を超えてしまうと。APIの実行時間も許容できないものになってしまいます。

ただ、ある疑問が思い浮かんできて。DynamoDBの結果整合性の考え方に従うと、関連するデータは疎結合に更新して、結果的に正しいとなっていればいいんじゃないかと考えました。

そこで、Step Functionsというものが登場します。Step Functionsは、複数のLambdaを組み合わせて連動的に実行したり、条件分岐などをいろいろ設定できるんですけど、これを使いました。

何がよかったかというと、処理を疎結合に分離できて、APIも1回、即座に返すことができます。それと、非同期でデータを更新することができます。複数のAWSサービスももちろん非同期に処理できますし、リトライ設定などもけっこう豊富にあります。例えば、最大試行回数だったり、バックオフを設定したりすることもできるので、リトライの期間を自在に操れるという感じですね。それは非同期で実行するときにはけっこう重要かなと思います。あとはLambdaの関係を視覚化できたりします。

それで、実際に視覚化したLambdaなんですけど、けっこう肩幅が広いStep Functionsができあがって(笑)。

これをパラレルに実行したり、一部複数のデータを扱うときはイテレータという機能を使って、一個一個をループしながら疎結合に更新する、みたいなことをやっています。

このトランザクションの件で思ったのは、この間のミートアップでも話されていた、マネージドならではの制限を把握しておくのが大事だなと。仕様の合意が超重要になってくるかなと思います。

サーバーレスは運用に入ってくるとけっこうコストが抑えられるんですけど、こういった事情もあって、初期費用は高くなる印象があるというか。実際に弊社での実績としても、(初期費用は)高くなっているかなという印象がありますね。ただ、数年単位で運用していく上でコスト的にも数百万単位でメリットが出てくるというふうになっています。

Lambdaの監視

これがたぶん最後で、Lambdaの監視です。監視はDatadogとCloudWatchをふつうに使っていて、1つのLambda関数単位で監視しています。これであとから追いやすくするのが目的です。サーバーエラーは基本的にはなくて、アプリケーション側のエラーが主になってきます。

これが実際にあった怖い話なんですけど、Lambda関数を無限に呼び出すような処理を書いてしまいました。なんでかというと、実際に処理を行うサービス側の呼び出しをしようというときに、Lambdaの呼び出し用モジュールを呼び出してしまって。10日間放置して、28億回実行されました。28億回も実行されて2桁万円で収まったっていうのはある意味すごいんですけど、けっこうな額を請求されてしまいました。

複数の要因がありまして、まずちょっとサービスの関数名が悪かったというのはあるんですけど、これは開発環境で起こったんですね。監視をあまりしないかと思うんですけど、開発環境でもLambdaが無限にスケールしてしまうので、気づかないという問題があります。これがサーバーレスならではの監視観点なのかなと思います。

実際にした対処としては、請求アラート。例えば、月3万円を超えると、AWS側でアラートを上げることができるんですけど、そういう設定をしたりだとか。開発環境でもDatadogでアラートを上げるようにするとか、あとは関数名を直すとか(笑)。そういういろんなことをやりました。

あとは、どうにかしたい点なんですけど。ローカル環境でLambdaの結合テストをやっていなくて、ユニットテストで現状、ほぼほぼ担保している感じになっています。ユニットテストはけっこう厚めに書いてるんですけど、結合テストという意味では書けていなくて、これは直近でなんとかしたい課題なので、誰か知っていたら教えてほしいです。

まとめになりますけど、アプリケーションの管理はAWS SAMで行います。DynamoDBはベストプラクティスに従うのと、DynamoDBを意識しない開発ができるようにするのが大事かなと思います。あとトランザクションはStep Functionsでがんばる。MQTTのTopicに関しては、ホワイトペーパーを見ながらCommandsとTelemetryのパターンで設計します。で、Lambdaの監視はクラウド破産に注意っていう。

(会場笑)

同僚が書籍を出していたりするので、興味がある方はあとで見てください。

あと、お約束なんですけど、採用もしてます。AWSに特化してスキルの高いエンジニアが多いのと、フルリモートワークなので。これ自分が行った写真なんですけど、けっこうフレキシブルに働けます。

ありがとうございました。

(会場拍手)