Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

A/B テストの管理基盤刷新とレポートの自動化(基盤編)

はじめに

こんにちは。Gunosy Tech Lab 所属の hyamamoto です。 今回は広告システムにおいて「A/B テストの管理基盤刷新とレポートの自動化」を行った話についてご紹介します。 基盤編と集計編の 2 部構成で、本記事では A/B テストの管理基盤の刷新についてご紹介したいと思います。

元々、今回のプロジェクトの最終的なモチベーションは A/B テストのレポートの自動化にあり、A/B テストの管理基盤刷新はスコープとしていませんでした。 しかしながら、レポート自動化にあたってボトルネックになった課題があったため、管理基盤から刷新をおこないました。

既存の A/B テスト管理基盤の課題

既存の A/B テスト管理基盤は A/B テストの管理基盤が広告システムごとに以下の2 つに分かれていました。

  • yaml で IaC 的に設定する基盤
  • GUI で設定する基盤

この結果、設定方法や集計方法が広告システムごとに異なるという問題がありました。

また、それぞれの基盤についても次のような課題がありました。

  • yaml で IaC 的に設定する基盤の課題
    • 履歴として A/B テストの設定が保存されていなかったため、人間がデプロイ情報を元に集計期間をクエリで絞りこんでいた
    • validation が甘く不適切な設定でデプロイされることがあった
    • サーバーと yaml ファイルの間に暗黙の約束が存在し、typo で設定が読み込めないことがあった
  • GUI ベースで設定する基盤の課題
    • A/B テスト特有のパラメータを付与しようにも GUI 側まで対応させるためのコストが高かった
    • デプロイフローが GitHub とは異なる管理画面を経由するため、過去のデプロイ履歴を開発者以外が把握しづらかった

特に前者の基盤において A/B テストの設定が履歴として保存されておらず、適用タイミングがデータとして残っていなかったことは、集計の自動化を妨げていた要因でした。 また、その他の課題に関しても長期的に運用していくなかでリスクやコストが高いものとなっていました。

新規 A/B テスト管理基盤

それでは今回作成した A/B テストの管理基盤についてご紹介していきます。

方針

既存の A/B テスト管理基盤の課題を受けて、今回は次のような方針で設計を刷新しました。

  • yaml による IaC 的な A/B テストの設定基盤の作成
  • Go 言語を用いて CLI を作成し CI/CD の整備
  • 全 A/B テストの設定を履歴として蓄積

基本方針としては yaml による IaC 的な設定基盤を継承しました。 理由としては A/B テストの設定を行うのはエンジニアであるため、高度な GUI を用意するよりも yaml を記述し、 設定内容を Pull-Request 経由でレビューを行う方が効率的だと考えたからです。 また、yaml は非構造なデータを扱うのに適しており、ある A/B テストに対して特有のパラメータを整備することも容易だと考えました。

構成概要

新規 A/B テスト管理基盤の大まかな構成は次のようになっています。

アーキテクチャ

  1. 開発者は A/B テストの設定を yaml で記述し、Pull-Request を出す
  2. Pull-Request を作成すると CI が走り、validation および現在デプロイされている A/B テストの設定との差分を Pull-Request にコメントする
  3. Pull-Request が approve されマージされると、CI が走り、A/B テストの設定をデプロイする
  4. 各 Service は S3 から A/B テストの設定を読み込み、ロジックに反映する
  5. S3 に配置された yaml は S3 の event hook によって Lambda に通知される
  6. Lambda は通知された情報から A/B テストの設定履歴として RDS に追記する

Go 言語による A/B テスト管理基盤の作成

今回 CLI 含め A/B テストの設定を管理するための基盤を Go 言語で作成しました。 そこで工夫した点についてご紹介したいと思います。

validation 機能の整備

既存システムの課題である validation の甘さを解消するため、Go 言語を用いて CLI を作成しました。 この際、構造体として A/B テストの定義を用意し、go-playground/validator を用いることで詳細な validation を行うことが可能になりました。 用意した validation の例としては control/treatment に設定されたパラメータグループの traffic 比率が等しく設定されているかのチェックが挙げられます。

そして、CLI のコマンドとして現在デプロイされている設定との差分を表示する機能を用意し、 その差分チェックの際に validation を呼び出すようにすることで、Pull-Request を作成した段階で開発者が設定ミスを発見したり、反映する A/B テストの差分を確認できるようにしました。

GitHub の Bot コメントサンプル

A/B テストの定義コードの自動生成

go generate を用いて A/B テストの定義コードを一部自動生成することで、新規に A/B テストの設定を追加する際の手間を削減しました。 というのも、新規の A/B テストを追加する際に追加で必要な定義は、その A/B テスト特有のパラメータに限られるためです。

例えば、sampling rate を設定する A/B テストを追加する際には、開発者は次のようなコードを追加し、go generate を実行することで、残りの必要なコードはすべて自動生成されます。

type FooABTestParameters struct {
  SamplingRate float64 `yaml:"samplingRate" json:"samplingRate" validate:"gte=0,lte=1"`
}

更に validation の annotation も付与すれば、特定の A/B テストで発生するパラメータではあるものの、頑健に管理することが可能です。 後は、sampling rate や配信する traffic 情報など A/B テストの設定を yaml で記述し、Pull-Request を出すだけで、A/B テストの設定をデプロイするための準備は完了します。

private package として A/B テストの定義コードを公開

今回作成した A/B テストを利用する Service も Go 言語で書かれているという背景から、A/B テストの定義コードを private package として公開し、Service 側から利用できるようにしました。 これによって、Service 側で yaml を読み込む際に新たに構造体を定義する必要がなくなりコードを共通化することができました。 結果、以前起きた「設定ファイルやコードの typo によって設定内容が Service に取り込まれない」というバグも未然に防ぐことができます。 更に、traffic の出し分け機能もこれまではそれぞれの Service 側で実装していましたが、この機能についても今回作成した package 側に実装することで共通化をはかりました。 以前までは traffic の出し分けについて Service ごとに異なるロジックで実装されており、集計時のクエリが異なるという課題もあったため、この点について解消できたのは分析上のメリットも大きいと考えています。

A/B テストの設定の履歴管理

既存の課題であった A/B テストの設定の履歴管理については、S3 に設定ファイルがデプロイされたタイミングで Lambda を発火させ、RDS に書き込むことで実現しました。 この Lambda では非構造的なデータである yaml を構造化しながら RDS に書き込むことを目的としています。 また、A/B テストが設定されたイベントとしてデータを RDS に書き込むことで 、A/B テストの開始期間と終了期間についてレコード作成時に自動付与される timestamp から特定できるようになりました *1

こうして書き込まれた履歴情報を RDS からデータウェアハウスに取り込むことで、A/B テストに付随するデータを集計することができるようになりました。 なお、Lambda のコードもモノレポ的に同一リポジトリに含めているため、構造化のためのロジックと A/B テストの設定のためのコードをシームレスに管理することができたのもよかったです。

まとめ

今回は A/B テストの設定を管理するためのツールとして、yaml とGo 言語を用いた IaC 的なアプローチを紹介しました。 これにより、既存の課題を解決しつつ A/B 集計の自動化に向けた基盤を用意することができました。

振り返って良かった点としては、変化の激しい A/B テストへの拡張性を保ちつつ、頑健な基盤を作ったことで開発者の体験を向上できたことが挙げられます。 一方、反省点としては、独自に CLI を作るのではなくて Terraform の Provider として提供したほうが実装が早く拡張性が高かったかもしれないなとは感じています。 また、A/B テストの振り分けロジックを Go 言語に寄せた結果、他言語のサービスが生まれたときには拡張しにくいという課題も感じています *2。 このあたりは現状の実装で問題が浮き彫りになれば対応していこうと思っています。

個人的な所感として、今現在 DR&MLOps と AdsML チームを兼務しているのですが、データ基盤とアプリケーションの境界面における課題解決ができたことはとても嬉しく、兼務としての役割を一定果たせたのかなと感じています。 紹介した今回の設計がお役に立てれば幸いです。

次は今回作成した基盤を活用し、「A/B テストの管理基盤刷新とレポートの自動化(自動化編)」について @m-hamashita さんに紹介していただきます! ぜひ続きも読んでいただけると幸いです。

tech.gunosy.io

*1:実際には Service が設定を読みこんだタイミングが開始時刻ではありますが、分単位の開始時刻の誤差は集計に影響ないとして許容しています。

*2:Sidecar として gRPC のサーバーを建てるなどのアプローチはありそうです。