Google Play Billing Client 2.0における消費型商品の決済の承認(acknowledgement)について

ユーザ・決済基盤部の宇津(@uzzu)です。

クックパッドでは複数のAndroidアプリでGoogle Play決済(定期購読、消費型商品)を利用しており、 ユーザ・決済基盤部ではそれらのアプリの決済情報を取り扱う共通決済基盤サービスクライアントライブラリを日々開発しています。 直近ではGoogle I/O 2019にて発表されたGoogle Play Billing Client 2.0にも対応し、Cookpad.apk #3のLT枠にてどのように対応していったか発表させて頂きました。

speakerdeck.com

本記事では同発表にて時間が足りず深堀りできなかった、消費型商品における決済の承認(acknowledgement)対応について解説します。 スライドと合わせて読んで頂ければ幸いです。

消費型商品における2.0とそれ以前との違い

2.0以前の消費型商品の購入フローは概ね以下の図のようになっていたかと思います。

f:id:himeatball:20190815060223p:plain
2.0以前の購入フロー

2.0からはこれに加えて、決済の承認が必要になります。 Google Play決済自体は決済処理時に走る(Pending Purchaseを除く)のですが、3日以内に開発者が決済の承認を行わない場合返金されます。 通信断や障害等で購入フローが正常に完了せず商品が付与されなかったユーザが自動的に救済されるようになるのは、サポートコスト削減の面でも非常に良いですね。

f:id:himeatball:20190815060447p:plain
2.0での購入フロー

一見、購入フローに処理が1ステップ追加されただけのように見えます。加えてリリースノートにも

For consumable products, use consumeAsync(), found in the client API.

とあるように、アプリ上でconsumeAsyncを呼び出す事で消費(consume)しつつ決済の承認も行われるので、図に追加した⑤については特にやる事はないのでは?と思われた方もいるかと思います。 しかしながら、商品付与が行われるタイミングにおける決済の承認状態は2.0では未承認、それ以前では承認済という違いがあり、 この違いによってアプリ改ざんに対するリスクを考慮する必要性がでてきます。

consumeAsyncを呼び出さないようにアプリを改ざんされる事を想定した場合、購入処理を実施すると以下のように処理されます。

  1. 消費型商品の購入ボタンを押す
  2. 購入フローに則り商品が付与される
  3. consumeAsyncを呼び出さない為消費が行われないが、商品は既に付与されている(決済が未承認の間、商品は消費されない為、再購入はできない)
  4. 3日後、決済が未承認の為返金される
  5. 返金されると消費型商品が再度購入可能になる
  6. 1に戻る

つまり、2.0以前の購入フローの実装のまま愚直に2.0対応してしまうと、アプリ改ざんによって3日毎に消費型商品を無料で取得する事ができてしまいます。

対策A: サーバサイドで決済を承認する

決済の承認はレシート検証同様にサーバサイドで行いたいという需要に応えるように、Purchases.products: acknowledge
が用意されています。 クックパッドの共通決済基盤サービスではこれを利用して決済を承認しユーザと決済情報の紐付けが正常に行われた上で、各サービスで商品の付与ができる状態とするようにしています。 商品付与後、アプリ上でconsumeAsyncします。

この対策方法はアプリ改ざんに対するリスク、及び決済の承認に関連するアプリ上での購入フローの実装が2.0とそれ以前とで変わらないのが利点です。 ただし、クックパッドのような決済サービスと商品を販売しているサービスが分離されている環境下においては、 決済状態と商品付与状態の整合性の担保ができている前提での対策方法になると考えています。 クックパッドの共通決済基盤サービスにおける整合性についての取り組みは以下の記事を参考にしてください。

https://techlife.cookpad.com/entry/2016/06/01/070000

加えて決済を承認するタイミングについて、商品を付与した上で決済を承認するか、あるいはレシート検証を終えた段階で一旦決済を承認した後に商品を付与するかを検討するかと思います。

クックパッドの共通決済基盤サービスではどうしているかというと、消費型商品においては購入フローの完了処理である所の消費処理がアプリ上でしか実施できない為、購入処理の冪等性を担保できるよう後者を選択しています。 定期購読においてはGoogle Play決済を終えた以降の購入フローをサーバサイドで完結できる為、前者で且つレスポンスタイムを上げる為にJob Queueで非同期に決済の承認を実施しています。

対策B: アプリ上で決済の承認を実施してからサーバにレシートを送信し、サーバ間通信で決済の承認状態を検証する

Billing Client Libraryに決済の承認をするためのメソッド(BillingClient#acknowledgePurchase)が用意されています。 Google Play決済を実施後にこれを呼び出してまず承認してしまい、その上でサーバにレシートを送信し、サーバサイドでPurchases.products: getを呼び出してacknowledgementStateを確認し、 承認済か否かを検証した上で商品を付与した後、アプリ上でconsumeAsyncするような購入フローにします。

この対策方法ではアプリ上に実装されている購入フローはもちろん、通信断等で滞留した決済の再開処理にも手を入れる必要がある為、対策Aよりは手がかかるものの、 サーバ間通信で決済の承認状態を検証する為、対策A同様に介入される余地はないと考えています。

その他の対策方法

例えばdeveloper notificationを頼りに商品を付与する方法があるかと思いますが、developer notificationは現在定期購読のみサポートしているのと、 仮にサポートされるようになったとしても、消費型商品において大半のユーザは購入完了したら遅延なくすぐに商品を使用したい為、 その仕組みを整えるのはそれなりに開発コストがかかりそうです。

決済処理フローはそのままにアプリ改ざん対策に本腰を入れていくとしても、アプリ改ざん対策はいたちごっこになってしまう為、運用コストの増大が予想されます。 素直に前述の対策Aないし対策Bを適用するのが良さそうです。

まとめ

本記事ではGoogle Play Billing Client 2.0における消費型商品の決済の承認(acknowledgement)について解説しました。 弊社において利用していない機能もあり(定期購読のupgrade/downgrade等)、決済の承認に関する網羅的な解説とまではなっていないですが、 Google Play Billing Client 2.0導入の手助けとなれば幸いです。

クックパッドではアプリ内課金をやっていくエンジニアを募集しています

インフラのコスト最適化の重要性と RI (リザーブドインスタンス) の維持管理におけるクックパッドでの取り組み

技術部 SRE グループの mozamimy です。

クックパッドでは、 SRE が中心となって、サービスを動かす基盤の大部分である AWS のコスト最適化を組織的に取り組んでいるため、今回はそれについてご紹介します。

前半では、そもそもの話として「なぜコスト最適化が重要なのか」「何が難しいのか」「何をすべきなのか」といったことを述べます。これは、当たり前すぎて逆に陽に語られることが少ない (とわたしは感じています) トピックで、一度しっかり言語化しておいてもいいかなと考えたからです。内容のほとんどはわたしの脳内ダンプで、クックパッドという会社のコンテキストや組織としてのステージが前提になっているため、大多数の組織について当てはまる内容とは限りません。

後半では、コスト最適化の一例として、リザーブドインスタンス (以下 RI と略記) を維持管理するためのフローと、それを支えるモニタリングシステムについて述べます。こちらは AWS を利用するいかなる組織においても、今日から使えるテクニックとなるでしょう。もしかすると、似たような仕組みを整えている組織もあるかもしれません。

パブリッククラウドのコスト最適化の重要性

オンプレミスであれクラウドであれ、インフラにかかるコストを最適化し、できる限り無駄のない状態を保つことが重要なのは言うまでもないでしょう。とりわけクラウドでは、必要なときに必要な分だけお金を対価にリソースを得られる反面、簡単な操作でリソースを増やすことができるため、気づいたときには大きな無駄が発生していたという自体も起こりえます。

組織において、お金という限られたリソースは大変重要かつ貴重なものです。たとえば、インフラにかかるコストを年間 $10,000 節約できたとすれば、その $10,000 は投資やその他の重要な部分に回すことができます。インフラコストは投資と違って単に失われるだけなので、エンジニアリングリソースを割いて最適化することは、十分に理にかなっているでしょう。クラウドであれば、API を通してリソースを操作できるため、ソフトウェアエンジニアリングで解決できる部分が大きいです。

コスト最適化に見て見ぬふりをしていると、じわじわと組織の首を絞めていきますし、節約できていれば本来有意義に使えたお金が失われることになります。キャッシュが枯渇してから慌てていては、もう手遅れです。

コスト最適化を考えるにあたって、以下の 2 つの軸が存在するとわたしは考えています。

  • リソースプールのコスト最適化: RI を適切に保つ、スポットインスタンスを積極的に利用する、など。
  • リソースプールの利用に対するコスト最適化: 各サービスで利用しているインフラのキャパシティ (ECS サービスのタスク数、RDS インスタンス、ElastiCache Redis/Memcached クラスタなど) が、適切にプロビジョン・スケーリングしていて無駄遣いしていない状態に保つこと。

コスト最適化の基本的なポイント

たとえば AWSだと、以下のような取り組みがあげられます。これらは AWS の公式ページにも書かれています。

  1. スポットインスタンスを積極的に利用する。
  2. RI の購入によってある程度のキャパシティを予約・前払いし、オンデマンドインスタンスやマネージドサービスを割安で利用する。
  3. Cost Explorer や Trusted Advisor といったサービスを利用してオーバープロビジョニングなリソースを発見し、インスタンスサイズやその数を適切に保つようオペレーションする。

これらのうち、クックパッドでは 1 番と 2 番、すなわちリソースプールそのもののコスト最適化については非常に熟れており、理想に近い状態を維持することができています。3 番のリソースプールの利用については、これからも継続的に取り組んでいくべき課題となっています。

1. スポットインスタンスの積極的な利用

コンテナオーケストレーション基盤としてクックパッドでは早くから ECS を採用しており、SRE の鈴木 (id:eagletmt) による Cookpad Tech Kitchen #20 での「Amazon ECSを安定運用するためにやっていること」という発表にあるように、スポットインスタンスを利用した ECS クラスタを安定して運用できています。Rails を始めとする HTTP をしゃべるアプリケーションサーバなど、状態を持たないサービスのほとんどは、スポットインスタンスで構成された ECS クラスタで動いています。これは、コストの最適化に大きく寄与しています。

2. RI (リザーブドインスタンス) の運用

このトピックは、記事の後半で述べるためここではいったん隅に置いておきます。

3. キャパシティを過不足ない適切な状態に保つ

文字で書いただけでは「いいじゃんやろうよ」という感じなのですが (ですよね?)、実際にはその道のりは非常に険しいです。たとえば...

  1. インスタンスサイズの変更のためにどうしても停止メンテナンスが必要になったらどうするか? 開発チームとの《調整》が発生。
  2. アプリケーションの改善でコストを最適化できる場合 (たとえばスロークエリを改善するなど) だと、SRE よりもそのアプリケーションを普段から触っていてドメイン知識のある開発チームが対応したほうが早い。
  3. SRE の限られた人的リソースですべてのインフラのリソースの状況をウォッチして対応し続けるのは組織の拡大に対してスケールしない。

といった問題があります。

これは、SRE という概念が解決しようとしてる問題に密接に関連しています。Google による SRE 本にも、コスト最適化のためのオペレーションはトイルとして扱われています。

エラーバジェットや信頼性といった真髄からはやや外れるため、コスト最適化に SRE という概念を絡めることには議論があるかもしれません。ただ、わたしは場当たり的なコスト最適化は技芸だと思っていて、それをエンジニアリングで解決することは立派な SRE way だし、SLO を守れる範囲でコストを切り詰めていくことは信頼性に深く関わることだと信じています。トイルをなくそうという SRE の方針とも合致しますね。

この問題はクックパッドの SRE で取り組んでいる、開発チームへの権限と責任の移譲が進み、適切にコストをモニタリングできる仕組みが整えば解決できる見込みがあります。少し前の記事ですが、権限と責任の移譲については、クックパッドの開発基盤、インフラ環境での開発で心がけているラストワンマイルでも触れられています。

各開発チームが、自分たちのサービスにかかっているインフラコストを把握できるようなり、自律的にリソースプールの利用を最適化できると、組織の拡大に対してスケールするようになるでしょう。開発チーム自身の手で実装起因の無駄を改善することができ、スケールダウンのための停止メンテナンスが必要になった場合でも、チーム内で調整を完結して行うことができます。

SRE はリソースプールそのものの最適化と、リソースプールの利用をモニタリングできる仕組みを提供することに専念し、必要に応じて開発チームを手伝うことはありますが、基本的には開発チームにコスト最適化の責任と権限を持ってもらうのです。

また、プロジェクトや部署ごとに、どの程度サービス運用のためのインフラコストがかかっているかを把握できるようなダッシュボードを作ることができると、財務管理上でもメリットになるでしょう。まるっと「インフラ代金」として予算管理しているものが、開発チームや部署、プロジェクトごとに予算を細かく設定し、追跡することができるのです。財務などのバックオフィスもインフラコストの状況を追跡できるようにしておく重要性は Whitepaper: The guide to financial governance in the cloud でも触れられています。

リソースプールの最適化に限界が見え始めた今、やるべきこと

SRE が責任として持っている、スポットインスタンスの利用推進や RI の適切な運用によって、開発チームが利用するリソースプール自体のコスト最適化は限界に近づきつつあります。次にやるべきことは、リソースプールの利用を最適化していくことで、これは組織全体として取り組めるように SRE がエンジニアリングで解決していく必要があります。

リソースプールの利用に対するコスト最適化はこれからの課題として、後半では、リソースプール自体のコスト最適の取り組みの一つとして、RI の維持管理のフローと、それを支えるモニタリングシステムについて説明します。

クックパッドにおける RI (リザーブドインスタンス) の維持管理と対応フロー

RI の費用削減効果を最大限に発揮させるためには、その状況の変化を察知して、以下のようなオペレーションによって理想の状態に戻るようにメンテナンスし続けるのが一般的です。

  • RI の追加購入
  • 動いているインスタンスタイプの変更
  • RI の exchange (2019-08-14 現在、convertible な EC2 の RI のみ可能)

これは CPU 利用率やキューの待ち行列の長さといったメトリクスに基づくキャパシティのスケールアウト・スケールインと似ています。RI の状況がしきい値を割ったときに、上述のリストにあげたようなオペレーションによって、理想的な状態に戻るようにメンテナンスし続けるのです。

クックパッドでは、後述の ri-utilization-plotter や github-issue-opener といった Lambda function を利用して、以下のような GitHub issue を使った対応フローをとれるようにしています。

  • RI の状況を監視し、変化があったことを検知してアラートをあげる。
  • アラートがあがると、自動的に GitHub のリポジトリに issue が作成される。
  • RI の追加購入などのオペレーションをする。
  • RI の状況が元に戻ったことを確認して issue を閉じる。

RI の状況ベースでの監視は、実際に状況が変わってからしか検知できないため、対応が後手に回ってしまいがちという弱点があります。RI の追加購入や exchange の際には、いくつ買っていくつ exchange するのかというプランを練ってチーム内でレビューする必要があるため、その作業が終わるまでは RI による費用削減効果が減ってしまうことになります。

少なくとも RI の失効は事前に知ることができるため、上述の RI の状況をベースとした監視に加え、以下のような対応フローも整えています。

  1. RI の失効が発生する 7 日前に PagerDuty の incident を発行してアラートをあげる。
  2. RI の追加購入を検討・実施し、incident を resolve 状態にする。

これらの仕組みにより、RI を理想に近い状態で運用することができています。

実際の対応フロー

百聞は一見にしかずということで、この仕組みを使った Grafana によるダッシュボードや、実際の対応フローを見てみましょう。

この仕組みでは、RI の状況は CloudWatch のカスタムメトリクスに蓄積されるため、以下のように Grafana から一覧することができます。

f:id:mozamimy:20190813143925p:plain
Grafana から確認できる CloudWatch カスタムメトリクスに集積された RI の状況

ちなみに、Cost Explorer API から取得できる値は、月に一回程度、変な値を示してしまう場合があるのですが、頻度も少ないですしそれは許容しています。このスクリーンショットでは、左上のグラフの谷になっている箇所ですね。

実際に RI の状況が変化してメトリクスがしきい値を割ると、GitHub に issue が自動的に立って場が作られ、そこで対応することになります。

f:id:mozamimy:20190813144027p:plain
RI の監視システムによってあがったアラートに GitHub 上で対応している様子

このスクリーンショットのシナリオでは、Amazon ES の RI カバレッジがしきい値を割ったことが検知されており、わたしがその原因を調べてコメントし、オペレーションによる一時的なものであることを確認して CloudWatch Alarm のしきい値を調整し、メトリクスがもとに戻ったことを確認して issue をクローズとしました。別のケースでは、RI を追加で購入したり exchange したりといったオペレーションをしたこともありました。

さて、ここからは実装の話に移ります。以下のトピックについて説明していきましょう。

  • RI の状況を知る
  • RI の状況が変わったときにアラートをあげる
  • RI の失効が近づいたときにアラートをあげる

RI (リザーブドインスタンス) の状況を知る

RI がどのような状況にあるかは、RI 利用率RI カバレッジによって知ることができます。

RI 利用率は、購入した RI がどの程度有効になっているか、すなわちどの程度余らせることなく有効に使えているかを示す割合です。RI カバレッジは、オンデマンドで動いているインスタンスの総量に対して、RI によってどの程度カバーされているかを示す割合です。

RI 利用率は 100% を維持できている状態が望ましいです。RI カバレッジも同様に、できる限り 100% に近い状態を保っているのが理想です。しかしながら、ある時刻において RI カバレッジが 100% になるように買ってしまうと、将来的な利用の変化に対応することが難しくなります。アプリケーションの改善により必要なキャパシティが減ったり、EC2 であれば、スポットインスタンス化やマネージドサービスに寄せることで RI が余るといったことも起こるでしょう。あまり考えたくはないですが、サービスのクローズによって不要になるリソースもあるかもしれませんね。そこで、ターゲットとなるカバレッジを、たとえば 70% などと決めておき、その値を下回らないように RI をメンテナンスするのがよいでしょう。

RI 利用率や RI カバレッジは Cost Explorer のコンソールや API から確認でき、フィルタを駆使することでリージョンやサービスごとに値を絞り込めます。

f:id:mozamimy:20190813145616p:plain
Cost Explorer から確認する RI 利用率の様子

この画面からは利用率だけでなく、RI によってどの程度コストを節約できたのかといった情報も表示できます。また、左側のメニューにある Reservation summary には、RI の情報をサマライズしたビューが用意されています。

f:id:mozamimy:20190813145723p:plain
Cost Explorer で RI のサマリを確認する

この画面では、すべてのサービスを横断した RI のサマリを確認でき、30 日以内に失効する RI も確認することができて便利です。

Cost Explorer の基となっているデータ (RI の情報だけでなく、請求に関するすべての情報を含む) は AWS Cost and Usage Report (以下 CUR と略記) によって S3 バケットに出力することができ、Athena などでクエリすることで Cost Explorer では実現できないような集計結果を SQL によって自在に引き出すことができます。

クックパッドでは、CUR を Redshift Spectrum からクエリできるような仕組みがデータ基盤グループによって整備されており、Tableau を用いてダッシュボードを作ることも可能になっています。

RI (リザーブドインスタンス) の状況の変化に対してアラートを仕掛ける

ここまでの説明で、Cost Explorer やその API、CUR などを駆使することで、RI の状況を確認できることがわかりました。次はそれらのメトリクスに対してアラートを仕掛けることを考えていきましょう。

なるべく AWS 標準の機能で済ませたい場合は、AWS Budgets が利用できます。reservation budget をしきい値とともに作成し、アラートの設定に SNS トピックやメールアドレスを設定することで通知を HTTP エンドポイントや Lambda function 、特定のメールアドレスに送ることができます。試していませんが、少し前のアップデートによって、AWS Chatbot を利用して Slack や Chime に簡単に通知できるようになったようです。

Launch: AWS Budgets Integration with AWS Chatbot | AWS Cost Management

これはこれで便利ですが、以下の理由で今回の要件を満たせないと考え、自前で仕組みを用意しようと判断しました。

  • reservation budget を daily で判定するように設定すると、しきい値を割っている間、毎日通知が届いてしまう。
  • 通知の細かい制御ができない。たとえば「3 日以上しきい値を割り続けていたら」のようなアラートを設定できない。
  • メールや Slack の通知だけでは見落としが発生する可能性が高く、後半の冒頭で述べたような対応フローを実現できない。

今回は、ri-utilization-plotter という Lambda function を中心とした、以下のような仕組みを構成しました。仕組みとしては非常に素朴ですが、よく動きます。

  1. 12 時間ごとに起動する Lambda function から、Cost Explorer API をたたき、取得した RI 利用率と RI カバレッジを CloudWatch のカスタムメトリクスにプロットする。
  2. そのカスタムメトリクスに CloudWatch Alarm を仕掛け、しきい値を割ったときに SNS topic 経由で Lambda function を起動し、GitHub の SRE のタスクをまとめるためのリポジトリに issue を立てる。

ちなみに、CloudWatch にメトリクスを寄せることで、Grafana を使って Prometheus といった他の様々なメトリクスと組み合わせたダッシュボードを作ることもできて便利というメリットもあります。

実際には、たとえば Redshift の RI はデータ基盤グループが管理しているため、SRE 用とは別のリポジトリに issue を立てるようにするなど、もう少し複雑な構成になっています。そのため、上記の構成図はエッセンスを抜き出したものとなっています。

ri-utilization-plotter は、AWS サーバーレスアプリケーションモデル (以下 SAM と略記) に則り、CloudFormation stackを aws-sam-cli を利用してデプロイしています。

メトリクスをプロットする ri-utilization-plotter と github-issue-opener の stack が分かれているのは、github-issue-opener は「SNS トピックに届いた通知をもとに GitHub に issue を作成する便利 Lambda function」として、ri-utilization-plotter からだけでなく、汎用的に利用できるように実装しているからです。このような構成をとることにより、他の Lambda を中心としたアプリケーションで GitHub App を用いたコードを自前で実装しなくても、SNS トピックに通知するというシンプルな操作だけで issue を立てることができるようになっています。

それでは、ri-utilization-plotter と github-issue-opener の中身を見ていきましょう。

ri-utilization-plotter

ソースコードはこちらにあります。https://github.com/mozamimy/ri-utilization-plotter

template.example.yml に記述されている RIUtilizationPlotter という論理名を持つ AWS::Serverless::Function リソースが本体で、make コマンドを経由して SAM CLI を実行して手元で実行できるようになっています。

この function は、event.example.json にあるように、

{
  "region": "us-east-1",
  "service": "Amazon ElastiCache",
  "linked_account": "123456789012",
  "granularity": "DAILY",
  "namespace": "RIUtilizationTest",
  "metric_name": "UtilizationPercentage",
  "ce_metric_type": "utilization"
}

のようなイベントを、CloudWatch Events から受け取ります。namespace に CloudWatch カスタムメトリクスのネームスペースを指定し、metric_name にメトリクスの名前を指定します。この例では RI 使用率をプロットする設定になっていますが、metric_name を CoveragePercentage のようにし、ce_metric_type を coverage にすると、RI カバレッジもプロットすることができます。ce_metric_type は Cost Explorer API のオプションとして渡す文字列で、utilization (RI 使用率) と coverage (RI カバレッジ) に対応しています。

上述のリポジトリには開発時にテスト用途として SAM CLI を利用することが前提の CloudFormation テンプレートしか含まれていません。これは意図したもので、Lambda function のソースコードと AWS の実環境へのデプロイの設定を分離するためにこうなっています。したがって、AWS の実環境にデプロイする場合には、そのための CloudFormation テンプレートを別途用意する必要があり、これは社内固有の設定も含むため社内のリポジトリで管理されています。

社内で管理されている実環境用の CloudFormation テンプレートは、Jsonnet を利用して JSON に変換することで生成しています。

たとえば、この場合だとリージョンとメトリクスのタイプ (utilization/coverage) の組み合わせごとに CloudWatch Event ルールが必要となるため、以下のように event.rule という関数を定義することで DRY になるようにしています。./lib/event.libsonnet というファイルに event.rule の定義を出力する rule という関数を作り、CloudFormation テンプレートの Resources の中でそれを呼び出すイメージです。

また、実行ファイルを含む zip ファイルは GitHub の release にあるため、デプロイ時に https://github.com/mozamimy/ri-utilization-plotter/releases/download/v1.0.0/ri-utilization-plotter.zip から curl でダウンロードし、jsonnet コマンドを実行するときに jsonnet template.jsonnet --ext-str codeUri=${PWD}/tmp/ri-utilization-plotter-v1.0.0.zip のようにオプションを与え、std.extVar('codeUri') のようにして実行可能ファイルを含む zip ファイルのパスを差し込むようにしています。

{
  AWSTemplateFormatVersion: '2010-09-09',
  Transform: 'AWS::Serverless-2016-10-31',
  Resources: {
    MainFunction: {
        // 省略
        Properties: {
            CodeUri: std.extVar('codeUri'),
            // 省略
        },
    },
    DLQ: {
        // 省略
    },
    EventRuleUtilizationApne1: event.rule(alias, 'ap-northeast-1', 'utilization'),
    EventRuleUtilizationUse1: event.rule(alias, 'us-east-1', 'utilization'),
    EventRuleCoverageApne1: event.rule(alias, 'ap-northeast-1', 'coverage'),
    EventRuleCoverageUse1: event.rule(alias, 'us-east-1', 'coverage'),
  },
}

Jsonnet はパワフルなテンプレート言語で、クックパッドでは ECS を使ったアプリケーションのデプロイを簡単に行うためのツールである Hako でも採用されています。

パワフルがゆえに何でもできてしまうという側面もあり、用法用量を間違えると複雑怪奇な設定ファイルができてしまうというダークサイドもありますが、個人的にはとても気に入っています。実は JSON は YAML として評価しても合法なため、設定ファイルに YAML を用いるソフトウェアでも Jsonnet を使うことができます。

github-issue-opener

ソースコードはこちらにあります。https://github.com/mozamimy/github-issue-opener

これは SNS topic で受けた通知をもとに GitHub に issue を立てる Lambda function です。

ISSUE_BODY_TEMPLATEISSUE_SUBJECT_TEMPLATE といった環境変数に issue のタイトルや本文の内容を Go のテンプレートして記述することができます。たとえば、{{.Message}} のように設定した場合、SNS メッセージのメッセージがそのままレンダリングされます。 https://github.com/mozamimy/github-issue-opener/blob/master/template.example.json を見れば、雰囲気が掴めるでしょう。

通知を送る側から issue のタイトルや本文を指定したい場合は、message attribute が使えます。https://github.com/mozamimy/github-issue-opener/blob/5699d16606c4da13edc40c2c674c7110aaec43e7/event.example.json#L18-L42 を見れば雰囲気が掴めるかと思います。

SNS メッセージの本文に JSON 文字列が入っていて、それをパースして issue テンプレートで利用したい場合は、PARSE_JSON_MODE 環境変数を 1 に設定することで実現できます。その場合、

{
    "Foo": "bar"
}

のような JSON 文字列がメッセージ本文に含まれていた場合、テンプレートからは {{.ParsedMessage.Foo}} のようにして値を取り出してレンダリングすることができます。

こちらも ri-utilization-plotter と同様に、AWS 実環境へのデプロイ用の CloudFormation テンプレートは社内のリポジトリで Jsonnet として管理されています。

issue テンプレートは

# RI Utilization Alert

{{.ParsedMessage.NewStateReason}}

- **Alarm Name**: {{.ParsedMessage.AlarmName}}
- **Namespace**: {{.ParsedMessage.Trigger.Namespace}}
- **Metric Name**: {{.ParsedMessage.Trigger.MetricName}}
{{- range .ParsedMessage.Trigger.Dimensions }}
  - **{{.name}}**: {{.value}}
{{- end }}

Please check current utilization and fix it. 

のような形で ./issue_template/body_ri_notify.md.tmpl に保存されており (一部実際のものから変更を加えています)、CloudFormation テンプレートからは

{
  AWSTemplateFormatVersion: '2010-09-09',
  Transform: 'AWS::Serverless-2016-10-31',
  Resources: {
    MainFunction: {
      // 省略
      Environment: {
        Variables: {
          // 省略
          ISSUE_BODY_TEMPLATE: importstr './issue_template/body_ri_notify.md.tmpl',
        },
      },
    },
  },
}

のような形で、Jsonnet の importstr でファイルを読んだそのままを ISSUE_BODY_TEMPLATE 環境変数に設定しています。

RI (リザーブドインスタンス) の失効が近づいたときにアラートをあげる

ri-utilization-plotter のように、RI の失効が近づいたときにアラートをあげる仕組みを内製しようとしていましたが、AWS Cost Explorer で予約の有効期限切れアラートを提供開始 にあるように、Cost Explorer 自体に 60 日前、30 日前、7 日前、当日にメールを送信する機能が実装されたため、それを利用することにしました。

ただし、単にメールを送るだけだと、後半の冒頭で述べたような対応フローをとることができません。見逃してしまうこともあるでしょう。

そこで、今回は PagerDuty の email integration のメールアドレスをこの通知に設定することで、low urgency でアラートを飛ばし SRE の誰かにアサインされるようにしました。週次のミーティングでも low urgency に設定されたアラートが残っていないか振り返りを行っているため、作業が漏れることもありません。万が一漏れた場合でも、ri-utilization-plotter を中心とした仕組みにより、後手になりつつも異常に気づくことができます。

RI (リザーブドインスタンス) の購入・exchange プランを練るときの細かい話

RI を追加購入したり exchange したりする場合は、現在どの程度オンデマンドインスタンスの利用があるのかを把握して、プランを立てる必要があります。

その際には、Cost Explorer の画面で、過去何日間かを指定してインスタンスタイプごとの RI 利用率や RI カバレッジを確認したり、今この瞬間に動いているインスタンスの一覧を得るために https://github.com/mozamimy/toolbox/blob/master/ruby/list_on_demand_ec2_instances/list.rb のようなスクリプトを利用したりしています。このスクリプトは、bundle exec ruby list.rb t3 のように実行すると、t3 ファミリの EC2 インスタンス一覧を出した上で、

Total instance count: 49
Normalized total count: 110.0
    as t3.small 110.0
    as t3.large 27.5

のように、トータルで何台オンデマンドインスタンスが動いているのかと、.small および .large に正規化した台数を算出します。

現状はこれらの値をもとに SRE が職人の技で丹精を込めてプランを立てていますが、将来的には自動化したいと考えています。素朴にやろうとするとすべての購入プランを網羅して最適な解を選ぶことになりますが、それはおそらく現実的ではありません。問題を適切にモデル化して既存のアルゴリズムを利用するなど、賢くやらねばなりません。ある意味で競技プログラミングのようですね。

まとめ

前半ではパブリッククラウドのコスト最適化についての考えを述べ、後半では、その取り組みの一部である RI の維持管理のための仕組みについて説明しました。

RI は、スポットインスタンスの積極的な利用に次ぐコスト最適化の強力な武器ですが、日々の手入れを怠るとその力を十分に発揮することができません。この記事では、クックパッドではその武器をどのように手入れしているかということについて説明しました。RI の管理にお悩みの方は、参考にしてみてはいかがでしょうか。

AWS re:Inforce 2019に参加してきました

技術部セキュリティグループの三戸 (@mittyorz) です。こんにちは。 去る6/25,26日に開催されたAWS re:Inforce 2019に参加しましたので、簡単ではありますが紹介させていただきたいと思います。 今回が初開催なため規模感や雰囲気などは未知数の中、クックパッドからはセキュリティグループの三戸・水谷 (@m_mizutani)とVP of Technologyの星 (@kani_b) のあわせて3名で参加いたしました。

AWS re:Inforce とは

AWS re:Inforceは、数あるAWSのカンファレンスの中でもセキュリティに特化したカンファレンスです。 セキュリティに関するイベントはre:Inventなどでもこれまであったわけですが、re:Inventが非常に巨大なイベントとなってしまったこと、クラウドベンダーとしてセキュリティにおいてどのような姿勢で望むのかを積極的にアピールする場としてre:Inforceが新たに用意されたのではないかなと思っています。 AWS以外にも様々なセキュリティベンダーがパートナーとして参加しており、ブースの様子などについては後ほど触れたいと思います。

Builderにとってのセキュリティ

初日、AWSのVPかつCISOであるSteve SchmidtによりKeynoteが行われました。この中で、セキュリティはセキュリティエンジニアだけが考えるものではなくサービスに関係する全てのメンバーが意識し関わるべきものであること、 サービスを作り上げるうえで必要なものを自分たちで選択し作り上げていくように、セキュリティにおいてもベンダーの製品をそのまま使うのではなく、要素を組み合わせ自分たちにとって本当に必要なものを作っていく必要があることが強く主張されていました。 その際に、AWSのクラウドサービスをどのように上手に選択するか、ということが大事になってくるわけです。

セキュリティベンダーの事例紹介でも同様で、単に顧客の環境に自社の製品を導入したという話ではなく、AWSのサービスと組み合わせることでどういうことが新たにできるようになったかということが説明されていました。 そのためには自分たちでセキュリティを作り上げていく必要があり、度々キーワードとして出てきた「Builder」にそのことが表れていると思います。

会場の様子

AWS re:Inforceは現地ボストンにおいて6/25,26の二日間に渡って開催されました。 AWSのカンファレンスといえばまずre:Inventを思い浮かべる方が多いかと思いますが、規模としてはそこまで巨大というわけではなくおおよそ一つの建物にまとまっていました*1

f:id:mittyorz:20190723152400j:plain
会場となったBoston Convention and Exhibition Centerの、おそらく正面入口

f:id:mittyorz:20190723152532j:plain
ベンダーによるブースなどが並ぶ、受付からすぐのスペース。会場は4階まであったので写っているのはごく一部と言えます

日本からは24日18時に成田から離陸して現地に24日18時に到着*2、翌日から二日間会場入りして、三日目木曜日には帰国に向け出発し金曜日夕方に日本に到着するというちょっと強行軍かなとも言えるスケジュールでした。 幸いにも恐れていた時差ボケにはならず、はじめての海外出張かつ大規模なカンファレンス初参加の割には落ち着いて見て回れたのではないかと自分では思っています。

セッションについて

日程は二日間でしたが、聴講タイプのいわゆるSessionの他、手を動かすWorkshopやSecurity Jam他、非常に沢山の ブースではベンダーの説明を聞いたり質問を行うことも出来ますし、待ち合わせ時間などに手元のPCから参加できるCTFも用意されていました。

1時間単位のセッションにいくつか参加したのと、4時間かけて点数を競い合うSecurity Jamに3名でチームを組んで参加しました。

以下、参加したセッションから特に印象に残った点などを簡単に紹介します。

Encrypting Everything with AWS (SEP402)

AWSの各サービスでどの部分でどのような暗号化をしているか、物理層の安全性をどう担保しようとしているか、について広く説明するセッションです。 ただ、時間のかなりの部分が「暗号論入門」という感じで、暗号アルゴリズム自体の説明になっていてちょっと期待とは違ったかなという感想でした。 セッションには難易度が設定されていて、これは「Expert」向けとされていたのですが、暗号アルゴリズムの数学的背景を予め知っているのであればだいぶわかりやすかったのではないかなと思います。 後半はAWSの各サービスでどのように暗号化が行われているかそのアルゴリズムも含め解説され、AWS SDKからの利用といったユーザが直接触れる部分から、インスタンス間やVPC間の通信をどのように行っているのか、更にはリージョン間の物理ネットワークの暗号化などレイヤーごとに説明されていました。 後述するNitro Controllerを用いて暗号化する際には、複数のキーストアから取得した鍵の「一部分」をNitron Controller内であわせることで実際に用いる鍵を生成し用いているという話が特に興味深かったです。

Firecracker: Secure and fast microVMs for serverless computing (SEP316)

LambdaおよびFargateの実行環境を効率的に隔離するために実装されたLinux KVMベースのHypervisior、Firecrackerについてのセッションです。 アカウントごとに実行環境をきちんと分離しつつ、オーバーヘッドの低減を両立させるのは難しいと説明しつつ、一方でFirecrackerを用いることでVMの起動をだいたい120ms以下で行い、一つのホストで1秒間に150VM起動できるようになると話していたのが興味深かったです。 Lambdaを実行している環境は、そのLambdaをデプロイしたアカウントごとに別々のゲストOSにHypervisorによって分離されているわけですが、このHypervisiorにFirecrackerを用いることで一つのホストによりたくさんの環境を詰め込める、ということが詳しく説明されていました*3。 Fargateについては、Firecrackerを用いることでEC2のインスタンスサイズの荒い粒度ではなくより細かくリソースが割り当てられるようになったこと、起動が非常に速いので「EC2 Warm Pool」を用意する必要がなくなり価格が大きく削減できたこと*4が説明されていました。 今後のロードマップでは、現在はIntel CPUのみがサポートされていますが、年内にはAMD CPUのサポートも計画されているとアナウンスされました。

Security benefits of the Nitro architecture (SEP401)

このセッションでは、Nitro Contollerと呼ばれる物理的なチップとそれを用いた全体のアーキテクチャについて説明がありました。 「Nitro」という言葉自体はAWSの最新世代のEC2インスタンスなどで用いられている仮想化技術というコンテキストでよく出てきますが、このシステムを構成するために通常のCPUや周辺機器とは独立して組み込まれているのがNitro Contollerです。 仮想化に伴うオーバーヘッドを低減するために、出来る限りNitro Controllerを用いて処理をオフロードする仕組みになっているのですが、インスタンスストレージへのアクセスを含めDisk I/O、Network I/OなどすべてのI/OはNitro Controllerを用いて透過的に暗号化される仕組みになっています。 また、暗号化に必要な情報はNitro Contoroller同士で自動的にやり取りされるとのこと。 Nitro Controller自体も独自に内部的なストレージを持っていて、ファームウェアのアップデートなどはシステムを再起動すること無く行えるようになっているそうです。 Nitro Controllerが改ざんされないようCPUからは書き込みができないようになっていたり、システムの起動時にはNitro Contorollerによってマザーボードのファームウェアがチェックされるようになっているなど、システムの健全性を担保するのにNitro Controllerが要になっていることがよく分かるセッションでした。

Security Jam

複数人でチームを組み*5、競技時間の4時間の間に出題された問題を出来るだけ問いて順位を競い合います。 競技時間終了と同時に出題ページがクローズされてしまったので記憶を元にちょっと紹介すると、

  • 複数のVPCにまたがって構成されたウェブサービスを再構成する問題
  • EKSのCI/CDと、EKS自体のセキュリティの設定を修正する問題

といったように、AWSの各サービスをきちんと理解していないと問題が解けないようにうまく設計されていて、普段の業務で触ってない部分の知識も容赦なく必要とされなかなか苦労しました。

f:id:mittyorz:20190723152620j:plain
全体で60席くらい用意されていましたが、オンラインで参加できるのでこのホール外から参加していたチームもあったようです

f:id:mittyorz:20190723152643j:plain
5-hour ENERGY という、ちょっとヤバそうなエナジードリンク

最終的に45チーム中11位という結果になりました。

セキュリティの競技だといわゆる「CTF」をまず思い浮かべる方が多いかと思いますが、普通のCTFと比較してみてAWSの環境自体を直す要素が強かったかなと思います。 また、AWSのアカウント・リソースが問題ごとに別々に用意されていて、出題ページからAWSのコンソールにログインすると独立した環境が自由に使えるようになったのは流石だなという感じでした。

ブース

企業ブースでは日本でもよく見る有名・老舗セキュリティベンダーがやはり多かったですが、一方で日本ではあまり見ないベンダのブースも多くありました。こういったブースだと直接担当の人と話して質問ができるのでなかなかカタログスペックなどから分からないことを聞けて面白かったです。全体の傾向としては、以下のような印象でした。 - コンテナ環境を対象としたサービスがメインストリームになりつつあり、逆に言うとEC2インスタンスなどにフォーカスするようなサービスの宣伝はあまり見なかった - クラウド環境全体の監査・コンプライアンスチェックを自動化するというサービスがかなり目立っていた

程度の差はありましたが、ブースエリアはそれほど混んでおらず気になった製品のブースで気軽に質問できるようになっていました。

おわりに

AWS re:Inforce 2019を簡単にですが紹介させていただきました。 AWS自身が提供するセキュリティ機能だけではなく、各社がクラウド向けに出してきているセキュリティ製品をどううまく活用していくのか、いうなれば「顧客が本当に必要だったもの」をきちんと構築できるようになりましょう、というメッセージを強く感じました。 新しい技術・製品がどんどん出てくる中で、適切な技術選択を行うのはとても難しいことですが、難しいからこそ面白い分野でもあると再確認できてとても良かったと思います。

クックパッドでは技術を用いてセキュリティを高め最高のサービスを提供することに興味のある仲間を募集しています。

セキュリティエンジニア

*1:もっとも、建物自体が巨大で、規模感としては東京国際展示場の各展示棟を一つにまとめたくらいをイメージすると近いのではないかなと思います

*2:搭乗時間と時差がちょうど一致

*3:動画で18:30頃から出てくる図を見るとわかりやすいかと思います

*4:https://aws.amazon.com/jp/blogs/compute/aws-fargate-price-reduction-up-to-50/

*5:一人でも参加可能なようでした