タグ: AWS

53ningen.com 大改修

 学生時代に適当に構築してツギハギだらけになっていた 53ningen.com が動いているサーバー gomi-old01 を良き形にしつつ gomi-web01 に移転した作業ログです。基本的にただ WordPress が動いていたサーバーを Ansible 化して、お引越ししたというだけの内容ではあります。やったことは具体的にはつぎのようなものです。

  1. DNS サーバーの Route 53 移行
  2. ドメインの Route 53 移管
  3. 稼働しているサーバーの Ansible 化
  4. データベースの引越し
  5. WordPress アプリケーションの引越し
  6. 新サーバーへ DNS 切り替え

また次のものは新規で導入しました

  • L7 レイヤーでのヘルスチェック導入
  • 障害発生時に DNS フェイルオーバーでエラー画面を表示

  • サーバーログの S3 への送出

  • elasticsearch, kibana
  • zabbix による監視, Slack 通知

1. DNS サーバーの Route 53 移行

 ドメインも Route 53 に移管しますが、その際に元のレジストラ(ムームードメイン)が提供するネームサーバーからドメインが削除されてしまうので、前もって DNS を Route 53 に切り替えておきます。

1.1. ホストゾーン(Hosted Zone)の作成

公式ドキュメント: 既存ドメインの DNS サービスを Amazon Route 53 に移行する

 ホストゾーンとはドメインに紐づくリソースレコードのセットです。おおよそゾーンファイルと対応していると考えて良いと思います。実際にゾーンファイルを取り込むこともできるようです(公式ドキュメント: ゾーンファイルをインポートしてリソースレコードセットを作成する)。

 ホストゾーンには、パブリックなものとVPC向けのプライベートなものがあります。マネジメントコンソールからドメイン名を入力すれば作成が完了しますが、今回はこのリソースを terraform で管理します。

terraform plan は以下のとおり

 terraform apply してホストゾーンを作成すると SOA レコードNS レコード も自動的に作成されます。今回はドメイン名とIPアドレスを紐づけるために A レコード の追加が必要になります。そのため、その記述を追加します。メールサーバーの移行が必要であれば MX レコード、サブドメインなどの運用をやっているのであれば CNAME レコード など適当な感じに必要なものも移行しておきましょう。ひとまず今回は A レコード のみを作ります(Route 53 が対応しているDNSリソースは サポートされる DNS リソースレコードタイプ を参照)。terraform apply を実行するとレコードが作成されます。

1.2. DNSの変更

 マネジメントコンソールの NS レコード欄の DNS サーバーを、ムームードメインのコントロールパネルで指定します。反映が有効になるまで少し(TTL分)時間がかかるため、少し待って dig で確認すると良いとおもいます。

1.3. コストの計算

公式ドキュメント: 料金

 お仕事ならこの計算作業する前にやるべきですが、個人のお遊びサーバーなので作業後に計算しました。

  • ホストゾーン
    • 0.50 USD(ホストゾーンごと)/月 – 最初の25のホストゾーン
    • 0.10 USD(ホストゾーンごと)/月 – それ以上のホストゾーン
  • 標準的クエリ
    • 0.400 USD(100 万クエリごと) – 最初の 10 億クエリ/月
    • 0.200 USD(100 万クエリごと) – 10 億クエリ以上/月

おおよそ 1ホストゾーン + 最初の10億クエリ = 0.50 + 0.40 = 0.90 USD

1.4. ヘルスチェックの設定 (Optional)

公式ドキュメント: Amazon Route 53 ヘルスチェックの作成と DNS フェイルオーバーの設定

 ヘルスチェック(L7)とDNSフェイルオーバーが利用できます。ヘルスチェックで障害を検知した場合に Cloudwatch Alarm と連携をさせたり、DNSフェイルオーバーを利用して待機系に参照を向けたりすることが可能です。

 ひとまずベーシックなウェブサーバーのヘルスチェックは以下のように設定できます。その他諸々 terraform を利用する際にオプション記述については terraform 公式ドキュメント: aws_route53_health_check を参照してください。

ヘルスチェックに関しては、

  • AWSエンドポイント
    • ヘルスチェック 1 件につき 0.50 USD*/月
    • オプション機能 1 件につき 1.00 USD/月
  • AWS以外のエンドポイント:
    • ヘルスチェック 1 件につき 0.75 USD*/月
    • オプション機能 1 件につき 2.00 USD/月
  • 任意のヘルスチェック機能は以下の通りです。
    • HTTPS
    • 文字列マッチング
    • 短インターバル
    • レイテンシー計測

となっています。

「AWS エンドポイント」とは、AWS の中で稼働するリソース(たとえば Amazon EC2 インスタンス)のうち、ヘルスチェックと同じ AWS アカウントの中でプロビジョニングされるか、ヘルスチェックと同じアカウントが請求先となっているものを指します。計算済みヘルスチェックとメトリクスベースのヘルスチェックは AWS エンドポイントのヘルスチェックとして請求されます。Elastic Load Balancing のリソースまたは Amazon S3 ウェブサイトバケットがエンドポイントであるヘルスチェックについては、お客様への請求は発生しません。Elastic Load Balancing のリソースおよび Amazon S3 ウェブサイトバケットをエンドポイントとするヘルスチェックについては、AWS によって自動的にプロビジョニングされ、Amazon Route 53 の一部として追加料金なしでご利用いただけます。

1.5 DNSフェイルオーバーの設定 (Optional)

公式ドキュメント: DNS フェイルオーバーの設定
terraform 公式ドキュメント: aws_route53_record

 ヘルスチェックを入れたついでに、障害発生時に S3 の静的ホスティング機能 + Cloudfront によるメンテナンスページのほうへ参照を向けるようなDNSフェイルオーバー設定を入れてみます。DNSフェイルオーバー設定については無料で利用することができます。最終的な構成図は以下のような形になります。


 
 ディザスタリカバリを想定した構成として、ap-northeast-1 リージョンで稼働させるアプリケーションサーバーに対して、メンテナンスページを静的ホスティングする際の S3 リージョンは地理・物理的に違う系統の適当なリージョンを選択します。今回は us-east-1 を選択しました。

 S3 の前段に置いた Cloudfront は主に S3 からの配信をキャッシュする役割と、メンテナンスを表すステータスコード 503 でリソースを返す役割を持っています。S3 バケットの作成とファイルの配置、静的ホスティング機能の有効化および Cloudfront の設定は以下のように terraform で記述することができます。

terraform 公式ドキュメント: aws_s3_bucket
terraform 公式ドキュメント: aws_cloudfront_distribution

resources/error には、適当なメンテナンス表示 html ファイルを、 policies/maintenance.53ningen.com_s3_policy.json には以下のようにバケットのファイルの読み取りを許可するポリシーを配置します。

つづいて Route 53 ホストゾーンにフェイルオーバーの設定をします。通常時は既存の A レコードが有効になるようにしつつ、ヘルスチェックに失敗した場合にメンテナンスページを静的にホスティングしている S3 のほうに参照を向けるような設定を入れます。また ttl を最初の設定より短く 60 秒に変更します。 terraform を用いて次のように記述できます。

 最後に動作確認として、動いているウェブサーバーを停止し、きちんとメンテナンス表示に切り替わるか確認します。またウェブサーバーを起動したときに、ヘルスチェックが通るようになり、きちんとアプリケーションサーバーに参照が戻るかも合わせてチェックしておきましょう。

2. ドメインの Route 53 移管

公式ドキュメント: ドメインの移管

ムームードメインで購入した 53ningen.com ドメインを Route 53 に移管します。ステップとしては以下のとおりです。

  1. WHOIS 情報としてレジストラの情報を代理公開している場合は、ムームードメインのコントロールパネルから自分のものに変更する
  2. 確認のメールが飛んでくるので踏む
  3. AUTH_CODE をメモ
  4. Route 53 の Domain registration から Transfer Domain to Route 53 に進みドメイン名を入力
  5. AUTH_CODE を入力
  6. 移管後のDNSとして先ほど作った Route 53 の Hosted Zone を指定する
  7. AWSから移管確認のメールが飛んでくるので踏む
  8. そのうち移管が完了する

Waiting for the current registrar to complete the transfer (step 7 of 14)
ドメインが移管の要件を満たしているか現在のレジストラが確認しています。このステップは、ドメインの TLD に応じて最大 10 日かかる場合があります。

結構待ったりいろんな確認メールのリンクを踏んだりする必要がありますが、あまりテクニカルな手順はないはず…。

3. 稼働しているサーバーの Ansible 化

地道に頑張るしかない。

4. データベースの引越し

まずは新サーバーで動いている MariaDB へのデータ移行と参照の切り替えを行うことにより、データベースサーバーのみを引っ越しします。手順は大まかに次のとおりです。

  1. 現行 DB の dump を取り、新 DB にインポートする
  2. アプリケーションから参照する MariaDB ユーザーに現行サーバーからの接続許可を与える
  3. TCP 3306 ポートを開ける
  4. 現行アプリケーション設定を書き換えて新 DB に向ける
  5. 動作確認

まずは現行 DB からのデータ移転作業は以下のような感じになります。

続いて、アプリケーションが接続する mysql のユーザーの作成と設定をします。現行アプリケーションサーバーからの接続を許可する設定を入れる必要があります。ansible で次のようなものを実行します。

またリモートからの接続をするために firewalld を設定して 3306 ポートを開けます。

最終的に現行アプリケーション設定 wp-config.php を書き換えて新 DB に向き先を切り替えます。

最後に動作確認をひと通りして、問題なさそうであればデータベースの引っ越しは完了となります。

5. WordPress アプリケーションの引越し

おおまかな手順は以下の通りです。

  1. 現行 WordPress の配置ディレクトリごと tar で固める
  2. 新サーバーに固めたものを展開して、適切なパスに配置する
  3. wp-config.php をの DB_HOSTlocalhost に戻す
  4. nginx の設定を更新し、外から WordPress の動作を確認できるようにする
  5. /etc/hosts を書き換え、新サーバー上の WordPress が正しく動いているか動作確認

WordPress アプリケーションの圧縮と展開は次のようなコマンドで簡単に行えます。。

 次に、直前の DB 参照切り替えで WordPress の設定を変えているので、新サーバー内に配置した WordPress の wp-config.phpDB_HOST 設定は localhost に向けなおします。これが終わったら、nginx.conf を更新して外部から動作確認できる状態にします。ざっくりと例えば次のように記述すれば OK です(その他各位適切な設定をいい感じにしてください)。

最後に、手元 /etc/hosts に以下を追記してひととおりアプリケーションの動作を確認します。

動作確認して問題がなければ WordPress の移行も完了になります。

6. DNS 切り替え

Route53 に登録している Aレコードの向き先を変更して、新サーバーに完全移行します。移行して動作に問題がなさそうであれば、MariaDB の gomi-old01 から接続できるユーザーを削除し、firewalld で 3306 ポートを閉じておきます。

Route53 で S3 バケットへ alias レコードを作った際のリダイレクトのふるまい

 Amazon S3 は、全てのリクエストをリダイレクトや、パスに応じたリダイレクトを設定できる機能を持っています。これは Static Web Hosting という設定を入れることにより実現可能です。実際の技術詳細については以下の公式ドキュメントを参照してください。

  • 公式ドキュメント: https://docs.aws.amazon.com/AmazonS3/latest/dev/how-to-page-redirect.html

リダイレクト設定

gochiusa.53ningen.com に対して gochiusa.com へのリダイレクト設定作業は以下の 3 STEP です

  1. S3 バケットを gochiusa.53ningen.com という名前で作成する
  2. Static Web Hosting 機能を有効にして、すべてのリクエストを gochiusa.com にリダイレクトする設定を入れる
  3. Route 53 に gochiusa.53ningen.com に対して先ほど作ったバケットへのエイリアスを張る A レコードを作成する

動作確認

作業後、設定を dig で確認します

解決される 52.219.68.26 は S3 の静的配信サーバーの IP の模様で、振る舞い的には Host ヘッダと同じ名前を持つ S3 バケットへリダイレクトをかけるという形のようです。それを確認するために次のようなリクエストを送ってみます。

x-amz-error-message: Request does not contain a bucket name. というメッセージからわかるようにバケットネームの指定がないというエラーで AWS S3 のトップページに飛ばされます。次に Host ヘッダをつけるとどうなるか試してみましょう。

The specified bucket does not exist というエラーメッセージに変わりました。ではそろそろ真面目に先ほど作った gochiusa.53ningen.com という値を Host ヘッダにつけて叩いてみましょう。

正常に 301 で http://gochiusa.com/ にリダイレクトされていることが確認できました。

実験からわかること

  1. S3 バケットへの alias レコードセットの制約の理由が推測できる
    • Route53 で S3 static web hosting しているバケットに alias を張る際は、公式ドキュメントに記載されているように Record Set の Name と S3 バケット名が一致している必要がある
    • これは Host ヘッダをみて、対象の S3 静的配信コンテンツへのリダイレクトを行なっている仕組みから生まれる制約だろうということが、上記の実験から分かる
  2. S3 バケットへの alias を張っているドメインを指す CNAME を張ると正常に動作しない
    • 実際に gochiusa2.53ningen.com に対して gochiusa.53ningen.com を指すような CNAME レコードセットを作ると以下のような実験結果になる

Route53へのDNSとドメインの移行

次のものを AWS に移行しました。

  • ドメイン移管(53ningen.com): ムームードメインから Route 53 へ
  • ドメインネームサーバー移行: ムームードメインから Route 53 へ

また、もとの運用では存在しなかった、次のようなものを導入しました。

  • L7レイヤーでのヘルスチェック
  • DNSフェイルオーバー

この資料は自分向けの作業ログであるため、実際には各ステップ冒頭に記された公式ドキュメントを理解した上で読むことを強く推奨します。

手元環境

 作業は基本的には terraform を使って行いました。ただし、ドメインの移管に関しては Route 53 のマネジメントコンソールとムームードメインのコントロールパネルを操作する必要があります。

1. DNS を Route 53 へ移行

 ドメインも Route 53 に移管しますが、その際に元のレジストラ(ムームードメイン)が提供するネームサーバーからドメインが削除されてしまうので、前もって DNS を Route 53 に切り替えておきます。

1.1. ホストゾーン(Hosted Zone)の作成

公式ドキュメント: 既存ドメインの DNS サービスを Amazon Route 53 に移行する

 ホストゾーンとはドメインに紐づくリソースレコードのセットです。おおよそゾーンファイルと対応していると考えて良いと思います。実際にゾーンファイルを取り込むこともできるようです(公式ドキュメント: ゾーンファイルをインポートしてリソースレコードセットを作成する)。

 ホストゾーンには、パブリックなものとVPC向けのプライベートなものがあります。マネジメントコンソールからドメイン名を入力すれば作成が完了しますが、今回はこのリソースを terraform で管理します。

terraform plan は以下のとおり

 terraform apply してホストゾーンを作成すると SOA レコードNS レコード も自動的に作成されます。今回はドメイン名とIPアドレスを紐づけるために A レコード の追加が必要になります。そのため、その記述を追加します。メールサーバーの移行が必要であれば MX レコード、サブドメインなどの運用をやっているのであれば CNAME レコード など適当な感じに必要なものも移行しておきましょう。ひとまず今回は A レコード のみを作ります(Route 53 が対応しているDNSリソースは サポートされる DNS リソースレコードタイプ を参照)。terraform apply を実行するとレコードが作成されます。

1.2. DNSの変更

 マネジメントコンソールの NS レコード欄の DNS サーバーを、ムームードメインのコントロールパネルで指定します。反映が有効になるまで少し時間がかかるため、少し待って dig で確認すると良いとおもいます。

1.3. コストの計算

公式ドキュメント: 料金

 お仕事ならこの計算作業する前にやるべきですが、個人のお遊びサーバーなので作業後に計算しました。

  • ホストゾーン
    • 0.50 USD(ホストゾーンごと)/月 – 最初の25のホストゾーン
    • 0.10 USD(ホストゾーンごと)/月 – それ以上のホストゾーン
  • 標準的クエリ
    • 0.400 USD(100 万クエリごと) – 最初の 10 億クエリ/月
    • 0.200 USD(100 万クエリごと) – 10 億クエリ以上/月

おおよそ 1ホストゾーン + 最初の10億クエリ = 0.50 + 0.40 = 0.90 USD

1.4. ヘルスチェックの設定 (Optional)

公式ドキュメント: Amazon Route 53 ヘルスチェックの作成と DNS フェイルオーバーの設定

 ヘルスチェック(L7)とDNSフェイルオーバーが利用できます。ヘルスチェックで障害を検知した場合に Cloudwatch Alarm と連携をさせたり、DNSフェイルオーバーを利用して待機系に参照を向けたりすることが可能です。

 ひとまずベーシックなウェブサーバーのヘルスチェックは以下のように設定できます。その他諸々 terraform を利用する際にオプション記述については terraform 公式ドキュメント: aws_route53_health_check を参照してください。

ヘルスチェックに関しては、

  • AWSエンドポイント
    • ヘルスチェック 1 件につき 0.50 USD*/月
    • オプション機能 1 件につき 1.00 USD/月
  • AWS以外のエンドポイント:
    • ヘルスチェック 1 件につき 0.75 USD*/月
    • オプション機能 1 件につき 2.00 USD/月
  • 任意のヘルスチェック機能は以下の通りです。
    • HTTPS
    • 文字列マッチング
    • 短インターバル
    • レイテンシー計測

となっています。

「AWS エンドポイント」とは、AWS の中で稼働するリソース(たとえば Amazon EC2 インスタンス)のうち、ヘルスチェックと同じ AWS アカウントの中でプロビジョニングされるか、ヘルスチェックと同じアカウントが請求先となっているものを指します。計算済みヘルスチェックとメトリクスベースのヘルスチェックは AWS エンドポイントのヘルスチェックとして請求されます。Elastic Load Balancing のリソースまたは Amazon S3 ウェブサイトバケットがエンドポイントであるヘルスチェックについては、お客様への請求は発生しません。Elastic Load Balancing のリソースおよび Amazon S3 ウェブサイトバケットをエンドポイントとするヘルスチェックについては、AWS によって自動的にプロビジョニングされ、Amazon Route 53 の一部として追加料金なしでご利用いただけます。

1.5 DNSフェイルオーバーの設定 (Optional)

公式ドキュメント: DNS フェイルオーバーの設定
terraform 公式ドキュメント: aws_route53_record

 ヘルスチェックを入れたついでに、障害発生時に S3 の静的ホスティング機能 + Cloudfront によるメンテナンスページのほうへ参照を向けるようなDNSフェイルオーバー設定を入れてみます。DNSフェイルオーバー設定については無料で利用することができます。最終的な構成図は以下のような形になります。


 
 ディザスタリカバリを想定した構成として、ap-northeast-1 リージョンで稼働させるアプリケーションサーバーに対して、メンテナンスページを静的ホスティングする際の S3 リージョンは地理・物理的に違う系統の適当なリージョンを選択します。今回は us-east-1 を選択しました。

 S3 の前段に置いた Cloudfront は主に S3 からの配信をキャッシュする役割と、メンテナンスを表すステータスコード 503 でリソースを返す役割を持っています。S3 バケットの作成とファイルの配置、静的ホスティング機能の有効化および Cloudfront の設定は以下のように terraform で記述することができます。

terraform 公式ドキュメント: aws_s3_bucket
terraform 公式ドキュメント: aws_cloudfront_distribution

resources/error には、適当なメンテナンス表示 html ファイルを、 policies/maintenance.53ningen.com_s3_policy.json には以下のようにバケットのファイルの読み取りを許可するポリシーを配置します。

つづいて Route 53 ホストゾーンにフェイルオーバーの設定をします。通常時は既存の A レコードが有効になるようにしつつ、ヘルスチェックに失敗した場合にメンテナンスページを静的にホスティングしている S3 のほうに参照を向けるような設定を入れます。また ttl を最初の設定より短く 60 秒に変更します。 terraform を用いて次のように記述できます。

 最後に動作確認として、動いているウェブサーバーを停止し、きちんとメンテナンス表示に切り替わるか確認します。またウェブサーバーを起動したときに、ヘルスチェックが通るようになり、きちんとアプリケーションサーバーに参照が戻るかも合わせてチェックしておきましょう。

2. ドメインを Route 53 に移管する

公式ドキュメント: ドメインの移管

ムームードメインで購入した 53ningen.com ドメインを Route 53 に移管します。ステップとしては以下のとおりです。

  1. WHOIS 情報としてレジストラの情報を代理公開している場合は、ムームードメインのコントロールパネルから自分のものに変更する
  2. 確認のメールが飛んでくるので踏む
  3. AUTH_CODE をメモ
  4. Route 53 の Domain registration から Transfer Domain to Route 53 に進みドメイン名を入力
  5. AUTH_CODE を入力
  6. 移管後のDNSとして先ほど作った Route 53 の Hosted Zone を指定する
  7. AWSから移管確認のメールが飛んでくるので踏む
  8. そのうち移管が完了する

Waiting for the current registrar to complete the transfer (step 7 of 14)
ドメインが移管の要件を満たしているか現在のレジストラが確認しています。このステップは、ドメインの TLD に応じて最大 10 日かかる場合があります。

結構待ったりいろんな確認メールのリンクを踏んだりする必要がありますが、あまりテクニカルな手順はないはず…。

ES2015+Flowなlambda関数をApexで管理する

ES2015 + 型チェッカー flow で書かれた AWS Lambda 関数を Apex で管理する方法について、つらつらとメモ。この記事は @nagisio 先生のご指導のおかげで成立しています(謝辞)。

  • キーワード
    • AWS Lambda
    • yarn
    • ES2015
    • flow
    • Apex

記事読むの面倒くさいので、動くリポジトリよこせという方はここをcloneすべし(動かしてから理解する方が楽なのですよねー :wakaru:)

ES2015 + flow + Apex の環境構築

yarn の導入

導入はかんたん。

  1. npm initpackage.json を作る
  2. npm install yarn --dev で yarn を導入する
    • global に入れたい方はどうぞ

package.json はきっとこんな感じになっているのではなかろうか

ESS2015 + flow の環境構築

まずは必要なものを片っ端からいれていく

次に .babelrc を次のようにする

node_modules/flow-bin/cli.js init でflow の設定ファイル .flowconfig を作り、以下のような感じにする

もう多分この時点で ES2015 + flow のコードが書けるので適当に src/hello/index.js に適当なコードを書いてみよう

続いてトランスパイルして functions 下に成果物を出力してみよう

成果物は次のようになっているだろう

良い感じにハローワークに行きたいくなる良いラムダ関数ですね。さて、ここまできたらあとは Apex を導入して AWS にデプロイするだけです。

ちょっとその前に、トランスパイルするコマンドが長ったらしくてだるいのでエイリアスを張っておきましょう。package.json の script の部分をちょちょいといじるといい感じに npm run build で諸々実行できます。

Apex の導入

今回 AWS Lambda 関数の管理には Apex をつかいます。Apexの挙動は非常に単純で project.json に指定されたリソースやタイムアウト時間、IAMロールの設定で、functions直下にある各ディレクトリをそれぞれラムダ関数とみなし、デプロイしてくれます。

導入はかんたん

project.json をイカのよう設定すればよいでゲソ(各自IAMロールは適切なものを指定してください)

apex deploy でデプロイできます。apex invoke hello で実行できます。

良い

ESLint の導入(お好みで)

ESLintを入れたい方は適当に必要そうなやつをどうぞ(適当)

.eslint を適当に書く

deploy 前に uglify の処理を入れる

lambda の起動時間は多分デプロイされる成果物のサイズが小さい方がはやいので、uglifyをかけたほうがよさそう。packaage.jsonを書き換えればいい感じに実現可。

npm run build:prod で functions 下にできる成果物を確認してみるとちゃんとできてるか確認出来る。

ステージ管理

このあたりの対応が一番筋がよさそう? Lambda with Apex: 環境変数で環境別にLambda環境を整える

おわりに

ES2015 + flow + Apex で快適な AWS Lambda ライフを!

大規模ネイティブアプリへのプッシュ通知機能導入にあたって考えたこと

この記事は 「ドワンゴ Advent Calendar 2016」23日目の記事です。

 この記事は、比較的規模の大きなサービスのネイティブアプリにプッシュ通知機能を導入する風景を淡々と描いたものです。過度な期待はしないでください。なお、一応お約束として書いておくと、この記事は個人の見解であり、所属する組織の公式見解ではありません。

はじめに

 Amazon SNSFirebase などのプッシュ通知配信基盤のおかげで、iOS/Android 端末へのモバイルプッシュ通知の実装自体は簡単にできるようになってきました。これらのサービス基盤の利用にはサーバーサイドの知識はほぼ必要なく、普段ネイティブアプリ開発しかやっていないような方でも簡単に利用することができる作りになっています。しかしながら、実際に比較的大きなサービスにモバイルプッシュ通知機能を導入・運用していくにあたっては、地味に考慮しなければならない点がいくつも見つかりました。

 Amazon SNS を利用したプッシュ通知の実装方法自体については公式のドキュメントをはじめとして、Qiita にも記事がゴロゴロ転がっているのでお好きなものを参照していただくとして、この記事では大規模サービスのネイティブアプリにプッシュ通知機能を導入する際に考えなければならないことをまとめていくことにします。

どんなプッシュ通知機能が必要なのか

 プッシュ通知機能は諸刃の剣です。ユーザーに対してサービス側から好きなタイミングでメッセージを送ることができる一方で、不要な通知はユーザーの気分を害します。そしてそれは、通知をオフにされたり、アンインストールされたりといったことに繋がるでしょう。

 ネイティブアプリにプッシュ通知機能を導入する際にまず考えなければならないことは、「なんのためにどんなプッシュ通知を送るのか」を決めることです。非常にマジレス的でつまらない、当たり前の内容ですね。

 しかし、世の中には意味不明なプッシュ通知を大量に送ってくるアプリがあり、私も日頃アンインストールしまくっているので、こうした当たり前のことを検討していないか、ユーザーへのいやがらせのためにプッシュ通知を送って楽しんでいる悪趣味な方がたくさんいるような雰囲気を感じます。

機能要件の想定とプッシュ通知のパターン

 さて、とあるところに作品を投稿できるサービスがあり、ネイティブアプリから作品が閲覧できるシステムがあったと仮定しましょう。ユーザーはきっと、気に入った作品が更新されたら通知が欲しいのではないでしょうか。また、サービス内の突発的なイベントや面白い作品の情報などについても通知が欲しいというケースがあると思います。

event.png

プッシュ通知パターンの分類

ここで登場した2つの通知パターンについて詳しくみていくと、特徴が違います。

  1. 気に入った作品の更新情報通知(個別通知
  2. イベントなどの情報通知(全体通知

 前者は、ある作品をお気に入り登録しているユーザーにだけ通知がいく個別通知になります。一方後者は、任意のタイミングで全ユーザーに通知がいく全体通知になります。そして、個別通知は比較的定形のメッセージ、全体通知は自由なメッセージを送りたいケースが多いのではないでしょうか。まとめると以下の表のようになります。

種別 内容 送信先 通知発生タイミング メッセージ形式
個別通知 お気に入り作品の更新情報など 個別のユーザー サービス運営者による制御が難しい ほぼ定形
全体通知 イベントやお知らせなど 全ユーザー サービス運営者により制御できる 自由

 世の中にたくさんのアプリが存在しますが、実はどんなアプリも通知のパターンとしては、だいたいこのようなものに落ち着くのではないでしょうか。

通知パターンの分類から検討事項を洗い出す

送りたいプッシュ通知の内容を分類した結果をみると、いくつか検討が必要な項目が見当たります。それぞれについて見ていきましょう。

検討1:個別通知の発火タイミング

 ユーザーが作品を投稿できるようなサービスの場合、そのイベントの発生時刻をサービス運営者が制御することは不可能といって良いでしょう。これは、真夜中にユーザーのデバイスに通知が行ってしまう可能性があるということを意味します。

 こうした場合、ユーザーのデバイスにプッシュ通知が到達したあと、すぐにメッセージを表示せずに溜めておき(サイレントプッシュ通知)、定時にユーザーに知らせるという手法を検討すると良いでしょう。

一般的な方はアプリを

  1. 通勤・通学中(7:00〜9:00)
  2. お昼休み(12:00〜13:00)
  3. 学校や会社終わり(15:00〜18:00)
  4. 寝る前(21:00〜24:00)

などに使うことが多いのではないでしょうか。このあたりは各アプリのユーザーの特性によって大きくかわる部分ではありますが、いずれにせよユーザーがいつ通知を欲しがるかを検討することは必要かと思います。

 そして必要であれば、サービス運営者が制御できないイベント通知に関してサイレントプッシュ通知を利用し情報をデバイスにスタックしたのちに、ユーザーへ定時にメッセージを出すことを検討してみてください。

検討2:全体通知メッセージの内容や遷移先のテスト

 全体通知として、サービス運営者がメッセージを自由に入力し、アプリ内での遷移先を自由に定めることができる仕組みを作った場合に考慮しなければならないことは、オペレーティングミスです。人間が自由に操作できる部分が大きければ大きいほど、この手のミスは起こりやすいでしょう。また、プッシュ通知は一度送信してしまうと、取り消しは基本的にはできないため、ミスに対する取り返しはほぼつきません。手が滑って適当なメッセージを送ってしまえば、炎上の要因にもなりうるでしょう。

 こうしたことは、送信予定のメッセージをあらかじめサービス運営者のみに送信できる仕組みを作れば、ある程度防ぐことができるのではないでしょうか。サービスの規模や特性などにもよりますが、プッシュ通知の事故のリスクとテストメッセージ送信の仕組みを天秤にかけ、必要かどうか判断することになるかと思います。

機能要件まとめ

以上のことを検討した結果、次のような機能を実装する必要があると仮定しましょう。

  • 特定ユーザーへイベントごとの定形メッセージを通知する機能(個別通知)
    • アプリ側でメッセージの出現タイミングを制御する機能(サイレントプッシュ+定時通知)
  • 全ユーザーに対し、自由なメッセージや遷移先を指定した通知を送る機能(全体通知)
    • 事前にサービス運営者にのみにテスト送信・動作確認できる機能(通知テスト機能)

以降はこれらの機能を Amazon SNS を用いて構成していくことについて考えていきます。

プッシュ通知機能をどのように実装するのか

 いよいよプッシュ通知システムの実装方法について検討していきます。ここまで技術的な話があまりなかったので、ただのポエムに成り下がるところでした。危ない…。

Amazon SNS モバイルプッシュ通知概論

 Amazon SNS を用いて iOS/Android デバイスにプッシュ通知を送るためには次のような手順を踏む必要があります。初見の方にはよく分からない単語が出てくるかと思いますが、ひとまず流れだけを見てみてください。

prepare (4).png

SNS を利用したモバイルプッシュ通知のための準備

 図を見ながら順を追って説明していくと、まずアプリ起動時に iOS アプリであれば APNS に、 Android アプリであれば GCM にデバイストークンを払い出させます。これは(各アプリごとに)デバイスを一意に特定するためのトークンです。

 続いてそのトークンをサービスのAPIサーバーなどへ送出します。この部分についてはアプリにAWS SDKを入れてしまい、APIサーバーを通さない実装もできるかと思います。しかし個人的には、プッシュ配信基盤をいつでも差し替えられるようにしておきたいので、デバイストークンをどう処理するかについてサーバー側が握れる仕組みの方が好みです。

 APIサーバーは、受け取ったデバイストークンを Amazon SNS 上に作った application へ登録し、払い出された endpoint_arn を受け取ります。application はプッシュ証明書等と紐付いた概念であるため、iOS/Android でそれぞれ別のものを作成する必要があります。また、application には通知を送りたい対象のデバイストークンを登録する必要があります。登録を行うと endpoint_arn というものが発行されます。これはデバイストークンと1対1に紐付いているものになります。デバイストークンのフォーマットは iOS/Android で異なるため、 AWS 側で扱いやすいように統一的な名前を発行しているのではないかと思います(※個人の感想です)。こうして払い出された endpoint_arn と デバイストークンはユーザー情報と紐付けて永続化しておきます。

 個別通知を送るだけであれば、application にデバイストークンを登録し、endpoint_arn を払い出すところまでで十分なのですが、全体通知をするために、今回は Amazon SNS のトピックという機能を使うこととしましょう。トピックは subscriber を要素として保持しており、トピック宛てにメッセージを送ると subscriber 全員にメッセージを配信してくれるという機能を持っています。したがって作成したトピックを、払い出されたすべての endpoint_arnsubscribe させれば、全体通知を実現することができます。

SNS を利用したモバイルプッシュ通知の流れ

 以上が Amazon SNS を利用したモバイルプッシュ通知のための下準備になります。続いて実際に通知を送信するときの流れについてざっくりと見ていきましょう。

prepare (5).png

 図のように、個別通知の場合は DB からメッセージを送りたい対象ユーザーの endpoint_arn を拾い、そこ宛てにメッセージを通知します。全体通知の場合はトピックに対してメッセージを通知します。すると Amazon SNS はAPNSやGCMなどの各プラットフォームの通知配信サービスへとメッセージを届けてくれます。そこから先は APNS や GCM がよしなに各ユーザーのデバイスへの通知を届けてくれるでしょう。

 ひとつ注意が必要なのは、デバイストークンは一定期間を経ると無効になるということです。無効になるタイミングは APNS/GCM 次第であるため、アプリ起動時には毎回デバイストークンをサーバーサイドに送り、endpoint_arn を払い出すようにする必要があります。また無効になったデバイストークンに紐づく endpoint_arn へメッセージをを送ると当然通知は失敗するのですが、その際に endpoint_arnが持つ属性である Enabledfalse になった状態で、 application に残り続けます。放置し続けてもよいのですが、たまり続けると実装方法によっては個別通知のパフォーマンスが低下したりなど問題が起こりうるので、なんらかの削除機構を作るのがよいのではないでしょうか。

以上が Amazon SNS を利用したモバイルプッシュ通知のざっくりとした流れになります。

実装するシステムの全体像

 Amazon SNS を用いたモバイルプッシュに関する基礎知識を確認したところで、前節で想定した次のような機能要件を実現するシステム構成の全体像を見てみましょう。ただし、サービスは基本的にオンプレミスで動いていることを前提とします。

機能要件

  • 特定ユーザーへイベントごとの定形メッセージを通知する機能(個別通知)
    • アプリ側でメッセージの出現タイミングを制御する機能(サイレントプッシュ+定時通知)
  • 全ユーザーに対し、自由なメッセージや遷移先を指定した通知を送る機能(全体通知)
    • 事前にサービス運営者にのみにテスト送信・動作確認できる機能(通知テスト機能)

user (3).png

 ご覧のように、結構複雑になります。すべてAWS上に乗っかっていたり、Direct Connectなどを利用しているのであれば、もう少しシンプルな作りにはできる部分はあるかと思います。あと、図中には登場していないのですが、ネイティブアプリ側のプッシュ通知処理実装はもちろん必須になります。

サーバーサイド各モジュールの実装

図中に登場した 6 つのモジュールの実装についてそれぞれ見ていきます。

1. デバイストークン受け取り用のエンドポイント作成

 ネイティブアプリからデバイストークンを受け取るエンドポイントを作成します。適当なパスを切り PUT で受け取るような形になるかと思います。サーバーはデバイストークンを受けとったあと、その処理は他のジョブに任せてさっさとレスポンスを返してあげましょう。そのためにキューにデバイストークンとユーザーIDとプラットフォームタイプ(iOS/Androidなど)の情報を含んだジョブを登録します。

 オンプレ環境にすでにジョブキューがあるのであればそれを使い、なければ Amazon SQS の料金が激安なので利用してみるのも手かと思います。

2. デバイストークン処理用のバッチジョブ作成

 ユーザーから受け取ったデバイストークンの処理を受け持つバッチジョブを作ります。このジョブがしなければならないことは以下の4つになります。

  1. 各プラットフォーム(iOS/Android)に対応する application にデバイストークンを登録し、endpoint_arn を払い出させる
  2. 全体通知用トピック を endpoint_arnsubscribe させる
  3. もし、全体通知テストユーザーだったらテスト用トピックも subscribe させる
  4. device_tokenendpoint_arn をユーザー情報に紐付けて永続化させる

 1. の手順については Amazon SNS の CreatePlatformEndpoint という操作を行うことになります。また、2.3. の手順については Subscribe という操作を行うことになります。詳細についてはドキュメントを確認してみてください。

3. 作品更新時にジョブをキューに積む

 既存の稼働しているサービスへの作品更新時に、プッシュ通知送信ジョブを作成し、キューに積んであげるように変更をいれましょう。この部分はきっとほんの少しの実装で済むのではないでしょうか。

4. 作品更新プッシュ通知ジョブ作成

 このジョブは、作品をお気に入りに登録しているユーザーの endpoint_arn のリストを取得し、ペイロードを並列で publish します。これには Amazon SNS の Publish という操作を行います。個人的にはこの Publish の宛先である endpoint_arn をリストで指定できるように AWS 側に機能追加してほしさはあるのですが、いまのところ 1 回の publish で 1 つの endpoint_arn しか指定できません。今後に期待しています…。

5. 全体通知送信フォームをサービス管理ツールに追加する

 ある程度の規模以上のサービスであればきっと、サービスを管理するツールは存在すると思います。これまで見てきたように作品更新通知についてはバッチジョブ連携により自動的に送信される仕組みになっています。しかし、イベント情報などを知らせる全体通知に関しては、サービス運営者の手によってメッセージや遷移先の指定をする必要があるため、サービス管理ツールにフォームを追加する必要があるでしょう。

 オペミスを防ぐために、最初にテスト通知を送り、確認したものしか本番送信できないような仕組みづくりなど、工夫次第で手間のかかりぐあいはだいぶかわる箇所でもあります。

 また通知の即時送信の仕組みしかないと、サービス運営者は決まった時間に送信ボタンを押さなければならず、運用コストは大きくなりそうです。必要であれば通知を予約する仕組みなどを作ると良いのではないでしょうか。

6. disabled なエンドポイントを削除するジョブ

 最後に無効になったエンドポイントを削除するジョブを実装しておきましょう。Amazon SNS の application が持つ endpoint_arn には、その属性が変化したときに SNS トピックに通知してくれる機能があります。 application の設定画面の以下のような部分に通知先の SNS トピックの ARN を設定する項目があるはずなので、そこを開いて disabled なエンドポイント削除トリガー用トピックのARNを指定してあげてください。

0e022af5-654a-24ad-f2d1-b5c7e9a92258.png

 続いて実際に削除処理を行うラムダ関数を用意しておきましょう。こちらはラムダ関数に飛んできたデータの中から endpoint_arn を抜き出し DeleteEndpoint という操作をすればOKですので、適当にラムダ関数を実装してください。

 ラムダ関数の実装が終わったら、 disabled なエンドポイント削除トリガー用トピックに、作ったラムダ関数を subscribe させれば、 disabled になった瞬間にラムダ関数が走り、エンドポイントが削除されます。大きなサービスで全ユーザーにプッシュ通知を送ったりすると、当然無効なエンドポイントもたくさん生まれるので、もしかすると AWS Lambda の同時実行制限に引っかかってしまうかもしれません。そのあたりは調整して、場合によっては上限緩和申請を行うとよいでしょう。

おわりに

 クラウドサービスが手頃な値段で誰でも簡単に利用できるようになってきた今、「アプリ開発エンジニア」や「サーバーサイドエンジニア」といった肩書きは意味をなさなくなってきています。思い立ったらすぐに iOS/Android アプリ開発者が API やプッシュ通知などサーバーサイドの機能に手を加え、運用していくことができるようになってきました。

 しかしながら、それなりの規模のサービスに導入しようとなると、まだまだ検討しなくてはいけないことが多いということも事実でした。今後ますますクラウドサービスが発展していっても、人間が考える余地は残り続けるはずなので、クライアント/サーバーの知識をバランスよく摂取して、ユーザーに快適に利用していただけるサービスづくりをこれからも進めていきたいですね。

 なお、今回検討した内容の一部は、この秋開催されたAWSのカンファレンス Re:invent にて発表された Amazon Pinpoint という新機能を用いるとよりきれいに実装できたり、もっと高機能にできる可能性があります。このあたりについては引き続き調査を行い、今後の改善につなげていきたいと考えています。

謝辞

 偉そうに記事を書いてますが、以上のことは自分一人で考えたわけではないので、一緒にお仕事をしたチームの方々に感謝の気持ちを記しておきます。今年も一年間、楽しい開発ライフをありがとうございました。

以上、ドワンゴ Advent Calendar 2016 23日目の記事でした。明日は @kwappa さんの記事です。楽しみにしております。