こんにちは。メドピアに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のモバイルプッシュ通知
いろいろなデバイス(プラットフォーム)に、送信側はそれほど処理を変えずに簡単にプッシュ通知できるサービスです。
他にもこんな特徴があります。
プッシュ通知の送信が失敗すると、状態変更(後述)されるまで送信しない
- 個別の送信先、またトピック機能を使う事で複数の送信先にも一度に送信できる
詳細
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側も同じ設定となります。
ユーザーID
トピック1が有効かどうか
トピック2が有効かどうか
user_push_notification_tokens
アプリ(ユーザー)のデバイストークンと、それに関連付けられるARN(Amazon Resource Name)を保存するテーブル。
ユーザーに対し、アプリを利用するプラットフォーム分のレコードを持ちます。
下記の属性を持つイメージです。
ユーザーID
プラットフォーム(ios or android)の指定
アプリから取得したトークン
Amazon SNSのendpoint作成時に取得するARN(Amazon Resource Name)
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に上書き保存します。
送信可否状態です。trueまたはfalseです。
endpoint作成時の初期値はtrueですが、アプリへのPush通知が失敗すると自動的にfalseに更新されます。
アプリがPush受信可能な状態だと信じてtrueを設定します(アプリから送信された最新のtokenなので)。
サンプルコードです。
(メソッド化されているので、Amazon SNS APIの実行や具体的な処理の詳細はリポジトリをご覧ください)
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)
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)
on_updated_setting(user_push_notification_token.user)
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)
unsubscribe_all_topics([user_push_notification_token])
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|
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等に下記のように記述します。
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
https://aws.amazon.com/jp/sns/pricing/
つらかった所
Amazon SNSのpush通知送信API(publish)は、1つのエンドポイントARN、もしくはトピックARNを対象するため、トピックに紐付かない複数のエンドポイントへの送信ができないようです。送信数分ループ処理をする事になりますが、送信数によってはサーバー負荷となってしまいそうです。
以上、トピック型のモバイルPush通知をRails + Amazon SNSで同じく実装した時に整理したことなどを纏めてみました。
これからPush通知の運用する中で、新たに問題点や改善点も出てきそうですが何かしらの形で共有できればと思います。
(☝︎ ՞ਊ ՞)☝︎是非読者になってください
メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!
■募集ポジションはこちら
https://medpeer.co.jp/recruit/entry/
■開発環境はこちら
https://medpeer.co.jp/recruit/workplace/development.html