こんにちは。メドピアにWebエンジニアで入社して約6ヶ月の佐藤です。
メドピアは2/26から銀座に移転しました。 銀座に移転しても花粉からは逃げられませんでしたが 、移転後はなぜか空気清浄機が増えて助かっています。
書いてある事
Amazon SNSの複数ユーザーに一度に通知を送る「トピック」を使ってRailsでPush通知を実装した際の処理フローを主に書いてあります。
トピック機能を使わず、Amazon SNSのプッシュ通知のみを実装した場合はわりとシンプルですが、
- トピック購読(subscribe)
- トピック購読の解除(unsubscribe)
等のトピック関連の処理が加わってくると状態管理が複雑になってきます。
また、
- トピック
- エンドポイント
- ARN(Amazon Resource Name)
などAmazon SNSやAWS上での用語も理解する必要があるため、その説明も簡単に入れました。
今後Amazon SNSを使ってpush通知(とトピック機能)を実装する上で何かしら参考になれば幸いです。
また、ブログ内のサンプルコードは下記のリポジトリに格納してあります。
ブログ内は処理の抽象化された部分が書かれているためaws_sdkの利用部分やDBへの反映部分は出てきませんが、 リポジトリ内の他ファイルにはそこも含めたものが上げてあります。
(ただ、弊社事情部分を削ってあるのでそのままでは動かない参考的なものです)
Amazon SNSのモバイルプッシュ通知
いろいろなデバイス(プラットフォーム)に、送信側はそれほど処理を変えずに簡単にプッシュ通知できるサービスです。
他にもこんな特徴があります。
- 送信可否状態をAmazon SNS側で管理
プッシュ通知の送信が失敗すると、状態変更(後述)されるまで送信しない
- 個別の送信先、またトピック機能を使う事で複数の送信先にも一度に送信できる
詳細 https://docs.aws.amazon.com/ja_jp/sns/latest/dg/SNSMobilePush.html
Amazon SNSのトピック
トピックは、日本語訳で「話題」や「論題」です。
Amazon SNSのトピック機能は、事前にトピック(Topic)を作成しておき、 ユーザーはそのトピックを購読(subscribe)する事でそのトピックへの通知を受け取ることが出来るようになります。
複数ユーザー(アプリ)を紐付けでおけば、APIの利用側はそのトピックに対して送った通知メッセージが複数ユーザーに届くようになります。
大量ユーザーへのPush通知も、トピックに紐付ければ1回の送信で行えるという事です。
https://docs.aws.amazon.com/ja_jp/sns/latest/dg/CreateTopic.html
アプリ側の機能概要
今回実装したアプリの、ユーザーから見たトピックの選択イメージはこんな感じです。
- 複数プラットフォーム(iOS+Android)でプッシュ通知を受け取れる
- ログアウト時にはpush通知が来ないようになる
- トピックごとに通知のON/OFFができる
アプリ側の設定画面の雰囲気です。
Amazon SNS側作業
AWS マネジメントコンソールで事前に下記の作業を行います。 (APIのユーザー管理やアクセス権限周りも必要あれば設定)
- アプリケーション作成
- トピック作成
これらはAPIで作成することもできますが、今回は事前に作成する形にしたのでAWS マネジメントコンソールでの作業を前提とします。
アプリケーション作成
プラットフォームと対になるものです。 iOS、Android分であれば計2つを作成します。
必要なもの
- iOSのp12ファイルとそのパスワード (Androidは試していませんが、GCMアクセスキーが必要となるはずです)
下記はiOSのアプリケーション作成画面です。
- アプリケーション名
- iOSの場合はDevelopmentまたはProductionの選択
- p12ファイルパスワードを入力
です。(証明書はp12ファイルから勝手に入力されます)
これで、アプリケーションの「ARN」である、「application_arn」が生成されます。(APIでの送信時に必要)
ARN(Amazon Resource Name)は、AWS上に何かリソースを作成した時に与えられる名前です。
この先に出てくる他のリソース(トピック、エンドポイント等)にもARNが設定され、 同じくAPIでそのリソースを指定する時などに使います。
トピック作成
トピックA、トピックBであれば計2つです。 iOS + Androidの2つのアプリケーションがある場合でも、通知単位を分けないのであればトピックは同じ物を利用します。 (逆に、iOSとAndroidで通知単位を分けたければ別に作ります)
これで、トピックの「ARN」である、「topic_arn」が生成されます。(APIでの送信時に必要)
Rails側のモデルのイメージ
今回の実装例に出てくるモデル(DB)については、下記のようなイメージです。
topic_1_…などtopicごとにカラムを持っていますが、topicごとにレコードを持つ形なども良いと思います。
また、実際にはプッシュ通知のメッセージを格納しているモデルなどもいろいろあるのですが、説明用に省きます(実際のものはリポジトリをご覧下さい)
user_push_notification_settings
アプリ(ユーザー)の通知許可フラグを持つテーブルです。 ユーザーに対して1レコード持つ形です。iOS側で反映したらAndroid側も同じ設定となります。
- user_id
ユーザーID
- topic_1_enabled
トピック1が有効かどうか
- topic_2_enabled
トピック2が有効かどうか
user_push_notification_tokens
アプリ(ユーザー)のデバイストークンと、それに関連付けられるARN(Amazon Resource Name)を保存するテーブル。 ユーザーに対し、アプリを利用するプラットフォーム分のレコードを持ちます。
下記の属性を持つイメージです。
- user_id
ユーザーID
- mobile_platform
プラットフォーム(ios or android)の指定
- device_token
アプリから取得したトークン
- endpoint_arn
Amazon SNSのendpoint作成時に取得するARN(Amazon Resource Name)
- topic_1_subscription_arn
Amazon SNSのトピック1をsubscribeした時に取得するARN(Amazon Resource Name)
アプリサーバー(Rails)の実装
Amazon SNSのモバイルPushとトピック通知を使うためにRails側の実装部分です。
- アプリからトークンを取得
- 取得したトークンをAmazon SNSのエンドポイントに紐付ける
- そのエンドポイントをトピックに紐付ける
- また、不要になったらエンドポイントやトピックとの紐付けを削除する
という処理です。 主に下記リクエスト時の処理が必要になります。
- トークンの新規保存、更新時
- トークンの破棄(ログアウト)時
- トピックの購読ON/OFF切り替え時
トークン(token)の変更時
1. アプリからtokenを取得
まずはアプリからtokenを取得し、Railsサーバーにリクエストします。
以下、実コードはAmazon SNSへのリクエスト処理はsidekiqで非同期処理で行っていますが、説明上Railsサーバーとしています
endpoint未作成? => YES の時
2. Amazon SNSのcreate_endpointを実行
まずはAmazon SNS側にendpointを作成します。
作成時に、そのendpointを表す 「endpoint_arn」がレスポンスに含まれます。
endpoint_arnはpush通知やトピックの紐付け(subscribe)を実行する時に必要になるのでDB等に保存します。
3. Amazon SNSのsubscribeを実行
2.で取得したendpoint_arnに、トピックの紐付け(subscribe)を行います。
subscribeのレスポンスには、「subscription_arn」が含まれます。 これは、トピックへの送信時には必要ありませんがunsubscribe時に必要になるのでDB等に保存します。
また、一度に複数トピックをsubscribeするAPIは無いため、トピック分subscribeを行う必要があります。
endpoint未作成? => NO の時
2.Amazon SNSのset_endpoint_attributesを実行
すでに作成されたendpoint内の属性を更新する、Amazon SNS のset_endpoint_attributesを実行します。 下記のパラメータを更新します。
- Token
アプリから送信された最新のtokenに上書き保存します。
- Enabled
送信可否状態です。trueまたはfalseです。 endpoint作成時の初期値はtrueですが、アプリへのPush通知が失敗すると自動的にfalseに更新されます。
アプリがPush受信可能な状態だと信じてtrueを設定します(アプリから送信された最新のtokenなので)。
サンプルコードです。 (メソッド化されているので、Amazon SNS APIの実行や具体的な処理の詳細はリポジトリをご覧ください)
# トークン(token)の変更時 def on_updated_token(user_push_notification_token) has_device_token = user_push_notification_token.device_token.present? has_endpoint_arn = user_push_notification_token.endpoint_arn.present? if has_endpoint_arn if has_device_token set_endpoint_attributes(user_push_notification_token) # b-2.Amazon SNSのset_endpoint_attributesを実行 else delete_endpoint(user_push_notification_token) unsubscribe_all_topics([user_push_notification_token]) end else return unless has_device_token create_platform_endpoint(user_push_notification_token) # a-2. Amazon SNSのcreate_endpointを実行 on_updated_setting(user_push_notification_token.user) # a-3. Amazon SNSのsubscribeを実行(後述のトピック通知設定の時と同じ処理) end end private def set_endpoint_attributes(user_push_notification_token) requester = AwsSnsRequester.new requester.set_endpoint_attributes(user_push_notification_token.endpoint_arn, user_push_notification_token.device_token) end def create_platform_endpoint(user_push_notification_token) requester = AwsSnsRequester.new response = requester.create_platform_endpoint(user_push_notification_token.user_id, user_push_notification_token.device_token, user_push_notification_token.mobile_platform.value) user_push_notification_token.endpoint_arn = response.endpoint_arn user_push_notification_token.save end def on_updated_setting(user) user_push_notification_setting = user.user_push_notification_setting UserPushNotificationSetting::SUBSCRIBE_TOPICS.each do |topic| if user_push_notification_setting.enabled_by(topic) subscribe_topics(topic, user.user_push_notification_tokens) else unsubscribe_topics(topic, user.user_push_notification_tokens) end end end
トークンの破棄(ログアウト等)時
1.アプリからtoken削除リクエストを送信
アプリからのログアウト時など、Railsサーバーに削除リクエストを送信します。
2.Amazon SNSのdelete_endpointを実行
作成済みのAmazon SNS上のendpointを削除します。 DB上に保存されたendpoint_arnも同時に削除します。
3.Amazon SNSのunsubscribe を実行
delete_endpointでendpointを削除しても、自動でそれに関連付けされたsubscription_arnも削除されるわけではありません(自動でやってほしい…)
なので、subscribeをしていた場合には、unsubscribeをしておく必要があります。
ドキュメントにも注意書きがあります。
https://docs.aws.amazon.com/ja_jp/sns/latest/api/API_DeleteEndpoint.html
When you delete an endpoint that is also subscribed to a topic, then you must also unsubscribe the endpoint from the topic
DB上に保存されたsubscription_arnも同時に削除します。
実装を一部抜粋してコメントを追記したものです。(メソッド化されているので、Amazon SNS APIの実行や具体的な処理の詳細はリポジトリをご覧ください)
※ トークンの更新時とコードと同じです。delete_endpointが追加されています。
def on_updated_token(user_push_notification_token) has_device_token = user_push_notification_token.device_token.present? has_endpoint_arn = user_push_notification_token.endpoint_arn.present? if has_endpoint_arn if has_device_token refresh_attributes(user_push_notification_token) else delete_endpoint(user_push_notification_token) # 2.Amazon SNSのdelete_endpointを実行 unsubscribe_all_topics([user_push_notification_token]) # 3.Amazon SNSのunsubscribe を実行 end else return unless has_device_token create_platform_endpoint(user_push_notification_token) on_updated_setting(user_push_notification_token.user) end end private def delete_endpoint(user_push_notification_token) AwsSnsRequester.new.delete_endpoint(user_push_notification_token.endpoint_arn) user_push_notification_token.endpoint_arn = "" user_push_notification_token.save end
トピックの購読ON/OFF切り替え時
トピック購読の切り替えはかなり単純です。 購読状態を見て、subscribeまたはunsubscribeするだけです。
1.アプリからトピックの選択状態を送信
トピックごとにtrue/falseなど、subscribeまたはunsubscribeするための状態を送信します。
2.Amazon SNSのsubscribe/unsubscribeを実行
アプリから送信されたトピックの選択状態に合わせ、Amazon SNSのsubscribeまたはunsubscribeを実行します。
DB上に保存されたsubscription_arnにも同時に反映します。
サンプルコードです。 (メソッド化されているので、Amazon SNS APIの実行や具体的な処理の詳細はリポジトリをご覧ください)
※トークンの更新時に行うコードと同じものです。
def on_updated_setting(user) user_push_notification_setting = user.user_push_notification_setting UserPushNotificationSetting::SUBSCRIBE_TOPICS.each do |topic| # 2.Amazon SNSのsubscribe/unsubscribeを実行 if user_push_notification_setting.enabled_by(topic) subscribe_topics(topic, user.user_push_notification_tokens) else unsubscribe_topics(topic, user.user_push_notification_tokens) end end end
その他
テスト(RSpecなど)の実装はClientStubsが便利
外部APIが絡むテストを実行する場合、スタブやモックなどが必要になる事も多いですが、aws_sdkには標準でスタブに関する機能があります。 これを使う事で、テストの実装もかなり楽になりました。 https://docs.aws.amazon.com/sdkforruby/api/Aws/ClientStubs.html
例えば、RSpecでのテスト実行時は全ての処理をスタブとするのであれば、spec/rails_helper.rb等に下記のように記述します。
# テスト実行時、aws_sdkは全体的にスタブにする if Rails.env.test? Aws.config[:stub_responses] = true end
aws_sdkでテスト時のレスポンスなどを任意のものにする
また、テスト時に場合によってはレスポンス内容を変えたくなったりする事もあると思います。 その場合は、下記のように一部のレスポンスを指定する事ができます。
aws_sdkのcreate_platform_endpointを実行した時にendpoint_arnが「new_endpoint_arn」としてレスポンスされるようにしたものです。
sns_response = Aws::SNS::Types::CreateEndpointResponse.new(endpoint_arn: "new_endpoint_arn") Aws.config[:sns][:stub_responses][:create_platform_endpoint] = sns_response
まとめ
実装面やその他ふくめて、良かった所/つらかった所です。
良かった所
- 大量配信の負荷をあまり気にしなくて良い
トピック機能を使った場合は、大量配信時にもAPIを使う側は1件送信するだけ。
- 事前準備が楽
Amazon SNS側のアプリケーションの設定やトピックの設定も、シンプルで設定しやすく特に迷うこともありませんでした。
- 料金安い
Amazon SNS リクエストのうち、毎月最初の 100 万件は無料
それ以降、Amazon SNS リクエスト 100 万件ごとに 0.50¬USD
つらかった所
Amazon SNS用語やエンドポイントとトピックの関係などに慣れる必要がある
大量配信先を事前に決められない時がつらい
Amazon SNSのpush通知送信API(publish)は、1つのエンドポイントARN、もしくはトピックARNを対象するため、トピックに紐付かない複数のエンドポイントへの送信ができないようです。送信数分ループ処理をする事になりますが、送信数によってはサーバー負荷となってしまいそうです。
以上、トピック型のモバイルPush通知をRails + Amazon SNSで同じく実装した時に整理したことなどを纏めてみました。
これからPush通知の運用する中で、新たに問題点や改善点も出てきそうですが何かしらの形で共有できればと思います。
(☝︎ ՞ਊ ՞)☝︎是非読者になってください
メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!
■募集ポジションはこちら
https://medpeer.co.jp/recruit/entry/
■開発環境はこちら