2018年7月24日火曜日

Go言語での決済システムとマイクロサービス化について

勉強の為に転載しました。
転載元はこちら


この記事はeureka Advent Calendar 2016の22日目の記事です。
21日目は弊社たこちゅーによるGo言語初心者がハマった2つのポイントでした。
ちなみに彼はよくLGTM画像にされていますので、ご自由にお使いくだ
さい←

まえがき

はじめまして、コンバンハ。森川です。
2016年も終わりに近づき、日に日に寒くなってきましたね。
あまりにも寒すぎて、オフィスのある外苑前から帰宅するたびに
「これはアカンやろ…アカンやろ…」状態だったので、半年間クリーニ
ング店に放置してあったコートを取りに行くという、2016年でも一二
を争う決断をしました。(英断)
平日に行ったため閉店が近い時間だったのですが、そのクリーニング店
はガラ空きでした。
店員さんは2名いて、「2人も必要なのかな…」と思いながらコートの
伝票を渡した所、中々見つからなかったらしく2名がかりで探しはじめ、3分経っても見つからず、
そうこうしている内に僕の後ろに4,5人くらい並び始めました。
これをちょっとインテリ・アンニュイ・メガネ男子っぽく表現すると、2つの
ワーカーが並行処理をし、タスクが詰まっているところに、待ち行列に
さらに5件エンキューされはじめました。
そうすると、僕の後ろの人々が閉店時間というガベージコレクターに
よって回収・破棄されてしまうのではないかとビクビク怯える気持ちと、
「今、この場を支配しているのはワタシ」という相反するキモチが
フツフツと湧いてきて、ニヤニヤしそうでした。(フィクションです)
これはインテリでもアンニュイでもなく、ただのメガネおっさんWebエンジニアですね。
というわけで、今日は決済周りの ボディをえぐられるような非常に苦し
い経験 あれこれについてお話したいと思います。
合言葉は、「これでみんな決済マスターだ!」です。

はじめに

この記事の対象者は↓のような人々です。
  • 決済処理は怖いと思っている人
  • 決済処理は恐ろしいと思っている人
  • 決済処理に関わってしまうと、もはや永久に安眠することができない
    と思っている人
この記事で学べることは以下の通りです。
  • 決済処理はそこまで怖くない
  • 上司の首は簡単には飛ばない
この記事の注意事項は以下の通りです。
  • 記載されている内容・仕様が最新の情報ではない可能性があります
  • 各決済プラットフォームのアップデートにより変更される可能性が高
    いです
  • 経験からの帰納や推測を多分に含んでおり、正しくない可能性があり
    ます
  • (圧倒的力不足で申し訳ないです。間違っていたら教えてください…!)

目次

うーん、長いですね…^^;

A. システム構成

最初にマクロな視点でPairsを含めた全体の構成、そして少し詳細な決済
システムの構成について説明いたします。

全体のシステム構成

以下の図はPairsと決済システムの大まかな構成図です。
  • 左側の水色っぽい(1)の箇所がPairs
  • 右下のピンク色の(2)の箇所が決済システム
  • 右上のピンク色の(3)の箇所が外部の決済プラットフォーム
となっています。
システムはほぼ全てAWS上で動かしており、アプリケーションはEC2
で動作しています。
本番ではDockerはあまり使っておらず、一部の機械学習系システムだけPythonコンテナを 雑に 使ってるくらいです。
Go言語という以外は、オーソドックスなAWSを利用したWebアプリケーションになっていると思います。
最近加えた変更としては、アプリケーションが出力するログをGoogle Stackdriver Loggingで一元化するようにしたことです。
各ログに含まれるのパラメータを自由にセットしたかったので、
fluentd経由ではなく、Goのログライブラリsirupsen/logrushook経由
で送っています。
なお工事中のKinesisは、Google BigQueryへ送っているログの一時置き
として検討・構築中です。
「SaaS」では似たようなものを色々と使っていますが、ここについては
またの機会にお話しします。
 (一生話さないフラグ)

決済システムの構成

決済システムはPairs側に比べると非常にシンプルな作りになっています。
PairsとはRESTのインターフェースで通信を行うようになっています。
DBにはAWS Auroraを使っています。
弊社だけの決済系なので、DBに負荷がかかることはないのですが、
Pairs側への導入にあたって検証利用という形でずっと使い続けています。
SQSは課金の自動更新に使用しています。
負荷も安定しておりキャッシュが必要な箇所はほぼないため、
memcachedやRedisは使用していません。

決済システム マイクロサービス化の背景

2015年、2016年にかけてPairsのサーバーサイドアプリケーションを
PHPからGoへと置き換えましたが、その時はモノリスなアプリ
ケーションで、決済もその中に組み込まれていました。
私が入社した当初は、原始地球ともいうべきカオスな様相を呈しており、カスタマーによる購入処理と、バッチによる自動更新処理で違う処理
関数を使っていて、そこら中に星くずのように煌めく様々なマーケ
ティングキャンペーンのロジックが散りばめられており、恵比寿に
いながらにして、さながら九龍城に迷いこんだ気分を味わうことが
出来ました。
ドキドキ・ロマンティックですね。
“新商品の追加を行うと、PC版でカスタマーの新規登録ができなくなる。”
『Pairsの開発中の話』より (共著: 小島ヒロキ・森川タクマ)
エブリデイがサマー・オブ・ラブ、そんな厳しい冬の虚無僧の時代を
経たために、
「もう我々の子孫にこんな辛い思いはさせたくないでござる!」と思う
のは当然のことで、私たちはGo版リプレイスにあたって、マイクロサー
ビスとして切り出すことにしました。
人類(エウレカ)の悲願ですね。

B. 決済の種類

多くの決済プラットフォームで使われる決済には2種類あります。
  • (a) 購入すると1回だけ支払いがあるもの
  • 例: ZOZOTOWNでの商品の購入、LINEでのスタンプの購入、Amazon Kindle向けの電子書籍の購入
  • (b) 購入すると定期的に支払いがあるもの
  • 例: 定額制サイトやサービスへの登録(着メロ取り放題、Yahoo!
    プレミアム、evernote、Netflix、インターネット契約)
ここではStripeさんやWebPayさんに倣って、
  • (a)のような単発の決済は「課金」(charge)
  • (b)のような自動更新のある決済は「定期課金」(subscription)
と呼んでいきます。
弊社のサービスのビジネスモデルはサブスクリプションモデルであり、
特に(b)の定期課金が重要になってきます。
サブスクリプションモデル、特にtoC向けは収益がいきなり落ちること
はめったに無いため、比較的安定しやすくなります。 そこまでたどり
着くのは簡単ではないですが...
そして自動的に引き落とされるということは、その分システムの安定性
が求められることになります。
決済システムの修正を担当したことがある人であれば、
「もし明日の朝出社して、数千件、数万件が自動で引き落としされてた
らどうしよう…」
とか寝る前に考えたことがあるのではないでしょうか。
そして「逃げちゃダメだ…逃げちゃダメだ…いや..逃げよう…」と何度も
つぶやいたことがあるはずです。

C. 決済プラットフォーム

一旦視点を変えまして、外部の決済プラットフォームの話をします。
先程の図の「3」の箇所となります。
現在、Pairsでサポートしている決済手段は以下の4つです。
  • クレジットカード
  • iOSのIAP (iTunes Connect In-App Purchase)
  • AndroidのIAB(Google Play In-app Billing)
  • PayPal(台湾版のみ)
iOS, Android等のネイティブアプリケーションでは、扱っている商品に
よっては必ずIAPやIABを使って決済をしなくてはなりません。
カスタマーにしてみればAppleやGoogleに対して支払うことになり、
安心感がありますし、購入手続きや月々の明細の管理も楽です。
そんないいことづくめなネイティブアプリのプラットフォーム決済ですが、手数料が高いです。
購入価格の30%が手数料としてプラットフォームに取られるため、
100万円売上があったとしても30万円は手数料として消えてしまい、
実質手元に入るのは70万円です。
美味しすぎる手数料ビジネスですネ
それに対してクレジットカードやPayPalは通常の手数料が約3%〜5%、
さらに一回当たりのシステム手数料(トランザクション手数料)として10円
〜50円ほどかかります。
この場合に、1個1000円の商品を千個売って月に100万円の売上があった
とすると、
  • 通常の手数料で3.4%で 3.4万円 (*100万1円からは3.2%, 1000万1円
    以上は2.9%)
  • 40円*千件 = 4万円
7.4万円が手数料として引かれ、92.6万円が手元に入ることになります。
サブスクリプションモデルの場合は、購入済みのシステムを勝手に切り
替えることはできないので、手数料、安全性、安定性、カスタマーに
とってのUX、実装・運用工数等々、様々な要因を考えた上で、対応
する決済プラットフォームを考える必要があります。
以下の比較表に簡単な違いをまとめています。
プラットフォーム 手数料 1ヶ月のサイクル 課金作成API 定期課金の
解約API クレジットカード 2〜5% + 数十円 自由 or 1ヶ月(独自) ○
(代行会社次第) PayPal 2.9〜3.6% + 40円 1ヶ月(独自) △ ○ iOS IAP 30% 1ヶ月(独自) × × Android IAB 30% 1ヶ月(独自) × ○
手数料を考えるときは、単純にトランザクション手数料の他にも、返金・
チャージバック時の手数料とカスタマーの返金率を考慮して、トータル
の手数料
で考えると良いと思います。
国によっては返金率が高くなることもあり、ベンダーの返金手数料が
高い場合は想定以上の手数料となる可能性があります。
1ヶ月のサイクル というのは、引き落としが行われるサイクルになります。
例えばPairsでは、クレジットカード決済の場合、1ヶ月プランを30日と
いうサイクルで販売しています。1ヶ月は30日だったり31日だったり
するため、31日の場合は同月に2回引き落としがされる可能性も
あります。
決済プラットフォーム間では、1ヶ月の期間判定が異なる場合があります。
通常の場合は月だけを+1することが多く、例えば7月1日に購入した場合
8月1日に更新されます。ただし、2月というイレギュラー月をまたぐ
場合は、各社で異なる期間判定をされる場合があります。
例えばiOS IAPの場合は1月29日〜31日のどこで決済しても、次の更新日
は3月1日になり、その次の更新日は3月29日になる一方で、Android IABの場合は1月29日〜31日で決済すると、次の更新日は2月28日になるが、
その次の更新日は元に戻り、3月は1月の購入日と一致する、といった
具合です。
この辺りの挙動は、仕様を正確に把握して事前に定義しようと思っても
難しいので、素直にプラットフォームから返却される有効期限を使う
ようにしましょう。 Yes as is.
課金作成APIというのは、こちらで自由に決済ができるかどうかという
ことになります。
「自由」といっても一度決済を行った経験があり、プラットフォーム
側にクレジットカード情報があることが前提条件となります。
多くのクレジットカード決済代行会社の場合は、前回の決済時のユーザ
ーIDやメールアドレス、電話番号といった情報を使うことで、同じ
クレジットカードでの引き落としが可能です。
代行会社側で自動引き落し機能が付いている場合もありますが、Pairsでは
エウレカ側で毎回決済リクエストを送っています。
こちらがリクエストを送信しない限り決済されることはなく、Pairs退会
済みのカスタマーから引き落とされることはないため、この点は安心で
すが、エラーハンドリングを適切に行わなかったり、ステージングと
本番を間違えるような致命的なミスを犯すと、多重に引き落としが発生
する可能性があるため注意が必要です。
多重引き落としの場合にも、すぐに気づけば返金処理を行うことでカス
タマーに迷惑をかけず、上司の首が飛ぶのを防ぐことができますが、
誤決済の件数が多い場合は返金処理が一気にできない場合があります。
アクワイアラは国際ブランドに対して「量が多く質の良い決済」の獲得
を代行している立場上、返金率・回数が想定を大きく上回ると
「こんなに返金しやがってお前んとこは一体どうなってんだ」となって
しまい、1日や1ヶ月での上限が決まってるんじゃないかと、ワタシは
推測しています。

クレジットカード

各プラットフォームについてですが、まずはクレジットカードについて
説明していきます。
日本ではクレジットカードが普及しており、オンライン決済手段として
一般的なものとなっています。
入力フォームにカード番号や有効期限を入力するのが一般的かと思います。
システムやカード会社によっては、CVC(カード番号とは別にカード上
に記載されている3,4桁の暗証番号のようなもの)やパスワードを入力させるものがあるかもしれません。
クレジットカードにはみなさんご存知の通り、VISAMasterCardJCB,
アメックスダイナース といった国際ブランドがあります。
これとは別にカード発行会社があり、この辺りを話すとキリがないため
省略しますが、アルファノートさんの「決済システムの仕組み」という
図が分かりやすかったので
リンク先を参照してもらえると、少しイメー
ジがつきやすいと思います。
図の中の「加盟店」が私たちのようなサービス提供会社にあたります。
サービス提供会社やエンジニアにとって直接関係があるのは、
決済代行会社(アクワイアラ・プロセッサ)と呼ばれるその名の通り決済
を代行してくれる事業者と契約し、彼らのシステムと接続して決済を行
います。
決済代行会社によって、以下の事項が変わってきます。
  • (a) 利用できるカードブランド
  • (b) 手数料
  • © 決済システムのAPI
(a)は飲食店や海外のお店などであると思いますが、「VISAとMasterCardは使えるけど、JCBとアメックスは使えない」のような
ケースです。
(b)も決済代行会社によって変わってきます。
決済の取引金額が大きくなると、手数料が安くなることが多いです。
また利用するカードの国際ブランドによって変わることもあります。
例えば、「VISAとMasterCardは3.5%で、その他は3.8%」 のように
なることがあります。
©はエンジニアとしては一番気になる箇所ですね。
APIやライブラリがいかに使いやすく、どのくらい機能が豊富かによって、実装工数やアーキテクチャ、稼働後の運用の負荷、実現できる機能
が変わってきます。
例: 自動引落し機能、カード情報の一部表示機能(下4桁表示)、サン
ドボックス環境、etc…
多くの決済代行会社を利用するには審査や導入費用が必要ですが、最近
StripeSPIKEのように、導入が簡単で使いやすいAPIを持ったクレジ
ットカード向け決済プラットフォームがあるので、そちらを検討するの
も良いかもしれません。
通常クレジットカード情報を取り扱うには、PCI DSSというクレジット
カード向けの国際的なセキュリティ基準の認定資格を取得する必要があ
ります。(ISMS(ISO27001)Pマークのようなもの)
クレジットカード情報入力フォームをカスタマーへ直接提供する場合は、クレジットカード情報を取得することになり、代行会社の審査時にPCI DSSの準拠・認定を求められます。
PCI DSS取得も簡単ではないですし、システム・ネットワーク構成も
厳重にする必要があります。
そしてクレジットカード情報なんて怖くて取得・保管したくありません。
そういう用途のために、iframeURLリダイレクトを使って決済代行会社
のシステムへ接続し、間接的に決済を行う方法があります。
Pairsでもそれを利用しています。
一度決済を行ってしまえば、該当カスタマーのクレジットカード情報は
決済代行会社に保存されているため、
次からはフォームに入力したメールアドレスや電話番号等の情報を使って、Pairs側から簡単に決済リクエストを送信することが出来ます。

PayPal

PayPalは元々、個人間の送金サービスとして発展してきた決済プラット
フォームです。
PayPalの追い立ちは英語のwikipediaに譲るとして、現在では欧米系サイ
トや日本でも対応しているサービスが多いと思います。
2015年のAnnual Reportを見ると売上高は92億ドルで19%成長(!)、
1.79億人の有効なアカウントがありこちらも11%成長となっており、今も
伸び続けているようです。
そんなPayPalで使える決済のパターンはいくつかありますが、それだけ
で1記事になる、というか既に誰か説明しているでしょ!
ってことで調べてみたところakiyoko blogさんの【PayPal 決済まとめ】PayPal の決済システムが分かりにくいので 画面遷移パターンごとに使える決済システム・API を整理してみたに詳しく記載されています。
かいつまんで説明すると、
  • 元サイト上で、iframeかURLリダイレクトでPayPalのログイン画面を
    出す
  • PayPal上で決済(または決済許可)を行う
  • PayPal側から元サイト上へ決済完了通知(または決済許可トークン
    付きで再リダイレクト)
  • (決済許可トークンを使って決済を実行)
という流れで、直接クレジットカードの番号を扱わない仕組みになって
います。
ええ、あなたの言いたいことは分かります。「文字だけじゃ分かりづらい」ですね。
まあ落ち着きましょう。
  • Pairs 購入画面
オレンジ色の購入ボタンを押すとPayPalへ遷移します。
  • PayPal ログイン画面
ログインすると、PayPalに登録されているクレジットカードで支払い
確認画面が出ます。
  • PayPal 支払い確認画面
社長のカード(色付き)で好きな買い物をする、最高の瞬間ですね!
「同意して続行」を押下すると決済トークン付きで元サイトへ遷移します。
この時点ではまだ決済はされていません。
  • Pairs 最終購入確認画面
購入完了ボタンを押すと、PayPalから受け取った決済トークンを使用
して決済処理を行うようになっています。ここで初めて実際の決済処理
が行われます。(決済トークンには有効期限があるため、
この状態で一定時間が経過すると無効になります。)
使用できるAPIにも古くからありClassic APIと呼ばれている
NVP / SOAP APIか、REST APIの2種類があります。
Pairsでは台湾版でPayPal決済を使っています。
実装当時はREST APIが台湾・台湾ドルをサポートしていなかったため、Classic API のExpress Checkout機能を使っています。
現在ではREST APIも使えるようです。もっと早く欲しかった...(;O;))
参考までに、Pairsでの決済の流れは以下の通りです。
  • (1) カスタマーがPairsの決済ページを開く
  • (2) カスタマーが決済手段としてPayPalを選択し、購入ボタンを
    押す
  • (3) Pairs側でSetExpressCheckout APIを使い、カスタマーをPayPal
    へリダイレクト
  • (4) カスタマーはPayPalサイト上でログインし、購入確認ボタンを
    押す
  • (5) PayPal側でカスタマーをPairsへ再リダイレクト。リダイレクト時
    に決済トークンが付いてくる
  • (6) カスタマーはPairs上で最終確認ボタンを押す。
  • (7) Pairs側(実際はeureka決済システム)で決済トークンを用いて、DoExpressCheckout APIを使い決済を行う
  • (8) 定期課金の場合は、CreateRecurringPaymentsProfile APIを使い、定期的に引き落としがされるようにする。(GetRecurringPaymentsProfileDetails APIを使い、正常に定期課金
    プロファイルが作成されているか確認もする)
ええ、あなたの言いたいことは分かります。「文字だけじゃ分かりづらい」ですね。(再掲)
(1)〜(6)までは上掲のスクリーンショットがカバーしています。
(7)〜(8)は↓のようなイメージです。
(安定のパワーポイント作図)
たまに(7)の決済が成功し、(8)の定期引き落とし処理に失敗することが
あります。
こういう場合はエラーとして購入失敗にし、決済については返金処理を
しています。
(たまにしか発生しないこと、そして(8)でエラーがでるということは、返金処理が失敗する可能性も高いことから今のところは手動運用にして
います。運用チームに多謝。)

PayPal管理画面

PayPalの販売者向けの管理画面では、課金データの確認や、返金処理、CSVデータのダウンロード等が可能です。
マスターアカウントから個別ユーザー用のアカウントが発行できます。
特定の権限を絞ってアカウントを作成できるため、担当者ごとにきちん
と作った方が良いですね。
元々はマスターアカウントだけで1Password Teamに入れてましたが、
危うさの限界線を超えたために個別アカウントを発行するようになり
ました。折角1Passwordで乱数発行しても、アカウント作成時のパスワードは手入力を求められるため、「最重要サービスのパスワードは最低限
30桁以上を要求するぜ!」
みたいな社内ポリシーがあると、何回も打ち
間違えて詰むので気をつけてください。

サンドボックス環境

大抵の決済プラットフォームにはサンドボックス環境と呼ばれる、
開発向けの環境が用意されています。
ここでは本番さながらに決済を行えますが、実際にお金が引き落とされ
ないという素晴らしい環境です。
PayPalもサンドボックス環境があり、接続先のURLが異なっています。
サンドボックス独自ルールが少ないため使いやすいですが、PayPal社
のステージング環境として使われているのか、たまに本番と違う
新UIになっていたりします。
そういった関係なのか分かりませんが、2ヶ月に1回くらいは数時間くら
い使えなくなります。
その間はPayPalでの決済テストが一切できなくなり、当日リリースと
かだったりすると、
「PayPalで決済テストができません」となぜか私に相談がきます。
そんなときは早く帰宅して寝てもらっています。

iTunes Connect In-App Purchase (iOSアプリ)

続いて我らがキング、Apple社がiOS向けに提供する決済プラットフォー
ムになります。
iOSアプリケーションでは、アプリ内課金のためにIn-App Purchase(以下
、IAPと表記)を使うことが出来ます。
大体のことは公式のIn-App Purchase プログラミングガイドに書いてあります。
詳しくはそちらを参照してください。
PDFのP9,P10辺りの「プロダクトのタイプ」という項目で、商品の種類
が5種類記載されています。このうち、Pairsでは、消耗型(Consumable)
と自動更新購読(Auto-renewable subscriptions)を使用しています。
消耗型は「Pairsポイント」のような、消費するコンテンツに対して
使っています。そのままですね。自動更新購読は「有料プラン」
「プレミアムオプション」のような、定期課金に使用しています。これ
もそのままですね。
エウレカではCouplesというカップル向けリア充コミュニケーション
アプリがありますが、メッセンジャー機能の中でテキスト以外にも
スタンプを送信することができます。
(FacebookやLINEさんのようなやつです)
その有料スタンプでは非消耗型(Non-consumable)を使っています。
(月額会員向けのCouples Plusという商品では自動更新購読を使っています)
決済とは全然関係ないですが、Couples Qという恋愛相談サービスも最近開始したので、恋人がいる方は使ってみてください。誰にも言えない
相談、修羅場をくぐってきた兵のアドバイスお待ちしております。
(ナチュラルな宣伝)
決済のフローについては、クライアントサイドだけでやる方法と
サーバーサイドを含める方法があります。APIサーバーがなかったり、mBaaSとかを使っているアプリであれば、iOSアプリだけで決済処理を
完結できますが、多くのサービスのようにサーバーサイドAPIが存在する場合は、サーバーサイドで決済の検証を行うのが確実です。
Pairsの場合は以下のフローとなっています。
  • (1) カスタマーがアプリ上でPairsの決済ページを開く
  • (2) ページ内の購入ボタンを押すとiOSの購入確認モーダルが表示
    される
  • (3) モーダルのOKボタンを押すと、Appleの購入レシートをiOS
    アプリ側からPairs側へ送信する
  • (4) Pairs側(実際はeureka決済システム)は受け取ったレシートを
    そのままAppleの決済APIへ渡す
  • (5) レスポンスとして決済情報が含まれているJSONデータが返却され、その情報を元に以下のような購入バリデーションを行う
  • statusが0かどうか
  • bundle_idが正しいかどうか
  • product_idが正しいかどうか
  • 未使用のtransaction_idかどうか
  • expires_dateが過ぎていないかどうか
  • (6) 全てOKであれば購入成功、NGな項目があれば購入エラーを返却
    する
なんだか分かりづらいですね。
ここでレシートという単語が出てきましたが、MD5ハッシュ化された
文字列です。
こいつをAppleのAPIへ送信すると、そのレシートに紐づく決済情報がJSON形式で返却されてきます。(5)のバリデーションのところで、
その中身を確認し、ちゃんと決済が行われたかどうか確認します。
レシートの形式には iOS6型とiOS7型があります。
古いiOS6型は単体の購入情報のみがあり、定期課金の有効ステータスを
確認することができます。新しいiOS7型は定期課金に紐づく課金履歴
情報が全て含まれており、個別の定期課金の有効ステータスは確認でき
ません。
イメージしづらいと思うので、実データを見てみましょう。
どうでしょうか。iOS7型はクレイジーですね。
といってもこれは実環境ではなく、サンドボックス環境のレシートを
元に整形したものなので、実際のレシートがここまで肥大化することは
ないと思います。
ただ、全ての課金履歴が含まれているようなので、1ヶ月サイクルで
定期課金が更新される度に、15行くらい増えます。
in_app と latest_receipt_info の両方に含まれるため、実質30行ほど
増えます。
これが3年間継続すると (30行 * 3年 * 12ヶ月) = 1080行 くらいに肥大化
することになります。
アプリ側からサーバーサイド側へPOST送信される、ハッシュ化された
レシート文字列自体もボリューミーになるため、nginxのclient_max_body_sizeを低めに制限している場合は気をつける必要が
あります。
検証時にはlatest_receipt_infoから末尾順に辿り、購入した商品と
一致するproduct_idとその有効期限を見ることになると思います。
iOS7型にすることで検証のロジックが少し複雑になりましたね。
定期課金の更新については、statusが 0 か 21006 かどちらかを調べる
だけなので、iOS6型は非常に楽ですね。
あれ、なぜ私たちはiOS7型に切り替えてしまったのでしょうか/(^o^)\
ここまで説明していて、パワポで作図したくないため良い感じの図が
無いかどうか探していたところ、サイバーエージェントさん公式エンジ
ニアブログに自動購読課金について【iOS編】という記事を見つけ、とても整理されて分かりやすくまとまっていました。「冒頭で紹介しろよ!」
って感じですね。ハハハ。
こちらの記事の著者の辻さんが、Go言語でのIAP, IAB向けに作られたdogenzaka/go-iapを利用させていただいています。本当にありがとう
ございます。

iTunes Connect 管理画面

Appleの管理画面から決済関連でできることは、決済データのCSVのダウンロードです。
私から言えることはそれだけです。よろしくお願いします。
マンパワーを使って毎回CSVをダウンロードしてもいいんですが、API経由でデータを取得することもできます。Node向けのライブラリもあるので、整形してサマリ表示するようなこともSlack経由でできます
(仕掛中)
なおCSVデータには誰が買ったか特定できる情報はないため、サービス
側で保存している決済情報と完全に突合するのは難しいです。
iTunes Connectの該当画面のスクリーンショットを上げようかと思いま
したが、なぜか私の権限が剥奪されているため、遷移できませんでした。
よろしくお願いします。

サンドボックス環境

iTunes Connect上でテスターのアカウントを追加し、Testflightでアプリ
を配信することでテストをすることが多いんじゃないかと思います。
PayPal同様に、サーバーサイドで検証する際は、サンドボックス環境と本番とで接続先URLが違います。
また自動更新の期間が短くなっており、購入後に6回更新されます。
途中解約はできません。
そして購読画面からも変更ができないため、途中解約やクロスグレード
変更、後述のアップグレードといった細かい挙動の確認はできません。
これは「計測するな、推測せよ!」 ということでしょうか?!

Google Play In-app Billing (Androidアプリ)

次はみんなのヒーロー、Google社がAndroid向けに提供する決済プラットフォームになります。
iOS同様、Androidアプリケーションでもアプリ内課金のためにIn-app Billing(以下、IABと表記)という仕組みを使うことができます。
フローについてはiOSと同様なので省略します。
そして大体のことは公式のドキュメントに載っています。
というか、こちらもサイバーエージェントさんの自動購読課金について【Android編】にまとまっています。
「この記事のおかげで、私から言えることはもうありません (`・ω・´)
キリッ」
で済ませたいところですが、上長に怒られそうなので一応説明いたします。
iOS IAPのレシートに当たる箇所で最低限必要なパラメータは、
productIdと purchaseTokenorderId になります。
productId はGoogle Play上で登録した商品のIDが入ります。
purchaseToken には購入トークン(乱数)が入り、iOSでいうところの
レシートの検証時に使います。
orderId は API操作では一切使いませんが、Googleペイメントセンター
の決済と紐づくオーダー番号が入ります。
オーダー番号を使ってWebUI上から手動で返金操作を行ったり、GooglePlayの収益レポートCSVと紐付けて会計チームに引き渡したり
する際に使用します。
purchaseTokenorderIdは一つの決済につき1つになります。purchaseTokenはシステム側で使用し、orderIdは人間が使うと思って
ください。
orderIdは元々 XXXXXXXXXXXXXXXX.YYYYYYYYYYYYYYYYY のような形でしたが
、2015年の夏頃から GPA.0000-0000-0000-0000 のような形に変わりました。
旧形式のXXXXXXXXXXXXXXXX.の部分はアプリによって固定だったため、PairsではGoogleのAPIへリクエストを行う前にフォーマットによる事前
バリデーションをかけていました。
よく訓練されたAndroider(特に海外)の方々はカジュアルに不正な
レシートを送信してくる傾向があり、こちらもカジュアルに400エラー対応をしていました。
そんな中、orderIdがカジュアルに新形式に移行し、決済のエラーレー
トが上がったことがありました(寒気)
しかも両方の形式が混在しており、全て失敗するわけではなくたまに
失敗するという状況でした(吐き気)
その日はむせび泣きました。
この教訓としてそれ以降、ワタシタチは勝手に変なバリデーションをか
けるのはやめました。
ベンダーのAPIへ素直にそのまま渡すことを熱烈におすすめします。
また、GoogleのAPIは非常に安定しています。
主観ですが、
Google >> PayPal >>>> Apple
というくらい安定しています。
キングApple様は元々安定していましたが、今年の5月くらいから不安定(エラー率が多い)な気がします。
GoogleもAppleもたまに特定のレシートで不明なエラーを返却してくる
ことがあります。
前回の更新時には正常に使えたのに、現在は40Xエラーを返却してくる
ケースです。
問い合わせたところ、「Googleをアカウント削除すると、既存のレシートで400エラーが返却される」ということで、400番が返ってきた場合
は定期課金を解約状態にしてしまって大丈夫なようです。
(Apple様の404 newNullResponse エラーは 「問い合わせ」 -> 「調査中」 -> 「優先度低」 という奥義を喰らっており不明なままです。推測ですが、おそらくGoogle Playと同じような状態じゃないかと思います)

Google Playの管理画面

Apple同様に、Google Play Developer Consoleから売上レポートのダウン
ロードができます。
アカウントごとに固有のCloud Storageへのリンクがあるようなので、
そこからCSVファイルをダウンロードできます。このCSVファイルにはDescriptionというカラムにorderIdが記載されているため、サービス
側で保存している決済情報と一対一の突合が可能です。
(↑この画面の下部にCloud Storageへのリンクがあります)
また、Google ペイメントセンターからは特定の決済の返金や定期購読
の解除を行うことができます。orderIdオーダー番号として記載されて
おり、検索も容易にできます。
11月まではGoogle Wallet Merchant CenterというUIだったのですが、
今はGoogle ペイメントセンターというシステムに切り替わっています。
前はURL指定で検索クエリが使えたため、Pairs, Couplesの管理画面から個別の課金へ直接遷移するリンクを生成することができましたが、Google ペイメントセンターになってからは、よりSPAな動きになり、URLから直接
個別の課金へ飛ぶことが出来なくなってしまっため、
ヒューマンが入力しないとあかん状態になっています。
誰かこっそり解決策を教えてください。タノミマス。

サンドボックス環境

AndroidはiOSと違って、そこまで詰まった経験がないのでようわからんのが正直なとこです。
詳しくは公式のドキュメントを見てみてください。
Googleアカウントをテスター登録するとテスト課金できますが、1ヶ月の有効期限は1日になり、さらにorderIdが取得できないようです。
そのためテスターアカウントを使うこともありますが、orderIdのバリ
デーションエラーを避けるために本物のアカウントを使ってカジュアルに課金をすることもあります。
Google ペイメントセンター上で返金処理をしましょう。)

アップグレード商品

元々Android IABには商品にTierという概念がありまして、
例えば、
  • ブロンズプラン 月額100円 メールサポートのみ 5営業日以内のレスポンス
  • シルバープラン 月額300円 月3回の電話サポートあり 3営業日以内のレスポンス SLA
  • ゴールドプラン 月額1000円 月100回の電話サポート 1営業日以内のレスポンス SLA
  • エンタープライズプラン 要お問い合わせ 無制限のサポート
このような、プラン間での上位・下位があった場合に、購入済み商品
から切り替え(アップグレード・ダウングレード)が可能な仕組みがありました。
これを行うと切り替え後プランの期間延長が行われるようです。
参考: アプリ内定期購入 — 定期購入のアップグレードとダウングレード
そしてApple様にはこの仕組みはありませんでしたが、
今夏WWDC2016にて決済関連のアップデートがあり、国別価格や価格変更時の据え置きに加え、アップグレードプランも対応しました。
上位・下位の関係がある商品の場合はこちらを適用しないといけません。例えば、Netflixには1画面プラン・2画面プラン・4画面プランがあり、
既にアップグレード・ダウングレードが出来るようです。
詳しい仕様、特に引き落とし開始日の変化や決済金額が不明なため
ヒアリング中ですが、まだ良くわかっていないため、
私から伝えられることは少ないです。ごめんなさいm(_ _)m

D. Goでの決済システムの実装

ようやく実装の話に入ってきます。
ここからはGo言語に興味がない人は全くつまらない話なので、
心苦しいです。
ちなみにPairs・Couples本体とはかなり構成が違うので、この記事を
見て「Pairsって○○で××なんでしょ?」とか弊社社員と話しても話が
通じないことがあります。心苦しいです。

主要ライブラリ

外部ライブラリはこの辺りを使っています。
よく聞かれるので頑張って列挙してみました。
あんま珍しいものは使ってないと思います。
種別 ライブラリ 備考 WAF・ルーティング
zenazn/goji DB・ORM go-xorm/xorm DB・ORM evalphobia/wizard xormをシャーディング対応したもの キャッシュ evalphobia/eurekache
メモリキャッシュのみ使用
HTTPクライアント h2non/gentleman franela/goreqから移行中… ログ sirupsen/logrus (彼の名前が小文字になりましたね…) ログフック logrus_appneta TraceViewへのエラーログフック ログフック logrus_fluentfluentdへのフック ログフック logrus_sentry Sentryへのフック ログフック Stackdriver Stackdriver loggingへのフック
モニタリング fukata/golang-stats-api-handler モニタリング
tracelytics/go-traceview TraceViewへの
トレースログ送信 テスト stretchr/testify
決済系 evalphobia/go-iapdogenzaka/go-iapにiOS6型レシート対応を
加えたものです 決済系 evalphobia/go-paypal-classic

ディレクトリ・パッケージ構成

大体以下のような形になってます。
├ main.go
├ config/               // 設定ファイル
├ routing/              // ルーティング定義
├ middleware/           // リクエスト処理時のミドルウェア
├ controller/           // コントローラー
├ service/              // ビジネスロジック置き場
│    └─ platform/     // 各決済プラットフォーム固有のロジック
├ model/                // DDDでいうところのEntityとRepository
├ library/              // 各種ライブラリ置き場
├ client/               // 外部向けのSDK
├ test/                 // テストヘルパー・fixture置き場
└ misc/                 // RakeタスクとかドキュメントとかSQLとか
それぞれ詳しくみていきます。

main.go

main.goでしていることは、
  • フラグのパース
  • 設定の読み込みと初期化
  • ルーティングとミドルウェア設定
  • HTTPサーバー起動
くらいです。至って普通ですね。

config

設定ファイル置き場です。
読み込みにはevalphobia/go-config-loaderというものを作って使って
ます。
例えば以下のようなログ用の設定があったとすると、
$ cat ./config/log.toml
[log]
[log.fluent]
host = "127.0.0.1"
port = 24224
enable = false
[log.sentry]
url = ""
enable = false
# ----ここまで----
$ cat ./config/dev/log.toml
[log]
[log.sentry]
url = "https://XXX:YYY@app.getsentry.com/999999"
enable = true
簡単な使い方は↓のようになります。
import(
    config "github.com/evalphobia/go-config-loader"
)
// (中略)
const confType = "toml"
conf := config.NewConfig()
conf.LoadConfigs("config/dev", confType) // ./config/dev から .toml
ファイルを全て読み込む
conf.LoadConfigs("config", confType)     // ./config から .toml
ファイルを全て読み込む
// ./config/log.tml のデータ
useFluentd := conf.ValueBool("log.fluent.enable") // => false
// ./config/dev/log.toml のデータ
useSentry := conf.ValueBool("log.sentry.enable") // => true
sentryURL := conf.ValueString("log.sentry.url")  // => https://XXX:YYY@app.getsentry.com/999999
LoadConfigs では先に読み込んだ項目が優先される仕様になっているため、固有の設定
から読み込むようにし、全体のデフォルト設定は最後に読み込むようにします。
routing
goji.SubRouter を使ってAPIの種類ごとにルーティング設定を保存しています。
ルーティングは全てcontrollerの関数を設定しています。
middleware
WAFのルーティングの文脈でいうミドルウェアを定義しています。(一番最初に何かの
WAFでミドルウェアって見た時はよく理解できませんでした...(/_;)) HTTPリクエストがコントローラ層で処理される前(と後)にロジックを仕込むことができます。
認証やHTTPヘッダー処理、パラメーター処理なんかを行ってます。
特に変わったことはしていませんが、http.Request からリクエストパラメータを取得
して、内部ロジックで利用する生データと、個人情報っぽいものを削除したログ用途のもの
に分けて、Contextに入れてます。こうすることで、不用意にログにヤバイデータが残され
ないようにしています。
controller
service層の関数を一つ呼び出し、その結果をJSONとして返却しています。
呼び出しの結果は全て map[string]interface{} で受け取っています。
ただしモニタリングやテスト用のコントローラはそのままロジックが書かれていることも
あります。
service
様々なロジックが書かれる場所です。
地道です。
platform
各決済プラットフォームごとの固有のロジックを置いています。
いわゆるファクトリパターンで決済の interface を作成し、service層ではplatformを意識せずに新規購入や更新処理を行っています。
たまにプラットフォーム独自のフローがあったりすると、全てにダミーのメソッドを追加しないといけないのが辛いです
テストは一部本番のデータを使っています。
本番のデータを使っているがために、1ヶ月ごとにリアルデータの有効期限が延長されます。
そのためアサーション部分を適宜変更しなければいけませんが、本番のレスポンス以外は
信じたくない病的思考のためこうしてます。
CIが通ったとき、有効期限が変わってCIが落ちたとき、そして修正後にまたCIが通ったときの安心感・満ち足りた高揚感はひとことでは言い表せません。
model
DDDの概念でいうエンティティとレポジトリが置かれています。
レポジトリで更新処理や特定の処理をしたときに、エンティティ側の非公開フィールドのフラグやデータをいじりたかったので、同じパッケージに格納しています。
(必須ではないので分けてもいいですが、そこまで不便さを感じていないためそのままです。)
library
内部ライブラリが置かれています。
ディレクトリ構成は標準ライブラリと似た命名になっています。
(import元で標準ライブラリと名前がかぶった時はこちらのパッケージ名を変えています。)
外部ライブラリを使う場合はここにアダプタを置き、サービス層や他の層では直接外部ライブラリを扱わないようにしています。
client
clientにはPairs側でimportして使うためのSDKを置いています。エンドポイントやパラメータ情報が書かれています。
 

新たにエンドポイントやパラメータを追加する場合はサーバー側だけでなく、こちら側にも更新を反映します。決済システム本体のコードとは分離していますが、唯一エラーコード
のファイルだけ共有しています。
ここにもテストコードを置いており、本体サーバーを起動して、fixutureを流した際の
レスポンスを検証しています。代わりにPairs側での決済リクエスト周りの単体テストは
簡素になっており、固定のダミーレスポンスを使うようにしています。
test
テスト用のヘルパーとfixtureしか置いてないです。
misc
バッチ用のRakeタスクが置いてあります。
「あれ、バッチ処理はRubyで書いてるの?」と思うかもしれませんが、実体はHTTP
リクエストを送信しているだけです。
主なバッチ処理は定期課金の更新処理とデータ不整合時のアラートです。
それぞれに順番的な依存関係を持たせないようにしており、各リクエストは数秒以内に
終わるようにしています。(と言ってもプラットフォーム側の応答速度にも依りますが...)
わざわざワーカーを作ったりCLIツールを作り、それぞれ監視を行ったり使い方を教えるのも面倒ですしシンプルさが失われると感じていて、全ての処理はcurlコマンドさえあれば出来るようになっています。
E. その他
定期課金の更新
上で説明したように、処理のインタフェースはHTTPサーバー&クライアント&cronで
構築しています。大きくエンキューとデキューのプロセスに分かれています。
有効期限が切れたり、確認状態になった定期課金をエンキューのプロセスでSQSへ突っ込
みます。一回あたりの処理件数は上限を設けています。通常は問題ありませんが、多く処理
しなければならない時、例えば日付が変わった瞬間は通常より多くのエンキュー処理が実行
されます。
この辺りはRakeタスク実行時のパラメータで、総リクエスト実行数が変更できるように
なっています。
各プラットフォームごとにSQSキューがあり、そのキューごとに個別のHTTPエンドポイン
トが存在します。HTTPリクエスト1回につき定期課金が1件処理されるため、こちらもRake
タスクのパラメータで増減が調整可能になっています。
またSQSキューのメッセージ数を監視することで、どこかのプラットフォームで異常が起きていないかを把握することができます。
ちなみにGo言語でのAWS SQSの使い方については先週末に記事にしたので、興味ある方は
見てみてください。(未だにFacebookシェア数が0なので愛しさや心強さは一切なく、
ただただセツナさを感じています。)
決済システムとプロダクト側とのデータ同期
決済システムでは決済データをデータベースへ保存しています。
またプロダクト(Pairs)側でも決済データをデータベースを保存しています。
基本的には決済システム側のデータがマスターとなり、プロダクト側ではその一部を保存
します。
プロダクト側から決済システムを通して決済プラットフォームへ決済処理を行う際のフロ
ーは大きく以下のようになります。
  • (1) プロダクトから決済システムへ購入リクエストを送信する
  • (2) 決済システムから決済プラットフォームへ購入(or 確認)リクエ
    ストを送信する
  • (3) 決済システムからプロダクトへレスポンスを返却する
ここで (2)が成功し(3)が失敗すると、決済システムだけにデータが保存される可能性が
あります。マイクロサービス化を進めるとデータの不整合を防ぐ仕組みが必要になります。
  • (4) プロダクトから決済システムへ確認リクエストを送信する
決済システムではデータベースに確認用のカラムを用意しており、(3)が成功すると確認
リクエスト(4)を送信し、同期完了とします。
TCPのACKに近いです。
データが不整合になってしまった場合はバッチで自動同期をするか、アラートが発生する
ようになっています。
ベンダリング
kovetskiy/manulでしてます。
たまにおかしくなるのでgit submoduleを生でいじることもあります。
CI
Travisを使っています。カバレッジはCodecovを使っています。
masterでCIをパスした場合はgitbookのビルドプロセスが走り、マニュアルが更新され
るようになっています。
デプロイ
ステージングのデプロイはRundeckとconsulを使っています。
元々はAWS CodeDeployを使っていましたが、Web UI上でブランチを指定してデプロイ
できるようにRundeckに変えました。本番へのデプロイは独自の人肌温いスクリプトを使っ
ています。
システム監視
インフラ・ミドルウェアレイヤーは、はてなさんのMackerelを使用しています。ありがとうございます。
ログ周りはSentrySolarWinds TraceViewGoogle Stackdriver loggingを
使っています。
アプリケーションレイヤーでは、基本的にcronバッチ&アラート通知(Email & Slack)が多いです。アラート通知がアプリケーションプロセス・HTTPサーバーに依存している
ため、ここで不具合が発生するとどうしようもない状況になります。
バッチ処理をHTTP経由にする弊害の例
決済システムの前段にはELBが入っており、オンライン向けの処理系統とバッチ処理向け
の処理系統を分けておらず、どちらも一つのアプリケーションとして動作しています。
先日、メモリリーク調査のために数日間バッチ処理系統を分けていました。
そんな中、Fatalエラー(logrusの各フック内でmapの並行アクセス起きてしまった...)が発生し、アプリケーションが落ちてしまいました。
本来はsupervisorで再起動されるはずですが、なぜかうまくされずに変な状態で生き残ってしまっていたようです。(プロセスKILL等では問題なく再起動される)
このため全てELBから外されしまい、バッチ処理が行われないようになってしまいました。
(仮で作ってもらったのでオートヒーリングが設定されていなかった...)
HTTPクライアント側にはHTTPステータスでのアラートを設定しておらず、アラートの
ほぼ全てをHTTPサーバー経由の処理が担っていたためにアラートが機能しなくなってしまっていました。
休日でしかもMacherelアラートも他の通知に紛れてしまっていたため、対応が遅れてし
まいました。(吐き気)
本当に気をつけてください。
データベース
最初の方で説明した通り、DBにはAmazon Auroraを使っています。
そこまでトラフィックがないため、アプリケーションサイドでは特に辛さやありがたみ、
畏敬の念を感じたことはないですが、インフラ運用面では何かあるかもしれないので、
恐らく来年中には弊社のオラオラ系テラフォーマー恩田氏が何か語ってくれるはずです。
テーブル数は10ちょいです。
直接決済に関わるテーブル以外だと、
  • マルチテナンシー用のアカウントテーブル
  • 監査ログテーブル
  • エラーログテーブル
が存在しています。
エラーログテーブルはステージングでの調査に使用することが多く、本番ではあまり活用
できていないので、もう少しチューニングをしていきたいと思っています。
WebUI
ありません。
上記のエラーログやBaremetricsのようなUIを提供したいのですが、
「ドヤァァァァ!」と作っても自己満になりそうなので、今のところはやっていません。
F. まとめ
ここまで見てきたように、決済のシステムを作るには各プラットフォームの知識が必要に
なってきます。ある意味では、実装を行うよりも詳しい仕様を把握する方が大変かもしれ
ません。
プラットフォーム間の差異を吸収するために、eurekaでは決済システムをマイクロサー
ビスとして切り出すことにしました。
サポートする決済手段が1,2種類ほどでは分ける必要性が薄いかもしれませんが、決済手
段が増えるに従い、別のシステムとして分けるメリットは増してくると思います。
決済の苦しみスバラシサを1%でもお届けできたか不明ですが、少しでもお役に立てたらと
思います。
はてぶ数が100以上、FBシェア数が200以上つくと、ブログ工数が確保され、この続き
を図付きで詳しく書かせてくれるということなので、コメントを添えてシェアリングパ
ーティーをしてもらえればと思います。
明日の23日は、まつけんはんによる「Pairsのインフラコストを最適化しました」となり
ます。
乞うご期待!
Like what you read? Give eureka_developers a round of applause.
From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.

0 コメント:

コメントを投稿