メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

トピック型のモバイルPush通知をRails + Amazon SNSで実装する

こんにちは。メドピアにWebエンジニアで入社して約6ヶ月の佐藤です。

メドピアは2/26から銀座に移転しました。 銀座に移転しても花粉からは逃げられませんでしたが 、移転後はなぜか空気清浄機が増えて助かっています。

書いてある事

Amazon SNSの複数ユーザーに一度に通知を送る「トピック」を使ってRailsでPush通知を実装した際の処理フローを主に書いてあります。

f:id:motsat:20180313115508p:plain:w500

トピック機能を使わず、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ができる

アプリ側の設定画面の雰囲気です。

f:id:motsat:20180312205655p:plain

Amazon SNS側作業

AWS マネジメントコンソールで事前に下記の作業を行います。 (APIのユーザー管理やアクセス権限周りも必要あれば設定)

  • アプリケーション作成
  • トピック作成

これらはAPIで作成することもできますが、今回は事前に作成する形にしたのでAWS マネジメントコンソールでの作業を前提とします。

アプリケーション作成

プラットフォームと対になるものです。 iOS、Android分であれば計2つを作成します。

必要なもの

  • iOSのp12ファイルとそのパスワード (Androidは試していませんが、GCMアクセスキーが必要となるはずです)


下記はiOSのアプリケーション作成画面です。

f:id:motsat:20180311214507p:plain

  • アプリケーション名
  • iOSの場合はDevelopmentまたはProductionの選択
  • p12ファイルパスワードを入力

です。(証明書はp12ファイルから勝手に入力されます)

これで、アプリケーションの「ARN」である、「application_arn」が生成されます。(APIでの送信時に必要)

ARN(Amazon Resource Name)は、AWS上に何かリソースを作成した時に与えられる名前です。

この先に出てくる他のリソース(トピック、エンドポイント等)にもARNが設定され、 同じくAPIでそのリソースを指定する時などに使います。

トピック作成

トピックA、トピックBであれば計2つです。 iOS + Androidの2つのアプリケーションがある場合でも、通知単位を分けないのであればトピックは同じ物を利用します。 (逆に、iOSとAndroidで通知単位を分けたければ別に作ります)

f:id:motsat:20180311214515p:plain

これで、トピックの「ARN」である、「topic_arn」が生成されます。(APIでの送信時に必要)

Rails側のモデルのイメージ

今回の実装例に出てくるモデル(DB)については、下記のようなイメージです。

f:id:motsat:20180313113150p:plain

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)の変更時

f:id:motsat:20180311213737p:plain

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

トークンの破棄(ログアウト等)時

f:id:motsat:20180311214119p:plain

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するだけです。

f:id:motsat:20180311214133p:plain

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

 https://aws.amazon.com/jp/sns/pricing/

つらかった所

  • Amazon SNS用語やエンドポイントとトピックの関係などに慣れる必要がある

  • 大量配信先を事前に決められない時がつらい

  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