2歩戻ったら2.5歩進みたい

関東で働くweb developerのブログ

プッシュ通知を作る前に知りたかった事のメモ(Firebase Cloud Messaging + AWS SQS & Lambdaでプッシュ通知を作った)

この記事について

プッシュ通知の基盤を作る機会があったのですが、前提となる知識や知見が足りなくて調査や検証に時間がかかったので、設計する前に知りたかったことや作り始めてから気づいたことなどをざざっとまとめました。 これからプッシュ通知を作る人に向けて参考になれば幸いです。

前提

具体的な説明に入る前に、前提となる要素と今回の開発で求められた要件を並べます。

対象のサービスやアーキテクチャについて

webアプリとモバイルアプリが共存しているサービスで会員数は100万人程度、webアプリケーションはRails製です。

このRailsのアプリケーションはwebアプリとして振る舞う以外にもモバイルアプリのAPIサーバーとしても振る舞っています(厳密にはサーバーが別れてますがコードベースは同じです)

モバイルアプリはiOS, AndroidともにFlutterで書かれていて、Railsのアプリケーションをリソース(API)サーバーとして見ています。

配信の要件

配信の要件はだいたい以下のような感じで、いわゆるモバイルアプリのプッシュ通知が出来ることです。

  • アプリの全ユーザーに対してアドホックにプッシュ通知が出来る
  • アプリの任意のユーザーに対してアドホックにプッシュ通知が出来る
  • 特定の条件に合致したユーザーに対してバッチでプッシュ通知が出来る
    • クーポンが期限切れになる前にプッシュ通知でリマインドする、みたいなやつです
  • アプリケーションの特定のイベントをトリガーとしてプッシュ通知が出来る

分析の要件

加えて分析やマーケティングを担当するチームが居るので最低限以下のような要件が求められます。

  • 送信したプッシュ通知の一覧が確認できる
    • 送信した通知に関して開封された数が確認できる
  • 任意のユーザーに送信された通知の一覧が確認できる
  • 任意のユーザーの開封後の行動をトラッキングできる

技術スタックについて

今回の開発では上記の機能要件に加えて、非機能要件として頭が痛くならない程度の予算で運用できることや配信サーバーの面倒を見ないでも運用できることを念頭に置きました。

既存のシステムとの整合性も考えた結果、今回はFirebase Cloud Messaging + AWS SQS & Lambdaで構成することにしました。

それぞれの技術について&選定の理由を説明します。

Firebase Cloud Messaging(FCM)

firebase.google.com

まずプッシュ通知を実現するためのサービスを選びます。アプリがFlutterでできている時点でほぼすべてのプッシュ通知サービスがSDKを用意していないという理由で選べませんでした。(こういう検索で出てくるサービスです↓)

www.google.com

ReproやKARTEはFlutterのSDKが用意されてました(すごい)が、現時点で料金面が折り合わないことやマーケティングツール以外からの起因で配信することを考えると初手でそこを選びに行けませんでした。(とりあえず自前で一通り作ってからマーケティングツールも入れる...みたいなのはできそうだと思ってるけどぶっちゃけあのへんのツール群をよく分かっていない)

という訳でFirebase Cloud Messagingが選ばれました。FlutterとFirebaseはすごく相性が良くてSDKが整備されているので正直モバイルアプリがFlutterな時点でFCM一択な感じはありました。 単純に他と比較しても料金が1円もかからないことやBigQueryとのデータ連携、ドキュメントの豊富さなどを鑑みても良いツールだと思います。

補足:FCMの配信の方式について

FCMの配信の方式は大きく分けると2つあります。

トークン指定配信

前提として、プッシュ通知を行うためにはプッシュ通知のトークンが必要になります(FCMだとregistraion tokenと言います)。これはプッシュ通知を許可した端末から払い出されるトークンで、これを指定してAPIを叩くとトークンを払い出した端末に対してプッシュ通知を送ることができます。

トークン指定配信(と勝手に呼んでるけどドキュメント上は特定のデバイスにメッセージを送信するという表現のみ)は直接配信先としてこのトークンの文字列を指定する配信です。

firebase.google.com

トピック配信

トークン指定配信は通知対象を直接指定するので分かりやすいですが、一回のAPIコールで指定できるのは500トークンまでです。仮に全ユーザーに対して配信するケースが存在した場合、(会員数にも寄りますが)配信に時間がかかったりサーバーから通知している場合はマシンリソースを逼迫させる可能性があります。

この弱みを解決してくれるのがトピック配信です。トピックというのはトークンを紐付けることの出来る対象で、トピックに対してメッセージを送ると事前に紐付けたトークンを持つ端末に対して通知を送ることが出来るというものです(トピックへの紐付けはクライアント/サーバーのどちらからでもできます)。

つまり、事前に配信したい対象が決まっているなら紐付けさえやっておけば、トピックに対する紐付け上限はない(はず)なので1リクエストで多くの端末に対して配信することができます。

ただし配信の遅延が最大で24時間(ドキュメント曰く)だそうなのでその辺りは理解して利用するのが良いと思います(恐らく紐付けたトークンの件数にも左右されると思いますが未確認です。手元で100個以下の紐付けならほぼ一瞬で配信されました)。

また、トピックを使う場合はトークンのライフサイクルと合わせた更新の設計が重要になります。例えば男性や女性と言ったセグメントでトピックを切った場合はトピックに紐付いたトークンを操作するのはトークン自体がrevokeされたり再生成されたりするタイミングだけですが、年齢のようなセグメントでトピックを生成した場合だとトークン自体のライフサイクルに加えて年一回定期的に変更するバッチを作る必要があります。

このため、私は初期フェーズでは全配信のためだけにトピックを使用することとし、配信の単位が固まってきたタイミングで再度トピックの設計を行うことにしました。

AWS SQS + Lambda

次にプッシュ通知のAPIを叩く君が必要になるのでそこの技術スタックを決める必要があります。

具体的にはアプリケーションの任意のイベントをトリガーにして対象のユーザーにプッシュ通知のAPIを叩いたり、アドホックなお知らせの配信をするために任意のタイミングで通知のAPIを叩いたりするコンポーネントです。

ざっくり言うとアプリケーションサーバーから直接FCMのAPIを叩く or 別の層を挟む のどっちにするのか、という議論です。現時点ではモバイルアプリの会員数はあまり多くない(~10万)のでマシンリソースのことは考えなくて良いのですが引き返しにくい判断(構成を変えたくなった時の難易度が高い)なので、サーバーを持たずにできるだけシステム間の結合が疎になるようにSQSを挟んでLambdaからAPIを叩くことにしました。

なお、Lambdaはnode.js + typescriptで作っているのでFCMのadmin SDKが使えます。(Railsから叩きたくなかった理由としてFCMのRubySDKが存在しなかったから、というのもあります)

システム全体図

という訳で改めて作ったシステム全体の図を示します。

f:id:canisterism:20211002010022p:plain

これを踏まえて、プッシュ通知が実際に行われるまでの大まかな流れは以下のような感じです。

トークン登録編

  1. アプリの起動時にトークンが払い出される
  2. アプリケーションサーバーにAPI経由でトークンがDBに登録される
  3. トークンを全体配信用のトピックに紐付けるためにSQSにキューイングする
  4. SQSからLambdaがキューを取得してFCMのトピック紐付けのAPIを叩く

全体配信編

  1. 配信用SQSに通知のデータをキューイングする
  2. SQSからLambdaがキューを取得してFCMのトピック配信のAPIを叩く
  3. ユーザーに通知が届く

ユーザー指定配信編

  1. DBから配信対象のユーザーを抽出し、ユーザーが持つトークンを取得する
  2. 配信用SQSに通知のデータをキューイングする
  3. SQSからLambdaがキューを取得してFCMのトークン配信のAPIを叩く
  4. ユーザーに通知が届く

設計上の注意点や気をつけたこと

上記の流れはそこまで複雑なことをやってないのですが、僕が設計する時にもっと早く知りたかったことや気をつけたことなどをざっと書いてきたいと思います。

アプリ側のトークン払い出し周辺の知識

先程も説明したように、プッシュ通知がなされるためには最初にユーザーの端末で通知トークンを払い出す必要があります。最初は「まぁダイアログ出して終わりやろ」と思ってたのですが意外に複雑でした。だいたいはFirebaseのドキュメントかFlutterならFlutterFireとfirebase_messaging | Flutter Packageに書いてありますのでよく読みましょう。(下に書いてるのはFlutterのAPI特有だったりするかもしれないので差っ引いて見てください)

  • 通知を許可するかユーザーに聞くダイアログが出るのはiOSのみ。Androidは何も出ずに許可されたのと同じステータスになる。
    • 1回でもダイアログを出すと2回目は出ない(ダイアログを出した後強制的にアプリを終了すれば2回目が出るかも?未検証です)。
      • そのため、「公式のダイアログを出す前に通知のメリットを説明する独自のダイアログを出す」みたいなことをしているアプリは多いです。仕様検討の際には気をつけたいですね。
  • 通知の許可ステータスはauthorized, denied, provisional, notDeterminedの4つ。
  • Flutterのfirebase_messagingで単純なプッシュ通知を行うだけなら両OSともgetToken()だけでよい(getAPNSToken()は使わないでOK)。
  • FCM via APNs Integration | FlutterFireに記載があるAPNのkeyはbundle IDに紐付いてないので環境別に分けなくてもよい(分けても良いと思いますが)

トークンのモデリングやライフサイクルにまつわる話

  • トークンは端末に対して払い出すので、アプリ上のユーザーと対応させたいなら工夫が必要
    • 例えばユーザーAがログインしている状態でトークン(aとする)を払い出した後にログアウトしてユーザーBでログインし直した時にaのトークンをどう扱うか?という話。
    • Twitterのように複数ユーザーの同時ログインを前提としているならそのまま通知して良いと思いますが、同時に1ユーザーのみログインを前提としている場合はログアウトしたらトークンを無効とする処理を入れるなどしないとユーザーAへの通知がユーザーBに行ってしまうので考慮する必要があります。ややこしいですね!
  • トークンはDB上で永続化するべし
    • 当たり前過ぎて書くか迷ったのですが作る前はこの辺も迷ったので書いておきます。
    • アプリ側でトークンが払い出されたらDBに永続化しましょう。でないとサーバー側のイベント起因で配信ができないので...
    • この時、ユーザーとトークンの関係は1:1ではなくて1:nであることに気をつけましょう。単純に複数台端末を持ってる可能性がありますからね。

開封率などデータ分析の話

この辺に書きました。 canisterism.hatenablog.com

トピックに纏わる話

  • 記事執筆時点でFCMのトピックは削除することが出来ません。一回作ったトピックを削除するためには紐付けたトークンを全部unsubscribeする必要があります。
  • トピックの一覧やトピックに紐付けたトークンを取得する方法がないため、管理したい場合は自前のDBで紐付けたトークンとトピックを管理する必要があります。

stackoverflow.com

アプリの通知のハンドリングの話

  • 後方互換性には気を使ったほうが良い
    • これはどっちかというとプッシュ通知と言うかアプリのバージョンアップで気をつけることですが、通知を受け取ることの出来るバージョンは平等に通知を受け取れることに留意して最初のバージョンをリリースするべきでしょう。つまり、v1.0でプッシュ通知機能をリリースした後、v1.1で新しい画面が追加された&その画面へ遷移するプッシュ通知も送られるようになった、みたいなことが起こるとv1.1で追加された画面への遷移を含む通知がv1.0にも届いてしまいます。ちゃんとフォールバック先を用意するなどの工夫が必要なので気をつけましょう。

まとめ

なんだかまとめきれたのか怪しいですが、僕が設計&実装する前に知りたかったことを詰め込んでみました。プッシュ通知したい時はだいたいどれぐらいのことを設計のスコープとして見込めばよいのかの肌感が伝わってれば嬉しいです。

他にもいろいろ考えた気がしますがちょっと思い出せないので思い出したら追記します。

参考リンク

Firebase Cloud Messaging | FlutterFire

FCMを使ったWEARプッシュ通知基盤リプレイス - ZOZO TECH BLOG

大規模プッシュ通知基盤大解剖 - @IT