-----------------------------------------------------------------------
■ 目次
- はじめに
- 何が問題なのか
- 作ったもの
- Cognito の変化を DB に反映する仕組み
- 測ってみた
- 検証中に引っかかったところ
- どちらを選ぶか
- まとめ
- 参考
-----------------------------------------------------------------------
■ はじめに
こんにちは、酒井です。
Cognito でログインを管理しているアプリで「管理画面のユーザー一覧に有効か無効かを出したい」という相談はよくあると思います。名前やメールアドレスはアプリの DB にあっても、有効か無効かは Cognito が持っています。そのため一覧の 1 行ごとに Cognito の API を呼ぶ作りになりがちです。
ユーザーが 10 人ならそれで困りません。では 100 人ならどうなるのでしょうか。状態を DB に持たせるなら、Cognito 側の変化をどう知ればよいのでしょうか。
小さなサンプルを作り、実際の Cognito で両方のやり方を試してみました。
■ 何が問題なのか
一覧の取得に DB を 1 回、Cognito を人数分呼ぶ形はいわゆる N+1 問題です。ユーザーが増えるほど一覧の表示は遅くなります。
API のレート上限にも気をつける必要があります。1 人ずつ状態を取る AdminGetUser は既定で毎秒 120 回までです(追加料金を払えば引き上げられます)。この上限はユーザープール単位ではなくアカウントとリージョンの単位でかかります。一覧を開く人が増えると、同じ上限を使うほかの処理まで巻き込んでエラーになるおそれがあります。
料金にも関わります。Cognito はその月に何らかの操作があったユーザーを月間アクティブユーザー(MAU)として数え、その人数で課金します。AWS のドキュメントによると AdminGetUser で詳しい属性を取ることもこの操作に含まれます。一方で ListUsers は含まれません。一覧を開くたびに全員分の AdminGetUser を呼ぶと、その月に一度もログインしていないユーザーまで MAU に数えられてしまいます。ドキュメントでも、余計な費用を避けたいなら ListUsers でまとめて取るか外部のデータベースに情報を持つよう勧めています。
■ 作ったもの
Laravel と MySQL を Docker で動かし、ユーザー一覧を返す処理を 2 通り用意しました。Cognito のユーザープールにはテストユーザーを 100 人作っています。
1 つ目は素直に 1 件ずつ Cognito に問い合わせるやり方です。Laravel(PHP)のコードで示します。

2 つ目は Cognito の状態を users テーブルの列(cognito_enabled と cognito_status)に写しておくやり方です。一覧ではその列を読むだけです。

2 つのやり方の違いを図にしました。

2 つ目のやり方で考えどころになるのは一覧の書き方ではありません。Cognito 側の変化をどうやって DB に反映するかです。
■ Cognito の変化を DB に反映する仕組み
Cognito にはサインアップ時などに Lambda を呼び出せるトリガーがあります。ただしドキュメントの「API 操作と Lambda トリガーの対応表」には無効化(AdminDisableUser)も有効化(AdminEnableUser)も削除(AdminDeleteUser)も載っていません。管理者による状態の変更はトリガーでは拾えないため、今回は別の道を選びました。
Cognito のユーザープールは、EventBridge に直接イベントを送る仕組みを持っていません。Cognito の API 呼び出しは CloudTrail に記録されます。その記録を EventBridge で拾って SQS のキューに入れ、Laravel のワーカーが読む流れです。

EventBridge から直接アプリを呼ばずに SQS を挟んだのは、ワーカーが止まっている間もイベントをためておくためです。
● EventBridge のルール
ルールには Cognito のユーザーに関わる操作だけを拾うパターンを JSON で書きます。AWS コンソールや CLI でルールを作るときに指定するものです。

送り先には SQS のキューを指定します。キューにはこのルールからの送信だけを許可するポリシーを付けておきます。
● イベントに username が入っていない
作る前に知っておきたいのがここです。実際に届いた AdminDisableUser のイベントでは、誰を無効化したかを示す username が伏せられていました(userPoolId と sub は例の値に置き換えています)。

Cognito はユーザーに関わる操作では CloudTrail に username を残さず、代わりに sub を記録します。sub はユーザーごとに振られる一意な ID です。今回のイベントでは additionalEventData に入っていました。AdminCreateUser のイベントも同じ形でした。
AdminGetUser は username でしかユーザーを引けません。sub から引くときは ListUsers にフィルターを付けて呼びます。CloudTrail についての AWS のドキュメントでも、sub からユーザーを探すにはこの方法が案内されています。
DB の側でも sub でユーザーを探せるよう users テーブルに cognito_sub 列を作り、一意制約を付けました。
先に書いたとおり ListUsers は MAU に数えられません。同期のために Cognito を呼んでも MAU は増えないので、AWS がドキュメントで勧めている形にそのまま沿っています。
● イベントの中身は使わない
イベントを受け取ったら「無効化されたので DB も無効にする」と書きたくなります。今回はそうせず、イベントは「このユーザーの状態が変わったかもしれない」という知らせとしてだけ扱いました。受け取ったら Cognito から今の状態を取り直して DB に書きます。
こうしておけば同じイベントが 2 回届いても大丈夫です。無効化と有効化のイベントが逆の順番で届いても、最後に DB に残るのは Cognito の今の状態です。後で書くように、この作りに助けられる場面が実際にありました。
● 取りこぼしたときのために
EventBridge のドキュメントでは、CloudTrail 経由のイベントの配信は「ベストエフォート」とされています。まれに届かないことがあるという意味です。ワーカーが長く止まっていたりルールの設定を間違えていたりと、アプリの運用や設定のミスで取りこぼすこともありえます。
そこで ListUsers で全員分を取って DB と突き合わせるコマンドも作りました。最初に DB へユーザーを取り込むときもこのコマンドを使います。ListUsers は 1 回で 60 人まで返すので、100 人なら 2 回の呼び出しで終わります。夜間に定期実行しておけば、DB と Cognito の状態がずれても翌朝には直ります。
■ 測ってみた
手元の Windows PC 上の Docker から東京リージョンの Cognito を呼んで測りました。
|
やり方 |
DB クエリ |
Cognito API |
一覧の表示にかかった時間 |
|
1 件ずつ API で取る |
1 回 |
100 回 |
約 13.2〜14.2 秒 |
|
DB に同期した列を読む |
1 回 |
0 回 |
約 2〜3 ミリ秒 |
2 回測ってどちらも同じくらいの結果でした。返ってきた 100 人分の状態も両方のやり方で一致しています。
1 件ずつ取るほうは API 1 回あたり 130〜140 ミリ秒ほどかかりました。ほとんどは手元の PC と AWS の間の通信時間です。同じリージョンの EC2 や Lambda から呼べばもっと短くなるはずですが、そちらは測っていません。それでも呼び出し回数がユーザー数に比例することは変わりません。ユーザーが増えれば同じように遅くなっていきます。
同期したほうの 2〜3 ミリ秒には同期の手間が入っていません。状態が変わるたびにワーカーが ListUsers を 1 回呼んでいます。ただしそれは変化があったときだけで、一覧を開くたびではありません。
次に同期の遅れを見るため、Cognito でユーザーを 1 人無効化しました。CloudTrail に記録された呼び出し時刻は 16:07:27 で、DB が更新されたのは 16:07:46 です。差は約 19 秒でした。このワーカーは 15 秒おきにキューを見に行く作りだったので、その待ち時間も含んでいます。測ったのは 1 回だけなので目安として見てください。
■ 検証中に引っかかったところ
● CloudTrail の証跡がないとイベントが来ない
EventBridge で CloudTrail 経由のイベントを受け取るには、ログを記録している証跡がアカウントに必要です。CloudTrail は証跡がなくても直近の操作履歴をコンソールで見られます。そのため「CloudTrail には出ているのに EventBridge に来ない」という状態になりえます。今回はアカウントにあった証跡をそのまま使いました。なお無効化のような書き込み系の操作なら、ルールの状態は通常の ENABLED のままで拾えます。
● ルールを作る前のイベントがあとから届いた
テストユーザー 100 人を作り終えてから EventBridge のルールを作りました。ところがワーカーを動かすと、ユーザー作成のイベントが 5 件届きました。CloudTrail から EventBridge へ届くまでの時間差で、ルールを作った時点ではまだ届いていなかった分だと思われます。状態を取り直す作りにしていたので、この 5 件を処理しても DB の内容は変わりませんでした。
● SQS の件数はすぐには減らない
イベントを処理したあとでキューの件数(ApproximateNumberOfMessages)を見ると、まだ 6 件と出ていました。もう一度受け取りに行っても何も来ず、少しすると 0 件になりました。名前のとおり概算の値です。処理が終わったかどうかの判断には使わないほうがよさそうです。
● 同期する側にもレート上限がある
sub でユーザーを引く ListUsers にも毎秒 30 回の上限があります。こちらは AdminGetUser と違って引き上げられません。CSV でユーザーを一括登録するとイベントが一度にたくさん届きます。そうした場面に備えて、ワーカーを増やしすぎないようにしておく必要があります。今回のワーカーは処理に失敗したメッセージを消さずに残し、SQS がもう一度届けてくれるのを待つ作りです。
■ どちらを選ぶか
同期するやり方のほうが速いのは確かです。ただ、いつでもこちらがよいとは限りません。
CloudTrail 経由のイベントは届くまでに時間差があり、まれに届かないこともあります。そのため一覧に少し前の状態が見えたり、取りこぼした分が突き合わせのコマンドを実行するまで古いままになったりします。用意するものも増えます。CloudTrail の証跡と EventBridge と SQS、それに常に動いているワーカーです。一覧に出るユーザーが数人で画面を開く人も限られているなら、1 件ずつ API で取るやり方で十分だと思います。
ユーザーが数十人を超える一覧では表示の速さの差がはっきり出てきます。「無効なユーザーだけを表示したい」といった絞り込みも、状態が DB の列にあれば SQL で書けます。
無効化した瞬間に一覧へ反映したいなら、組み合わせる方法もあります。無効化の操作をする画面の処理で DB も一緒に更新しておきます。イベントによる同期は、AWS コンソールから直接操作されたときなどの取りこぼし対策に回します。
費用の面も見ておきます。Cognito の Lite と Essentials のプランには、直接サインインするユーザーとソーシャルログインのユーザーについて月 10,000 MAU の無料枠があります。この枠を超える規模では、1 件ずつ AdminGetUser で取るやり方は MAU を増やします。速さだけでなく料金の面でも不利です。同期の仕組みのほうは、EventBridge の default バスに届く AWS の管理イベントが無料で、SQS も月 100 万リクエストまで無料です。ただし証跡のログを S3 に保存していたり Amazon GuardDuty のように証跡を分析するサービスを有効にしていたりすると、API の呼び出しが記録されるぶんわずかに費用が増えます。
■ まとめ
Cognito のユーザー状態を 1 件ずつ API で取ると、100 人の一覧で 13 秒以上かかりました。使う AdminGetUser は MAU に数えられるので、ユーザーが多いと料金にも響きます。CloudTrail と EventBridge と SQS で変化を拾って DB に同期しておけば、一覧は数ミリ秒で表示できました。Cognito 側の変化は(1 回の計測で)およそ 20 秒で DB に反映されました。
作るうえで押さえておきたい点は 2 つです。
1 つ目は CloudTrail のイベントに username がなく sub しか入っていないことです。ユーザーを特定するには sub を条件に ListUsers を呼びます。
2 つ目はイベントに書かれた操作をそのまま DB に反映しないことです。イベントが届いたら sub で Cognito から今の状態を取り直し、その結果を DB に書きます。こうしておけば、同じイベントが 2 回届いても順番が入れ替わっても DB は Cognito と同じ状態になります。検証中もユーザー作成のイベントが 5 件遅れて届きましたが、DB の内容は崩れませんでした。
一覧に Cognito の状態を出す機能を作るときの参考になればうれしいです。
■ 参考
- Customizing user pool workflows with Lambda triggers
- Amazon Cognito logging in AWS CloudTrail
- AWS service events delivered via AWS CloudTrail
- Amazon Cognito user pools events(EventBridge Events Reference)
- Delivery level for AWS service events
- Quotas in Amazon Cognito
- Amazon Cognito の料金
- Amazon EventBridge の料金
- Amazon SQS の料金

